Python-21-socket编程
一、基礎(chǔ)知識
1. C/S架構(gòu)
C/S架構(gòu)即客戶機/服務(wù)器模式。
它可以分為客戶機和服務(wù)器兩層:
第一層:? 在客戶機系統(tǒng)上結(jié)合了界面顯示與業(yè)務(wù)邏輯;
第二層:? 通過網(wǎng)絡(luò)結(jié)合了數(shù)據(jù)庫服務(wù)器。
簡單的說就是第一層是用戶表示層,第二層是數(shù)據(jù)庫層。
這里需要補充的是,客戶端不僅僅是一些簡單的操作,它也是會處理一些運算,業(yè)務(wù)邏輯的處理等。也就是說,客戶端也做著一些本該由服務(wù)器來做的一些事情,如圖所示:
2. TCP/IP模型
互聯(lián)網(wǎng)協(xié)議按照功能不同分為osi七層或tcp/ip五層或tcp/ip四層
每層運行常見物理設(shè)備
我們將應(yīng)用層,表示層,會話層并作應(yīng)用層,從tcp/ip五層協(xié)議的角度來闡述每層的由來與功能,搞清楚了每層的主要協(xié)議就理解了整個互聯(lián)網(wǎng)通信的原理。
首先,用戶感知到的只是最上面一層應(yīng)用層,自上而下每層都依賴于下一層,所以我們從最下一層開始切入,比較好理解
每層都運行特定的協(xié)議,越往上越靠近用戶,越往下越靠近硬件
物理層
物理層由來:上面提到,孤立的計算機之間要想一起玩,就必須接入internet,言外之意就是計算機之間必須完成組網(wǎng)
物理層功能:主要是基于電器特性發(fā)送高低電壓(電信號),高電壓對應(yīng)數(shù)字1,低電壓對應(yīng)數(shù)字0
數(shù)據(jù)鏈路層
數(shù)據(jù)鏈路層由來:單純的電信號0和1沒有任何意義,必須規(guī)定電信號多少位一組,每組什么意思
數(shù)據(jù)鏈路層的功能:定義了電信號的分組方式
以太網(wǎng)協(xié)議:
早期的時候各個公司都有自己的分組方式,后來形成了統(tǒng)一的標(biāo)準(zhǔn),即以太網(wǎng)協(xié)議ethernet
ethernet規(guī)定
- 一組電信號構(gòu)成一個數(shù)據(jù)包,叫做‘幀’
- 每一數(shù)據(jù)幀分成:報頭head和數(shù)據(jù)data兩部分
| ? ? ? ?head | ? ? ? ? ? ? ? ? ? ? ? ?data ? ? ? ? ? ? ? ? ? ? ? ? ? ?? |
?
head包含:(固定18個字節(jié))
- 發(fā)送者/源地址,6個字節(jié)
- 接收者/目標(biāo)地址,6個字節(jié)
- 數(shù)據(jù)類型,6個字節(jié)
data包含:(最短46字節(jié),最長1500字節(jié))
- 數(shù)據(jù)包的具體內(nèi)容
head長度+data長度=最短64字節(jié),最長1518字節(jié),超過最大限制就分片發(fā)送
mac地址:
head中包含的源和目標(biāo)地址由來:ethernet規(guī)定接入internet的設(shè)備都必須具備網(wǎng)卡,發(fā)送端和接收端的地址便是指網(wǎng)卡的地址,即mac地址
mac地址:每塊網(wǎng)卡出廠時都被燒制上一個世界唯一的mac地址,長度為48位2進制,通常由12位16進制數(shù)表示(前六位是廠商編號,后六位是流水線號)
廣播:
有了mac地址,同一網(wǎng)絡(luò)內(nèi)的兩臺主機就可以通信了(一臺主機通過arp協(xié)議獲取另外一臺主機的mac地址)
ethernet采用最原始的方式,廣播的方式進行通信,即計算機通信基本靠吼
網(wǎng)絡(luò)層
網(wǎng)絡(luò)層由來:有了ethernet、mac地址、廣播的發(fā)送方式,世界上的計算機就可以彼此通信了,問題是世界范圍的互聯(lián)網(wǎng)是由
一個個彼此隔離的小的局域網(wǎng)組成的,那么如果所有的通信都采用以太網(wǎng)的廣播方式,那么一臺機器發(fā)送的包全世界都會收到,
這就不僅僅是效率低的問題了,這會是一種災(zāi)難
上圖結(jié)論:必須找出一種方法來區(qū)分哪些計算機屬于同一廣播域,哪些不是,如果是就采用廣播的方式發(fā)送,如果不是,
就采用路由的方式(向不同廣播域/子網(wǎng)分發(fā)數(shù)據(jù)包),mac地址是無法區(qū)分的,它只跟廠商有關(guān)
網(wǎng)絡(luò)層功能:引入一套新的地址用來區(qū)分不同的廣播域/子網(wǎng),這套地址即網(wǎng)絡(luò)地址
IP協(xié)議:
- 規(guī)定網(wǎng)絡(luò)地址的協(xié)議叫ip協(xié)議,它定義的地址稱之為ip地址,廣泛采用的v4版本即ipv4,它規(guī)定網(wǎng)絡(luò)地址由32位2進制表示
- 范圍0.0.0.0-255.255.255.255
- 一個ip地址通常寫成四段十進制數(shù),例:172.16.10.1
ip地址分成兩部分
- 網(wǎng)絡(luò)部分:標(biāo)識子網(wǎng)
- 主機部分:標(biāo)識主機
注意:單純的ip地址段只是標(biāo)識了ip地址的種類,從網(wǎng)絡(luò)部分或主機部分都無法辨識一個ip所處的子網(wǎng)
例:172.16.10.1與172.16.10.2并不能確定二者處于同一子網(wǎng)
子網(wǎng)掩碼
所謂”子網(wǎng)掩碼”,就是表示子網(wǎng)絡(luò)特征的一個參數(shù)。它在形式上等同于IP地址,也是一個32位二進制數(shù)字,它的網(wǎng)絡(luò)部分全部為1,主機部分全部為0。比如,IP地址172.16.10.1,如果已知網(wǎng)絡(luò)部分是前24位,主機部分是后8位,那么子網(wǎng)絡(luò)掩碼就是11111111.11111111.11111111.00000000,寫成十進制就是255.255.255.0。
知道”子網(wǎng)掩碼”,我們就能判斷,任意兩個IP地址是否處在同一個子網(wǎng)絡(luò)。方法是將兩個IP地址與子網(wǎng)掩碼分別進行AND運算(兩個數(shù)位都為1,運算結(jié)果為1,否則為0),然后比較結(jié)果是否相同,如果是的話,就表明它們在同一個子網(wǎng)絡(luò)中,否則就不是。
比如,已知IP地址172.16.10.1和172.16.10.2的子網(wǎng)掩碼都是255.255.255.0,請問它們是否在同一個子網(wǎng)絡(luò)?兩者與子網(wǎng)掩碼分別進行AND運算,
172.16.10.1:10101100.00010000.00001010.000000001
255255.255.255.0:11111111.11111111.11111111.00000000
AND運算得網(wǎng)絡(luò)地址結(jié)果: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運算得網(wǎng)絡(luò)地址結(jié)果:10101100.00010000.00001010.000000001->172.16.10.0
結(jié)果都是172.16.10.0,因此它們在同一個子網(wǎng)絡(luò)。
總結(jié)一下,IP協(xié)議的作用主要有兩個,一個是為每一臺計算機分配IP地址,另一個是確定哪些地址在同一個子網(wǎng)絡(luò)。
ip數(shù)據(jù)包
ip數(shù)據(jù)包也分為head和data部分,無須為ip包定義單獨的欄位,直接放入以太網(wǎng)包的data部分
head:長度為20到60字節(jié)
data:最長為65,515字節(jié)。
而以太網(wǎng)數(shù)據(jù)包的”數(shù)據(jù)”部分,最長只有1500字節(jié)。因此,如果IP數(shù)據(jù)包超過了1500字節(jié),它就需要分割成幾個以太網(wǎng)數(shù)據(jù)包,分開發(fā)送了。
| 以太網(wǎng)頭 ?? | ? ? ? ? ? ? ip 頭? | ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?ip數(shù)據(jù) ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |
?
ARP協(xié)議
arp協(xié)議由來:計算機通信基本靠吼,即廣播的方式,所有上層的包到最后都要封裝上以太網(wǎng)頭,然后通過以太網(wǎng)協(xié)議發(fā)送,在談及以太網(wǎng)協(xié)議時候,我門了解到
通信是基于mac的廣播方式實現(xiàn),計算機在發(fā)包時,獲取自身的mac是容易的,如何獲取目標(biāo)主機的mac,就需要通過arp協(xié)議
arp協(xié)議功能:廣播的方式發(fā)送數(shù)據(jù)包,獲取目標(biāo)主機的mac地址
協(xié)議工作方式:每臺主機ip都是已知的
例如:主機172.16.10.10/24訪問172.16.10.11/24
一:首先通過ip地址和子網(wǎng)掩碼區(qū)分出自己所處的子網(wǎng)
| 場景 | 數(shù)據(jù)包地址 |
| 同一子網(wǎng) | 目標(biāo)主機mac,目標(biāo)主機ip |
| 不同子網(wǎng) | 網(wǎng)關(guān)mac,目標(biāo)主機ip |
?
?
?
?
二:分析172.16.10.10/24與172.16.10.11/24處于同一網(wǎng)絡(luò)(如果不是同一網(wǎng)絡(luò),那么下表中目標(biāo)ip為172.16.10.1,通過arp獲取的是網(wǎng)關(guān)的mac
| ? | 源mac | 目標(biāo)mac | 源ip | 目標(biāo)ip | 數(shù)據(jù)部分 |
| 發(fā)送端主機 | 發(fā)送端mac | FF:FF:FF:FF:FF:FF | 172.16.10.10/24 | 172.16.10.11/24 | 數(shù)據(jù) |
?
?
?
三:這個包會以廣播的方式在發(fā)送端所處的自網(wǎng)內(nèi)傳輸,所有主機接收后拆開包,發(fā)現(xiàn)目標(biāo)ip為自己的,就響應(yīng),返回自己的mac
傳輸層
傳輸層的由來:網(wǎng)絡(luò)層的ip幫我們區(qū)分子網(wǎng),以太網(wǎng)層的mac幫我們找到主機,然后大家使用的都是應(yīng)用程序,你的電腦上可能同時開啟qq,暴風(fēng)影音,等多個應(yīng)用程序,
那么我們通過ip和mac找到了一臺特定的主機,如何標(biāo)識這臺主機上的應(yīng)用程序,答案就是端口,端口即應(yīng)用程序與網(wǎng)卡關(guān)聯(lián)的編號。
傳輸層功能:建立端口到端口的通信
補充:端口范圍0-65535,0-1023為系統(tǒng)占用端口
tcp協(xié)議:
可靠傳輸,TCP數(shù)據(jù)包沒有長度限制,理論上可以無限長,但是為了保證網(wǎng)絡(luò)的效率,通常TCP數(shù)據(jù)包的長度不會超過IP數(shù)據(jù)包的長度,以確保單個TCP數(shù)據(jù)包不必再分割。
| 以太網(wǎng)頭 | ip 頭 ? ? ? ? ? ? ? | tcp頭 ? ? ? ? ? ? ? | 數(shù)據(jù) ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |
?
udp協(xié)議:
不可靠傳輸,”報頭”部分一共只有8個字節(jié),總長度不超過65,535字節(jié),正好放進一個IP數(shù)據(jù)包。
| 以太網(wǎng)頭 | ip頭 ? ? ? ? ? ? ? ?? | ? ? udp頭 ? ? ? ? ? ? ? ? ? ? ? ? ?? | 數(shù)據(jù) ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?? |
?
tcp報文
tcp三次握手和四次揮手
應(yīng)用層
應(yīng)用層由來:用戶使用的都是應(yīng)用程序,均工作于應(yīng)用層,互聯(lián)網(wǎng)是開發(fā)的,大家都可以開發(fā)自己的應(yīng)用程序,數(shù)據(jù)多種多樣,必須規(guī)定好數(shù)據(jù)的組織形式
應(yīng)用層功能:規(guī)定應(yīng)用程序的數(shù)據(jù)格式。
例:TCP協(xié)議可以為各種各樣的程序傳遞數(shù)據(jù),比如Email、WWW、FTP等等。那么,必須有不同協(xié)議規(guī)定電子郵件、網(wǎng)頁、FTP數(shù)據(jù)的格式,這些應(yīng)用程序協(xié)議就構(gòu)成了”應(yīng)用層”。
?
3. socket層
Socket是應(yīng)用層與TCP/IP協(xié)議族通信的中間軟件抽象層,它是一組接口。在設(shè)計模式中,Socket其實就是一個門面模式,它把復(fù)雜的TCP/IP協(xié)議族隱藏在Socket接口后面,對用戶來說,一組簡單的接口就是全部,讓Socket去組織數(shù)據(jù),以符合指定的協(xié)議。
所以,我們無需深入理解tcp/udp協(xié)議,socket已經(jīng)為我們封裝好了,我們只需要遵循socket的規(guī)定去編程,寫出的程序自然就是遵循tcp/udp標(biāo)準(zhǔn)的。
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
二、套接字
1. 套接字的分類
套接字起源于 20 世紀(jì) 70 年代加利福尼亞大學(xué)伯克利分校版本的 Unix,即人們所說的 BSD Unix。 因此,有時人們也把套接字稱為“伯克利套接字”或“BSD 套接字”。一開始,套接字被設(shè)計用在同 一臺主機上多個應(yīng)用程序之間的通訊。這也被稱進程間通訊,或 IPC。套接字有兩種(或者稱為有兩個種族),分別是基于文件型的和基于網(wǎng)絡(luò)型的。?
基于文件類型的套接字家族
套接字家族的名字:AF_UNIX
unix一切皆文件,基于文件的套接字調(diào)用的就是底層的文件系統(tǒng)來取數(shù)據(jù),兩個套接字進程運行在同一機器,可以通過訪問同一個文件系統(tǒng)間接完成通信
基于網(wǎng)絡(luò)類型的套接字家族套接字家族的名字:AF_INET
(還有AF_INET6被用于ipv6,還有一些其他的地址家族,不過,他們要么是只用于某個平臺,要么就是已經(jīng)被廢棄,或者是很少被使用,或者是根本沒有實現(xiàn),所有地址家族中,AF_INET是使用最廣泛的一個,python支持很多種地址家族,但是由于我們只關(guān)心網(wǎng)絡(luò)編程,所以大部分時候我么只使用AF_INET)
服務(wù)端套接字函數(shù)s.bind() 綁定(主機,端口號)到套接字
s.listen() 開始TCP監(jiān)聽
s.accept() 被動接受TCP客戶的連接,(阻塞式)等待連接的到來
客戶端套接字函數(shù)
s.connect() 主動初始化TCP服務(wù)器連接
s.connect_ex() connect()函數(shù)的擴展版本,出錯時返回出錯碼,而不是拋出異常
公共用途的套接字函數(shù)
s.recv() 接收TCP數(shù)據(jù)
s.send() 發(fā)送TCP數(shù)據(jù)(send在待發(fā)送數(shù)據(jù)量大于己端緩存區(qū)剩余空間時,數(shù)據(jù)丟失,不會發(fā)完)
s.sendall() 發(fā)送完整的TCP數(shù)據(jù)(本質(zhì)就是循環(huán)調(diào)用send,sendall在待發(fā)送數(shù)據(jù)量大于己端緩存區(qū)剩余空間時,數(shù)據(jù)不丟失,循環(huán)調(diào)用send直到發(fā)完)
s.recvfrom() 接收UDP數(shù)據(jù)
s.sendto() 發(fā)送UDP數(shù)據(jù)
s.getpeername() 連接到當(dāng)前套接字的遠端的地址
s.getsockname() 當(dāng)前套接字的地址
s.getsockopt() 返回指定套接字的參數(shù)
s.setsockopt() 設(shè)置指定套接字的參數(shù)
s.close() 關(guān)閉套接字
面向鎖的套接字方法
s.setblocking() 設(shè)置套接字的阻塞與非阻塞模式
s.settimeout() 設(shè)置阻塞套接字操作的超時時間
s.gettimeout() 得到阻塞套接字操作的超時時間
面向文件的套接字的函數(shù)
s.fileno() 套接字的文件描述符
s.makefile() 創(chuàng)建一個與該套接字相關(guān)的文件
2.基于TCP的套接字
服務(wù)端:
from socket import *ip_port = ('192.168.50.85', 8000) sever = socket(AF_INET, SOCK_STREAM) # AF_INET 基于網(wǎng)絡(luò) SOCK_STREAM 基于TCP sever.bind(ip_port) # 綁定到套接字 sever.listen(5) # 開始監(jiān)聽 while True:conn, addr = sever.accept() # 等待連接while True:try:msg = conn.recv(1024) # 收消息print('客戶端%s發(fā)來的消息:%s' % (addr, msg.decode('utf-8')))conn.send(msg.upper()) # 發(fā)消息except Exception:breakconn.close() sever.close()客戶端:
from socket import * client = socket(AF_INET, SOCK_STREAM) client.connect(('192.168.50.85', 8000)) # 連接到服務(wù)器 while True:msg = input('>>:').strip()client.send(msg.encode('utf-8')) # 不能發(fā)送str,必須是二進制格式data = client.recv(1024)print('收到服務(wù)端的消息', data.decode('utf-8')) client.close()基于TCP遠程執(zhí)行命令
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 # 執(zhí)行命令,得到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() 服務(wù)端 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('得到的結(jié)果是', data.decode('gbk')) 14 client.close() 客戶端 1 請輸入命令:dir 2 得到的結(jié)果是 驅(qū)動器 D 中的卷是 DATA 3 卷的序列號是 A473-AA80 4 5 D:\python\Project\python基礎(chǔ)\網(wǎng)絡(luò)編程 的目錄 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服務(wù)端.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服務(wù)端.py 14 5 個文件 3,627 字節(jié) 15 2 個目錄 200,399,257,600 可用字節(jié) 運行結(jié)果在重啟服務(wù)端時可能會遇到
這個是由于服務(wù)端仍然存在四次揮手的time_wait狀態(tài)在占用地址(如果不懂,請深入研究1.tcp三次握手,四次揮手 2.syn洪水攻擊 3.服務(wù)器高并發(fā)情況下會有大量的time_wait狀態(tài)的優(yōu)化方法)
解決方法:
#加入一條socket配置,重用ip和端口phone=socket(AF_INET,SOCK_STREAM) phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加 phone.bind(('127.0.0.1',8080)) 1 發(fā)現(xiàn)系統(tǒng)存在大量TIME_WAIT狀態(tài)的連接,通過調(diào)整linux內(nèi)核參數(shù)解決, 2 vi /etc/sysctl.conf 3 4 編輯文件,加入以下內(nèi)容: 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 然后執(zhí)行 /sbin/sysctl -p 讓參數(shù)生效。 11 12 net.ipv4.tcp_syncookies = 1 表示開啟SYN Cookies。當(dāng)出現(xiàn)SYN等待隊列溢出時,啟用cookies來處理,可防范少量SYN攻擊,默認(rèn)為0,表示關(guān)閉; 13 14 net.ipv4.tcp_tw_reuse = 1 表示開啟重用。允許將TIME-WAIT sockets重新用于新的TCP連接,默認(rèn)為0,表示關(guān)閉; 15 16 net.ipv4.tcp_tw_recycle = 1 表示開啟TCP連接中TIME-WAIT sockets的快速回收,默認(rèn)為0,表示關(guān)閉。 17 18 net.ipv4.tcp_fin_timeout 修改系統(tǒng)默認(rèn)的 TIMEOUT 時間 方法二3. 基于UDP的套接字
服務(wù)端:
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遠程執(zhí)行命令
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) 服務(wù)端 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('得到結(jié)果:', data.decode('gbk')) 12 client.close() 客戶端 1 >>:dir 2 得到結(jié)果: 驅(qū)動器 D 中的卷是 DATA 3 卷的序列號是 A473-AA80 4 5 D:\python\Project\python基礎(chǔ)\網(wǎng)絡(luò)編程 的目錄 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服務(wù)端.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服務(wù)端.py 14 5 個文件 4,215 字節(jié) 15 2 個目錄 200,399,253,504 可用字節(jié) 運行結(jié)果三、粘包
1. 什么是粘包
須知:只有TCP有粘包現(xiàn)象,UDP永遠不會粘包,為何,且聽我娓娓道來
首先需要掌握一個socket收發(fā)消息的原理
?
發(fā)送端可以是一K一K地發(fā)送數(shù)據(jù),而接收端的應(yīng)用程序可以兩K兩K地提走數(shù)據(jù),當(dāng)然也有可能一次提走3K或6K數(shù)據(jù),或者一次只提走幾個字節(jié)的數(shù)據(jù),也就是說,應(yīng)用程序所看到的數(shù)據(jù)是一個整體,或說是一個流(stream),一條消息有多少字節(jié)對應(yīng)用程序是不可見的,因此TCP協(xié)議是面向流的協(xié)議,這也是容易出現(xiàn)粘包問題的原因。而UDP是面向消息的協(xié)議,每個UDP段都是一條消息,應(yīng)用程序必須以消息為單位提取數(shù)據(jù),不能一次提取任意字節(jié)的數(shù)據(jù),這一點和TCP是很不同的。怎樣定義消息呢?可以認(rèn)為對方一次性write/send的數(shù)據(jù)為一個消息,需要明白的是當(dāng)對方send一條信息的時候,無論底層怎樣分段分片,TCP協(xié)議層會把構(gòu)成整條消息的數(shù)據(jù)段排序完成后才呈現(xiàn)在內(nèi)核緩沖區(qū)。
例如基于tcp的套接字客戶端往服務(wù)端上傳文件,發(fā)送時文件內(nèi)容是按照一段一段的字節(jié)流發(fā)送的,在接收方看了,根本不知道該文件的字節(jié)流從何處開始,在何處結(jié)束
所謂粘包問題主要還是因為接收方不知道消息之間的界限,不知道一次性提取多少字節(jié)的數(shù)據(jù)所造成的。
此外,發(fā)送方引起的粘包是由TCP協(xié)議本身造成的,TCP為提高傳輸效率,發(fā)送方往往要收集到足夠多的數(shù)據(jù)后才發(fā)送一個TCP段。若連續(xù)幾次需要send的數(shù)據(jù)都很少,通常TCP會根據(jù)優(yōu)化算法把這些數(shù)據(jù)合成一個TCP段后一次發(fā)送出去,這樣接收方就收到了粘包數(shù)據(jù)。
udp的recvfrom是阻塞的,一個recvfrom(x)必須對唯一一個sendinto(y),收完了x個字節(jié)的數(shù)據(jù)就算完成,若是y>x數(shù)據(jù)就丟失,這意味著udp根本不會粘包,但是會丟數(shù)據(jù),不可靠
tcp的協(xié)議數(shù)據(jù)不會丟,沒有收完包,下次接收,會繼續(xù)上次繼續(xù)接收,己端總是在收到ack時才會清除緩沖區(qū)內(nèi)容。數(shù)據(jù)是可靠的,但是會粘包。
兩種情況下會發(fā)生粘包
- 發(fā)送端需要等緩沖區(qū)滿才發(fā)送出去,造成粘包(發(fā)送數(shù)據(jù)時間間隔很短,數(shù)據(jù)量很小,會合到一起,產(chǎn)生粘包)
- 接收方不及時接收緩沖區(qū)的包,造成多個包接收(客戶端發(fā)送了一段數(shù)據(jù),服務(wù)端只收了一小部分,服務(wù)端下次再收的時候還是從緩沖區(qū)拿上次遺留的數(shù)據(jù),產(chǎn)生粘包)
2. 解決粘包的方法
1. 接收端不知道發(fā)送端將要傳送的字節(jié)流的長度,所以解決粘包的方法就是圍繞,如何讓發(fā)送端在發(fā)送數(shù)據(jù)前,把自己將要發(fā)送的字節(jié)流總大小讓接收端知曉,然后接收端來一個死循環(huán)接收完所有數(shù)據(jù)
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)# 執(zhí)行命令,得到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() 服務(wù)端 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('得到的結(jié)果是', recv_msg.decode('gbk')) 26 client.close() 客戶端程序的運行速度遠快于網(wǎng)絡(luò)傳輸速度,所以在發(fā)送一段字節(jié)前,先用send去發(fā)送該字節(jié)流長度,這種方式會放大網(wǎng)絡(luò)延遲帶來的性能損耗
2. 為字節(jié)流加上自定義固定長度報頭,報頭中包含字節(jié)流長度,然后一次send到對端,對端在接收時,先從緩存中取出定長的報頭,然后再取真實數(shù)據(jù)
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 # 執(zhí)行命令,得到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() 服務(wù)端 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) 一次接收所有數(shù)據(jù) 25 print('得到的結(jié)果是', recv_msg.decode('gbk')) 26 client.close() 客戶端struct模塊
該模塊可以把一個類型,如數(shù)字,轉(zhuǎn)成固定長度的bytes
l = struct.pack('i',1111111111111)
反解
struct.unpack('i', l)
四、socketserver模塊實現(xiàn)并發(fā)
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)# 發(fā)消息self.request.sendall(data.upper())except Exception:breakif __name__ == '__main__':s = socketserver.ThreadingTCPServer(('192.168.50.85', 8000), MyServer)s.serve_forever()?
轉(zhuǎn)載于:https://www.cnblogs.com/lsf123456/p/11212605.html
總結(jié)
以上是生活随笔為你收集整理的Python-21-socket编程的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: GDI+有Bitmap类。
- 下一篇: websocket python爬虫_p