socket編程(二)
TCP下粘包問題
兩種情況下會發生粘包。
1、發送端需要等緩沖區滿才發送出去,造成粘包(發送數據時間間隔很短,數據了很小,會合到一起,產生粘包)
發送方:AB #其實放在緩存裏沒發送
發送方:B #其實放在緩存裏沒發送
發送方:CD #緩存滿了,發一波
接收方:ABBCD #及時從緩存裏接收信息,我擦,發這是啥答案?
兩同學傳答案因粘包發生誤會,後果嚴重
2、接收方不及時接收緩沖區的包,造成多個包接收(客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收的時候還是從緩沖區拿上次遺留的數據,產生粘包)
發送方:211 #其實放在緩存裏沒發送
發送方:12 #其實放在緩存裏沒發送
發送方:985 #緩存滿了,發一波
接收方:21112985 #沒有及時從緩存裏接收信息,我擦第一題結果這麽大?
兩同學傳答案因粘包發生誤會,後果嚴重
拆包的發生情況
當發送端緩沖區的長度大於網卡的MTU時,tcp會將這次發送的數據拆成幾個數據包發送出去。
為何tcp是可靠傳輸,udp是不可靠傳輸
tcp在數據傳輸時,發送端先把數據發送到自己的緩存中,然後協議控制將緩存中的數據發往對端,對端返回一個ack=1,發送端則清理緩存中的數據,對端返回ack=0,則重新發送數據,所以tcp是可靠的
而udp發送數據,對端是不會返回確認信息的,因此不可靠
send(字節流)和recv(1024)及sendall
recv裏指定的1024意思是從緩存裏一次拿出1024個字節的數據
send的字節流是先放入己端緩存,然後由協議控制將緩存內容發往對端,如果待發送的字節流大小大於緩存剩余空間,那麽數據丟失,用sendall就會循環調用send,數據不會丟失。
粘包問題主要還是因為接收方不知道消息之間的界限,不知道一次性提取多少字節的數據所造成的。
- TCP(transport control protocol,傳輸控制協議)是面向連接的,面向流的,提供高可靠性服務。收發兩端(客戶端和服務器端)都要有一一成對的socket,因此,發送端為了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle算法),將多次間隔較小且數據量小的數據,合並成一個大的數據塊,然後進行封包。這樣,接收端,就難於分辨出來了,必須提供科學的拆包機制。 即面向流的通信是無消息保護邊界的。
- UDP(user datagram protocol,用戶數據報協議)是無連接的,面向消息的,提供高效率服務。不會使用塊的合並優化算法,, 由於UDP支持的是一對多的模式,所以接收端的skbuff(套接字緩沖區)采用了鏈式結構來記錄每一個到達的UDP包,在每個UDP包中就有了消息頭(消息來源地址,端口等信息),這樣,對於接收端來說,就容易進行區分處理了。 即面向消息的通信是有消息保護邊界的。
- tcp是基於數據流的,於是收發的消息不能為空,這就需要在客戶端和服務端都添加空消息的處理機制,防止程序卡住,而udp是基於數據報的,即便是你輸入的是空內容(直接回車),那也不是空消息,udp協議會幫你封裝上消息頭。
udp的recvfrom是阻塞的,一個recvfrom(x)必須對唯一一個sendinto(y),收完了x個字節的數據就算完成,若是y>x數據就丟失,這意味著udp根本不會粘包,但是會丟數據,不可靠
tcp的協議數據不會丟,沒有收完包,下次接收,會繼續上次繼續接收,己端總是在收到ack時才會清除緩沖區內容。數據是可靠的,但是會粘包。
為了解決粘包問題我們可以考慮發送消息時同時發送關於消息的長度信息,接收方安長度信息提取消息
發送方:(3)211 #其實放在緩存裏沒發送
發送方:(2)12 #其實放在緩存裏沒發送
發送方:(3)985 #緩存滿了,發一波
接收方:(3)211(2)12(3)985 #沒有及時從緩存裏接收信息,但是收到了長度信息不用方,按長度信息讀取得答案:211 12 985
下面是一個解決粘包的實例
服務端
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3 """
4 基於TCP實現遠程執行命令,發送數據長度信息解決粘包問題,這是服務端
5 """
6 import socket,json,struct
7 import subprocess
8
9 ip_port=(‘服務端IP‘,9000)
10 back_log=5
11 buffer_size=1024
12 phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
13
14 #setsockopt解決重啟服務端服務端仍然存在四次揮手的time_wait狀態在占用地址
15 phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #加入一條socket配置,重用ip和端口,
16
17 phone.bind(ip_port)#綁定(主機,端口號)到套接字
18
19 phone.listen(back_log)#開始監聽
20
21 while True: #連接循環
22 conn,addr=phone.accept()
23 while True: #通信循環
24 cmd=conn.recv(buffer_size) #接收消息,recv裏指定的1024意思是從緩存裏一次拿出1024個字節的數據
25 if not cmd:break #cmd為空跳出循環
26 print(‘cmd:%s‘ %cmd)
27 res=subprocess.Popen(cmd.decode(‘utf-8‘),
28 shell=True,
29 stdout=subprocess.PIPE,
30 stderr=subprocess.PIPE
31 )#此函數將解碼後的cmd給shell去解釋,stdout輸出參數,stderr報錯參數
32 err=res.stderr.read() #讀出報錯
33 print(err)
34 if err:
35 back_msg=err
36 else:
37 back_msg=res.stdout.read()
38 #發送
39 headers={‘data_size‘:len(back_msg)} #包含數據長度信息的報頭
40 head_json=json.dumps(headers) #將報頭序列化用於傳輸
41 head_json_bytes=bytes(head_json,encoding=‘utf-8‘) #再字節化
42
43 #為了讓客戶端知道報頭的長度,用struck將報頭長度這個數字轉成固定長度:4個字節
44 conn.send(struct.pack(‘i‘,len(head_json_bytes))) #先發報頭長度,這4個字節裏只包含了一個數字,該數字是報頭的長度
45 conn.send(head_json_bytes) #再發報頭
46 conn.sendall(back_msg) #再發真實內容
47 #s.send() 發送TCP數據(send在待發送數據量大於己端緩存區剩余空間時,數據丟失,不會發完)
48 #s.sendall() 發送完整的TCP數據(本質就是循環調用send,sendall在待發送數據量大於己端緩存區剩余空間時,數據不丟失,循環調用send直到發完)
49 conn.close() #關閉套接字
客戶端
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3 """
4 基於TCP實現遠程執行命令,發送數據長度信息解決粘包問題,這是客戶端
5 """
6 import socket,json,struct
7
8 ip_port=(‘服務端IP‘,9000)
9 back_log=5
10 buffer_size=1024
11 client=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
12
13 #s.connect() 主動初始化TCP服務器連接
14 #s.connect_ex() connect()函數的擴展版本,出錯時返回出錯碼,而不是拋出異常
15 client.connect_ex(ip_port)
16
17 while True:
18 cmd=input(‘>>: ‘)
19 if not cmd:continue #防止輸入空值
20 if cmd == ‘quit‘:break
21 client.send(bytes(cmd,encoding=‘utf-8‘)) #將命令編碼後轉為字節發送
22
23 head=client.recv(4) #接收長度為4個字節的報頭長度信息
24 head_json_len=struct.unpack(‘i‘,head)[0] #將報頭長度信息解包,得到報頭長度
25 head_json=json.loads(client.recv(head_json_len).decode(‘utf-8‘))
26 #利用報頭長度取出報頭並解碼、反序列化,得到報頭
27 data_len=head_json[‘data_size‘] #從報頭裏取出數據長度
28
29 recv_size=0
30 recv_data=b‘‘
31 while recv_size < data_len:
32 recv_data += client.recv(1024) #一次跨1024字節收數據
33 recv_size += len(recv_data) #計算已得到數據長度
34
35 #print(recv_data.decode(‘utf-8‘))
36 print(recv_data.decode(‘gbk‘)) #windows默認gbk編碼
以上實現了客戶端與服務器的連接並解決了粘包問題,但是不能實現並發,服務器端只能一對一服務,不能一對多服務
為了實現並發我們引入socketserver,以下代碼只針對實現並發
並發服務端
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3
4 import socketserver
5
6 class MyServer(socketserver.BaseRequestHandler):
7 def handle(self):
8 print(self.request) #conn
9 print(self.client_address) #addr
10
11 while True:
12 try:
13 #收消息
14 data=self.request.recv(1024)
15 print("收到消息",data)
16 #發消息
17 self.request.sendall(data.upper())
18 except Exception as e:
19 print(e)
20 break
21 if __name__ == ‘__main__‘:
22 s=socketserver.ThreadingTCPServer((‘192.168.1.106‘,9000),MyServer)
23 s.serve_forever()
客戶端
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3 import socket,json,struct
4
5 ip_port=(‘192.168.1.106‘,9000)
6 back_log=5
7 buffer_size=1024
8 client=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
9
10 #s.connect() 主動初始化TCP服務器連接
11 #s.connect_ex() connect()函數的擴展版本,出錯時返回出錯碼,而不是拋出異常
12 client.connect_ex(ip_port)
13
14 while True:
15 cmd=input(‘>>: ‘)
16 if not cmd:continue #防止輸入空值
17 if cmd == ‘quit‘:break
18 client.send(bytes(cmd,encoding=‘utf-8‘)) #將命令編碼後轉為字節發送
19
20 data=client.recv(buffer_size)
21 print(‘收到服務端發來的消息‘,data.decode(‘utf-8‘))
22
23 client.close()
此時可用多個客戶端與服務器交互
socket編程(二)