基於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() 創建一個與該套接字相關連的文件
相關文章