一. 楔子
兩個程序之間想要傳遞一個數據,需要用到網絡通信.
二. 軟件開發的架構:
第一種: 應用類: qq,微信,網盤,優酷 這一類是屬於需要安裝的桌面應用.
第二種: web類: 百度,知乎,博客園,等使用瀏覽器訪問就可以直接使用的應用.
這些應用的本質其實就是兩個程序之間的通訊,而這兩個分類又對應了兩個軟件開發的架構.
1.C/S 架構
C/S即: client與server,中文意思: 客戶端與服務器端架構,這種架構也是從用戶層面,(也是物理層面划分的).
2.B/S 架構
B/S即:browser和server,中文意思:瀏覽器端與服務器端架構,這種架構是從用戶層面來划分.
browser瀏覽器,其實也是一種client客戶端,只是這個客戶端不需要大家去安裝什么應用程序,只是需要通過http請求服務器端相關的資源(網頁資源),客戶端browser瀏覽器就能進行增刪改查.
三. 網絡基礎.
1. osi七層模型.
2.socket概念
socket是應用層與TCP/IP協議族通信的中間軟件抽象層,他是一組接口.在設計模式中,socket其實就是一個門面模式,它把復雜的TCP/IP協議族隱藏在socket接口后面,對用戶來說,一組簡單的接口就是全部,讓socket去組織數據,以符合指定的協議.
其實站在你的角度上看,socket就是一個模塊.我們通過調用模塊中實現的方法
建立兩個進程之間的連接和通信. 也有人將socket說成ip+port,因為ip是用來標識互聯網中的一台主機的位置,
而port是用來標識這台機器上的一個應用程序,
所以我們只要確立了ip和port就能找到一個應用程序,並且使用socket模塊來與之通信.
3. tcp協議和udp協議
TCP 可靠的.面向連接的協議,傳輸效率低全雙工通信(發送緩存,接收緩存),面向字節流.
使用TCP的應用: web瀏覽器;電子郵件;文件傳輸程序.
UDP 不可靠的,無連接的服務,傳輸效率高(發送錢延遲小),一對一,一對多,多對多,面向報文,盡最大努力服務,無擁塞控制,使用UDP的應用: 域名系統(DNS); 視頻流,IP語音.
四.套接字(socket)初使用.
1. 基於TCP協議的socket
tcp是基於連接的,必須先啟動服務器,然后再啟動客戶端去連接服務器.
server端

import socket sk = socket.socket() sk.bind(('127.0.0.1',8898)) #把地址綁定到套接字 sk.listen() #監聽鏈接 conn,addr = sk.accept() #接受客戶端鏈接 ret = conn.recv(1024) #接收客戶端信息 print(ret) #打印客戶端信息 conn.send(b'hi') #向客戶端發送信息 conn.close() #關閉客戶端套接字 sk.close() #關閉服務器套接字(可選)
client端

import socket sk = socket.socket() # 創建客戶套接字 sk.connect(('127.0.0.1',8000)) # 嘗試連接服務器 sk.send(b'hello') ret = sk.recv(1024) # 對話(發送/接收) print(ret) sk.close() # 關閉客戶套接字
問題: 重啟服務端時可能會遇到,
解決方法:

# 加入一條socket配置,重用ip和端口. import socket from socket import SOL_SOCKET,SO_REUSEADDR sk = socket.socket() sk.setsockopt(SQL_SOCKET,SO_REUSEADDR,1) # 就是它,在bind前加. sk.bind('127.0.0.1',8000) #把地址綁定到套接字 sk.listen() #監聽鏈接 conn,addr = sk.accept() #接收客戶端鏈接 ret = conn.recv(1024) #接收客戶端信息 print(ret) # 打印客戶端信息 conn.send(b'hi') # 向客戶端發送信息 conn.close() # 關閉客戶端套接字 sk.close() # 關閉服務器套接字(可選)
2.基於UDP協議的socket
udp是無鏈接的,啟動服務之后可以直接接收消息,不需要提前建立鏈接.
簡單使用
server端

import socket udp_sk = socket.socket(type=socket.SOCK_DGRAM) # 創建一個服務器的套接字 udp_sk.bind(('127.0.0.1',9000)) # 綁定服務器套接字 msg,addr = udp_sk.recvfrom(1024) print(msg) udp_sk.sendto(b'hi',addr) #對話(接收與發送) udp_sk.close() #關閉服務器套接字
client端

import socket ip_port = ('127.0.0.1',9000) udp_sk = socket.socket(type=socket.SOCK_DGRAM) udp_sk.sendto(b'hello',ip_port) back_msg,addr = udp_sk.recvfrom(1024) print(back_msg.decode('utf-8'),addr)
3. socket參數的詳解
socket.socket(family=AF_INET,type=SOCK_STEAM,proto=0,fileno=None)
創建socket對象的參數說明:
family | 地址系列應為AF_INET(默認值),AF_INET6,AF_UNIX,AF_CAN或AF_RDS。 (AF_UNIX 域實際上是使用本地 socket 文件來通信) |
type | 套接字類型應為SOCK_STREAM(默認值),SOCK_DGRAM,SOCK_RAW或其他SOCK_常量之一。 SOCK_STREAM 是基於TCP的,有保障的(即能保證數據正確傳送到對方)面向連接的SOCKET,多用於資料傳送。 SOCK_DGRAM 是基於UDP的,無保障的面向消息的socket,多用於在網絡上發廣播信息 |
proto | 協議號通常為0,可以省略,或者在地址族為AF_CAN的情況下,協議應為CAN_RAW或CAN_BCM之一. |
fileno | 如果指定了fileno,則其他參數將被忽略,導致帶有指定文件描述符的套接字返回。 與socket.fromfd()不同,fileno將返回相同的套接字,而不是重復的。 這可能有助於使用socket.close()關閉一個獨立的插座 |
五.黏包
1.黏包現象
讓我們基於tcp先制作一個遠程執行命名的程序(命令ls -l; llllll,pwd)

res = subprocess.Popen(cmd.decode('utf-8'), shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE) 結果的編碼是以當前所在的系統為准的,如果是windows,那么res.stdout.read()讀出來的就是gbk編碼的,在接收端需要gbk解碼 且只能從管道里讀一次結果
同時執行多條命令之后,得到的結果很可能這樣一部分,在執行其他命令的時候又接收之前執行的另一部分結果,這種顯現就是黏包.
2. 基於tcp協議實現的黏包
tcp-server

tcp - server from socket import * import subprocess ip_port = ('127.0.0.1',8888) BUFSIZE = 1024 tcp_socket_server = socket(AF_INET,SOCK_STREAM) tcp_socket_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) tcp_socket_server.bind(ip_port) tcp_socket_server.listen(5) while True: cmd = conn.recv(BUFSIZE) if len(cmd) == 0:break res = subprocess.Poen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE ) stderr = res.stderr.read() stdout = res.stdout.read() conn.send(stderr) conn.send(stdout)
tcp-client

# tcp-client import socket BUFSIZE = 1024 ip_port = ('127.0.0.1',8888) s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) res = s.connect_ex(ip_port) while True: msg = input('>>:').strip() if len(msg) == 0:continue if msg == 'quit':break s.send(msg.encode('utf-8')) act_res = s.recv(BUFSIZE) print(act_res.decode('utf-8'),end='')
注意: 只要TCP有黏包現象.UDP永遠不會黏包.
3.黏包成因
1.TCP協議中的數據傳遞,
tcp協議的拆包機制.
當發送端緩沖區的長度大於網卡的MTU時,tcp會將這次發送的數據拆成幾個數據包發送出去.
MTU是Maximum Transmission Unit的縮寫. 意思是網絡上傳送的最大數據包. MTU的單位是字節,大部分網絡設備的MTU都是1500.如果本機的MTU比網關的MTU大,
大的數據包就會被拆開傳送,這樣會產生數據包碎片,增加丟包率,降低網絡速度.
面向流的通信特點和Nagle算法
TCP(transport control protocol,傳輸控制協議)是面向連接的,面向流的,提供高可靠性服務。
收發兩端(客戶端和服務器端)都要有一一成對的socket,因此,發送端為了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle算法),將多次間隔較小且數據量小的數據,合並成一個大的數據塊,然后進行封包。
這樣,接收端,就難於分辨出來了,必須提供科學的拆包機制。 即面向流的通信是無消息保護邊界的。
對於空消息:tcp是基於數據流的,於是收發的消息不能為空,這就需要在客戶端和服務端都添加空消息的處理機制,防止程序卡住,而udp是基於數據報的,即便是你輸入的是空內容(直接回車),也可以被發送,udp協議會幫你封裝上消息頭發送過去。
可靠黏包的tcp協議:tcp的協議數據不會丟,沒有收完包,下次接收,會繼續上次繼續接收,己端總是在收到ack時才會清除緩沖區內容。數據是可靠的,但是會粘包。
會發生黏包的兩種情況
情況一 , 發送方的緩存機制.
發送端需要等緩沖區滿才發送出去,造成黏包(發送數據時間間隔很短,數據了很小,會合到一起,產生黏包)
服務端

from socket import * ip_port = ('127.0.0.1',8080) tcp_socket_server = socket(AF_INET,SOCK_STEARM) tcp_socket_server.bind(ip_port) tcp_socket_server.listen(5) conn,addr = tcp_socket_server.accept() data1 = conn.recv(10) data2 = conn.recv(10) print('----->',data1.decode('utf-8')) print('----->',data2.decode('utf-8')) conn.close()
客戶端

import socket BUFSIZE = 1024 ip_port = ('127.0.0.1',8080) s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) res = s.connect_ex(ip_port) s.send('hello'.encode('utf-8')) s.send('egg'.encode('utf-8'))
情況二,接收方的緩存機制
接收方不及時接收緩存區的包,造成多個包接收(客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收的時候還是從緩存區拿上次遺留的數據,產生黏包)
服務端

from socket import * ip_port = ('127.0.0.1',8080) tcp_socket_server = socket(AF_INET,SOCK_STREAM) tcp_socket_server.bind(ip_port) tcp_socket_server.listen(5) conn,addr = tcp_socket_server.accept() data1 = conn.recv(2) # 一次沒有收完整 data2 = conn,recv(10) # 下次收的時候,會先取舊的數據,然后取新的. print('----->',data1.decode('utf-8')) print('----->',data2.decode('utf-8')) conn.close()
客戶端

import socket BUFSIZE = 1024 ip_port = ('127.0.0.1',8080) s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) res = s.connect_ex(ip_port) s.send('hello egg'.encode('utf-8'))
總結:
黏包現象只發生在tcp協議中:
1.從表面上看,黏包問題主要是因為發送方和接收方的緩存機制,tcp協議面向流通信的特點.
2. 實際上,主要還是因為接收方不知道消息之間的界限,不知道一次性提取多少字節的數據造成的.
4.黏包的解決方案
解決方案一
問題的根源在於,接收端不知道發送端將要傳送的字節流的長度,所以解決黏包的方法就是圍繞,如何讓發送端在發送數據前,把自己將要發送的字節流總大小讓接收端知曉,然后接收端來一個死循環接收完所有數據.
服務端

import socket,subprocess ip_port = ('127.0.0.1',8080) s = socket.socket(socket.AF_INET,socket.SOCK_STRMAM) s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) s.bind(ip_port) s.listen(5) while True: conn,addr = s.accept() print('客戶端',addr) while True: msg = conn.recv(1024) if not msg:break res = subprocess.Popen(msg.decode('utf-8'), shell=True, stdin=subprocess, stderr=subprocess.PIPE, stdout=subprocess.PIPE) err = res.stderr.read() if err: ret = err else: ret = res.stdout.read() data_length = len(ret) conn.send(str(data_length).encode('utf-8')) data = conn.recv(1024).decode('utf-8') if data == 'recv_ready': conn.sendall(ret) conn.close()
客戶端

import socket,time s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) res = s.connect_ex(('127.0.0.1',8080)) while True: msg = input('>>:').strip() if len(msg) == 0:continue if msg == 'quit':break s.send(msg.encode('utf-8')) length = int(s.recv(1024).decode('utf-8')) s.send('recv_ready'.encode('utf-8')) send_size = 0 recv_size = 0 data = b'' while recv_size < length: data += s.recv(1024) recv_size += len(data) print(data.decode('utf-8'))
存在的問題:
程序的運行速度遠快於網絡傳輸速度,所以在發送一段字節前,先用send去發送該字節流長度,這種方式會放大網絡延遲帶來的性能損耗.
5. 解決方案進階.
剛剛的方法,問題在於我們在發送. 我們可以借助一個模塊,這個模塊可以要發送的數據長度轉換成固定長度的字節,這樣客戶端每次接收消息之前只要先接收這個固定長度字節的內容看一看接下來要接收的信息大小,那么最終接收的數據只要達到這個值就停止,就能剛好不多不少的接收完整的數據了.
struct模塊
該模塊可以把一個類型,如數字,轉成固定長度的bytes
>>> struct.pack('i',11111111) struct.error: 'i' format requires -214748348 <= number <= 2147483647 # 這個是范圍
# struct模塊 import json,struct # 假設通過客戶端上傳1T:10737418000的文件a.txt 為避免黏包,必須自定制報頭 header = {'file_size':1073741824000,'file_name':'/a/b/c/d/e/a.txt','md5':'8f6fbf8347faa4924a76856701edb0f3'} # 1T數據,文件路徑和md5值. #為了該報頭能傳送,需要序列化並且轉為bytes head_bytes = bytes(json.dumps(header),encoding='utf-8') # 序列化並轉成bytes,用於傳輸 # 為了讓客戶端知道報頭的長度,用struck將報頭長度這個數字轉成固定長度:4個字節 head_len_bytes = struct.pack('i',len(head_bytes)) # 這4個字節里只包含了一個數字,該數字是報頭的長度 #客戶端開始發送 conn.send(head_len_bytes) #先發報頭的長度,4個bytes conn.send(head_bytes) # 先發報頭的字節格式 conn.sendall(文件內容) # 然后發真實內容的字節格式 #服務端開始接收 head_len_bytes = s.recv(4) #先收報頭4個bytes,得到報頭長度的字節格式 x = struct.unpack('i',head_len_bytes)[0] #提取報頭的長度 head_bytes = s.recv(x) #按照報頭長度x,收取報頭的bytes格式 header = json.loads(json.dumps(header)) #提取報頭 #最后根據報頭的內容提取真實的數據,比如: real_data_len = s.recv(header['file_size']) s.recv(real_data_len)
關於struct的詳細用法

# 關於struct的詳細用法 #http://www.cnblogs.com/coser/archive/2011/12/17/2291160.html __author__ = 'Linhaifeng' import struct import binascii import ctypes values1 = (1,'abc'.encode('utf-8',2.7)) values2 = ('defg'.encode('utf-8'),101) s1 = struct.Struct('I3sf') s2 = struct.Struct('4sI') print(s1.size,s2.size) prebuffer = ctypes.create_string_buffer(s1.size+s2.size) print('Before : ',binascii.hexlify(prebuffer)) # t = binascii.hexlify('asdfaf'.encode('utf-8')) # print(t) s1.pack_into(prebuffer,0,*values1) s2.pack_into(prebuffer,s1.size,*values2) print('After pack',binascii.hexlify(prebuffer)) print(s1.unpack_from(prebuffer,0)) print(s2.unpack_from(prebuffer,s1.size)) s3 = struct.Struct('ii') s3.pack_into(prebuffer,0,123,123) print('After pack',binascii.hexlify(prebuffer)) print(s3.unpack_from(prebuffer,0))
使用struct解決黏包
借助struct模塊,我們知道長度數字可以被轉換成一個標准大小的4字節數字,因此可以利用這個特點來預先發送數據長度.
發送時 | 接收時 |
先發送struct轉換好的數據長度4字節 | 先接收4個字節使用struct轉換成數字來獲取要接收的數據長度 |
再發送數據 | 再按照長度接收數據 |
服務端(自定制報頭)

import socket,struct,json import subprocess phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加. phone.bind(('127.0.0.1',8080)) phone.listen(5) while True: conn,addr = phone.accept() while True: cmd = conn,recv(1024) if not cmd:break print('cmd: %s ' % cmd) res = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) err = res.stderr.read() print(err) if err: back_msg = err else: back_msg = res.stdout.read() conn.send(struct.pack('i',len(back_msg))) # 先發back_msg的長度 conn.sendall(back_msg) # 在發真實的內容 conn.close()
客戶端(自定制報頭)

import socket,time,struct s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) res = s.connect_ex(('127.0.0.1',8080)) while True: msg = input('>>:').strip() if len(msg) == 0:continue if msg == 'quit':break s.send(msg.encode('utf-8')) l = s.recv(4) x = struct.unpack('i',1)[0] print(type(x),x) # print(struct.unpack('I',1)) r_s = 0 data = b'' while r_s < x: r_d = s.recv(1024) data += r_d r_s += len(r_d) #print(data.decode('utf-8')) print(data.decode('gbk')) #windows默認gbk編碼
我們還可以把報頭做成字典,字典里包含將要發送的真實數據的詳細信息,然后json序列化,然后用struck將序列化后的數據長度打包成4個字節.
發送時 | 接收時 |
先發報頭長度 | 先收報頭長度,用struct取出來 |
再編碼報頭內容然后發送 | 根據取出的長度收取報頭內容,然后解碼,反序列化 |
最后發真實內容 | 從反序列化的結果中取出待數據的詳細信息,然后去取真實的數據內容 |
服務端: 定制稍微復雜一點的報頭.

import socket,struct,json import subprocess phone = socket.socket(sock,AF_INET,socket.SOCK_STREAM) phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加. phone.bind(('127.0.0.1',8080)) phone.listen(5) while True: conn,addr = phone.accept() while True: cmd = conn.recv(1024) if not cmd:break print('cmd: %s '% cmd) res = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) err = res.stderr.read() print(err) if err: back_msg = err else: back_msg = res.stdout.read() headers = {'data_size':len(back_msg)} head_json = json.dumps(headers) head_json_bytes = bytes(head_json,encoding='utf-8') conn.send(struct.pack('i',len(head_json_bytes))) # 先發報頭的長度 conn.send(head_json_bytes) # 再發報頭 conn.sendall(back_msg) # 再發真實的內容 conn.close()
客戶端

from socket import * import struct,json ip_port = ('127.0.0.1',8080) client = socket(AF_INET,SOCK_STREAM) client.connect(ip_port) while True: cmd = input('>>:') if not cmd:continue client.send(bytes(cmd,encoding='utf-8')) head = client.recv(4) head_json_len = struct.unpack('i',head)[0] head_json = json.loads(client.recv(head_json_len).decode('utf-8')) data_len = head_json['data_size'] recv_size = 0 recv_data = b'' while recv_size < data_len: recv_data += client.recv(1024) recv_size += len(recv_data) print(recv_data.decode('utf-8')) #print(recv_data.decode('gbk')) # windows默認gbk編碼