socket編程python
中篇對socket的搭建服務端與客戶端的鏈接進行了代碼實現化,以及socket內置方法的認識及運用。算法
粘包現象的出現shell
在中篇中,對於tcp和udp製做了一個遠程執行命令的程序(1:執行錯誤命令 2:執行ls 3:執行ifconfig)編程
在tcp下:在運行時會發生粘包緩存
在udp下:在運行時永遠不會發生粘包服務器
什麼是粘包網絡
在上篇中,對於socket的收發消息的原理進行了一些闡釋數據結構
所謂粘包,主要仍是由於接受方不知道消息之間的界限,不知道一次提取多少字節的數據所形成的。併發
此外,發送方引發的粘包是由TCP協議自己形成的,TCP爲提升傳輸效率,發送方每每要收集到足夠多的數據後才發送一個TCP段。若連續幾回須要send的數據都不多,一般TCP會根據優化算法把這些數據合成一個TCP段後一次發送出去,這樣接收方就收到了粘包數據。異步
- TCP(transport control protocol,傳輸控制協議)是面向鏈接的,面向流的,提供高可靠性服務。收發兩端(客戶端和服務器端)都要有一一成對的socket,所以,發送端爲了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle算法),將屢次間隔較小且數據量小的數據,合併成一個大的數據塊,而後進行封包。這樣,接收端,就難於分辨出來了,必須提供科學的拆包機制。 即面向流的通訊是無消息保護邊界的。
- UDP(user datagram protocol,用戶數據報協議)是無鏈接的,面向消息的,提供高效率服務。不會使用塊的合併優化算法,, 因爲UDP支持的是一對多的模式,因此接收端的skbuff(套接字緩衝區)採用了鏈式結構來記錄每個到達的UDP包,在每一個UDP包中就有了消息頭(消息來源地址,端口等信息),這樣,對於接收端來講,就容易進行區分處理了。 即面向消息的通訊是有消息保護邊界的。
- tcp是基於數據流的,因而收發的消息不能爲空,這就須要在客戶端和服務端都添加空消息的處理機制,防止程序卡住,而udp是基於數據報的,即使是你輸入的是空內容(直接回車),那也不是空消息,udp協議會幫你封裝上消息頭,實驗略
粘包有倆種現象
1、發送數據時間間隔很短,數據了很小,會合到一塊兒,產生粘包
from socket import * ip_port=('127.0.0.1',8082) back_log=5 buffer_size=1024 tcp_server=socket(AF_INET,SOCK_STREAM) tcp_server.bind(ip_port) tcp_server.listen(back_log) conn,addr=tcp_server.accept() data1=conn.recv(buffer_size) #指定buffer_size ,獲得的結果就是經過Nagle算法,隨機接收次數。 print('第1次數據',data1) data2=conn.recv(buffer_size) print('第2次數據',data2) data3=conn.recv(buffer_size) print('第3次數據',data3)
from socket import * import time ip_port=('127.0.0.1',8082) back_log=5 buffer_size=1024 tcp_client=socket(AF_INET,SOCK_STREAM) tcp_client.connect(ip_port) tcp_client.send('hello'.encode('utf-8')) tcp_client.send('world'.encode('utf-8')) tcp_client.send('egon'.encode('utf-8'))
第1次數據 b'helloworldegon' 第2次數據 b'' 第3次數據 b''
2、客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收的時候仍是從緩衝區拿上次遺留的數據,產生粘包
from socket import * ip_port=('127.0.0.1',8080) back_log=5 buffer_size=1024 tcp_server=socket(AF_INET,SOCK_STREAM) tcp_server.bind(ip_port) tcp_server.listen(back_log) conn,addr=tcp_server.accept() data1=conn.recv(1) print('第1次數據',data1) # data2=conn.recv(5) # print('第2次數據',data2) # # data3=conn.recv(1) # print('第3次數據',data3)
from socket import * import time ip_port=('127.0.0.1',8080) back_log=5 buffer_size=1024 #接收的數據只有1024 tcp_client=socket(AF_INET,SOCK_STREAM) tcp_client.connect(ip_port) tcp_client.send('helloworldegon'.encode('utf-8')) time.sleep(1000)
第1次數據 b'h' 第2次數據 b'ellow' #發送的數據過大,接收的數據設置的較小,就會出現致使粘包 第3次數據 b'o'
補充知識:
一、tcp是可靠傳輸
tcp在數據傳輸時,發送端先把數據發送到本身的緩存中,而後協議控制將緩存中的數據發往對端,對端返回一個ack=1,發送端則清理緩存中的數據,對端返回ack=0,則從新發送數據,因此tcp是可靠的。
二、udp是不可靠傳輸
udp發送數據,對端是不會返回確認信息的,所以不可靠。
基於中篇的實現遠程命令的例子,做出解決粘包的方法
#low版解決粘包版本服務端 from socket import * import subprocess ip_port=('127.0.0.1',8080) back_log=5 buffer_size=1024 tcp_server=socket(AF_INET,SOCK_STREAM) tcp_server.bind(ip_port) tcp_server.listen(back_log) while True: conn,addr=tcp_server.accept() print('新的客戶端連接',addr) while True: #收消息 try: cmd=conn.recv(buffer_size) if not cmd:break print('收到客戶端的命令',cmd) #執行命令,獲得命令的運行結果cmd_res res=subprocess.Popen(cmd.decode('utf-8'),shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE, stdin=subprocess.PIPE) err=res.stderr.read() if err: cmd_res=err else: cmd_res=res.stdout.read() #發送消息 if not cmd_res: cmd_res='執行成功'.encode('gbk') length=len(cmd_res) #計算長度 conn.send(str(length).encode('utf-8')) #把長度發給客戶端 client_ready=conn.recv(buffer_size) #卡着一個recv if client_ready == b'ready': #若是收到客戶端的ready消息,就說明準備好了。 conn.send(cmd_res) #就能夠send給客戶端發送消息啦! except Exception as e: print(e) break
#low版解決粘包版客戶端 from socket import * ip_port=('127.0.0.1',8080) back_log=5 buffer_size=1024 tcp_client=socket(AF_INET,SOCK_STREAM) tcp_client.connect(ip_port) while True: cmd=input('>>: ').strip() if not cmd:continue if cmd == 'quit':break tcp_client.send(cmd.encode('utf-8')) #解決粘包 length=tcp_client.recv(buffer_size) #接收發送過來的長度(1024*8=8192,2**8192=能夠接收的長度) tcp_client.send(b'ready') #客戶端再send給服務端,告訴服務端我準備好啦! length=int(length.decode('utf-8')) #先解碼,轉成字符串的長度 #解決思路:就是提早發一個頭過去,告訴客戶端須要接收的長度(分兩步:一、發送發度 二、再次發送數據) recv_size=0 #接收的尺寸 recv_msg=b'' #最後要拼接起來 while recv_size < length: #要收多大?,要先判斷接收的尺寸<length recv_msg += tcp_client.recv(buffer_size) #接收到的數據,拼接buffer_size, recv_size=len(recv_msg) #1024 #衡量本身接收了多少數據,有沒有收完(統計recv_msg的長度) print('命令的執行結果是 ',recv_msg.decode('gbk')) tcp_client.close()
通過以上代碼處理,再次進行 ipconfig dir這些命令則能夠恢復正常,不會出現粘包問題
總結:
(爲什麼low): 程序的運行速度遠快於網絡傳輸速度,因此在發送一段字節前,先用send去發送該字節流長度,這種方式會放大網絡延遲帶來的性能損耗。
接下來,有一種新的方式處理粘包的問題
import socket,time,subprocess,pickle,struct ip_port=('127.0.0.1',8080) s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) 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.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE) err=res.stderr.read() if err: ret=err else: ret=res.stdout.read() l=struct.pack('i',len(ret)) conn.sendall(l+ret) # conn.send(str(len(ret)).encode('utf-8')) 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',l)[0] print(type(x),x) # print(struct.unpack('I',l)) 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('gbk'))
在解決了TCP的粘包問題,那麼又該怎麼解決TCP的併發問題
SocketServer是基於socket寫成的一個更強大的模塊。
SocketServer簡化了網絡服務器的編寫。它有4個類:TCPServer,UDPServer,UnixStreamServer,UnixDatagramServer。這4個類是同步進行處理的,另外經過ForkingMixIn和ThreadingMixIn類來支持異步。
在python3中該模塊是socketserver
在python2中該模塊是Socketserver
服務器
服務器要使用處理程序,必須將其出入到服務器對象,定義了5個基本的服務器類型(就是「類」)。BaseServer,TCPServer,UnixStreamServer,UDPServer,UnixDatagramServer。注意:BaseServer不直接對外服務。
服務器:
要使用處理程序,必須將其傳入到服務器的對象,定義了四個基本的服務器類。
(1)TCPServer(address,handler) 支持使用IPv4的TCP協議的服務器,address是一個(host,port)元組。Handler是BaseRequestHandler或StreamRequestHandler類的子類的實例。
(2)UDPServer(address,handler) 支持使用IPv4的UDP協議的服務器,address和handler與TCPServer中相似。
(3)UnixStreamServer(address,handler) 使用UNIX域套接字實現面向數據流協議的服務器,繼承自TCPServer。
(4)UnixDatagramServer(address,handler) 使用UNIX域套接字實現數據報協議的服務器,繼承自UDPServer。
這四個類的實例都有如下方法。
一、s.socket 用於傳入請求的套接字對象。
二、s.sever_address 監聽服務器的地址。如元組("127.0.0.1",80)
三、s.RequestHandlerClass 傳遞給服務器構造函數並由用戶提供的請求處理程序類。
四、s.serve_forever() 處理無限的請求 #無限處理client鏈接請求
五、s.shutdown() 中止serve_forever()循環
SocketServer模塊中主要的有如下幾個類:
一、BaseServer 包含服務器的核心功能與混合類(mix-in)的鉤子功能。這個類主要用於派生,不要直接生成這個類的類對象,能夠考慮使用TCPServer和UDPServer類。
二、TCPServer 基本的網絡同步TCP服務器
三、UDPServer 基本的網絡同步UDP服務器
四、ForkingTCPServer 是ForkingMixIn與TCPServer的組合
五、ForkingUDPServer 是ForkingMixIn與UDPServer的組合
六、ThreadingUDPServer 是ThreadingMixIn和UDPserver的組合
七、ThreadingTCPServer 是ThreadingMixIn和TCPserver的組合
八、BaseRequestHandler 必須建立一個請求處理類,它是BaseRequestHandler的子類並重載其handle()方法。
九、StreamRequestHandler 實現TCP請求處理類的
十、DatagramRequestHandler 實現UDP請求處理類的
十一、ThreadingMixIn 實現了核心的線程化功能,用於與服務器類進行混合(mix-in),以提供一些異步特性。不要直接生成這個類的對象。
十二、ForkingMixIn 實現了核心的進程化功能,用於與服務器類進行混合(mix-in),以提供一些異步特性。不要直接生成這個類的對象。
建立服務端的步驟:
1:首先必須建立一個請求處理類
2:它是BaseRequestHandler的子類
3:該請求處理類是BaseRequestHandler的子類並從新寫其handle()方法
4:必需要有一個handle()方法,規則定義死的
實例化 請求處理類傳入服務器地址和請求處理程序類
最後實例化調用serve_forever() #無限處理client請求
記住一個原則:對tcp來講:self.request=conn
在這裏做一個簡單的小例子
# TCP下實現的併發 import socketserver class Myserver(socketserver.BaseRequestHandler): # 必需要繼承這個類 def handle(self): # 必需要有這個方法 print(self.request) # 至關於conn print(self.client_address) # 鏈接過來的客戶端地址 while True: try: data = self.request.recv(1024) if not data:break print("收到來自%s的消息是: %s" %(self.client_address,data.decode("utf-8"))) nr = input(">>>") self.request.sendall(nr.encode("utf-8")) except Exception: break if __name__ == '__main__': # ip_port = input("請輸入ip和端口") obj = socketserver.ThreadingTCPServer(("127.0.0.1",6060),Myserver) obj.serve_forever()
import socket ip_port = ("127.0.0.1",6060) buffer_size = 1024 s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.connect(ip_port) while True: nr = input(">>>").strip() # if not nr:continue s.sendall(bytes(nr, encoding="utf-8")) res = s.recv(buffer_size) print("來自遠方的消息",str(res, encoding="utf-8"))
源碼剖析。。。未完待續……