基於TCP與UDP協議的socket通訊
基於TCP與UDP協議的socket通訊
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 ([email protected]) (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() | 建立一個與該套接字相關連的檔案 |