Python併發程式設計(三):網路程式設計之粘包現象
阿新 • • 發佈:2019-01-07
目錄
一、什麼是粘包
須知:只有TCP有粘包現象,UDP永遠不會粘包
粘包不一定會發生
如果發生了:1.可能是在客戶端已經粘了
2.客戶端沒有粘,可能是在服務端粘了
首先需要掌握一個socket收發訊息的原理
應用程式所看到的資料是一個整體,或說是一個流(stream),一條訊息有多少位元組對應用程式是不可見的,因此TCP協議是面向流的協議,這也是容易出現粘包問題的原因。(因為TCP是流式協議,不知道啥時候開始,啥時候結束)。而UDP是面向訊息的協議,每個UDP段都是一條訊息,應用程式必須以訊息為單位提取資料,不能一次提取任意位元組的資料,這一點和TCP是很不同的
所謂粘包問題主要還是因為接收方不知道訊息之間的界限,不知道一次性提取多少位元組的資料所造成的。
二、發生粘包的兩種情況
2.1、傳送端需要等緩衝區滿才傳送出去,造成粘包(傳送資料時間間隔很短,資料了很小,會當做一個包發出去,產生粘包)
服務端
1 from socket import * 2 phone = socket(AF_INET,SOCK_STREAM) 3 phone.setsockopt(SOL_SOCKET,SOCK_STREAM,1) 4 phone.bind(('127.0.0.1',8080)) 5 phone.listen(5) 6 print('start running...') 7 8 coon,addr = phone.accept() #等待連線 9 10 data1 = coon.recv(10) 11 data2 = coon.recv(10) 12 13 print('------------>',data1.decode('utf-8')) 14 print('------------>',data2.decode('utf-8')) 15 coon.close() 16 phone.close()
客戶端
1 from socket import *
2 import time
3 phone = socket(AF_INET,SOCK_STREAM)
4 phone.connect(('127.0.0.1',8080))
5
6 phone.send('hello'.encode('utf-8'))
7 phone.send('helloworld'.encode('utf-8'))
8 phone.close()
2.2、接收方不及時接收緩衝區的包,造成多個包接收(客戶端傳送了一段資料,服務端只收了一小部分,服務端下次再收的時候還是從緩衝區拿上次遺留的資料,產生粘包)
服務端
1 from socket import *
2 phone = socket(AF_INET,SOCK_STREAM)
3 phone.setsockopt(SOL_SOCKET,SOCK_STREAM,1)
4 phone.bind(('127.0.0.1',8080))
5 phone.listen(5)
6 print('start running...')
7
8 coon,addr = phone.accept() #等待連線
9
10 data1 = coon.recv(2) #一次沒有接收完整
11 data2 = coon.recv(10) #下一次接收的時候會先取舊的資料,然後取新的
12 # data3 = coon.recv(1024) #接收等5秒後的資訊
13 print('------------>',data1.decode('utf-8'))
14 print('------------>',data2.decode('utf-8'))
15 # print('------------>',data3.decode('utf-8'))
16 coon.close()
17 phone.close()
客戶端
1 from socket import *
2 import time
3 phone = socket(AF_INET,SOCK_STREAM)
4 phone.connect(('127.0.0.1',8080))
5
6 phone.send('hello'.encode('utf-8'))
7 time.sleep(5)
8 phone.send('haiyan'.encode('utf-8'))
9 phone.close()
三、解決粘包的方法
問題的根源在於:接收端不知道傳送端將要傳送的位元組流的長度,所以解決粘包的方法就是圍繞:如何讓傳送端在傳送資料前,把自己將要傳送的位元組流總大小讓接收端知曉,然後接收端來一個死迴圈接收完所有資料
服務端
1 import socket
2 import subprocess
3 import struct
4 phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #買手機
5 phone.bind(('127.0.0.1',8080)) #繫結手機卡
6 phone.listen(5) #阻塞的最大數
7 print('start runing.....')
8 while True: #連結迴圈
9 coon,addr = phone.accept()# 等待接電話
10 print(coon,addr)
11 while True: #通訊迴圈
12 # 收發訊息
13 cmd = coon.recv(1024) #接收的最大數
14 print('接收的是:%s'%cmd.decode('utf-8'))
15 #處理過程
16 res = subprocess.Popen(cmd.decode('utf-8'),shell = True,
17 stdout=subprocess.PIPE, #標準輸出
18 stderr=subprocess.PIPE #標準錯誤
19 )
20 stdout = res.stdout.read()
21 stderr = res.stderr.read()
22 #先發報頭(轉成固定長度的bytes型別,那麼怎麼轉呢?就用到了struct模組)
23 #len(stdout) + len(stderr)#統計資料的長度
24 header = struct.pack('i',len(stdout)+len(stderr))#製作報頭
25 coon.send(header)
26 #再發命令的結果
27 coon.send(stdout)
28 coon.send(stderr)
29 coon.close()
30 phone.close()
客戶端
1 import socket
2 import struct
3 phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
4 phone.connect(('127.0.0.1',8080)) #連線服
5 while True:
6 # 發收訊息
7 cmd = input('請你輸入命令>>:').strip()
8 if not cmd:continue
9 phone.send(cmd.encode('utf-8')) #傳送
10 #先收報頭
11 header_struct = phone.recv(4) #收四個
12 unpack_res = struct.unpack('i',header_struct)
13 total_size = unpack_res[0] #總長度
14 #後收資料
15 recv_size = 0
16 total_data=b''
17 while recv_size<total_size: #迴圈的收
18 recv_data = phone.recv(1024) #1024只是一個最大的限制
19 recv_size+=len(recv_data) #
20 total_data+=recv_data #
21 print('返回的訊息:%s'%total_data.decode('gbk'))
22 phone.close()
四、解決粘包問題升級版:完整的解決了
服務端
1 import socket
2 import subprocess
3 import struct
4 import json
5 phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #買手機
6 phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
7 phone.bind(('127.0.0.1',8080)) #繫結手機卡
8 phone.listen(5) #阻塞的最大數
9 print('start runing.....')
10 while True: #連結迴圈
11 coon,addr = phone.accept()# 等待接電話
12 print(coon,addr)
13 while True: #通訊迴圈
14 # 收發訊息
15 cmd = coon.recv(1024) #接收的最大數
16 print('接收的是:%s'%cmd.decode('utf-8'))
17 #處理過程
18 res = subprocess.Popen(cmd.decode('utf-8'),shell = True,
19 stdout=subprocess.PIPE, #標準輸出
20 stderr=subprocess.PIPE #標準錯誤
21 )
22 stdout = res.stdout.read()
23 stderr = res.stderr.read()
24 # 製作報頭
25 header_dic = {
26 'total_size': len(stdout)+len(stderr), # 總共的大小
27 'filename': None,
28 'md5': None
29 }
30 header_json = json.dumps(header_dic) #字串型別
31 header_bytes = header_json.encode('utf-8') #轉成bytes型別(但是長度是可變的)
32 #先發報頭的長度
33 coon.send(struct.pack('i',len(header_bytes))) #傳送固定長度的報頭
34 #再發報頭
35 coon.send(header_bytes)
36 #最後發命令的結果
37 coon.send(stdout)
38 coon.send(stderr)
39 coon.close()
40 phone.close()
客戶端
1 import socket
2 import struct
3 import json
4 phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
5 phone.connect(('127.0.0.1',8080)) #連線伺服器
6 while True:
7 # 發收訊息
8 cmd = input('請你輸入命令>>:').strip()
9 if not cmd:continue
10 phone.send(cmd.encode('utf-8')) #傳送
11 #先收報頭的長度
12 header_len = struct.unpack('i',phone.recv(4))[0] #吧bytes型別的反解
13 #在收報頭
14 header_bytes = phone.recv(header_len) #收過來的也是bytes型別
15 header_json = header_bytes.decode('utf-8') #拿到json格式的字典
16 header_dic = json.loads(header_json) #反序列化拿到字典了
17 total_size = header_dic['total_size'] #就拿到資料的總長度了
18 #最後收資料
19 recv_size = 0
20 total_data=b''
21 while recv_size<total_size: #迴圈的收
22 recv_data = phone.recv(1024) #1024只是一個最大的限制
23 recv_size+=len(recv_data) #有可能接收的不是1024個位元組,或許比1024多呢,
24 # 那麼接收的時候就接收不全,所以還要加上接收的那個長度
25 total_data+=recv_data #最終的結果
26 print('返回的訊息:%s'%total_data.decode('gbk'))
27 phone.close()
五、struct模組
1 #該模組可以把一個型別,如數字,轉成固定長度的bytes型別
2 import struct
3 res = struct.pack('i',12345)
4 print(res,len(res),type(res)) #長度是4
5
6 res2 = struct.pack('i',12345111)
7 print(res,len(res),type(res2)) #長度也是4
8
9 unpack_res =struct.unpack('i',res2)
10 print(unpack_res) #(12345111,)
11 print(unpack_res[0]) #12345111