【目錄】
一、【回顧】TCP/UDP
二、 粘包現象
三、什么是粘包
四、解決粘包的low比處理方法
五、峰哥解決粘包的方法
六、認證客戶端的鏈接合法性
七、socketserver實作并發
二、 粘包現象
注意:
1、res=subprocess.Popen(cmd.decode('utf-8'),shell=True,stderr=subprocess.PIPE,stdout=subprocess.PIPE)
的結果的編碼是以當前所在的系統為準的;如果是windows,那么res.stdout.read()讀出的就是GBK編碼的,
在接收端需要用GBK解碼,且只能從管道里讀一次結果
2、命令ls -l ; lllllll ; pwd 的結果,是既有正確stdout結果,又有錯誤stderr結果
1、基于tcp先制作一個遠程執行命令的程式(1:執行錯誤命令 2:執行ls 3:執行ifconfig)
基于tcp的socket,在運行時會發生粘包——
#_*_coding:utf-8_*_ from socket import * import subprocess ip_port=('127.0.0.1',8080) BUFSIZE=1024 tcp_socket_server=socket(AF_INET,SOCK_STREAM) 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=act_res.stderr.read() stdout=act_res.stdout.read() conn.send(stderr) conn.send(stdout)服務端
#_*_coding:utf-8_*_ import socket BUFSIZE=1024 ip_port=('127.0.0.1',8080) 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='')客戶端
2、基于udp制作一個遠程執行命令的程式(1:執行錯誤命令 2:執行ls 3:執行ifconfig)
基于udp的socket,在運行時永遠不會發生粘包——
#_*_coding:utf-8_*_ from socket import * import subprocess ip_port=('127.0.0.1',9003) bufsize=1024 udp_server=socket(AF_INET,SOCK_DGRAM) udp_server.bind(ip_port) while True: #收訊息 cmd,addr=udp_server.recvfrom(bufsize) print('用戶命令----->',cmd) #邏輯處理 res=subprocess.Popen(cmd.decode('utf-8'),shell=True,stderr=subprocess.PIPE,stdin=subprocess.PIPE,stdout=subprocess.PIPE) stderr=res.stderr.read() stdout=res.stdout.read() #發訊息 udp_server.sendto(stderr,addr) udp_server.sendto(stdout,addr) udp_server.close()服務端
from socket import * ip_port=('127.0.0.1',9003) bufsize=1024 udp_client=socket(AF_INET,SOCK_DGRAM) while True: msg=input('>>: ').strip() udp_client.sendto(msg.encode('utf-8'),ip_port) data,addr=udp_client.recvfrom(bufsize) print(data.decode('utf-8'),end='')客戶端
三、什么是粘包
須知:只有tcp有粘包現象,udp永遠不會粘包
1、 首先需要掌握一個socket收發訊息的原理

(1)tcp協議--收發訊息的原理
發送端可以是一k一k地發送資料,而接收端的應用程式可以兩k兩k地提走資料,當然也有可能一次提走3k或6k資料,或者一次只提走幾個位元組的資料,也就是說,應用程式所看到的資料是一個整體,或說是一個流(stream),
一條訊息有多少位元組對應用程式是不可見的,因此tcp協議是面向流的協議,這也是容易出現粘包問題的原因,
(2)udp協議--收發訊息的原理
udp協議是面向訊息的協議,每個udp段都是一條訊息,應用程式必須以訊息為單位提取資料,不能一次提取任意位元組的資料,這一點和tcp是很不同的,
(3)怎樣定義訊息呢?
可以認為對方一次性write/send的資料為一個訊息,需要明白的是當對方send一條資訊的時候,無論底層怎樣分段分片,tcp協議層會把構成整條訊息的資料段排序完成后才呈現在內核緩沖區,
(4)# 例如
基于tcp的套接字客戶端往服務端上傳檔案,發送時檔案內容是按照一段一段的位元組流發送的,在接收方看了,根本不知道該檔案的位元組流從何處開始,在何處結束,
# 所謂粘包問題主要還是因為接收方不知道訊息之間的界限,不知道一次性提取多少位元組的資料所造成的,
2、從協議本身看——發送方引起的粘包是由tcp協議本身造成的
發送方引起的粘包是由TCP協議本身造成的——TCP為提高傳輸效率,發送方往往要收集到足夠多的資料后才發送一個TCP段,
若連續幾次需要send的資料都很少,通常TCP會根據優化演算法把這些資料合成一個TCP段后,一次發送出去,這樣接收方就收到了粘包資料,
TCP(transport control protocol,傳輸控制協議)是面向連接的,面向流的,提供高可靠性服務,
接收端和發送端(客戶端和服務器端)都要有一 一成對的socket,因此,發送端為了將多個發往 接收端的包 更有效地發到對方,使用了優化方法(Nagle演算法),將多次間隔較小且資料量小的資料,合并成一個大的資料塊,然后進行封包,這樣,接收端 就難于分辨出來了,必須提供科學的拆包機制, 即面向流的通信是無訊息保護邊界的,
UDP(user datagram protocol,用戶資料報協議)是無連接的,面向訊息的,提供高效率服務,
不會使用 塊的合并優化演算法,, 由于UDP支持的是一對多的模式,所以接收端的 skbuff(套接字緩沖區)采用了 鏈式結構 來記錄每一個到達的UDP包,在每個UDP包中就有了訊息頭(訊息來源地址,埠等資訊),這樣,對于接收端來說,就容易進行區分處理了, 即面向訊息的通信是有訊息保護邊界的,
TCP是基于資料流的,于是收發的訊息不能為空,這就需要在 客戶端和服務端 都添加 空訊息的處理機制,防止程式卡住 UDP是基于資料報的,即便是你輸入的是空內容(直接回車),那也不是空訊息,UDP協議會幫你封裝上訊息頭
tcp的協議資料不會丟,沒有收完包,下次接收,會繼續上次繼續接收,己端總是在收到ack時才會清除緩沖區內容, 資料是可靠的,但是會粘包,
udp的recvfrom是阻塞的,一個recvfrom(x)必須對唯一 一個sendinto(y),收完了x個位元組的資料就算完成,若是y>x資料就丟失, 這意味著udp根本不會粘包,但是會丟資料,不可靠,
3、兩種情況下會發生粘包,
(1)發送端需要等緩沖區滿才發送出去,造成粘包(發送資料時間間隔很短,資料了很小,會合到一起,產生粘包)
#_*_coding:utf-8_*_ from socket import * ip_port=('127.0.0.1',8080) tcp_socket_server=socket(AF_INET,SOCK_STREAM) tcp_socket_server.bind(ip_port) tcp_socket_server.listen(5) conn,addr=tcp_socket_server.accept() data1=conn.recv(10) data2=conn.recv(10) print('----->',data1.decode('utf-8')) print('----->',data2.decode('utf-8')) conn.close()服務端
#_*_coding:utf-8_*_ import socket BUFSIZE=1024 ip_port=('127.0.0.1',8080) s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) res=s.connect_ex(ip_port) s.send('hello'.encode('utf-8')) s.send('feng'.encode('utf-8'))客戶端
(2)接收方不及時接識訓沖區的包,造成多個包接收(客戶端發送了一段資料,服務端只收了一小部分,服務端下次再收的時候還是從緩沖區拿上次遺留的資料,產生粘包)
#_*_coding:utf-8_*_ from socket import * ip_port=('127.0.0.1',8080) tcp_socket_server=socket(AF_INET,SOCK_STREAM) tcp_socket_server.bind(ip_port) tcp_socket_server.listen(5) conn,addr=tcp_socket_server.accept() data1=conn.recv(2) #一次沒有收完整 data2=conn.recv(10)#下次收的時候,會先取舊的資料,然后取新的 print('----->',data1.decode('utf-8')) print('----->',data2.decode('utf-8')) conn.close()服務端
#_*_coding:utf-8_*_ import socket BUFSIZE=1024 ip_port=('127.0.0.1',8080) s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) res=s.connect_ex(ip_port) s.send('hello feng'.encode('utf-8'))客戶端
4、拆包的發生情況
當發送端緩沖區的長度大于網卡的MTU時,tcp會將這次發送的資料拆成幾個資料包發送出去,
補充問題一:為何tcp是可靠傳輸,udp是不可靠傳輸
基于tcp的資料傳輸,請參考另一篇文章 http://www.cnblogs.com/linhaifeng/articles/5937962.html,
tcp在資料傳輸時,發送端先把資料發送到自己的快取中,然后協議控制將快取中的資料發往對端,對端回傳一個ack=1,發送端則清理快取中的資料,對端回傳ack=0,則重新發送資料,所以tcp是可靠的
而udp發送資料,對端是不會回傳確認資訊的,因此不可靠
補充問題二:send(位元組流)和recv(1024)及sendall
recv里指定的1024意思是從快取里一次拿出1024個位元組的資料
send的位元組流是先放入己端快取,然后由協議控制將快取內容發往對端,如果待發送的位元組流大小大于快取剩余空間,那么資料丟失,用sendall就會回圈呼叫send,資料不會丟失
四、--low版本--解決粘包的方法
粘包問題的根源在于,接收端不知道發送端將要傳送的位元組流的長度
所以解決粘包的方法就是圍繞,如何讓發送端在發送資料前,把自己將要發送的位元組流總大小 讓接收端知曉,
然后 接收端 來一個 死回圈 接收完所有資料,
【為何low】:
程式的運行速度遠快于網路傳輸速度,所以在發送一段位元組前,先用send去發送該位元組流長度,
這種方式會放大網路延遲帶來的性能損耗——
#_*_coding:utf-8_*_ import socket,subprocess 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() data_length=len(ret) conn.send(str(data_length).encode('utf-8')) data=conn.recv(1024).decode('utf-8') if data =https://www.cnblogs.com/bigorangecc/p/= 'recv_ready': conn.sendall(ret) conn.close()服務端
#_*_coding:utf-8_*_ import socket,time 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')) length=int(s.recv(1024).decode('utf-8')) s.send('recv_ready'.encode('utf-8')) send_size=0 recv_size=0 data=b'' while recv_size < length: data+=s.recv(1024) recv_size+=len(data) print(data.decode('utf-8'))客戶端
五、--超級版--解決粘包的方法
【超級版解決方法】
為位元組流加上自定義固定長度報頭,報頭中包含位元組流長度,然后一次send到對端,
對端在接收時,先從快取中取出定長的報頭,然后再取真實資料
1、struct模塊
該模塊可以把一個型別,如數字,轉成固定長度的bytes
>>> struct.pack('i',1111111111111)
,,,,,,,,,
struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #這個是范圍

#_*_coding:utf-8_*_ #http://www.cnblogs.com/coser/archive/2011/12/17/2291160.html __author__ = 'Linhaifeng' import struct import binascii import ctypes values1 = (1, 'abc'.encode('utf-8'), 2.7) values2 = ('defg'.encode('utf-8'),101) s1 = struct.Struct('I3sf') s2 = struct.Struct('4sI') print(s1.size,s2.size) prebuffer=ctypes.create_string_buffer(s1.size+s2.size) print('Before : ',binascii.hexlify(prebuffer)) # t=binascii.hexlify('asdfaf'.encode('utf-8')) # print(t) s1.pack_into(prebuffer,0,*values1) s2.pack_into(prebuffer,s1.size,*values2) print('After pack',binascii.hexlify(prebuffer)) print(s1.unpack_from(prebuffer,0)) print(s2.unpack_from(prebuffer,s1.size)) s3=struct.Struct('ii') s3.pack_into(prebuffer,0,123,123) print('After pack',binascii.hexlify(prebuffer)) print(s3.unpack_from(prebuffer,0))關于struct的詳細用法
import json,struct #假設通過客戶端上傳1T:1073741824000的檔案a.txt #為避免粘包,必須自定制報頭 header={'file_size':1073741824000,'file_name':'/a/b/c/d/e/a.txt','md5':'8f6fbf8347faa4924a76856701edb0f3'} #1T資料,檔案路徑和md5值 #為了該報頭能傳送,需要序列化并且轉為bytes head_bytes=bytes(json.dumps(header),encoding='utf-8') #序列化并轉成bytes,用于傳輸 #為了讓客戶端知道報頭的長度,用struck將報頭長度這個數字轉成固定長度:4個位元組 head_len_bytes=struct.pack('i',len(head_bytes)) #這4個位元組里只包含了一個數字,該數字是報頭的長度 #客戶端開始發送 conn.send(head_len_bytes) #先發報頭的長度,4個bytes conn.send(head_bytes) #再發報頭的位元組格式 conn.sendall(檔案內容) #然后發真實內容的位元組格式 #服務端開始接收 head_len_bytes=s.recv(4) #先收報頭4個bytes,得到報頭長度的位元組格式 x=struct.unpack('i',head_len_bytes)[0] #提取報頭的長度 head_bytes=s.recv(x) #按照報頭長度x,收取報頭的bytes格式 header=json.loads(json.dumps(header)) #提取報頭 #最后根據報頭的內容提取真實的資料,比如 real_data_len=s.recv(header['file_size']) s.recv(real_data_len)
2、自定義報頭
import socket,struct,json import subprocess phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加 phone.bind(('127.0.0.1',8080)) phone.listen(5) while True: conn,addr=phone.accept() while True: cmd=conn.recv(1024) if not cmd:break print('cmd: %s' %cmd) res=subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) err=res.stderr.read() print(err) if err: back_msg=err else: back_msg=res.stdout.read() conn.send(struct.pack('i',len(back_msg))) #先發back_msg的長度 conn.sendall(back_msg) #在發真實的內容 conn.close()服務端(自定制報頭)
#_*_coding:utf-8_*_ 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('utf-8')) print(data.decode('gbk')) #windows默認gbk編碼客戶端(自定制報頭)
3、把報頭做成字典
我們可以把報頭做成字典,字典里包含將要發送的真實資料的詳細資訊,然后 json序列化,
然后用 struck 將 序列化后的資料長度 打包成4個位元組(4個自己足夠用了)
* 發送時:
先發報頭長度
再編碼報頭內容然后發送
最后發真實內容
* 接收時:
先手報頭長度,用struct取出來
根據取出的長度收取報頭內容,然后解碼,反序列化
從反序列化的結果中取出待取資料的詳細資訊,然后去取真實的資料內容
import socket,struct,json import subprocess phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加 phone.bind(('127.0.0.1',8080)) phone.listen(5) while True: conn,addr=phone.accept() while True: cmd=conn.recv(1024) if not cmd:break print('cmd: %s' %cmd) res=subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) err=res.stderr.read() print(err) if err: back_msg=err else: back_msg=res.stdout.read() headers={'data_size':len(back_msg)} head_json=json.dumps(headers) head_json_bytes=bytes(head_json,encoding='utf-8') conn.send(struct.pack('i',len(head_json_bytes))) #先發報頭的長度 conn.send(head_json_bytes) #再發報頭 conn.sendall(back_msg) #在發真實的內容 conn.close()服務端:定制稍微復雜一點的報頭
from socket import * import struct,json ip_port=('127.0.0.1',8080) client=socket(AF_INET,SOCK_STREAM) client.connect(ip_port) while True: cmd=input('>>: ') if not cmd:continue client.send(bytes(cmd,encoding='utf-8')) head=client.recv(4) head_json_len=struct.unpack('i',head)[0] head_json=json.loads(client.recv(head_json_len).decode('utf-8')) data_len=head_json['data_size'] recv_size=0 recv_data=b'' while recv_size < data_len: recv_data+=client.recv(1024) recv_size+=len(recv_data) print(recv_data.decode('utf-8')) #print(recv_data.decode('gbk')) #windows默認gbk編碼客戶端
六、認證客戶端的鏈接合法性
暫略
七、socketserver實作并發
基于tcp的套接字,關鍵就是兩個回圈,一個鏈接回圈,一個通信回圈
socketserver模塊中分兩大類:server類(解決鏈接問題)和 request類(解決通信問題)
1、server類(解決鏈接問題)

2、request類(解決通信問題)

3、socketserver原始碼分析總結:
ftpserver=socketserver.ThreadingTCPServer(('127.0.0.1',8080),FtpServer)
ftpserver.serve_forever()
基于tcp的socketserver我們自己定義的類中的
self.server即套接字物件
self.request即一個鏈接
self.client_address即客戶端地址
基于udp的socketserver我們自己定義的類中的
self.request是一個元組(第一個元素是客戶端發來的資料,第二部分是服務端的udp套接字物件),如(b'adsf', <socket.socket fd=200, family=AddressFamily.AF_INET, type=SocketKind.SOCK_DGRAM, proto=0, laddr=('127.0.0.1', 8080)>)
self.client_address即客戶端地址
import socketserver import struct import json import os class FtpServer(socketserver.BaseRequestHandler): coding='utf-8' server_dir='file_upload' max_packet_size=1024 BASE_DIR=os.path.dirname(os.path.abspath(__file__)) def handle(self): print(self.request) while True: data=self.request.recv(4) data_len=struct.unpack('i',data)[0] head_json=self.request.recv(data_len).decode(self.coding) head_dic=json.loads(head_json) # print(head_dic) cmd=head_dic['cmd'] if hasattr(self,cmd): func=getattr(self,cmd) func(head_dic) def put(self,args): file_path = os.path.normpath(os.path.join( self.BASE_DIR, self.server_dir, args['filename'] )) filesize = args['filesize'] recv_size = 0 print('----->', file_path) with open(file_path, 'wb') as f: while recv_size < filesize: recv_data = self.request.recv(self.max_packet_size) f.write(recv_data) recv_size += len(recv_data) print('recvsize:%s filesize:%s' % (recv_size, filesize)) ftpserver=socketserver.ThreadingTCPServer(('127.0.0.1',8080),FtpServer) ftpserver.serve_forever()FtpServer
import socket import struct import json import os class MYTCPClient: address_family = socket.AF_INET socket_type = socket.SOCK_STREAM allow_reuse_address = False max_packet_size = 8192 coding='utf-8' request_queue_size = 5 def __init__(self, server_address, connect=True): self.server_address=server_address self.socket = socket.socket(self.address_family, self.socket_type) if connect: try: self.client_connect() except: self.client_close() raise def client_connect(self): self.socket.connect(self.server_address) def client_close(self): self.socket.close() def run(self): while True: inp=input(">>: ").strip() if not inp:continue l=inp.split() cmd=l[0] if hasattr(self,cmd): func=getattr(self,cmd) func(l) def put(self,args): cmd=args[0] filename=args[1] if not os.path.isfile(filename): print('file:%s is not exists' %filename) return else: filesize=os.path.getsize(filename) head_dic={'cmd':cmd,'filename':os.path.basename(filename),'filesize':filesize} print(head_dic) head_json=json.dumps(head_dic) head_json_bytes=bytes(head_json,encoding=self.coding) head_struct=struct.pack('i',len(head_json_bytes)) self.socket.send(head_struct) self.socket.send(head_json_bytes) send_size=0 with open(filename,'rb') as f: for line in f: self.socket.send(line) send_size+=len(line) print(send_size) else: print('upload successful') client=MYTCPClient(('127.0.0.1',8080)) client.run()FtpClient
參考資料:
https://zhuanlan.zhihu.com/p/110296719
https://www.cnblogs.com/linhaifeng/articles/6129246.html#_label9
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/156190.html
標籤:Python
下一篇:一、Python爬蟲-認識爬蟲
