參考:http://www.cnblogs.com/Eva-J/articles/8244551.html#_label5
1.黏包的表現(以客戶端遠程操作服務端命令為例)
注:只有在TCP協議通信的情況下,才會產生黏包問題
基于TCP協議實現的黏包


#!/usr/bin/env python # -*- coding: utf-8 -*- # tcp_server_cmd.pyimport socket import subprocessip_port = ('127.0.0.1', 8080) #服務端地址及端口 BUFFERSIZE = 1024 #設置緩沖區大小 tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #設置為通過TCP協議通信(默認) tcp_server_socket.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR, 1)#用于socket關閉后,重用socket tcp_server_socket.bind(ip_port) #綁定ip和端口 tcp_server_socket.listen() #開始監聽客戶端連接while True:conn, addr = tcp_server_socket.accept() #與客戶端建立連接print('客戶端地址:', addr)while True:cmd = conn.recv(BUFFERSIZE).decode('utf-8') #接收客戶端輸入print('cmd:', cmd)if len(cmd)<1 or cmd == 'quit': breakres = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,stderr=subprocess.PIPE) #執行客戶端輸入命令#以下標準輸出信息都只能讀取一次std_out = res.stdout.read() #獲取輸出到標準輸出設備的成功信息std_err = res.stderr.read() #獲取輸出到標準輸出設備的錯誤信息print("stdout:",std_out.decode('gbk'))print("stderr:",std_err.decode('gbk'))conn.send(std_out)conn.send(std_err)conn.close() #關閉連接 tcp_server_socket.close() #關閉socket


#!/usr/bin/env python # -*- coding: utf-8 -*- #tcp_client_cmd.pyimport socketip_port = ('127.0.0.1', 8080) #服務端地址及端口 BUFFERSIZE = 1024 #設置緩沖區大小 tcp_client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #獲取socket對象 tcp_client_socket.connect(ip_port) #與服務端建立連接while True:cmd = input("Please input cmd<<< ").strip() #輸入命令if len(cmd) < 1: continue #跳過本次循環,開始下一次循環elif cmd == 'quit': tcp_client_socket.send(cmd.encode('utf-8')) #發送中斷請求給服務端break #中斷循環 tcp_client_socket.send(cmd.encode('utf-8'))ret = tcp_client_socket.recv(BUFFERSIZE)print(ret.decode('gbk'))tcp_client_socket.close()
基于UDP協議實現(無黏包現象)


#!/usr/bin/env python # -*- coding: utf-8 -*- # udp_server_cmd.pyimport socket import subprocessip_port = ('127.0.0.1', 8080) BUFFERSIZE = 2048udp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) #設置為通過UDP協議通信 udp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) udp_server_socket.bind(ip_port)while True:cmd, addr = udp_server_socket.recvfrom(BUFFERSIZE)print('client ip:',addr)cmd = cmd.decode('utf-8')print('cmd:',cmd)if len(cmd)<1 or cmd == 'quit':breakres = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,stderr=subprocess.PIPE)std_out = res.stdout.read()std_err = res.stderr.read()print('stdout:', std_out.decode('gbk'))print('stderr:', std_err.decode('gbk'))udp_server_socket.sendto(std_out, addr)udp_server_socket.sendto(std_err, addr)udp_server_socket.close()


#!/usr/bin/env python # -*- coding: utf-8 -*- # udp_client_cmd.pyimport socketip_port = ('127.0.0.1', 8080) BUFFERSIZE = 2048udp_client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) udp_client_socket.connect(ip_port)while True:cmd = input("Please input cmd<<< ").strip()if len(cmd)<1: continueelif cmd == 'quit': udp_client_socket.sendto(cmd.encode('utf-8'), ip_port)breakudp_client_socket.sendto(cmd.encode('utf-8'), ip_port)ret, addr = udp_client_socket.recvfrom(BUFFERSIZE)print(ret.decode('gbk'))udp_client_socket.close()
2.黏包的成因(基于TCP協議傳輸)
- tcp協議的拆包機制
- tcp面向流的通信是無消息保護邊界的
- tcp的Nagle優化算法:若連續幾次需要send的數據都很少,通常TCP會根據優化算法把這些數據合成一個TCP段后一次發送出去,這樣接收方就收到了粘包數據
- 接收方和發送方的緩存機制
3.導致黏包的根本因素
- 接收方不知道消息之間的界限,不知道一次性提取多少字節的數據
4.黏包的解決方法
由于導致黏包的根本原因是接收端不知道發送端將要傳送的字節流的長度,故有如下兩種解決方案
方案一:在發送消息前,將要發送的字節流總大小讓接收端知曉,然后接收端來一個死循環接收完所有數據


#!/usr/bin/env python # -*- coding: utf-8 -*- # tcp_server_cmd.py""" 實現客戶端遠程操作服務端命令 """ import socket import subprocessip_port = ('127.0.0.1', 8080) #服務端地址及端口 BUFFERSIZE = 1024 #設置緩沖區大小 tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #設置為通過TCP協議通信(默認) tcp_server_socket.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR, 1)#用于socket關閉后,重用socket tcp_server_socket.bind(ip_port) #綁定ip和端口 tcp_server_socket.listen() #開始監聽客戶端連接 flag = Truewhile flag:conn, addr = tcp_server_socket.accept() #與客戶端建立連接print('client ip addr:', addr)while True:cmd = conn.recv(BUFFERSIZE).decode('utf-8') #接收客戶端輸入if len(cmd)<1 or cmd == 'quit': flag = False #防止死循環,在多個客戶端連接時,可以去掉breakres = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,stderr=subprocess.PIPE) #執行客戶端輸入命令#以下標準輸出信息都只能讀取一次std_err = res.stderr.read() #獲取輸出到標準輸出設備的錯誤信息if std_err: #判斷返回信息的類型ret = std_errelse:ret = res.stdout.read() #獲取輸出到標準輸出設備的成功信息"""以下是方案一的核心部分"""conn.send(str(len(ret)).encode('utf-8')) #發送要發送信息的長度print("ret:",ret.decode('gbk'))data = conn.recv(BUFFERSIZE).decode('utf-8') #接收客戶端準備確認信息if data == 'recv_ready': conn.sendall(ret) #發送所有信息 conn.close() #關閉連接 tcp_server_socket.close() #關閉socket


#!/usr/bin/env python # -*- coding: utf-8 -*- #client_tcp_cmd.pyimport socketip_port = ('127.0.0.1', 8080) #服務端地址及端口 BUFFERSIZE = 1024 #設置緩沖區大小 tcp_client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #獲取socket對象 tcp_client_socket.connect(ip_port) #與服務端建立連接while True:cmd = input("Please input cmd<<< ").strip() #輸入命令if len(cmd) < 1: continue #跳過本次循環,開始下一次循環elif cmd == 'quit': tcp_client_socket.send(cmd.encode('utf-8')) #發送中斷請求給服務端break #中斷循環 tcp_client_socket.send(cmd.encode('utf-8')) #發送要執行的命令"""以下是方案一的核心部分"""info_len = tcp_client_socket.recv(BUFFERSIZE).decode('utf-8') #接收要接收的信息長度 tcp_client_socket.send(b'recv_ready') #給服務端發送已經準備好接收信息 data = b''ret_size = 0while ret_size < int(info_len): #判斷信息是否已接收完data += tcp_client_socket.recv(BUFFERSIZE) #接收指定大小的信息ret_size += len(data) #將已經接收的信息長度累加print(data.decode('gbk'))tcp_client_socket.close() #關閉socket
存在的問題: 程序的運行速度遠快于網絡傳輸速度,所以在發送一段字節前,先用send去發送該字節流長度,這種方式會放大網絡延遲帶來的性能損耗
方案二:針對方案一的問題,引入struct模塊,struct模塊可以將發送的數據長度轉換成固定長度的字節


#!/usr/bin/env python # -*- coding: utf-8 -*- # tcp_server_cmd.py""" 實現客戶端遠程操作服務端命令 """ import socket import subprocess import struct import jsonip_port = ('127.0.0.1', 8080) #服務端地址及端口 BUFFERSIZE = 1024 #設置緩沖區大小 tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #設置為通過TCP協議通信(默認) tcp_server_socket.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR, 1)#用于socket關閉后,重用socket tcp_server_socket.bind(ip_port) #綁定ip和端口 tcp_server_socket.listen() #開始監聽客戶端連接 flag = Truewhile flag:conn, addr = tcp_server_socket.accept() #與客戶端建立連接print('client ip addr:', addr)while True:cmd = conn.recv(BUFFERSIZE).decode('utf-8') #接收客戶端輸入if len(cmd)<1 or cmd == 'quit': flag = False #防止死循環,在多個客戶端連接時,可以去掉breakres = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,stderr=subprocess.PIPE) #執行客戶端輸入命令#以下標準輸出信息都只能讀取一次std_err = res.stderr.read() #獲取輸出到標準輸出設備的錯誤信息if std_err: #判斷返回信息的類型back_info = std_errelse:back_info = res.stdout.read() #獲取輸出到標準輸出設備的成功信息"""以下是方案二的核心部分(定制化報頭)"""head = {'data_size':len(back_info)}head_json = json.dumps(head) #將python對象轉化為json字符串head_bytes = bytes(head_json, encoding='utf-8') #將json字符串轉化為bytes字節碼對象head_struct_len = struct.pack('i', len(head_bytes)) #使用struct將定制化的報頭打包為4個字節的長度conn.send(head_struct_len) #發送定制報頭的長度,4個字節conn.send(head_bytes) #發送定制報頭信息print("back_info:",back_info.decode('gbk'))conn.sendall(back_info) #發送所有的真實信息 conn.close() #關閉連接 tcp_server_socket.close() #關閉socket


#!/usr/bin/env python # -*- coding: utf-8 -*- #client_tcp_cmd.pyimport socket import struct import jsonip_port = ('127.0.0.1', 8080) #服務端地址及端口 BUFFERSIZE = 1024 #設置緩沖區大小 tcp_client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #獲取socket對象 tcp_client_socket.connect(ip_port) #與服務端建立連接while True:cmd = input("Please input cmd<<< ").strip() #輸入命令if len(cmd) < 1: continue #跳過本次循環,開始下一次循環elif cmd == 'quit': tcp_client_socket.send(cmd.encode('utf-8')) #發送中斷請求給服務端break #中斷循環 tcp_client_socket.send(cmd.encode('utf-8')) #發送要執行的命令"""以下是方案二的核心部分(定制化報頭)"""head_struct = tcp_client_socket.recv(4) #接收4字節的定制報頭head_json_len = struct.unpack('i', head_struct)[0] #struct解包定制報頭后是一個tuple,如(1024,)head_json = tcp_client_socket.recv(head_json_len).decode('utf-8') #將接收的bytes字節碼報頭解碼為json字符串head = json.loads(head_json) #將json字符串轉化為python對象print('head:',head)data = b''ret_size = 0while ret_size < head['data_size']: #判斷信息是否已接收完data += tcp_client_socket.recv(BUFFERSIZE) #接收指定緩沖大小的信息ret_size += len(data) #將已經接收的信息長度累加print(data.decode('gbk')) #windows默認編碼是gbk tcp_client_socket.close() #關閉socket
5.TCP和UDP協議的簡介
?待補充。。。
6.補充
1.[WinError 10013] 以一種訪問權限不允許的方式做了一個訪問套接字的嘗試
原因:端口被占用導致
解決:
Windows下 C:\> netstat -ano|findstr 8080 #查找8080端口占用進程號 TCP 127.0.0.1:8080 0.0.0.0:0 LISTENING 17496 C:\> tasklist |findstr 17496 #查找17496進程號對應的程序 python.exe 17496 Console 1 10,664 K C:\> taskkill /pid 17496 /F #殺掉17496進程 成功: 已終止 PID 為 17496 的進程。Linux下 [root@localhost]# netstat -nltup | grep 80 #查找80端口上的程序 tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 1479/nginx [root@localhost]# ps -ef | grep nginx #查找nginx對應進程號 root 1479 1 0 Jul23 ? 00:00:00 nginx: master process ./nginx [root@localhost]# kill -9 1479 #殺掉1479進程
?2.struct模塊可打包和解包的數據類型
3.socket模塊方法說明


服務端套接字函數 s.bind() 綁定(主機,端口號)到套接字 s.listen() 開始TCP監聽 s.accept() 被動接受TCP客戶的連接,(阻塞式)等待連接的到來客戶端套接字函數 s.connect() 主動初始化TCP服務器連接 s.connect_ex() connect()函數的擴展版本,出錯時返回出錯碼,而不是拋出異常公共用途的套接字函數 s.recv() 接收TCP數據 s.send() 發送TCP數據 s.sendall() 發送TCP數據 s.recvfrom() 接收UDP數據 s.sendto() 發送UDP數據 s.getpeername() 連接到當前套接字的遠端的地址 s.getsockname() 當前套接字的地址 s.getsockopt() 返回指定套接字的參數 s.setsockopt() 設置指定套接字的參數 s.close() 關閉套接字面向鎖的套接字方法 s.setblocking() 設置套接字的阻塞與非阻塞模式 s.settimeout() 設置阻塞套接字操作的超時時間 s.gettimeout() 得到阻塞套接字操作的超時時間面向文件的套接字的函數 s.fileno() 套接字的文件描述符 s.makefile() 創建一個與該套接字相關的文件
?