基於TCP與UDP協議的socket通訊
- 2020 年 6 月 27 日
- 筆記
- Python網路編程
C/S架構與初識socket
在開始socket介紹之前,得先知道一個Client端/服務端架構,也就是 C/S
架構,互聯網中處處充滿了 C/S
架構(Client/Server),比如我們需要玩英雄聯盟,就必須連接至英雄聯盟的伺服器上,那麼對於我們玩家來說它的英雄聯盟伺服器就是Server端,而我們必須要有一個英雄聯盟Client端才能夠去和英雄聯盟Server端進行數據交互。
TCP/IP五層網路模型介紹 | |
---|---|
層級 | 功能 |
應用層 | 跑應用協議的,如:HTTP,FTP等等,主要職責便是規定應用數據的格式。可以自定義協議,但是必須要有head部分與data部分。 |
傳輸層 | 跑埠協議的,如:TCP / UDP等等,主要職責便是用於區分該系統上的唯一一個網路應用程式。 |
網路層 | IP地址子網掩碼等等相關都在網路層,如:IP協議,主要職責便是用來區分廣播域,防止網路風暴的發生。 |
數據鏈路層 | 劃分電訊號以及IP地址與MAC地址相互轉換,如:乙太網協議,ARP協議等等,用來區分電訊號與支援通訊的。 |
物理層 | 傳輸電訊號,網路數據傳輸的基石。 |
電腦網路的核心就是一堆協議,想開發基於網路通訊的軟體就必須遵守這些協議。但是由於學習協議的代價巨大:TCP/IP等等協議就是研究生研究這玩意兒的,等你研究完了黃花菜都涼了。
那麼可以不用去了解這些協議也能做到開發網路通訊軟體的需求嗎?可以,socket
提供了這一可能性,socket
位於應用層和傳輸層之間,也就相當於加了一層socket
抽象層,它向下封裝了各種協議,用戶只需要通過socket
提供的介面就能完成該需求,而並不需要深入的去研究某些協議。比如(TCP,UDP)等等…
MAC地址存在於網卡之上,是全世界唯一的標識主機位置的一種資訊,而埠號則是為了區分作業系統上各個應用程式而衍生出的概念,IP地址綁定於網卡,MAC地址也綁定於網卡。那麼有了IP地址 + 埠號,就能夠去標識整個互聯網中的一個獨一無二的應用程式了。
所以:socket
也被人稱為 ip + port…
套接字發展史
套接字,就是socket
,由於進程中本身是不允許通訊的,但是可以通過套接字來發送或者接受數據,可以對其進行像對文件一樣的打開,讀寫,和關閉操作。並且套接字允許應用程式將I/O(輸入輸出)插入到網路中,並與網路中的其他應用程式進行通訊,基於網路的套接字就是IP地址加埠的組合(ip + port)
套接字起源於20世紀70年代加利福尼亞大學伯克利分校版本的Unix,它最初的設計是為了讓同一台主機上的多個應用程式之間進行通訊,也就是進程通訊或者被稱為IPC
,套接字有兩種(基於文件,基於網路)
下面我們就來介紹這兩種套接字家族。(套接字家族你可以將它理解為一種種類,反正就是一種是基於文件的,一種是基於網路的就行了。)
基於文件的套接字家族
名稱: AF_UNIX
作用:Unix一切皆文件,基於文件的套接字調用的就是底層的文件系統來存取數據,兩個套接字進程運行在同一台機器上,可以通過訪問同一個文件系統間接完成通訊。
基於網路的套接字家族
名稱: AF_INET
作用:有了IP + PORT 我們可以與互聯網上的任何應用程式進行通訊,這就是它的作用。除此之外還有一個叫 AF_INET6
的玩意兒,也就是基於IPV6的東西,AF_INET
是IPV4,目前廣泛採用。除此之外還有許多成員,不做過多介紹。
套接字工作流程介紹
我們需要自己編寫一個套接字Client端以及Server端,故應該選用基於網路的套接字家族。而其中基於TCP協議的套接字工作流程與基於UDP協議的套接字工作流程又不一樣。
基於TCP協議的套接字工作流程圖
由於TCP協議本身比較複雜,故使用基於TCP協議的套接字編寫程式整體流程也較為複雜。
基於UDP協議的套接字工作流程圖
基於UDP協議的套接字工作流程相比於基於TCP協議的套接字工作流程來說簡單一些,因為不用建立雙向鏈接通道。
TCP協議
TCP協議是一種基於位元組流的形式,什麼叫流呢?其實就是像水龍頭一樣打開嘩啦啦的沒有確切的邊界,這個就叫流。
TCP協議會去創建一個雙向鏈接通道,用於收發消息,如圖:
要去建立這個通道必須是要經歷三次握手,要關閉這個通道也必須經歷四次揮手,沒有這個通道,Server端與Client端就無法正常通訊。
此外TCP協議還有一個別稱叫做好人協議,這個在下面章節中會做詳細介紹。
TCP協議報文格式
先不急介紹三次握手啊,雙向鏈接通道這些玩意兒。在研究這兩個東西之前我們先要看一下TCP協議的報文格式。(著重看一下ACK與SYN)
TCP協議中的六個標誌分別是,URG、ACK、PSH、RST、SYN、FIN。
TCP報文是TCP層傳輸的數據單元,也叫報文段。
1、埠號:用來標識同一台電腦的不同的應用進程。
1)源埠:源埠和IP地址的作用是標識報文的返回地址。
2)目的埠:埠指明接收方電腦上的應用程式介面。
TCP報頭中的源埠號和目的埠號同IP數據報中的源IP與目的IP唯一確定一條TCP連接。
2、序號和確認號:是TCP可靠傳輸的關鍵部分。序號是本報文段發送的數據組的第一個位元組的序號。在TCP傳送的流中,每一個位元組一個序號。e.g.一個報文段的序號為300,此報文段數據部分共有100位元組,則下一個報文段的序號為400。所以序號確保了TCP傳輸的有序性。確認號,即ACK,指明下一個期待收到的位元組序號,表明該序號之前的所有數據已經正確無誤的收到。確認號只有當ACK標誌為1時才有效。比如建立連接時,SYN報文的ACK標誌位為0。
3、數據偏移/首部長度:4bits。由於首部可能含有可選項內容,因此TCP報頭的長度是不確定的,報頭不包含任何任選欄位則長度為20位元組,4位首部長度欄位所能表示的最大值為1111,轉化為10進位為15,15*32/8 = 60,故報頭最大長度為60位元組。首部長度也叫數據偏移,是因為首部長度實際上指示了數據區在報文段中的起始偏移值。
4、保留:為將來定義新的用途保留,現在一般置0。
5、控制位:URG ACK PSH RST SYN FIN,共6個,每一個標誌位表示一個控制功能。
1)URG:緊急指針標誌,為1時表示緊急指針有效,為0則忽略緊急指針。
2)ACK:確認序號標誌,為1時表示確認號有效,為0表示報文中不含確認資訊,忽略確認號欄位。
3)PSH:push標誌,為1表示是帶有push標誌的數據,指示接收方在接收到該報文段以後,應儘快將這個報文段交給應用程式,而不是在緩衝區排隊。
4)RST:重置連接標誌,用於重置由於主機崩潰或其他原因而出現錯誤的連接。或者用於拒絕非法的報文段和拒絕連接請求。
5)SYN:同步序號,用於建立連接過程,在連接請求中,SYN=1和ACK=0表示該數據段沒有使用捎帶的確認域,而連接應答捎帶一個確認,即SYN=1和ACK=1。
6)FIN:finish標誌,用於釋放連接,為1時表示發送方已經沒有數據發送了,即關閉本方數據流。
6、窗口:滑動窗口大小,用來告知發送端接受端的快取大小,以此控制發送端發送數據的速率,從而達到流量控制。窗口大小時一個16bit欄位,因而窗口大小最大為65535。
7、校驗和:奇偶校驗,此校驗和是對整個的 TCP 報文段,包括 TCP 頭部和 TCP 數據,以 16 位字進行計算所得。由發送端計算和存儲,並由接收端進行驗證。
8、緊急指針:只有當 URG 標誌置 1 時緊急指針才有效。緊急指針是一個正的偏移量,和順序號欄位中的值相加表示緊急數據最後一個位元組的序號。 TCP 的緊急方式是發送端向另一端發送緊急數據的一種方式。
9、選項和填充:最常見的可選欄位是最長報文大小,又稱為MSS(Maximum Segment Size),每個連接方通常都在通訊的第一個報文段(為建立連接而設置SYN標誌為1的那個段)中指明這個選項,它表示本端所能接受的最大報文段的長度。選項長度不一定是32位的整數倍,所以要加填充位,即在這個欄位中加入額外的零,以保證TCP頭是32的整數倍。
10、數據部分: TCP 報文段中的數據部分是可選的。在一個連接建立和一個連接終止時,雙方交換的報文段僅有 TCP 首部。如果一方沒有數據要發送,也使用沒有任何數據的首部來確認收到的數據。在處理超時的許多情況中,也會發送不帶任何數據的報文段。
TCP協議之三次握手
上面我們說過,Server端與Client端想要進行通訊,必須要經歷三次握手這麼一個流程。它的圖示如下:
SYN_SENT:Client端發送一次建立鏈接請求且沒有收到Server端回應時會進入該狀態。Linux作業系統下可用netstat命令
查看當前狀態。一般來說該狀態持續時間非常短,幾乎不可測。
ESTABLISHED:當某一方進入該狀態,則代表可以向另一方發送數據了。
LISTEN:Server端在等待Client端建立三次握手的連接時會進入該狀態。
SYN_RCVD:Server端進入該狀態代表已收到ClientClient端的三次握手鏈接請求。並回復了SYN以及ACK
SYN: 建立鏈接的標誌位
ACK:確認請求的標誌位
seq: 可以理解為一段暗號,用於確認該資訊未被修改。
上圖Client端發送了一個SYN請求,而Server端則回應了一個ACK並且在原有的x上加了一個1,Client端收到後就知道Server端允許建立鏈接且該資訊未被中間篡改,此時Client端就進入ESTABLISHED狀態,一旦進入這個狀態代表鏈接通道已建立好,Client端可以給Server端發送消息了。
此外,Server端還給Client端發送了一個SYN請求,並且附帶seq是y,Client端就知道原來Server端也想要和自己建立一個鏈接通道,於是回復ACK = y + 1,當Server端讀到該消息依舊是具有兩層含義。
1.這段消息未被修改
2.y+1代表我同意你的這條請求
SYN洪水攻擊:
當Server端長期進入SYN_RCVD狀態時就要當心是否遭受了SYN洪水攻擊。因為TCP三次握手對於Server端來講會無限的回復Client端發來的SYN請求,收到一條就回一條。如果有黑客模擬成千上萬台Client端對Server端發送SYN請求在發送第一次握手後就溜溜球了那麼伺服器還傻乎乎的等第三次的握手回信,這麼做會讓Server端的壓力很大。所以TCP協議也被稱為好人協議…
半鏈接池backlog:
伺服器如果一次性收到很多的請求,它無法做到同時都回應這麼多。就進行排隊機制,將先來的請求放到backlog里,後面的就慢慢等唄,就相當於你在和你女朋友打電話的時候(backlog為1),你的好哥們兒們給你打電話讓你開黑上網了。
那對於你的好哥們兒們來說就是 —> 對不起,請不要掛機,你撥打的電話正在通話中
這對應到網路上,就是半鏈接池外的請求 —> 等待伺服器的回應 (SYN請求和ACK確認)即,你想要建立雙向鏈接通道?再等等。
防止SYN 洪水攻擊的有效策略其中一點就是:增大backlog鏈接池的最大數量(一般不用次策略)
或者也可以:縮小Server端對每個請求的返回次數(如果Server端發現Client端沒理自己,就會不斷的回應上次的資訊。初始值為5s,過5s發一次,然後變成3s,再過3s發一次,變成1s,再發一次…直到不想發了就不會理睬這個請求了。)
平常打開一個網頁打不開的時候,有一種可能性就是人家的backlog滿了,你就只能排在外邊兒等
可靠傳輸協議的由來
TCP協議為何被稱為可靠傳入協議是有原因的,如下圖:(三次握手時的數據交互並不是走雙向鏈接通道,而對於下圖的數據傳輸來說則是走的雙向鏈接通道了。)
UDP協議則沒有這種確認的機制,對於安全性來說下降了不少但是對於速度上有了明顯的提示。故DHCP服務以及DNS域名解析都是使用UDP協議,因為它速度更快。
TCP協議之四次揮手
為什麼創建鏈接需要3步,而斷開鏈接則需要4步呢?
可以看到,三次握手之前是沒有數據傳輸的,並且其中第二次是一次性發送了一個請求和一個確認。所以減少了一次操作。而四次揮手涉及到數據的傳輸,所以不可能簡化成三次揮手。(四次揮手也是不同於三次握手,四次揮手也是建立在雙向鏈接通道的基礎之上的,而三次握手的時候該雙向通道還未建立成功)
FIN_WAIT_1:代表主動發起斷開鏈接請求
FIN_WAIT_2:代表此時的Client端不會再主動向Server端發送數據
TIME_WAIT:代表Client端還要回復最後一條確認消息,回復完畢後雙向鏈接正式關閉
CLOSE_WAIT:代表關閉等待
LAST_ACK:代表持續的確認(即只要Client端沒有回復第4條資訊,Server端就不斷嘗試發送斷開鏈接的FIN請求)
請記住:在實際生活場景中,服務端主動斷開鏈接的情況比較多,因為它涉及到了和很多客戶端的通訊,還有的客戶端還在排隊,所以不可能對一個客戶端浪費太多時間。這句話你可以理解為:
伺服器是個渣男 ,很多女孩子(Client端)都喜歡他,都給他寫情書,他回復完了一個女孩子的情書後立馬會拆開下一封情書,並不會只留戀於一封。
UDP協議
UDP協議是一種基於數據報的格式(也被稱為基於消息),不同於TCP的位元組流格式。UDP的數據報格式是有頭有尾的,這一點很重要。對應下圖:
另外UDP協議的數據傳輸是不需要建立雙向鏈接通道的,並且UDP發消息與TCP不太一樣。它發一次就不會管了,不管對方有沒有收到都不會再發,所以這也是UDP協議被稱為不可靠傳輸協議的由來。
基於TCP協議的socket簡單通訊
我們決定在兩台機器上進行套接字通訊。本機作為Client端,而雲端伺服器作為Server端,整個過程先從流程圖開始一步一步的進行實驗。
伺服器資訊如下:


[root@tencent-server MySocketServer]# uname -a Linux tencent-server 3.10.0-862.el7.x86_64 #1 SMP Fri Apr 20 16:44:24 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux [root@tencent-server MySocketServer]# cat /proc/version Linux version 3.10.0-862.el7.x86_64 (builder@kbuilder.dev.centos.org) (gcc version 4.8.5 20150623 (Red Hat 4.8.5-28) (GCC) ) #1 SMP Fri Apr 20 16:44:24 UTC 2018
伺服器資訊
客戶機資訊如下:


C:\Users\Administrator>systeminfo 主機名: DESKTOP-BTUC3PT OS 名稱: Microsoft Windows 10 專業工作站版 OS 版本: 10.0.18363 暫缺 Build 18363 OS 製造商: Microsoft Corporation OS 配置: 獨立工作站 OS 構建類型: Multiprocessor Free 註冊的所有人: Windows User 註冊的組織: P R C 產品 ID: 00391-90134-77505-AA010 初始安裝日期: 2020/5/6, 13:23:01 系統啟動時間: 2020/6/20, 0:55:40 系統製造商: Shinelon Computer 系統型號: TN15S 系統類型: x64-based PC 處理器: 安裝了 1 個處理器。 [01]: Intel64 Family 6 Model 60 Stepping 3 GenuineIntel ~2801 Mhz BIOS 版本: American Megatrends Inc. 1.04, 2016/1/26 Windows 目錄: C:\Windows 系統目錄: C:\Windows\system32 啟動設備: \Device\HarddiskVolume1 系統區域設置: zh-cn;中文(中國) 輸入法區域設置: zh-cn;中文(中國) 時區: (UTC+08:00) 北京,重慶,香港特別行政區,烏魯木齊 物理記憶體總量: 8,079 MB 可用的物理記憶體: 2,687 MB 虛擬記憶體: 最大值: 10,827 MB 虛擬記憶體: 可用: 3,257 MB 虛擬記憶體: 使用中: 7,570 MB 頁面文件位置: C:\pagefile.sys 域: WORKGROUP 登錄伺服器: \\DESKTOP-BTUC3PT 修補程式: 安裝了 10 個修補程式。 [01]: KB4552931 [02]: KB4513661 [03]: KB4516115 [04]: KB4517245 [05]: KB4528759 [06]: KB4537759 [07]: KB4552152 [08]: KB4560959 [09]: KB4561600 [10]: KB4560960 網卡: 安裝了 3 個 NIC。 [01]: Realtek RTL8723AE Wireless LAN 802.11n PCI-E NIC 連接名: WLAN 啟用 DHCP: 是 DHCP 伺服器: 192.168.1.1 IP 地址 [01]: 192.168.1.103 [02]: fe80::b53b:15ba:3b3d:2a2 [02]: Realtek PCIe GBE Family Controller 連接名: 乙太網 狀態: 媒體連接已中斷 [03]: Bluetooth Device (Personal Area Network) 連接名: 藍牙網路連接 狀態: 媒體連接已中斷 Hyper-V 要求: 虛擬機監視器模式擴展: 是 韌體中已啟用虛擬化: 是 二級地址轉換: 是 數據執行保護可用: 是
客戶機資訊
Server端程式碼如下:
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # ==== 基於TCP協議的socket通訊之Server ==== import socket # 1.實例化socket對象 # SOCKET_DGRAM為UDP協議,SOCKET_STREAM為TCP協議 server = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM) # 2.綁定IP地址與PORT埠號 server.bind(("172.17.0.16",6666)) # 127.0.0.1 為迴環地址,用於測試時使用。而我們在遠程環境下則使用本機私網IP # 3. 設置半連接池,代表最大有5個可以等待建立三次握手的Client端 server.listen(5) # 4. 阻塞等待三次握手請求 conn,client_addr = server.accept() # 4.1 conn:雙向鏈接通道 # 4.2 client_addr: 服務端地址資訊 # 5. 收消息,1024代表一次性讀取1024位元組。 data = conn.recv(1024) # 6.發消息 conn.send(data.upper()) # 7.關閉雙向通道(釋放佔用的系統資源,因為底層都是由作業系統操作) conn.close() # 8.關閉伺服器(釋放Python應用程式佔用的記憶體資源,可選) server.close()
Client端程式碼如下:
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # ==== 基於TCP協議的socket通訊之Client ==== import socket # 1. 實例化socket對象 client = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM) # 2. 發送請求鏈接 client.connect(("xxx.xxx.xxx.xxx",6666)) # 設置為伺服器公網IP # 3. 開始通訊 client.send("hello,world".encode("utf-8")) print(client.recv(1024).decode("utf-8")) # 4. 關閉客戶機 client.close()
先運行Server端,再運行Client端。得到以下結果
可以看到我們的消息成功的發送回來了。實驗成功!
增加雙層循環
我們的Server端在將資訊做了一個upper()
處理後就關閉了,這顯然不符合邏輯所以我們需要為它增加一個循環(可以稱之為通訊循環)讓它能不斷的進行處理資訊而不是只處理一次就關閉運行。
這個時候我們將測試環境搬回到本地。並對程式碼做出一些改進:
Server端改進程式碼如下:
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # ==== 基於TCP協議的socket通訊之Server ==== import socket # 1.實例化socket對象 # SOCKET_DGRAM為UDP協議,SOCKET_STREAM為TCP協議 server = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM) # 2.綁定IP地址與PORT埠號 server.bind(("127.0.0.1",6666)) # 127.0.0.1 為迴環地址,用於測試時使用。而我們在遠程環境下則使用本機私網IP # 3. 設置半連接池,代表最大有5個可以等待建立三次握手的Client端 server.listen(5) # 4. 阻塞等待三次握手請求 conn,client_addr = server.accept() # 4.1 conn:雙向鏈接通道 # 4.2 client_addr: 服務端地址資訊 # 改進1:服務端能夠不斷的處理客戶端發來的請求 while 1: # 5. 收消息,1024代表一次性讀取1024位元組。 data = conn.recv(1024) # 6.發消息 conn.send(data.upper()) # 7.關閉雙向通道(釋放佔用的系統資源,因為底層都是由作業系統操作) conn.close() # 8.關閉伺服器(釋放Python應用程式佔用的記憶體資源,可選) server.close()
Client端改進程式碼如下:
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # ==== 基於TCP協議的socket通訊之Client ==== import socket # 1. 實例化socket對象 client = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM) # 2. 發送請求鏈接 client.connect(("127.0.0.1",6666)) # 設置為伺服器公網IP # 改進1:我們可以自行的發送任何想發的數據 while 1: message = input(">>>").strip() # 3. 開始通訊 client.send(message.encode("utf-8")) print(client.recv(1024).decode("utf-8")) # 4.關閉通訊 client.close()
這個時候我們就可以源源不斷的給Server端發送消息,而不是發送一次就結束了。
還有一個問題,即我們的Server端只能接受一個用戶,這顯然太low了,有什麼好的解決方案嗎?暫時沒有。因為還沒學習多執行緒相關知識,所以我們只能退而求其次的對Server端多增加一個外層循環,用來源源不斷的與不同的Client端建立雙向鏈接通道。(非並發性的,可以將它稱之為鏈接循環)
Server端程式碼如下:
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # ==== 基於TCP協議的socket通訊之Server ==== import socket # 1.實例化socket對象 # SOCKET_DGRAM為UDP協議,SOCKET_STREAM為TCP協議 server = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) # 2.綁定IP地址與PORT埠號 server.bind(("127.0.0.1", 6666)) # 127.0.0.1 為迴環地址,用於測試時使用。而我們在遠程環境下則使用本機私網IP # 3. 設置半連接池,代表最大有5個可以等待建立三次握手的Client端 server.listen(5) # 改進2:可以讓服務端接收多個客戶端發送的建立雙向鏈接通道的請求(非並發性) while 1: # 4. 阻塞等待三次握手請求 conn, client_addr = server.accept() # 4.1 conn:雙向鏈接通道 # 4.2 client_addr: 服務端地址資訊 # 改進1:服務端能夠不斷的處理客戶端發來的請求 while 1: # 5. 收消息,1024代表一次性讀取1024位元組。 data = conn.recv(1024) # 6.發消息 conn.send(data.upper()) # 7.關閉雙向通道(釋放佔用的系統資源,因為底層都是由作業系統操作) conn.close() # 8.關閉伺服器(釋放Python應用程式佔用的記憶體資源,可選) server.close()
Server端異常崩潰的BUG
如果你認為上面的程式碼已經初具雛形,那麼就大錯特錯了。如果你按照以下的步驟進行操作會發現Server端會異常終止掉:
1.開啟Server端運行服務
2.開啟Client端與Server端進行通訊
3.停止Client端的運行,異常出現。
Traceback (most recent call last): File "C:/Users/Administrator/PycharmProjects/learn/服務端.py", line 22, in <module> # 改進1:服務端能夠不斷的處理客戶端發來的請求 ConnectionResetError: [WinError 10054] 遠程主機強迫關閉了一個現有的連接。
這是為什麼呢?因為這個鏈接通道是雙向的,一方關閉鏈接通道後這個鏈接通道就會崩塌。從而導致Server端發生異常,並且這種異常在不同的平台之下還有不同的表現形式:
類UNIX平台下:Server端的
recv()
會無限收到空Windows平台下: Server端直接拋出
ConnectionResetError
的異常
如何解決?方式很簡單。添加上try
與except
捕捉該異常,並且做一個if判斷。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # ==== 基於TCP協議的socket通訊之Server ==== import socket # 1.實例化socket對象 # SOCKET_DGRAM為UDP協議,SOCKET_STREAM為TCP協議 server = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM) # 2.綁定IP地址與PORT埠號 server.bind(("127.0.0.1",6666)) # 127.0.0.1 為迴環地址,用於測試時使用。而我們在遠程環境下則使用本機私網IP # 3. 設置半連接池,代表最大有5個可以等待建立三次握手的Client端 server.listen(5) # 改進2:可以讓服務端接收多個客戶端發送的建立雙向鏈接通道的請求(非並發性) while 1: # 4. 阻塞等待三次握手請求 conn,client_addr = server.accept() # 4.1 conn:雙向鏈接通道 # 4.2 client_addr: 服務端地址資訊 # 改進1:服務端能夠不斷的處理客戶端發來的請求 while 1: try: # bug修復:針對windows環境 # 5. 收消息,1024代表一次性讀取1024位元組。 data = conn.recv(1024) if not data: # bug修復:針對類UNIX環境 break # 6.發消息 conn.send(data.upper()) except ConnectionResetError as e: print(client_addr, "關閉了雙向鏈接") break # 7.關閉雙向通道(釋放佔用的系統資源,因為底層都是由作業系統操作,由於雙向鏈接通道已經斷開。所以這裡我們也將此雙向鏈接進行關閉,否則就會一直佔用系統資源) conn.close() # 8.關閉伺服器(釋放Python應用程式佔用的記憶體資源,可選,該句可以刪除。因為畢竟Server端一般情況下不會關閉) server.close()


#!/usr/bin/env python3 # -*- coding:utf-8 -*- # ==== 基於TCP協議的socket通訊之Server無注釋版 ==== import socket server = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM) server.bind(("127.0.0.1",6666)) server.listen(5) while 1: conn,client_addr = server.accept() while 1: try: # bug修復:針對windows環境 data = conn.recv(1024) if not data: # bug修復:針對類UNIX環境 break conn.send(data.upper()) except ConnectionResetError as e: print(client_addr, "關閉了雙向鏈接") break conn.close()
基於TCP協議的socket通訊之Server無注釋版
Client端發送空會卡住的BUG
我們的Server端已經優化完畢了,但是Client端還有一個BUG沒解決。嘗試用以下步驟就可以觸發該BUG
1.開啟Server端運行服務
2.開啟Client端與Server端進行通訊
3.Client端直接敲出回車(代表發出一個空)
可以發現此時的Client端進入了recv()
狀態,而Server端也還是recv()
狀態,這說明一個問題。該消息根本沒能發出去,那麼到底是為什麼會有這個bug呢?我們得從其底層原理說起。
其實不管是
send()
還是recv()
都是socket
應用程式對作業系統發出一次系統調用。在此期間CPU工作狀態會從用戶態轉變至內核態,而用戶態的記憶體數據是不能直接與內核態的記憶體數據發生交互的,所以只能靠一種映射關係(可以理解為拷貝,但是並不準確)來映射出需要發送的內容。如果Client端輸入一個回車,那麼對於內核態中的內核緩衝區來說是接收不到該數據的。其表現形式為:1.socket應用程式認為自己的回車(空消息)已經發送出去了
2.但實際上底層的內核緩衝區並沒有將這則空消息映射出來也就造成了其實並未發送任何數據
另外,關於消息的收發其實是涉及到
隊列
的概念,即先進先出。Ps:下面這幅圖這樣畫可以便於理解,但是socket應用程式應該是在調用某項系統介面後才會如此,另外這種映射關係更確切的說其實是這樣的:你
send()
什麼消息不用給我內核(事實上也給不了),我內核知道自己生成這些數據。反之recv()
同理
了解了底層原理後,我們看一下解決方案。其實只要設置成不讓Client端發送空消息即可,也就是一個if
判斷能解決的事兒。
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # ==== 基於TCP協議的socket通訊之Client ==== import socket # 1. 實例化socket對象 client = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM) # 2. 發送請求鏈接 client.connect(("127.0.0.1",6666)) # 設置為伺服器公網IP # 改進1:我們可以自行的發送任何想發的數據 while 1: message = input(">>>").strip() if not message: # bug修復:針對輸入空消息會卡住的情況 continue if message == "quit": # 改進2:用戶輸入quit會斷開鏈接 break # 3. 開始通訊 client.send(message.encode("utf-8")) print(client.recv(1024).decode("utf-8")) # 4.關閉通訊 client.close()
基於UDP協議的socket簡單通訊
我們依然將測試環境放在本機。並按照基於UDP協議的套接字工作流程圖進行程式碼的編寫。
Server端程式碼如下:
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # ==== 基於UDP協議的socket通訊之Server ==== import socket # 1.實例化socket對象 # SOCKET_DGRAM為UDP協議,SOCKET_STREAM為TCP協議 server = socket.socket(family=socket.AF_INET,type=socket.SOCK_DGRAM) # 2.綁定IP地址與PORT埠號 server.bind(("127.0.0.1",6666)) # 127.0.0.1 為迴環地址,用於測試時使用。而我們在遠程環境下則使用本機私網IP # 3.獲取到收發消息的內容以及其IP地址 data,client_addr = server.recvfrom(1024) # 4.發消息 server.sendto(data.upper(),client_addr) # 5.關閉伺服器(釋放Python應用程式佔用的記憶體資源,可選) server.close()
Client端程式碼如下:
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # ==== 基於UDP協議的socket通訊之Client ==== import socket # 1. 實例化socket對象 client = socket.socket(family=socket.AF_INET,type=socket.SOCK_DGRAM) # 2. 發送數據 client.sendto("hello,word".encode("utf-8"),("127.0.0.1",6666)) # 3. 讀取數據 data,server_addr = client.recvfrom(1024) print(data.decode("utf-8")) # 4.關閉通訊 client.close()
增加單層循環
由於基於UDP協議通訊不會建立雙向鏈接通道,所以我們只需要增加一個通訊循環即可。
Server端改進程式碼如下:
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # ==== 基於UDP協議的socket通訊之Server ==== import socket # 1.實例化socket對象 # SOCKET_DGRAM為UDP協議,SOCKET_STREAM為TCP協議 server = socket.socket(family=socket.AF_INET,type=socket.SOCK_DGRAM) # 2.綁定IP地址與PORT埠號 server.bind(("127.0.0.1",6666)) # 127.0.0.1 為迴環地址,用於測試時使用。而我們在遠程環境下則使用本機私網IP while 1: # 改進1:增加通訊循環 # 3.獲取到收發消息的內容以及其IP地址 data,client_addr = server.recvfrom(1024) # 4.發消息 server.sendto(data.upper(),client_addr) # # 5.關閉伺服器(釋放Python應用程式佔用的記憶體資源,可選) # server.close()
Client端改進程式碼如下:
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # ==== 基於UDP協議的socket通訊之Client ==== import socket # 1. 實例化socket對象 client = socket.socket(family=socket.AF_INET,type=socket.SOCK_DGRAM) # 改進1:我們可以自行的發送任何想發的數據 while 1: message = input(">>>").strip() if message == "quit": # 改進2:用戶輸入quit會斷開鏈接 break # 2. 發送數據 client.sendto(message.encode("utf-8"),("127.0.0.1",6666)) # 3. 讀取數據 data,server_addr = client.recvfrom(1024) print(data.decode("utf-8")) # 4.關閉通訊 client.close()
BUG測試
我們對該兩段程式碼進行BUG測試均為發現異常。
1.強制停止Client端是否會導致Server端異常崩潰?
沒有導致,原因是因為UDP協議的通訊不基於雙向鏈接通道。
2.客戶端發送回車或者任意空消息是否會導致
recvfrom()
卡住?沒有導致,這個還是要從UDP的數據格式說起,因為UDP是數據報格式的發送,所以即便消息體是空,也還有一個消息頭在裡面。所以UDP的整段數據是不可能為空的,也就不會導致內核緩衝區讀不到數據而卡住。
解決埠佔用問題
在進行socket
編程中肯定會遇到埠被佔用的情況,實際上就是伺服器再向客戶端發送最後一條ACK回應,也就是四次揮手中的第四步。此時伺服器的狀態應該處於:TIME_WAIT(等待一段時間確保雙向鏈接通道中的資訊全部讀取完畢)。這是屬於正常情況,請勿驚慌。解決方式如下:


#加入一條socket配置,重用ip和埠 from socket import * server=socket(AF_INET,SOCK_STREAM) server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加 server.bind(('127.0.0.1',6666))
解決方式1 加入一條socket配置,重用ip和埠


發現系統存在大量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 時間
解決方式2 Linux環境下這樣操作:
擴展:socket全方法詳解
函數 | 描述 |
---|---|
伺服器端套接字 | |
s.bind() | 綁定地址(host,port)到套接字, 在AF_INET 下,以元組(host,port)的形式表示地址。 |
s.listen() | 開始TCP監聽。backlog 指定在拒絕連接之前,作業系統可以掛起的最大連接數量。該值至少為1,大部分應用程式設為5就可以了。 |
s.accept() | 被動接受TCP客戶端連接,(阻塞式)等待連接的到來 |
客戶端套接字 | |
s.connect() | 主動初始化TCP伺服器連接,。一般address 的格式為元組(hostname,port),如果連接出錯,返回socket.erro r錯誤。 |
s.connect_ex() | connect() 函數的擴展版本,出錯時返回出錯碼,而不是拋出異常 |
公共用途的套接字函數 | |
s.recv() | 接收TCP數據,數據以字元串形式返回,bufsize 指定要接收的最大數據量。flag 提供有關消息的其他資訊,通常可以忽略。 |
s.send() | 發送TCP數據,將string中的數據發送到連接的套接字。返回值是要發送的位元組數量,該數量可能小於string的位元組大小。 |
s.sendall() | 完整發送TCP數據,完整發送TCP數據。將string中的數據發送到連接的套接字,但在返回之前會嘗試發送所有數據。成功返回None ,失敗則拋出異常。 |
s.recvfrom() | 接收UDP數據,與recv() 類似,但返回值是(data,address)。其中data是包含接收數據的字元串,address是發送數據的套接字地址。 |
s.sendto() | 發送UDP數據,將數據發送到套接字,address 是形式為(ipaddr,port)的元組,指定遠程地址。返回值是發送的位元組數。 |
s.close() | 關閉套接字 |
s.getpeername() | 返回連接套接字的遠程地址。返回值通常是元組(ipaddr,port)。 |
s.getsockname() | 返回套接字自己的地址。通常是一個元組(ipaddr,port) |
s.setsockopt(level,optname,value) | 設置給定套接字選項的值。 |
s.getsockopt(level,optname[.buflen]) | 返回套接字選項的值。 |
s.settimeout(timeout) | 設置套接字操作的超時期,timeout 是一個浮點數,單位是秒。值為None 表示沒有超時期。一般,超時期應該在剛創建套接字時設置,因為它們可能用於連接的操作(如connect() ) |
s.gettimeout() | 返回當前超時期的值,單位是秒,如果沒有設置超時期,則返回None 。 |
s.fileno() | 返回套接字的文件描述符。 |
s.setblocking(flag) | 如果flag 為0,則將套接字設為非阻塞模式,否則將套接字設為阻塞模式(默認值)。非阻塞模式下,如果調用recv() 沒有發現任何數據,或send() 調用無法立即發送數據,那麼將引起socket.error 異常。 |
s.makefile() |