1. 程式人生 > >29_網路程式設計-黏包

29_網路程式設計-黏包

一、黏包成因 TCP(transport control protocol,傳輸控制協議)是面向連線的,面向流的,提供高可靠性服務。 收發兩端(客戶端和伺服器端)都要有一一成對的socket,因此,傳送端為了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle演算法),將多次間隔較小且資料量小的資料,合併成一個大的資料塊,然後進行封包。 這樣,接收端,就難於分辨出來了,必須提供科學的拆包機制。 即面向流的通訊是無訊息保護邊界的。 對於空訊息:tcp是基於資料流的,於是收發的訊息不能為空,這就需要在客戶端和服務端都新增空訊息的處理機制,防止程式卡住,而udp是基於資料報的,即便是你輸入的是空內容(直接回車),也可以被髮送,udp協議會幫你封裝上訊息頭髮送過去。
可靠黏包的tcp協議:tcp的協議資料不會丟,沒有收完包,下次接收,會繼續上次繼續接收,己端總是在收到ack時才會清除緩衝區內容。資料是可靠的,但是會粘包。 基於tcp的套接字客戶端往服務端上傳檔案,傳送時檔案內容是按照一段一段的位元組流傳送的,在接收方看了,根本不知道該檔案的位元組流從何處開始,在何處結束 此外,傳送方引起的粘包是由TCP協議本身造成的,TCP為提高傳輸效率,傳送方往往要收集到足夠多的資料後才傳送一個TCP段。若連續幾次需要send的資料都很少,通常TCP會根據優化演算法把這些資料合成一個TCP段後一次傳送出去,這樣接收方就收到了粘包資料。 總結:黏包直接原因是接收方不知道訊息的界限 不知道一次性提取多少資料;根本原因是tcp自身為提供效率,TCP會根據 優化演算法 把這些資料合成一個TCP段後一次傳送出去,這樣接收方就收到了粘包資料
    只有TCP會黏包,UDP永遠不會黏包(udp基於資料報的,但會出現接收資料超出自己設定的範圍)     粘包不一定會發生,如果發生了:1.可能是在客戶端已經粘了    2.客戶端沒有粘,可能是在服務端粘了 socket資料傳輸過程中的使用者態與核心態說明 你的程式實際上無權直接操作網絡卡的,你操作網絡卡都是通過作業系統給使用者程式暴露出來的介面,那每次你的程式要給遠端發資料時,其實是先把資料從使用者態copy到核心態,這樣的操作是耗資源和時間的,頻繁的在核心態和使用者態之前交換資料勢必會導致傳送效率降低, 因此socket 為提高傳輸效率,傳送方往往要收集到足夠多的資料後才傳送一次資料給對方。若連續幾次需要send的資料都很少,通常TCP socket 會根據優化演算法把這些資料合成一個TCP段後一次傳送出去,這樣接收方就收到了粘包資料。
傳送端可以是一K一K地傳送資料,而接收端的應用程式可以兩K兩K地提走資料,當然也有可能一次提走3K或6K資料,或者一次只提走幾個位元組的資料。 也就是說,應用程式所看到的資料是一個整體,或說是一個流(stream),一條訊息有多少位元組對應用程式是不可見的,因此TCP協議是面向流的協議,這也是容易出現粘包問題的原因。 而UDP是面向訊息的協議,每個UDP段都是一條訊息,應用程式必須以訊息為單位提取資料,不能一次提取任意位元組的資料,這一點和TCP是很不同的。 怎樣定義訊息呢?可以認為對方一次性write/send的資料為一個訊息,需要明白的是當對方send一條資訊的時候,無論底層怎樣分段分片,TCP協議層會把構成整條訊息的資料段排序完成後才呈現在核心緩衝區。     UDP永遠不會黏包 UDP(user datagram protocol,使用者資料報協議)是無連線的,面向訊息的,提供高效率服務。 不會使用塊的合併優化演算法,, 由於UDP支援的是一對多的模式,所以接收端的skbuff(套接字緩衝區)採用了鏈式結構來記錄每一個到達的UDP包,在每個UDP包中就有了訊息頭(訊息來源地址,埠等資訊),這樣,對於接收端來說,就容易進行區分處理了。 即面向訊息的通訊是有訊息保護邊界的。 對於空訊息:tcp是基於資料流的,於是收發的訊息不能為空,這就需要在客戶端和服務端都新增空訊息的處理機制,防止程式卡住,而udp是基於資料報的,即便是你輸入的是空內容(直接回車),也可以被髮送,udp協議會幫你封裝上訊息頭髮送過去。 不可靠不黏包的udp協議:udp的recvfrom是阻塞的,一個recvfrom(x)必須對唯一一個sendinto(y),收完了x個位元組的資料就算完成,若是y;x資料就丟失,這意味著udp根本不會粘包,但是會丟資料,不可靠。 用UDP協議傳送時,用sendto函式最大能傳送資料的長度為:65535- IP頭(20) – UDP頭(8)=65507位元組。用sendto函式傳送資料時,如果傳送資料長度大於該值,則函式會返回錯誤。(丟棄這個包,不進行傳送) 用TCP協議傳送時,由於TCP是資料流協議,因此不存在包大小的限制(暫不考慮緩衝區的大小),這是指在用send函式時,資料長度引數不受限制。而實際上,所指定的這段資料並不一定會一次性發送出去,如果這段資料比較長,會被分段傳送,如果比較短,可能會等待和下一次資料一起傳送。 2、發生黏包的兩種情況     (1) 傳送端需要等緩衝區滿才傳送出去,造成粘包(傳送資料時間間隔很短,資料了很小,會合到一起,產生粘包)     服務端
 1 #_*_coding:utf-8_*_
 2 from socket import *
 3 ip_port=('127.0.0.1',8080)
 4 
 5 tcp_socket_server=socket(AF_INET,SOCK_STREAM)
 6 tcp_socket_server.bind(ip_port)
 7 tcp_socket_server.listen(5)
 8 
 9 conn,addr=tcp_socket_server.accept()
10 
11 data1=conn.recv(10)
12 data2=conn.recv(10)
13 
14 print('----->',data1.decode('utf-8'))
15 print('----->',data2.decode('utf-8'))
16 
17 conn.close()
    客戶端
1 import socket
2 BUFSIZE=1024
3 ip_port=('127.0.0.1',8080)
4 
5 s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
6 res=s.connect_ex(ip_port)
7 
8 s.send('hello'.encode('utf-8'))
9 s.send('egg'.encode('utf-8'))
    (2) 接收方不及時接收緩衝區的包,造成多個包接收(客戶端傳送了一段資料,服務端只收了一小部分,服務端下次再收的時候還是從緩衝區拿上次遺留的資料,產生粘包)     服務端
 1 #_*_coding:utf-8_*_
 2 from socket import *
 3 ip_port=('127.0.0.1',8080)
 4 
 5 tcp_socket_server=socket(AF_INET,SOCK_STREAM)
 6 tcp_socket_server.bind(ip_port)
 7 tcp_socket_server.listen(5)
 8 
 9 
10 conn,addr=tcp_socket_server.accept()
11 
12 
13 data1=conn.recv(2) #一次沒有收完整
14 data2=conn.recv(10)#下次收的時候,會先取舊的資料,然後取新的
15 
16 print('----->',data1.decode('utf-8'))
17 print('----->',data2.decode('utf-8'))
18 
19 conn.close()
    客戶端
1 #_*_coding:utf-8_*_
2 import socket
3 BUFSIZE=1024
4 ip_port=('127.0.0.1',8080)
5 
6 s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
7 res=s.connect_ex(ip_port)
8 
9 s.send('hello egg'.encode('utf-8'))
總結:     黏包現象只發生在tcp協議中:     1.從表面上看,黏包問題主要是因為傳送方和接收方的快取機制、tcp協議面向流通訊的特點。     2.實際上,主要還是因為接收方不知道訊息之間的界限,不知道一次性提取多少位元組的資料所造成的 二、黏包解決方案一     問題的根源在於,接收端不知道傳送端將要傳送的位元組流的長度,所以解決粘包的方法就是圍繞,如何讓傳送端在傳送資料前,把自己將要傳送的位元組流總大小讓接收端知曉,然後接收端來一個死迴圈接收完所有資料。     存在的問題:程式的執行速度遠快於網路傳輸速度,所以在傳送一段位元組前,先用send去傳送該位元組流長度,這種方式會放大網路延遲帶來的效能損耗     服務端
 1 #_*_coding:utf-8_*_
 2 import socket,subprocess
 3 ip_port=('127.0.0.1',8080)
 4 s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
 5 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
 6 
 7 s.bind(ip_port)
 8 s.listen(5)
 9 
10 while True:
11     conn,addr=s.accept()
12     print('客戶端',addr)
13     while True:
14         msg=conn.recv(1024)
15         if not msg:break
16         res=subprocess.Popen(msg.decode('utf-8'),shell=True,\
17                             stdin=subprocess.PIPE,\
18                          stderr=subprocess.PIPE,\
19                          stdout=subprocess.PIPE)
20         err=res.stderr.read()
21         if err:
22             ret=err
23         else:
24             ret=res.stdout.read()
25         data_length=len(ret)
26         conn.send(str(data_length).encode('utf-8'))
27         data=conn.recv(1024).decode('utf-8')
28         if data == 'recv_ready':
29             conn.sendall(ret)
30     conn.close()

    客戶端

 1 #_*_coding:utf-8_*_
 2 import socket,time
 3 s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
 4 res=s.connect_ex(('127.0.0.1',8080))
 5 
 6 while True:
 7     msg=input('>>: ').strip()
 8     if len(msg) == 0:continue
 9     if msg == 'quit':break
10 
11     s.send(msg.encode('utf-8'))
12     length=int(s.recv(1024).decode('utf-8'))
13     s.send('recv_ready'.encode('utf-8'))
14     send_size=0
15     recv_size=0
16     data=b''
17     while recv_size < length:
18         data+=s.recv(1024)
19         recv_size+=len(data)
20 
21     print(data.decode('utf-8'))