一、基礎知識
1. C/S架構
C/S架構即客戶機/服務器模式。
它可以分為客戶機和服務器兩層:
第一層:? 在客戶機系統上結合了界面顯示與業務邏輯;
第二層:? 通過網絡結合了數據庫服務器。
簡單的說就是第一層是用戶表示層,第二層是數據庫層。
這里需要補充的是,客戶端不僅僅是一些簡單的操作,它也是會處理一些運算,業務邏輯的處理等。也就是說,客戶端也做著一些本該由服務器來做的一些事情,如圖所示:
2. TCP/IP模型
互聯網協議按照功能不同分為osi七層或tcp/ip五層或tcp/ip四層
每層運行常見物理設備
我們將應用層,表示層,會話層并作應用層,從tcp/ip五層協議的角度來闡述每層的由來與功能,搞清楚了每層的主要協議就理解了整個互聯網通信的原理。
首先,用戶感知到的只是最上面一層應用層,自上而下每層都依賴于下一層,所以我們從最下一層開始切入,比較好理解
每層都運行特定的協議,越往上越靠近用戶,越往下越靠近硬件
物理層
物理層由來:上面提到,孤立的計算機之間要想一起玩,就必須接入internet,言外之意就是計算機之間必須完成組網
物理層功能:主要是基于電器特性發送高低電壓(電信號),高電壓對應數字1,低電壓對應數字0
數據鏈路層
數據鏈路層由來:單純的電信號0和1沒有任何意義,必須規定電信號多少位一組,每組什么意思
數據鏈路層的功能:定義了電信號的分組方式
以太網協議:
早期的時候各個公司都有自己的分組方式,后來形成了統一的標準,即以太網協議ethernet
ethernet規定
- 一組電信號構成一個數據包,叫做‘幀’
- 每一數據幀分成:報頭head和數據data兩部分
? ? ? ?head | ? ? ? ? ? ? ? ? ? ? ? ?data ? ? ? ? ? ? ? ? ? ? ? ? ? ?? |
?
head包含:(固定18個字節)
- 發送者/源地址,6個字節
- 接收者/目標地址,6個字節
- 數據類型,6個字節
data包含:(最短46字節,最長1500字節)
- 數據包的具體內容
head長度+data長度=最短64字節,最長1518字節,超過最大限制就分片發送
mac地址:
head中包含的源和目標地址由來:ethernet規定接入internet的設備都必須具備網卡,發送端和接收端的地址便是指網卡的地址,即mac地址
mac地址:每塊網卡出廠時都被燒制上一個世界唯一的mac地址,長度為48位2進制,通常由12位16進制數表示(前六位是廠商編號,后六位是流水線號)
廣播:
有了mac地址,同一網絡內的兩臺主機就可以通信了(一臺主機通過arp協議獲取另外一臺主機的mac地址)
ethernet采用最原始的方式,廣播的方式進行通信,即計算機通信基本靠吼
網絡層
網絡層由來:有了ethernet、mac地址、廣播的發送方式,世界上的計算機就可以彼此通信了,問題是世界范圍的互聯網是由
一個個彼此隔離的小的局域網組成的,那么如果所有的通信都采用以太網的廣播方式,那么一臺機器發送的包全世界都會收到,
這就不僅僅是效率低的問題了,這會是一種災難
上圖結論:必須找出一種方法來區分哪些計算機屬于同一廣播域,哪些不是,如果是就采用廣播的方式發送,如果不是,
就采用路由的方式(向不同廣播域/子網分發數據包),mac地址是無法區分的,它只跟廠商有關
網絡層功能:引入一套新的地址用來區分不同的廣播域/子網,這套地址即網絡地址
IP協議:
- 規定網絡地址的協議叫ip協議,它定義的地址稱之為ip地址,廣泛采用的v4版本即ipv4,它規定網絡地址由32位2進制表示
- 范圍0.0.0.0-255.255.255.255
- 一個ip地址通常寫成四段十進制數,例:172.16.10.1
ip地址分成兩部分
- 網絡部分:標識子網
- 主機部分:標識主機
注意:單純的ip地址段只是標識了ip地址的種類,從網絡部分或主機部分都無法辨識一個ip所處的子網
例:172.16.10.1與172.16.10.2并不能確定二者處于同一子網
子網掩碼
所謂”子網掩碼”,就是表示子網絡特征的一個參數。它在形式上等同于IP地址,也是一個32位二進制數字,它的網絡部分全部為1,主機部分全部為0。比如,IP地址172.16.10.1,如果已知網絡部分是前24位,主機部分是后8位,那么子網絡掩碼就是11111111.11111111.11111111.00000000,寫成十進制就是255.255.255.0。
知道”子網掩碼”,我們就能判斷,任意兩個IP地址是否處在同一個子網絡。方法是將兩個IP地址與子網掩碼分別進行AND運算(兩個數位都為1,運算結果為1,否則為0),然后比較結果是否相同,如果是的話,就表明它們在同一個子網絡中,否則就不是。
比如,已知IP地址172.16.10.1和172.16.10.2的子網掩碼都是255.255.255.0,請問它們是否在同一個子網絡?兩者與子網掩碼分別進行AND運算,
172.16.10.1:10101100.00010000.00001010.000000001
255255.255.255.0:11111111.11111111.11111111.00000000
AND運算得網絡地址結果:10101100.00010000.00001010.000000001->172.16.10.0
172.16.10.2:10101100.00010000.00001010.000000010
255255.255.255.0:11111111.11111111.11111111.00000000
AND運算得網絡地址結果:10101100.00010000.00001010.000000001->172.16.10.0
結果都是172.16.10.0,因此它們在同一個子網絡。
總結一下,IP協議的作用主要有兩個,一個是為每一臺計算機分配IP地址,另一個是確定哪些地址在同一個子網絡。
ip數據包
ip數據包也分為head和data部分,無須為ip包定義單獨的欄位,直接放入以太網包的data部分
head:長度為20到60字節
data:最長為65,515字節。
而以太網數據包的”數據”部分,最長只有1500字節。因此,如果IP數據包超過了1500字節,它就需要分割成幾個以太網數據包,分開發送了。
以太網頭 ?? | ? ? ? ? ? ? ip 頭? | ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?ip數據 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |
?
ARP協議
arp協議由來:計算機通信基本靠吼,即廣播的方式,所有上層的包到最后都要封裝上以太網頭,然后通過以太網協議發送,在談及以太網協議時候,我門了解到
通信是基于mac的廣播方式實現,計算機在發包時,獲取自身的mac是容易的,如何獲取目標主機的mac,就需要通過arp協議
arp協議功能:廣播的方式發送數據包,獲取目標主機的mac地址
協議工作方式:每臺主機ip都是已知的
例如:主機172.16.10.10/24訪問172.16.10.11/24
一:首先通過ip地址和子網掩碼區分出自己所處的子網
場景 | 數據包地址 |
同一子網 | 目標主機mac,目標主機ip |
不同子網 | 網關mac,目標主機ip |
?
?
?
?
二:分析172.16.10.10/24與172.16.10.11/24處于同一網絡(如果不是同一網絡,那么下表中目標ip為172.16.10.1,通過arp獲取的是網關的mac
? | 源mac | 目標mac | 源ip | 目標ip | 數據部分 |
發送端主機 | 發送端mac | FF:FF:FF:FF:FF:FF | 172.16.10.10/24 | 172.16.10.11/24 | 數據 |
?
?
?
三:這個包會以廣播的方式在發送端所處的自網內傳輸,所有主機接收后拆開包,發現目標ip為自己的,就響應,返回自己的mac
傳輸層
傳輸層的由來:網絡層的ip幫我們區分子網,以太網層的mac幫我們找到主機,然后大家使用的都是應用程序,你的電腦上可能同時開啟qq,暴風影音,等多個應用程序,
那么我們通過ip和mac找到了一臺特定的主機,如何標識這臺主機上的應用程序,答案就是端口,端口即應用程序與網卡關聯的編號。
傳輸層功能:建立端口到端口的通信
補充:端口范圍0-65535,0-1023為系統占用端口
tcp協議:
可靠傳輸,TCP數據包沒有長度限制,理論上可以無限長,但是為了保證網絡的效率,通常TCP數據包的長度不會超過IP數據包的長度,以確保單個TCP數據包不必再分割。
以太網頭 | ip 頭 ? ? ? ? ? ? ? | tcp頭 ? ? ? ? ? ? ? | 數據 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |
?
udp協議:
不可靠傳輸,”報頭”部分一共只有8個字節,總長度不超過65,535字節,正好放進一個IP數據包。
以太網頭 | ip頭 ? ? ? ? ? ? ? ?? | ? ? udp頭 ? ? ? ? ? ? ? ? ? ? ? ? ?? | 數據 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?? |
?
tcp報文
tcp三次握手和四次揮手
應用層
應用層由來:用戶使用的都是應用程序,均工作于應用層,互聯網是開發的,大家都可以開發自己的應用程序,數據多種多樣,必須規定好數據的組織形式
應用層功能:規定應用程序的數據格式。
例:TCP協議可以為各種各樣的程序傳遞數據,比如Email、WWW、FTP等等。那么,必須有不同協議規定電子郵件、網頁、FTP數據的格式,這些應用程序協議就構成了”應用層”。
?
3. socket層
Socket是應用層與TCP/IP協議族通信的中間軟件抽象層,它是一組接口。在設計模式中,Socket其實就是一個門面模式,它把復雜的TCP/IP協議族隱藏在Socket接口后面,對用戶來說,一組簡單的接口就是全部,讓Socket去組織數據,以符合指定的協議。
所以,我們無需深入理解tcp/udp協議,socket已經為我們封裝好了,我們只需要遵循socket的規定去編程,寫出的程序自然就是遵循tcp/udp標準的。
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
二、套接字
1. 套接字的分類
套接字起源于 20 世紀 70 年代加利福尼亞大學伯克利分校版本的 Unix,即人們所說的 BSD Unix。 因此,有時人們也把套接字稱為“伯克利套接字”或“BSD 套接字”。一開始,套接字被設計用在同 一臺主機上多個應用程序之間的通訊。這也被稱進程間通訊,或 IPC。套接字有兩種(或者稱為有兩個種族),分別是基于文件型的和基于網絡型的。?
基于文件類型的套接字家族
套接字家族的名字:AF_UNIX
unix一切皆文件,基于文件的套接字調用的就是底層的文件系統來取數據,兩個套接字進程運行在同一機器,可以通過訪問同一個文件系統間接完成通信
基于網絡類型的套接字家族套接字家族的名字:AF_INET
(還有AF_INET6被用于ipv6,還有一些其他的地址家族,不過,他們要么是只用于某個平臺,要么就是已經被廢棄,或者是很少被使用,或者是根本沒有實現,所有地址家族中,AF_INET是使用最廣泛的一個,python支持很多種地址家族,但是由于我們只關心網絡編程,所以大部分時候我么只使用AF_INET)
服務端套接字函數
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() 創建一個與該套接字相關的文件
2.基于TCP的套接字
服務端:
from socket import *ip_port = ('192.168.50.85', 8000)
sever = socket(AF_INET, SOCK_STREAM) # AF_INET 基于網絡 SOCK_STREAM 基于TCP
sever.bind(ip_port) # 綁定到套接字
sever.listen(5) # 開始監聽
while True:conn, addr = sever.accept() # 等待連接while True:try:msg = conn.recv(1024) # 收消息print('客戶端%s發來的消息:%s' % (addr, msg.decode('utf-8')))conn.send(msg.upper()) # 發消息except Exception:breakconn.close()
sever.close()
客戶端:
from socket import *
client = socket(AF_INET, SOCK_STREAM)
client.connect(('192.168.50.85', 8000)) # 連接到服務器
while True:msg = input('>>:').strip()client.send(msg.encode('utf-8')) # 不能發送str,必須是二進制格式data = client.recv(1024)print('收到服務端的消息', data.decode('utf-8'))
client.close()
基于TCP遠程執行命令


1 from socket import * 2 import subprocess 3 4 ip_port = ('192.168.50.85', 8080) 5 sever = socket(AF_INET, SOCK_STREAM) 6 sever.bind(ip_port) 7 sever.listen(5) 8 while True: 9 conn, addr = sever.accept() 10 while True: 11 try: 12 cmd = conn.recv(1024) 13 if not cmd: 14 break 15 print('收到客戶端的命令', cmd) 16 # 執行命令,得到cmd_res 17 res = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE, 18 stderr=subprocess.PIPE) 19 err = res.stderr.read() 20 if err: 21 cmd_res = err 22 else: 23 cmd_res = res.stdout.read() 24 conn.send(cmd_res) 25 except Exception as e: 26 print(e) 27 break 28 conn.close()


1 from socket import * 2 ip_port = ('192.168.50.85', 8080) 3 client = socket(AF_INET, SOCK_STREAM) 4 client.connect(ip_port) 5 while True: 6 cmd = input('請輸入命令:').strip() 7 if not cmd: 8 continue 9 if cmd == 'quit': 10 break 11 client.send(cmd.encode('utf-8')) 12 data = client.recv(1024) 13 print('得到的結果是', data.decode('gbk')) 14 client.close()


1 請輸入命令:dir 2 得到的結果是 驅動器 D 中的卷是 DATA 3 卷的序列號是 A473-AA80 4 5 D:\python\Project\python基礎\網絡編程 的目錄 6 7 2019/07/19 15:16 <DIR> . 8 2019/07/19 15:16 <DIR> .. 9 2019/07/19 15:13 864 TCP客戶端.py 10 2019/07/19 15:16 1,456 TCP服務端.py 11 2019/07/19 14:00 675 UDP客戶端1.py 12 2019/07/19 14:00 354 UDP客戶端2.py 13 2019/07/19 14:21 278 UDP服務端.py 14 5 個文件 3,627 字節 15 2 個目錄 200,399,257,600 可用字節
在重啟服務端時可能會遇到
這個是由于服務端仍然存在四次揮手的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))


1 發現系統存在大量TIME_WAIT狀態的連接,通過調整linux內核參數解決, 2 vi /etc/sysctl.conf 3 4 編輯文件,加入以下內容: 5 net.ipv4.tcp_syncookies = 1 6 net.ipv4.tcp_tw_reuse = 1 7 net.ipv4.tcp_tw_recycle = 1 8 net.ipv4.tcp_fin_timeout = 30 9 10 然后執行 /sbin/sysctl -p 讓參數生效。 11 12 net.ipv4.tcp_syncookies = 1 表示開啟SYN Cookies。當出現SYN等待隊列溢出時,啟用cookies來處理,可防范少量SYN攻擊,默認為0,表示關閉; 13 14 net.ipv4.tcp_tw_reuse = 1 表示開啟重用。允許將TIME-WAIT sockets重新用于新的TCP連接,默認為0,表示關閉; 15 16 net.ipv4.tcp_tw_recycle = 1 表示開啟TCP連接中TIME-WAIT sockets的快速回收,默認為0,表示關閉。 17 18 net.ipv4.tcp_fin_timeout 修改系統默認的 TIMEOUT 時間
3. 基于UDP的套接字
服務端:
from socket import *ip_port = ('192.168.50.85', 8080)
sever = socket(AF_INET, SOCK_DGRAM)
sever.bind(ip_port)
while True:msg, addr = sever.recvfrom(1024)print(msg, addr)sever.sendto(msg.upper(), addr)
客戶端:
from socket import *
ip_port = ('192.168.50.85', 8080)
client = socket(AF_INET, SOCK_DGRAM)
while True:msg = input('>>:').strip()if not msg:continueclient.sendto(msg.encode('utf-8'), ip_port)msg, addr = client.recvfrom(1024)print(msg.decode('utf-8'), addr)
基于UDP遠程執行命令


1 from socket import * 2 import subprocess 3 4 ip_port = ('192.168.50.85', 8080) 5 server = socket(AF_INET, SOCK_DGRAM) 6 server.bind(ip_port) 7 while True: 8 cmd, addr = server.recvfrom(1024) 9 res = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) 10 err = res.stderr.read() 11 if err: 12 cmd_res = err 13 else: 14 cmd_res = res.stdout.read() 15 server.sendto(cmd_res, addr)


1 from socket import * 2 3 ip_port = ('192.168.50.85', 8080) 4 client = socket(AF_INET, SOCK_DGRAM) 5 while True: 6 msg = input('>>:').strip() 7 if not msg: 8 continue 9 client.sendto(msg.encode('utf-8'), ip_port) 10 data, addr = client.recvfrom(1024) 11 print('得到結果:', data.decode('gbk')) 12 client.close()


1 >>:dir 2 得到結果: 驅動器 D 中的卷是 DATA 3 卷的序列號是 A473-AA80 4 5 D:\python\Project\python基礎\網絡編程 的目錄 6 7 2019/07/19 16:40 <DIR> . 8 2019/07/19 16:40 <DIR> .. 9 2019/07/19 15:24 862 TCP客戶端.py 10 2019/07/19 15:29 1,523 TCP服務端.py 11 2019/07/19 16:40 709 UDP客戶端1.py 12 2019/07/19 14:00 354 UDP客戶端2.py 13 2019/07/19 16:35 767 UDP服務端.py 14 5 個文件 4,215 字節 15 2 個目錄 200,399,253,504 可用字節
三、粘包
1. 什么是粘包
須知:只有TCP有粘包現象,UDP永遠不會粘包,為何,且聽我娓娓道來
首先需要掌握一個socket收發消息的原理
?
發送端可以是一K一K地發送數據,而接收端的應用程序可以兩K兩K地提走數據,當然也有可能一次提走3K或6K數據,或者一次只提走幾個字節的數據,也就是說,應用程序所看到的數據是一個整體,或說是一個流(stream),一條消息有多少字節對應用程序是不可見的,因此TCP協議是面向流的協議,這也是容易出現粘包問題的原因。而UDP是面向消息的協議,每個UDP段都是一條消息,應用程序必須以消息為單位提取數據,不能一次提取任意字節的數據,這一點和TCP是很不同的。怎樣定義消息呢?可以認為對方一次性write/send的數據為一個消息,需要明白的是當對方send一條信息的時候,無論底層怎樣分段分片,TCP協議層會把構成整條消息的數據段排序完成后才呈現在內核緩沖區。
例如基于tcp的套接字客戶端往服務端上傳文件,發送時文件內容是按照一段一段的字節流發送的,在接收方看了,根本不知道該文件的字節流從何處開始,在何處結束
所謂粘包問題主要還是因為接收方不知道消息之間的界限,不知道一次性提取多少字節的數據所造成的。
此外,發送方引起的粘包是由TCP協議本身造成的,TCP為提高傳輸效率,發送方往往要收集到足夠多的數據后才發送一個TCP段。若連續幾次需要send的數據都很少,通常TCP會根據優化算法把這些數據合成一個TCP段后一次發送出去,這樣接收方就收到了粘包數據。
- TCP(transport control protocol,傳輸控制協議)是面向連接的,面向流的,提供高可靠性服務。收發兩端(客戶端和服務器端)都要有一一成對的socket,因此,發送端為了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle算法),將多次間隔較小且數據量小的數據,合并成一個大的數據塊,然后進行封包。這樣,接收端,就難于分辨出來了,必須提供科學的拆包機制。 即面向流的通信是無消息保護邊界的。
- UDP(user datagram protocol,用戶數據報協議)是無連接的,面向消息的,提供高效率服務。不會使用塊的合并優化算法,, 由于UDP支持的是一對多的模式,所以接收端的skbuff(套接字緩沖區)采用了鏈式結構來記錄每一個到達的UDP包,在每個UDP包中就有了消息頭(消息來源地址,端口等信息),這樣,對于接收端來說,就容易進行區分處理了。?即面向消息的通信是有消息保護邊界的。
- tcp是基于數據流的,于是收發的消息不能為空,這就需要在客戶端和服務端都添加空消息的處理機制,防止程序卡住,而udp是基于數據報的,即便是你輸入的是空內容(直接回車),那也不是空消息,udp協議會幫你封裝上消息頭,實驗略
udp的recvfrom是阻塞的,一個recvfrom(x)必須對唯一一個sendinto(y),收完了x個字節的數據就算完成,若是y>x數據就丟失,這意味著udp根本不會粘包,但是會丟數據,不可靠
tcp的協議數據不會丟,沒有收完包,下次接收,會繼續上次繼續接收,己端總是在收到ack時才會清除緩沖區內容。數據是可靠的,但是會粘包。
兩種情況下會發生粘包
- 發送端需要等緩沖區滿才發送出去,造成粘包(發送數據時間間隔很短,數據量很小,會合到一起,產生粘包)
# 服務端
from socket import *
ip_port = ('192.168.50.85', 8000)
sever = socket(AF_INET, SOCK_STREAM) # AF_INET 基于網絡 SOCK_STREAM 基于TCP
sever.bind(ip_port) # 綁定到套接字
sever.listen(5) # 開始監聽
while True:conn, addr = sever.accept() # 等待連接msg1 = conn.recv(1024)print('第一次收到的數據', msg1)msg2 = conn.recv(1024)print('第二次收到的數據', msg2)msg3 = conn.recv(1024)print('第三次收到的數據', msg3)conn.close()# 客戶端
from socket import *
client = socket(AF_INET, SOCK_STREAM)
client.connect(('192.168.50.85', 8000)) # 連接到服務器
client.send(b'hello')
client.send(b'world')
client.send(b'123')# 結果
第一次收到的數據 b'helloworld123'
第二次收到的數據 b''
第三次收到的數據 b''
- 接收方不及時接收緩沖區的包,造成多個包接收(客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收的時候還是從緩沖區拿上次遺留的數據,產生粘包)
# 服務端
from socket import *
ip_port = ('192.168.50.85', 8000)
sever = socket(AF_INET, SOCK_STREAM) # AF_INET 基于網絡 SOCK_STREAM 基于TCP
sever.bind(ip_port) # 綁定到套接字
sever.listen(5) # 開始監聽
while True:conn, addr = sever.accept() # 等待連接msg1 = conn.recv(1)print('第一次收到的數據', msg1)msg2 = conn.recv(3)print('第二次收到的數據', msg2)msg3 = conn.recv(5)print('第三次收到的數據', msg3)conn.close()# 客戶端
from socket import *
client = socket(AF_INET, SOCK_STREAM)
client.connect(('192.168.50.85', 8000)) # 連接到服務器
client.send(b'helloworld')# 結果
第一次收到的數據 b'h'
第二次收到的數據 b'ell'
第三次收到的數據 b'oworl'
2. 解決粘包的方法
1. 接收端不知道發送端將要傳送的字節流的長度,所以解決粘包的方法就是圍繞,如何讓發送端在發送數據前,把自己將要發送的字節流總大小讓接收端知曉,然后接收端來一個死循環接收完所有數據


from socket import * import subprocess buffer_size = 1024 ip_port = ('192.168.50.85', 8000) sever = socket(AF_INET, SOCK_STREAM) sever.bind(ip_port) sever.listen(5) while True:conn, addr = sever.accept()while True:try:cmd = conn.recv(buffer_size)if not cmd:breakprint('收到客戶端的命令', cmd)# 執行命令,得到cmd_resres = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE,stderr=subprocess.PIPE)err = res.stderr.read()if err:cmd_res = errelse:cmd_res = res.stdout.read()# 解決粘包問題length = len(cmd_res)conn.send(str(length).encode('utf-8'))client_ready = conn.recv(buffer_size)if client_ready ==b'ready':conn.send(cmd_res)except Exception as e:print(e)breakconn.close()


1 from socket import * 2 3 buffer_size = 1024 4 ip_port = ('192.168.50.85', 8000) 5 client = socket(AF_INET, SOCK_STREAM) 6 client.connect(ip_port) 7 while True: 8 cmd = input('請輸入命令:').strip() 9 if not cmd: 10 continue 11 if cmd == 'quit': 12 break 13 client.send(cmd.encode('utf-8')) 14 15 # 解決粘包 16 length = client.recv(buffer_size) 17 client.send(b'ready') 18 length = int(length.decode('utf-8')) 19 recv_size = 0 20 recv_msg = b'' 21 while recv_size < length: 22 recv_msg += client.recv(buffer_size) 23 recv_size = len(recv_msg) 24 25 print('得到的結果是', recv_msg.decode('gbk')) 26 client.close()
程序的運行速度遠快于網絡傳輸速度,所以在發送一段字節前,先用send去發送該字節流長度,這種方式會放大網絡延遲帶來的性能損耗
2. 為字節流加上自定義固定長度報頭,報頭中包含字節流長度,然后一次send到對端,對端在接收時,先從緩存中取出定長的報頭,然后再取真實數據


1 from socket import * 2 import struct 3 import subprocess 4 buffer_size = 1024 5 ip_port = ('192.168.50.85', 8000) 6 sever = socket(AF_INET, SOCK_STREAM) 7 sever.bind(ip_port) 8 sever.listen(5) 9 while True: 10 conn, addr = sever.accept() 11 while True: 12 try: 13 cmd = conn.recv(buffer_size) 14 if not cmd: 15 break 16 print('收到客戶端的命令', cmd) 17 # 執行命令,得到cmd_res 18 res = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE, 19 stderr=subprocess.PIPE) 20 err = res.stderr.read() 21 if err: 22 cmd_res = err 23 else: 24 cmd_res = res.stdout.read() 25 26 # 解決粘包問題 27 length = len(cmd_res) 28 29 data_length = struct.pack('i', length) 30 conn.send(data_length) 31 conn.send(cmd_res) 32 33 except Exception as e: 34 print(e) 35 break 36 conn.close()


1 from socket import * 2 import struct 3 4 buffer_size = 1024 5 ip_port = ('192.168.50.85', 8000) 6 client = socket(AF_INET, SOCK_STREAM) 7 client.connect(ip_port) 8 while True: 9 cmd = input('請輸入命令:').strip() 10 if not cmd: 11 continue 12 if cmd == 'quit': 13 break 14 client.send(cmd.encode('utf-8')) 15 16 # 解決粘包 17 data_length = client.recv(4) 18 length = struct.unpack('i', data_length)[0] 19 recv_size = 0 20 recv_msg = b'' 21 while recv_size < length: 22 recv_msg += client.recv(buffer_size) 23 recv_size = len(recv_msg) 24 # recv_msg = client.recv(length) 一次接收所有數據 25 print('得到的結果是', recv_msg.decode('gbk')) 26 client.close()
struct模塊
該模塊可以把一個類型,如數字,轉成固定長度的bytes
l = struct.pack('i',1111111111111)
反解
struct.unpack('i', l)
四、socketserver模塊實現并發
import socketserverclass MyServer(socketserver.BaseRequestHandler):def handle(self):print('conn is:', self.request) # connprint('addr is:', self.client_address) # addrwhile True:try:# 收消息data = self.request.recv(1024)if not data:breakprint('收到的消息是:', data)# 發消息self.request.sendall(data.upper())except Exception:breakif __name__ == '__main__':s = socketserver.ThreadingTCPServer(('192.168.50.85', 8000), MyServer)s.serve_forever()
?