一、socket的定義
Socket是應用層與TCP/IP協議族通信的中間軟件抽象層,它是一組接口。在設計模式中,Socket其實就是一個門面模式,它把復雜的TCP/IP協議族隱藏在Socket接口后面,對用戶來說,一組簡單的接口就是全部,讓Socket去組織數據,以符合指定的協議。所以,我們無需深入理解tcp/udp協議,socket已經為我們封裝好了,我們只需要遵循socket的規定去編程,寫出的程序自然就是遵循tcp/udp標准的。
補充:也有人將socket說成ip+port,ip是用來標識互聯網中的一台主機的位置,而port是用來標識這台機器上的一個應用程序,ip地址是配置到網卡上的,而port是應用程序開啟的,ip與port的綁定就標識了互聯網中獨一無二的一個應用程序,而程序的pid是同一台機器上不同進程或者線程的標識
二、套接字發展史及分類
套接字起源於 20 世紀 70 年代加利福尼亞大學伯克利分校版本的 Unix,即人們所說的 BSD Unix。 因此,有時人們也把套接字稱為“伯克利套接字”或“BSD 套接字”。一開始,套接字被設計用在同 一台主機上多個應用程序之間的通訊。這也被稱進程間通訊,或 IPC。套接字有兩種(或者稱為有兩個種族),分別是基於文件型的和基於網絡型的。
- 基於文件類型的套接字家族
套接字家族的名字:AF_UNIX
unix一切皆文件,基於文件的套接字調用的就是底層的文件系統來取數據,兩個套接字進程運行在同一機器,可以通過訪問同一個文件系統間接完成通信
- 基於網絡類型的套接字家族
套接字家族的名字:AF_INET
(還有AF_INET6被用於ipv6,還有一些其他的地址家族,不過,他們要么是只用於某個平台,要么就是已經被廢棄,或者是很少被使用,或者是根本沒有實現,所有地址家族中,AF_INET是使用最廣泛的一個,python支持很多種地址家族,但是由於我們只關心網絡編程,所以大部分時候我么只使用AF_INET)
三、套接字的工作流程
一個生活中的場景。你要打電話給一個朋友,先撥號,朋友聽到電話鈴聲后提起電話,這時你和你的朋友就建立起了連接,就可以講話了。等交流結束,掛斷電話結束此次交談。
生活中的場景就解釋了套接字的工作原理
先從服務器端說起。服務器端先初始化Socket,然后與端口綁定(bind),對端口進行監聽(listen),調用accept阻塞,等待客戶端連接。在這時如果有個客戶端初始化一個Socket,然后連接服務器(connect),如果連接成功,這時客戶端與服務器端的連接就建立了。客戶端發送數據請求,服務器端接收請求並處理請求,然后把回應數據發送給客戶端,客戶端讀取數據,最后關閉連接,一次交互結束。
四、socket函數使用
- socket函數用法
import socket socket.socket(socket_family,socket_type,protocal=0) #socket_family 可以是 AF_UNIX 或 AF_INET。socket_type 可以是 SOCK_STREAM 或 SOCK_DGRAM。protocol 一般不填,默認值為 0。 #獲取tcp/ip套接字 tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #獲取udp/ip套接字 udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) #由於 socket 模塊中有太多的屬性。我們在這里破例使用了'from module import *'語句。使用 'from socket import *',我們就把 socket 模塊里的所有屬性都帶到我們的命名空間里了,這樣能 大幅減短我們的代碼。 #例如tcpSock = socket(AF_INET, SOCK_STREAM)
- 服務端套接字函數
s.bind() #綁定(主機,端口號)到套接字
s.listen() #開始TCP監聽
s.accept() #被動接受TCP客戶的連接,(阻塞式)等待連接的到來
- 客戶端套接字函數
s.connect() #主動初始化TCP服務器連接
s.connect_ex() #connect()函數的擴展版本,出錯時返回出錯碼,而不是拋出異常
- 公共用途的套接字函數
s.recv() #接收TCP數據
s.send() #發送TCP數據(send在待發送數據量大於己端緩存區剩余空間時,數據丟失,不會發完)
s.sendall() #發送完整的TCP數據(本質就是循環調用send,sendall在待發送數據量大於己端緩存區剩余空間時,數據不丟失,循環調用send直到發完)
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() #創建一個與該套接字相關的文件
打電話的流程演示
服務端.py
import socket phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #買手機 phone.bind(('127.0.0.1',8080)) #插電話卡 phone.listen(5) #開機,backlog print('starting....') conn,addr=phone.accept() #接電話 print(conn) print('client addr',addr) print('ready to read msg') client_msg=conn.recv(1024) #收消息 print('client msg: %s' %client_msg) conn.send(client_msg.upper()) #發消息 conn.close() phone.close()
客戶端.py
import socket phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.connect(('127.0.0.1',8080)) #撥通電話 phone.send('hello'.encode('utf-8')) #發消息 back_msg=phone.recv(1024) print(back_msg) phone.close()
輸出
服務端:
starting.... <socket.socket fd=4, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 65142)> client addr ('127.0.0.1', 65142) ready to read msg client msg: b'hello'
客戶端
b'HELLO'
五、基於TCP的套接字
- tcp服務端
ss = socket() #創建服務器套接字 ss.bind() #把地址綁定到套接字 ss.listen() #監聽鏈接 inf_loop: #服務器無限循環 cs = ss.accept() #接受客戶端鏈接 comm_loop: #通訊循環 cs.recv()/cs.send() #對話(接收與發送) cs.close() #關閉客戶端套接字 ss.close() #關閉服務器套接字(可選)
- tcp客戶端
cs = socket() # 創建客戶套接字 cs.connect() # 嘗試連接服務器 comm_loop: # 通訊循環 cs.send()/cs.recv() # 對話(發送/接收) cs.close() # 關閉客戶套接字
socket通信流程與打電話流程類似,我們就以打電話為例來實現一個low版的套接字通信
服務端
import socket ip_port=('127.0.0.1',9000) #電話卡 BUFSIZE=1024 #收發消息的尺寸 s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #買手機 s.bind(ip_port) #手機插卡 s.listen(5) #手機待機 conn,addr=s.accept() #手機接電話 # print(conn) # print(addr) print('接到來自%s的電話' %addr[0]) msg=conn.recv(BUFSIZE) #聽消息,聽話 print(msg,type(msg)) conn.send(msg.upper()) #發消息,說話 conn.close() #掛電話 s.close() #手機關機
客戶端
import socket ip_port=('127.0.0.1',9000) BUFSIZE=1024 s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.connect_ex(ip_port) #撥電話 s.send('nitouxiang nb'.encode('utf-8')) #發消息,說話(只能發送字節類型) feedback=s.recv(BUFSIZE) #收消息,聽話 print(feedback.decode('utf-8')) s.close() #掛電話
輸出
服務端
接到來自127.0.0.1的電話 b'nitouxiang nb' <class 'bytes'>
客戶端
NITOUXIANG NB
上述流程的問題是,服務端只能接受一次鏈接,然后就徹底關閉掉了,實際情況應該是,服務端不斷接受鏈接,然后循環通信,通信完畢后只關閉鏈接,服務器能夠繼續接收下一次鏈接,下面是修改版
服務端
import socket ip_port = ('127.0.0.1',8081) #電話卡 BUFSIZE=1024 s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #買手機 s.bind(ip_port) #手機插卡 s.listen(5) #手機待機 while True: #新增接收鏈接循環,可以不停的接電話 conn,addr=s.accept() #手機接電話 print('接到來自%s的電話' %addr[0]) while True: ##新增通信循環,可以不斷的通信,收發消息 msg=conn.recv(BUFSIZE) #聽消息,聽話 if len(msg) == 0:break #如果不加,那么正在鏈接的客戶端突然斷開,recv便不再阻塞,死循環發生 print(msg,type(msg)) conn.send(msg.upper()) #發消息,說話 conn.close() #掛電話 s.close() #手機關機
客戶端
import socket ip_port=('127.0.0.1',8081) BUFSIZE=1024 s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.connect_ex(ip_port) #撥電話 while True: #新增通信循環,客戶端可以不斷發收消息 msg=input('>>: ').strip() if len(msg) == 0:continue s.send(msg.encode('utf-8')) #發消息,說話(只能發送字節類型) feedback=s.recv(BUFSIZE) #收消息,聽話 print(feedback.decode('utf-8')) s.close() #掛電話
補充:
在重啟服務端時可能會遇到
這個是由於你的服務端仍然存在四次揮手的time_wait狀態在占用地址(如果不懂,請深入研究1.tcp三次握手,四次揮手 2.syn洪水攻擊 3.服務器高並發情況下會有大量的time_wait狀態的優化方法)
解決辦法
方法一
#加入一條socket配置,重用ip和端口 phone=socket(AF_INET,SOCK_STREAM) phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加 phone.bind(('127.0.0.1',8080))
方法二
發現系統存在大量TIME_WAIT狀態的連接,通過調整linux內核參數解決, vi /etc/sysctl.conf 編輯文件,加入以下內容: net.ipv4.tcp_syncookies = 1 net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_tw_recycle = 1 net.ipv4.tcp_fin_timeout = 30 然后執行 /sbin/sysctl -p 讓參數生效。 net.ipv4.tcp_syncookies = 1 表示開啟SYN Cookies。當出現SYN等待隊列溢出時,啟用cookies來處理,可防范少量SYN攻擊,默認為0,表示關閉; net.ipv4.tcp_tw_reuse = 1 表示開啟重用。允許將TIME-WAIT sockets重新用於新的TCP連接,默認為0,表示關閉; net.ipv4.tcp_tw_recycle = 1 表示開啟TCP連接中TIME-WAIT sockets的快速回收,默認為0,表示關閉。 net.ipv4.tcp_fin_timeout 修改系統默認的 TIMEOUT 時間
六、基於UDP的套接字
- udp服務端
ss = socket() #創建一個服務器的套接字 ss.bind() #綁定服務器套接字 inf_loop: #服務器無限循環 cs = ss.recvfrom()/ss.sendto() # 對話(接收與發送) ss.close() # 關閉服務器套接字
- udp客戶端
cs = socket() # 創建客戶套接字 comm_loop: # 通訊循環 cs.sendto()/cs.recvfrom() # 對話(發送/接收) cs.close() # 關閉客戶套接字
示例
服務端
import socket ip_port=('127.0.0.1',9000) BUFSIZE=1024 udp_server_client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) udp_server_client.bind(ip_port) while True: msg,addr=udp_server_client.recvfrom(BUFSIZE) print(msg,addr) udp_server_client.sendto(msg.upper(),addr)
客戶端
import socket ip_port=('127.0.0.1',9000) BUFSIZE=1024 udp_server_client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) while True: msg=input('>>: ').strip() if not msg:continue udp_server_client.sendto(msg.encode('utf-8'),ip_port) back_msg,addr=udp_server_client.recvfrom(BUFSIZE) print(back_msg.decode('utf-8'),addr)
輸出
客戶端
>>: 123 123 ('127.0.0.1', 9000) >>: 3 3 ('127.0.0.1', 9000) >>: 4 4 ('127.0.0.1', 9000)
服務端
b'123' ('127.0.0.1', 53066) b'3' ('127.0.0.1', 53066) b'4' ('127.0.0.1', 53066)
模擬QQ聊天,多個客戶端和服務端通信
服務端
import socket ip_port=('127.0.0.1',8081) udp_server_sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) #買手機 udp_server_sock.bind(ip_port) while True: qq_msg,addr=udp_server_sock.recvfrom(1024) print('來自[%s:%s]的一條消息:\033[1;44m%s\033[0m' %(addr[0],addr[1],qq_msg.decode('utf-8'))) back_msg=input('回復消息: ').strip() udp_server_sock.sendto(back_msg.encode('utf-8'),addr)
客戶端1
import socket BUFSIZE=1024 udp_client_socket=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) qq_name_dic={ 'TOM':('127.0.0.1',8081), 'JACK':('127.0.0.1',8081), '一棵樹':('127.0.0.1',8081), '武大郎':('127.0.0.1',8081), } while True: qq_name=input('請選擇聊天對象: ').strip() while True: msg=input('請輸入消息,回車發送: ').strip() if msg == 'quit':break if not msg or not qq_name or qq_name not in qq_name_dic:continue udp_client_socket.sendto(msg.encode('utf-8'),qq_name_dic[qq_name]) back_msg,addr=udp_client_socket.recvfrom(BUFSIZE) print('來自[%s:%s]的一條消息:\033[1;44m%s\033[0m' %(addr[0],addr[1],back_msg.decode('utf-8'))) udp_client_socket.close()
客戶端2
import socket BUFSIZE=1024 udp_client_socket=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) qq_name_dic={ 'TOM':('127.0.0.1',8081), 'JACK':('127.0.0.1',8081), '一棵樹':('127.0.0.1',8081), '武大郎':('127.0.0.1',8081), } while True: qq_name=input('請選擇聊天對象: ').strip() while True: msg=input('請輸入消息,回車發送: ').strip() if msg == 'quit':break if not msg or not qq_name or qq_name not in qq_name_dic:continue udp_client_socket.sendto(msg.encode('utf-8'),qq_name_dic[qq_name]) back_msg,addr=udp_client_socket.recvfrom(BUFSIZE) print('來自[%s:%s]的一條消息:\033[1;44m%s\033[0m' %(addr[0],addr[1],back_msg.decode('utf-8'))) udp_client_socket.close()
輸出
客戶端1
請選擇聊天對象: JACK 請輸入消息,回車發送: 約不 來自[127.0.0.1:8081]的一條消息:不約 請輸入消息,回車發送:
客戶端2
請選擇聊天對象: TOM 請輸入消息,回車發送: 123 來自[127.0.0.1:8081]的一條消息:321 請輸入消息,回車發送:
服務端
來自[127.0.0.1:62851]的一條消息:123 回復消息: 321 來自[127.0.0.1:60378]的一條消息:約不 回復消息: 不約
七、recv與recvfrom
發消息,都是將數據發送到己端的發送緩沖中,收消息都是從己端的緩沖區中收。
- tcp:send發消息,recv收消息
- udp:sendto發消息,recvfrom收消息
1.send與sendinto
tcp是基於數據流的,而udp是基於數據報的:
- send(bytes_data):發送數據流,數據流bytes_data若為空,自己這段的緩沖區也為空,操作系統不會控制tcp協議發空包
- sendinto(bytes_data,ip_port):發送數據報,bytes_data為空,還有ip_port,所有即便是發送空的bytes_data,數據報其實也不是空的,自己這端的緩沖區收到內容,操作系統就會控制udp協議發包。
2.recv與recvfrom
tcp協議:
(1)如果收消息緩沖區里的數據為空,那么recv就會阻塞(阻塞很簡單,就是一直在等着收)
(2)只不過tcp協議的客戶端send一個空數據就是真的空數據,客戶端即使有無窮個send空,也跟沒有一個樣。
(3)tcp基於鏈接通信
- 基於鏈接,則需要listen(backlog),指定半連接池的大小
- 基於鏈接,必須先運行的服務端,然后客戶端發起鏈接請求
- 對於mac系統:如果一端斷開了鏈接,那另外一端的鏈接也跟着完蛋recv將不會阻塞,收到的是空(解決方法是:服務端在收消息后加上if判斷,空消息就break掉通信循環)
- 對於windows/linux系統:如果一端斷開了鏈接,那另外一端的鏈接也跟着完蛋recv將不會阻塞,收到的是空(解決方法是:服務端通信循環內加異常處理,捕捉到異常后就break掉通訊循環)
udp協議
(1)如果如果收消息緩沖區里的數據為“空”,recvfrom也會阻塞
(2)只不過udp協議的客戶端sendinto一個空數據並不是真的空數據(包含:空數據+地址信息,得到的報仍然不會為空),所以客戶端只要有一個sendinto(不管是否發送空數據,都不是真的空數據),服務端就可以recvfrom到數據。
(3)udp無鏈接
- 無鏈接,因而無需listen(backlog),更加沒有什么連接池之說了
- 無鏈接,udp的sendinto不用管是否有一個正在運行的服務端,可以己端一個勁的發消息,只不過數據丟失
- recvfrom收的數據小於sendinto發送的數據時,在mac和linux系統上數據直接丟失,在windows系統上發送的比接收的大直接報錯
- 只有sendinto發送數據沒有recvfrom收數據,數據丟失
注意:
1.你單獨運行上面的udp的客戶端,你發現並不會報錯,相反tcp卻會報錯,因為udp協議只負責把包發出去,對方收不收,我根本不管,而tcp是基於鏈接的,必須有一個服務端先運行着,客戶端去跟服務端建立鏈接然后依托於鏈接才能傳遞消息,任何一方試圖把鏈接摧毀都會導致對方程序的崩潰。
2.上面的udp程序,你注釋任何一條客戶端的sendinto,服務端都會卡住,為什么?因為服務端有幾個recvfrom就要對應幾個sendinto,哪怕是sendinto(b'')那也要有。
基於tcp先制作一個遠程執行命令的程序(1:執行錯誤命令 2:執行ls 3:執行ifconfig)
客戶端
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='')
服務端
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.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)
輸出
客戶端
>>: ls 1.py 客戶端.py 客戶端1.py 客戶端2.py 服務端.py >>: ifconfig en0 en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500 ether 78:4f:43:5b:a5:4c inet6 fe80::d0:d821:dbf0:3d67%en0 prefixlen 64 secured scopeid 0x5 inet 192.168.31.165 netmask 0xffffff00 broadcast 192.168.31.255 nd6 options=201<PERFORMNUD,DAD> media: autoselect status: active >>: ifconfig lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 16384 options=1203<RXCSUM,TXCSUM,TXSTATUS,SW_TIMESTAMP> inet 127.0.0.1 netmask 0xff000000 inet6 ::1 prefixlen 128 inet6 fe80::1%lo0 prefixlen 64 scopeid 0x1 nd6 options=201<PERFORMNUD,DAD> gif0: flags=8010<POINTOPOINT,MULTICAST> mtu 1280 stf0: flags=0<> mtu 1280 en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500 ether 78:4f:43:5b:a5:4c inet6 fe80::d0:d821:dbf0:3d67%en0 prefixlen 64 secured scopeid 0x5 inet 192.168.31.165 netmask 0xffffff00 broadcast 192.168.31.255 nd6 options=201<PERFORMNUD,DAD> media: autoselect status: active en1: flags=963<UP,BROADCAST,SMART,RUNNING,PROMISC,SIMPLEX> mtu 1500 options=60<TSO4,TSO6> ether e2:00:ec:98:eb:00 media: autoselect <full-duplex> status: inactive en3: flags=963<UP,BROADCAST,SMART,RUNNING,PROMISC,SIMPLEX> mtu 1500 options=60<TSO4,TSO6> ether e2:00:ec:98:eb:01 media: autoselect <full-duplex> status: inactive en2: flags=963<UP,BROADCAST,SMART,RUNNING,PROMISC,SIMPLEX> mtu 1500>>: >>:
服務端
客戶端 ('127.0.0.1', 58194)
上述程序是基於tcp的socket,在運行時會發生粘包
服務端
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='')
上述程序是基於udp的socket,在運行時永遠不會發生粘包
注意注意注意:
res=subprocess.Popen(cmd.decode('utf-8'),
shell=True,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE)
的結果的編碼是以當前所在的系統為准的,如果是windows,那么res.stdout.read()讀出的就是GBK編碼的,在接收端需要用GBK解碼且只能從管道里讀一次結果
八、粘包
1.什么是粘包
粘包:發送方發送兩個字符串”hello”+”world”,接收方卻一次性接收到了”helloworld”。
只有TCP有粘包現象,UDP永遠不會粘包。
所謂粘包問題主要還是因為接收方不知道消息之間的界限,不知道一次性提取多少字節的數據所造成的。
補充:
分包:發送方發送字符串”helloworld”,接收方卻接收到了兩個字符串”hello”和”world”。
TCP是以段(Segment)為單位發送數據的,建立TCP鏈接后,有一個最大消息長度(MSS)。如果應用層數據包超過MSS,就會把應用層數據包拆分,分成兩個段來發送。這個時候接收端的應用層就要拼接這兩個TCP包,才能正確處理數據。
補充:
一個socket收發消息的原理
2.粘包如何產生
TCP為了提高網絡的利用率,會使用一個叫做Nagle的算法。該算法是指,發送端即使有要發送的數據,如果很少的話,會延遲發送。如果應用層給TCP傳送數據很快的話,就會把兩個應用層數據包“粘”在一起,TCP最后只發一個TCP數據包給接收端。
tcp的協議數據不會丟,沒有收完包,下次接收,會繼續上次繼續接收,己端總是在收到ack時才會清除緩沖區內容。數據是可靠的,但是會粘包。
兩種情況下會發生粘包。
發送端需要等緩沖區滿才發送出去,造成粘包(發送數據時間間隔很短,數據了很小,會合到一起,產生粘包)

from socket import * ip_port=('127.0.0.1',8085) 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()

import socket BUFSIZE=1024 ip_port=('127.0.0.1',8085) 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'))
輸出
服務端
-----> hellofeng #出現粘包現象
----->
接收方不及時接收緩沖區的包,造成多個包接收(客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收的時候還是從緩沖區拿上次遺留的數據,產生粘包)

from socket import * ip_port=('127.0.0.1',8089) 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()

import socket BUFSIZE=1024 ip_port=('127.0.0.1',8089) s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) res=s.connect_ex(ip_port) s.send('hello feng'.encode('utf-8'))
輸出
-----> he
-----> llo feng
補充:
recv里指定的1024意思是從緩存里一次拿出1024個字節的數據
send的字節流是先放入己端緩存,然后由協議控制將緩存內容發往對端,如果待發送的字節流大小大於緩存剩余空間,那么數據丟失,用sendall就會循環調用send,數據不會丟失
3.如何解決粘包問題
為字節流加上自定義固定長度報頭,報頭中包含字節流長度,然后一次send到對端,對端在接收時,先從緩存中取出定長的報頭,然后再取真實數據
struct模塊
該模塊可以把一個類型,如數字,轉成固定長度的bytes
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)
示例:
我們可以把報頭做成字典,字典里包含將要發送的真實數據的詳細信息,然后json序列化,然后用struck將序列化后的數據長度打包成4個字節(4個自己足夠用了)
發送時:
先發報頭長度
再編碼報頭內容然后發送
最后發真實內容
接收時:
先手報頭長度,用struct取出來
根據取出的長度收取報頭內容,然后解碼,反序列化
從反序列化的結果中取出待取數據的詳細信息,然后去取真實的數據內容

import socket import subprocess import struct import json phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #買手機 phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) phone.bind(('127.0.0.1',8082)) #插電話卡 phone.listen(5) #開機,backlog while True: print('starting....') conn,addr=phone.accept() print('cliet addr',addr) while True: try: cmd=conn.recv(1024) if not cmd:break res=subprocess.Popen(cmd.decode('utf-8'),shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) err=res.stderr.read() if err: cmd_res=err else: cmd_res=res.stdout.read() # conn.send(struct.pack('i',len(cmd_res))) #先報報頭 head_dic={'filename':None,'hash':None,'total_size':len(cmd_res)} head_json=json.dumps(head_dic) head_bytes=head_json.encode('utf-8') #先發送報頭的長度 conn.send(struct.pack('i',len(head_bytes))) #再發送報頭的bytes conn.send(head_bytes) #最后發送真實的數據 conn.send(cmd_res) except Exception: break conn.close() phone.close()

import socket import struct import json phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.connect(('127.0.0.1',8082)) #撥通電話 while True: #通信循環 cmd=input('>>: ').strip() if not cmd:continue #防止客戶端發空 phone.send(cmd.encode('utf-8')) #發消息 #先收報頭的長度 head_struct=phone.recv(4) head_len=struct.unpack('i',head_struct)[0] #再收報頭的bytes head_bytes=phone.recv(head_len) head_json=head_bytes.decode('utf-8') head_dic=json.loads(head_json) #最后根據報頭里的詳細信息取真實的數據 print(head_dic) total_size=head_dic['total_size'] recv_size=0 data=b'' while recv_size < total_size: #10240 +1 recv_data=phone.recv(1024) data+=recv_data recv_size+=len(recv_data) print(data.decode('utf-8')) phone.close()
輸出

>>: ls {'total_size': 69, 'hash': None, 'filename': None} 1.py c.py s.py 客戶端.py 客戶端1.py 客戶端2.py 服務端.py >>: ifconfig {'total_size': 2990, 'hash': None, 'filename': None} lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 16384 options=1203<RXCSUM,TXCSUM,TXSTATUS,SW_TIMESTAMP> inet 127.0.0.1 netmask 0xff000000 inet6 ::1 prefixlen 128 inet6 fe80::1%lo0 prefixlen 64 scopeid 0x1 nd6 options=201<PERFORMNUD,DAD> gif0: flags=8010<POINTOPOINT,MULTICAST> mtu 1280 stf0: flags=0<> mtu 1280 en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500 ether 78:4f:43:5b:a5:4c inet6 fe80::d0:d821:dbf0:3d67%en0 prefixlen 64 secured scopeid 0x5 inet 192.168.31.165 netmask 0xffffff00 broadcast 192.168.31.255 nd6 options=201<PERFORMNUD,DAD> media: autoselect status: active en1: flags=963<UP,BROADCAST,SMART,RUNNING,PROMISC,SIMPLEX> mtu 1500 options=60<TSO4,TSO6> ether e2:00:ec:98:eb:00 media: autoselect <full-duplex> status: inactive en3: flags=963<UP,BROADCAST,SMART,RUNNING,PROMISC,SIMPLEX> mtu 1500 options=60<TSO4,TSO6> ether e2:00:ec:98:eb:01 media: autoselect <full-duplex> status: inactive en2: flags=963<UP,BROADCAST,SMART,RUNNING,PROMISC,SIMPLEX> mtu 1500 options=60<TSO4,TSO6> ether e2:00:ec:98:eb:04 media: autoselect <full-duplex> status: inactive en4: flags=963<UP,BROADCAST,SMART,RUNNING,PROMISC,SIMPLEX> mtu 1500 options=60<TSO4,TSO6> ether e2:00:ec:98:eb:05 media: autoselect <full-duplex> status: inactive bridge0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500 options=63<RXCSUM,TXCSUM,TSO4,TSO6> ether e2:00:ec:98:eb:00 Configuration: id 0:0:0:0:0:0 priority 0 hellotime 0 fwddelay 0 maxage 0 holdcnt 0 proto stp maxaddr 100 timeout 1200 root id 0:0:0:0:0:0 priority 0 ifcost 0 port 0 ipfilter disabled flags 0x2 member: en1 flags=3<LEARNING,DISCOVER> ifmaxaddr 0 port 6 priority 0 path cost 0 member: en2 flags=3<LEARNING,DISCOVER> ifmaxaddr 0 port 8 priority 0 path cost 0 member: en3 flags=3<LEARNING,DISCOVER> ifmaxaddr 0 port 7 priority 0 path cost 0 member: en4 flags=3<LEARNING,DISCOVER> ifmaxaddr 0 port 9 priority 0 path cost 0 nd6 options=201<PERFORMNUD,DAD> media: <unknown type> status: inactive p2p0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 2304 ether 0a:4f:43:5b:a5:4c media: autoselect status: inactive awdl0: flags=8943<UP,BROADCAST,RUNNING,PROMISC,SIMPLEX,MULTICAST> mtu 1484 ether 76:77:9e:05:58:d1 inet6 fe80::7477:9eff:fe05:58d1%awdl0 prefixlen 64 scopeid 0xc nd6 options=201<PERFORMNUD,DAD> media: autoselect status: active utun0: flags=8051<UP,POINTOPOINT,RUNNING,MULTICAST> mtu 2000 inet6 fe80::9572:8b04:187d:f5c6%utun0 prefixlen 64 scopeid 0xd nd6 options=201<PERFORMNUD,DAD> utun1: flags=8051<UP,POINTOPOINT,RUNNING,MULTICAST> mtu 1380 inet6 fe80::5fbd:ded2:9ab2:309c%utun1 prefixlen 64 scopeid 0x12 nd6 options=201<PERFORMNUD,DAD> en5: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500 ether ac:de:48:00:11:22 inet6 fe80::aede:48ff:fe00:1122%en5 prefixlen 64 scopeid 0x4 nd6 options=281<PERFORMNUD,INSECURE,DAD> media: autoselect status: active >>:
服務端
starting.... cliet addr ('127.0.0.1', 59162)