本章主要講述Python中實現socket通訊,由於socket通訊的服務端比較複雜,並且客戶端很是簡單,因此客戶端基本上都是用sockct模塊實現,而服務端用有不少模塊可使用,乾貨在後面,一塊兒來看看吧。算法
Socket概念shell
Socket是應用層與TCP/IP協議族通訊的中間軟件抽象層,它是一組接口。在設計模式中,Socket其實就是一個門面模式,它把複雜的TCP/IP協議族隱藏在Socket接口後面,對用戶來講,一組簡單的接口就是所有,讓Socket去組織數據,以符合指定的協議。json
基於TCP協議的socketwindows
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端socket
import sockettcp
sk = socket.socket() # 建立客戶套接字sk.connect(('127.0.0.1',8898)) # 嘗試鏈接服務器sk.send(b'hello!')
ret = sk.recv(1024) # 對話(發送/接收)print(ret)
sk.close() # 關閉客戶套接字
問題:有的同窗在重啓服務端時可能會遇到報錯
#加入一條socket配置,重用ip和端口import socketfrom socket import SOL_SOCKET,SO_REUSEADDR
sk = socket.socket()
sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加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() #關閉服務器套接字(可選)
tcp是基於連接的,必須先啓動服務端,而後再啓動客戶端去連接服務端
![img](../配圖/Mon Oct 21 2019 13:29:15 GMT+0800 (CST).png)
基於UDP協議的socket
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)
udp是無連接的,啓動服務以後能夠直接接受消息,不須要提早創建連接
黏包現象
res=subprocess.Popen(cmd.decode('utf-8'),
shell=True,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE)
輸出的結果的編碼是以當前所在的系統爲準的,若是是windows,那麼res.stdout.read()讀出的就是GBK編碼的,在接收端須要用GBK解碼,且只能從管道里讀一次結果
同時執行多條命令以後,獲得的結果極可能只有一部分,在執行其餘命令的時候又接收到以前執行的另一部分結果,這種顯現就是黏包
基於tcp協議實現的黏包
Server端
#_coding:utf-8_from socket import *import subprocess
ip_port=('127.0.0.1',8888)
BUFSIZE=1024tcp_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:
conn,addr=tcp_socket_server.accept() print('客戶端',addr) while True: cmd=conn.recv(BUFSIZE) if len(cmd) == 0:break res=subprocess.Popen(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)
Client端
#_coding:utf-8_import socket
BUFSIZE=1024ip_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面向流的通訊特色和Nagle算法
TCP(transport control protocol,傳輸控制協議)是面向鏈接的,面向流的,提供高可靠性服務。
收發兩端(客戶端和服務器端)都要有一一成對的socket,所以,發送端爲了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle算法),將屢次間隔較小且數據量小的數據,合併成一個大的數據塊,而後進行封包。
這樣,接收端,就難於分辨出來了,必須提供科學的拆包機制。 即面向流的通訊是無消息保護邊界的。
對於空消息:tcp是基於數據流的,因而收發的消息不能爲空,這就須要在客戶端和服務端都添加空消息的處理機制,防止程序卡住,而udp是基於數據報的,即使是你輸入的是空內容(直接回車),也能夠被髮送,udp協議會幫你封裝上消息頭髮送過去。
可靠黏包的tcp協議:tcp的協議數據不會丟,沒有收完包,下次接收,會繼續上次繼續接收,己端老是在收到ack時纔會清除緩衝區內容。數據是可靠的,可是會粘包。
UDP不會發生黏包
UDP(user datagram protocol,用戶數據報協議)是無鏈接的,面向消息的,提供高效率服務。
不會使用塊的合併優化算法,, 因爲UDP支持的是一對多的模式,因此接收端的skbuff(套接字緩衝區)採用了鏈式結構來記錄每個到達的UDP包,在每一個UDP包中就有了消息頭(消息來源地址,端口等信息),這樣,對於接收端來講,就容易進行區分處理了。 即面向消息的通訊是有消息保護邊界的。
對於空消息:tcp是基於數據流的,因而收發的消息不能爲空,這就須要在客戶端和服務端都添加空消息的處理機制,防止程序卡住,而udp是基於數據報的,即使是你輸入的是空內容(直接回車),也能夠被髮送,udp協議會幫你封裝上消息頭髮送過去。
不可靠不黏包的udp協議:udp的recvfrom是阻塞的,一個recvfrom(x)必須對惟一一個sendinto(y),收完了x個字節的數據就算完成,如果y;x數據就丟失,這意味着udp根本不會粘包,可是會丟數據,不可靠。
會發生黏包的兩種狀況
狀況一 發送方的緩存機制
發送端須要等緩衝區滿才發送出去,形成粘包(發送數據時間間隔很短,數據了很小,會合到一塊兒,產生粘包)
狀況二 接收方的緩存機制
接收方不及時接收緩衝區的包,形成多個包接收(客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收的時候仍是從緩衝區拿上次遺留的數據,產生粘包)
總結
黏包現象只發生在tcp協議中:
1.從表面上看,黏包問題主要是由於發送方和接收方的緩存機制、tcp協議面向流通訊的特色。
2.實際上,主要仍是由於接收方不知道消息之間的界限,不知道一次性提取多少字節的數據所形成的
黏包解決方案
解決方案一
問題的根源在於,接收端不知道發送端將要傳送的字節流的長度,因此解決粘包的方法就是圍繞,如何讓發送端在發送數據前,把本身將要發送的字節流總大小讓接收端知曉,而後接收端來一個死循環接收完全部數據。
存在的問題:
程序的運行速度遠快於網絡傳輸速度,因此在發送一段字節前,先用send去發送該字節流長度,這種方式會放大網絡延遲帶來的性能損耗
解決方案進階
剛剛的方法,問題在於咱們咱們在發送
咱們能夠藉助一個模塊,這個模塊能夠把要發送的數據長度轉換成固定長度的字節。這樣客戶端每次接收消息以前只要先接受這個固定長度字節的內容看一看接下來要接收的信息大小,那麼最終接受的數據只要達到這個值就中止,就能恰好很少很多的接收完整的數據了。
struct模塊
該模塊能夠把一個類型,如數字,轉成固定長度的bytes
import struct
obj = struct.pack('i',123456)
print(len(obj)) # 4obj = struct.pack('i',898898789)
print(len(obj)) # 4# 不管數字多大,打包後長度恆爲4
使用struct解決黏包
藉助struct模塊,咱們知道長度數字能夠被轉換成一個標準大小的4字節數字。所以能夠利用這個特色來預先發送數據長度。
shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) stdout=obj.stdout.read() stderr=obj.stderr.read() # 1. 先製做固定長度的報頭 header=struct.pack('i',len(stdout) + len(stderr)) # 2. 再發送報頭 conn.send(header) # 3. 最後發送真實的數據 conn.send(stdout) conn.send(stderr)# client端 #1. 先收報頭,從報頭裏解出數據的長度 header=client.recv(4) total_size=struct.unpack('i',header)[0] #2. 接收真正的數據 cmd_res=b'' recv_size=0 while recv_size < total_size: data=client.recv(1024) recv_size+=len(data) cmd_res+=data print(cmd_res.decode('gbk'))
咱們還能夠把報頭作成字典,字典裏包含將要發送的真實數據的詳細信息,而後json序列化,而後用struck將序列化後的數據長度打包成4個字節(4個本身足夠用了)
'filename': 'a.txt', 'md5': 'asdfasdf123123x1', 'total_size': len(stdout) + len(stderr) } header_json = json.dumps(header_dic) header_bytes = header_json.encode('utf-8') # 2. 先發送4個bytes(包含報頭的長度) conn.send(struct.pack('i', len(header_bytes))) # 3 再發送報頭 conn.send(header_bytes) # 4. 最後發送真實的數據 conn.send(stdout) conn.send(stderr)# client端 #1. 先收4bytes,解出報頭的長度 header_size=struct.unpack('i',client.recv(4))[0] #2. 再接收報頭,拿到header_dic header_bytes=client.recv(header_size) header_json=header_bytes.decode('utf-8') header_dic=json.loads(header_json) print(header_dic) total_size=header_dic['total_size'] #3. 接收真正的數據 cmd_res=b'' recv_size=0 while recv_size < total_size: data=client.recv(1024) recv_size+=len(data) cmd_res+=data print(cmd_res.decode('gbk'))
總結:先發字典報頭,再發字典數據,最後發真實數據
SocketServer模塊介紹
try: data = self.request.recv(1024) # 對於tcp,self.request至關於conn對象 if len(data) == 0:break print(data) self.request.send(data.upper()) except ConnectionResetError: breakif __name__ == '__main__': server = socketserver.ThreadingTCPServer(('127.0.0.1',8081),MyTcpServer) server.serve_forever()# UDP socketserver使用import socketserverclassMyUdpServer(socketserver.BaseRequestHandler): defhandle(self): while True: data, sock = self.request print(data) sock.sendto(data.upper(), self.client_address)if __name__ == '__main__': server = socketserver.ThreadingUDPServer(('127.0.0.1', 8080), MyUdpServer) server.serve_forever()