概述
本文透過一個最簡單的Socket通訊來對每一步做通俗易懂的講解讓你瞭解這些函式到底是幹什麼用的。下面的程式碼雖然是用Pyhton實現的,但是你要知道這些通訊機制並不是Python所定義的,因為這些東西都必須符合通用規範。在類Unix作業系統上建立Socket也是這些步驟而Python其實就是使用的系統呼叫。同時本文包括關於Socket的基礎知識,這些知識對於你去理解Socket很有幫助。
你的第一個Socket程式
基礎知識
套接字:標識每個端點的IP和埠就叫做一個套接字。比如:(192.168.50.100:80)
套接字對兒:包含兩個端點(本機和對端)的組合,也就是本地IP和埠以及遠端主機IP和埠。比如:(192.168.50.112:59091 192.168.50.100:80)
監聽套接字:socket函式產生一個尚未開啟的主動套接字,bind把該套接字具體到一個地址,該套接字只有本地IP+埠。伺服器在呼叫listen函式之後那麼該套接字就變成一個監聽套接字。告訴核心該套接字可以接收連線請求。
連線套接字:如果有客戶端連線進來那麼就會為每一個連線過來的客戶端產生一個連線套接字(本地IP、PORT 遠端IP、port)。
套接字函式:套接字函式使用描述符訪問套接字,一個套接字可以對應多個描述符,但是一個描述符只能屬於一個套接字。套接字就是應用層到傳輸層或其他協議層的訪問介面。而訪問任意套接字就要用到描述符,因為透過描述符才能呼叫套接字函式。
Socket傳送緩衝區:每一個TCP套接字有一個傳送緩衝區,這個緩衝區你可以更改大小。當應用程式呼叫wirte的時候,核心從應用程式的緩衝區複製所有資料到套接字傳送緩衝區,如果應用程式緩衝區中的資料大於套接字傳送緩衝區(可能本身就大於,有可能套接字傳送緩衝區本來就有資料剩餘的空間小於要傳送的資料)這時候應用程式將會被設定為睡眠也就是核心將不從write函式返回,應用程式就卡在這裡了。卡在這裡幹嘛呢?其實就是等待,等要傳送的資料全部複製到套接字傳送緩衝區之後才返回,不過這裡雖然返回了也不代表已經把資料傳送到遠端主機了,這種返回僅僅代表告訴應用程式你可以重新使用應用程式緩衝區並且往裡面寫資料。套接字在核心空間,應用程式在使用者空間,write就是把資料從使用者空間複製到核心空間。複製完了之後的真實傳送資料以及TCP的資料可靠機制這些東西都是有TCP協議棧來保證的無需上層應用程式來關係。有人就問套接字傳送緩衝區感覺多餘啊,理想狀態下的確多餘,但是有2個必要好處:
- TCP是可靠連線,它需要保證你要傳送的資料確實傳送成功,TCP把緩衝區的資料傳送到對端,它還要等對端的ACK確認,之後確認之後它才會把緩衝區的資料刪除。
- 解耦,應用程式把資料丟進來就好了,剩下的工作我來在,你去幹其他的事情。
在UDP中雖然也有一個套接字傳送緩衝區,但是其作用僅僅是標識這個UDP資料包的大小上限,它不會去做保證可靠的事情,所以它的緩衝區也不會儲存應用程式要傳送的資料。
伺服器端
1 #!/usr/bin/python 2 # -*- coding: UTF-8 -*- 3 4 # 匯入 socket 模組 5 import socket 6 7 """ 8 建立 socket 套接字物件,如果成功返回非負數的描述符,如果失敗返回-1,嚴格來說這裡的套接字是一個主動套接字。而且是一個未連線 9 (CLOSED狀態)的主動套接字。 10 family 指定協議族 11 AF_INET IPv4協議 12 AF_INET5 IPv6協議 13 AF_LOCAL Unix套接字協議 14 AF_ROUTE 路由套接字 15 AF_KEY 金鑰套接字 16 type 指定套接字型別 17 SOCK_STREAM 位元組流套接字 TCP 18 SOCK_DGRAM 資料包套接字 UDP 19 SOCK_SEQPACKET 有序分組套接字 20 SOCK_RAW 原始套接字 21 proto 設定某個協議的值,預設是0, 0表示根據給定family和type的組合自動設定當前系統預設值 22 IPPROTO_TCP TCP傳輸協議 23 IPPROTO_UDP UDP傳輸協議 24 IPPROTO_SCTP SCTP傳輸協議 25 如果socket()什麼都不填寫則表示IPv4的TCP協議。上面這些引數的值不是python語言裡socket函式所獨有的,而是系統呼叫函式具有的。 26 """ 27 s = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM, proto=socket.IPPROTO_TCP) # 等於 s = socket.socket() 28 29 """ 30 該函式會把一個宣告瞭型別的套接字具體化。也就是你上面宣告瞭使用什麼網路層協議和傳輸層協議,如果把伺服器和客戶端通訊比作寫信, 31 你要給我寫信你就得遵守我的規範比如一次寫多少字因為太多了我看不過來,用什麼樣的信封等等的一些要求,規範訂完了那別人的知道 32 怎麼找到伺服器啊也就是你的郵寄地址,因為規範誰都可以用,N個人可以使用同一個規範,但是如何區別N個人呢?這就是IP和埠。 33 所以bind就是宣告一個地址並且是符合上面定義的規範的地址。其實IP和埠本身毫無意義,192.168.1.100:80 這樣的地址之所以可以 34 表示一個符合IP和TCP的規範的地址是因為在IP和TCP協議中對地址格式進行了定義。 35 36 套接字實在socket()函式呼叫時就建立了,只是這時候是一個沒有明確地址的套接字,它是套接字主體,這裡的IP和埠是繫結在套接字 37 上的客體。 38 39 繫結的時候需要指定具體IP和埠嗎?其實不用,你可以把IP和埠寫成空你看看程式能執行麼?當然可以,這裡的可以是說它可以執行 40 並監聽在某個IP和埠上,至於是哪個IP和埠取決於當前系統的設定。但作為伺服器來說通常需要指定IP和埠,在RPC通訊中伺服器 41 繫結不需要指定埠,這是例外。 42 如果不指定IP和埠我怎麼知道伺服器監聽在哪裡呢?這時候你就需要使用 getsockname()函式來獲取。 43 """ 44 s.bind(("127.0.0.1", 12345)) 45 46 """ 47 listen(backlog)函式把一個未連線的主動套接字變成一個被動套接字,也就是告訴核心接受連線到該套接字的請求。這時候套接字狀態將從CLOSED 48 變成LISTEN狀態。這個函式的引數是一個整數其含義是套接字佇列的最大連線個數。 49 核心為套接字準備2個佇列: 50 一個是半連線佇列也就是伺服器收到客戶端SYN並回復SYN_ACK之後等待三次握手完成的佇列,套接字狀態(SYN_RCVD) 51 一個是全連線佇列也就是三次握手建立完畢之後的佇列,完成之後客戶端連線就會從半連線佇列移動到全連線佇列的末尾,套接字狀態(ESTABLISHED) 52 上面兩個佇列到底指的是這2個佇列中的哪一個還是他倆之和又或者說是兩者中的最大值這個不一定具體得看具體作業系統在這個系統呼叫 53 上的定義。 54 在紅帽Linux核心裡backlog指的是全連線佇列大小 net.core.somaxconn;而半連線佇列大小有另外一個值 tcp_max_syn_backlog。那麼這個 55 backlog有預設值但是應用程式可以修改。 56 """ 57 s.listen(5) 58 59 while True: 60 """ 61 獲取一個客戶端連線,這個函式其實就是從全連線佇列的隊首返回一個已經完成三次握手的連線,如果這個全連線佇列為空則程式掛起 62 也就是睡眠直到該佇列有一個可用連線被返回。該函式呼叫成功將會返回一個連線套接字和協議地址。 63 """ 64 c, addr = s.accept() 65 print("連線套接字:", c) 66 print('連線地址:', addr) 67 data = "Hello world!" 68 # 傳送資料 69 c.send(data.encode(encoding="utf-8")) 70 71 """ 72 我們通常使用close()函式來關閉一個套接字連線,其實它並不是真正的關閉,它只是讓這個連線套接字的引用計數器減1,只有當這個 73 某個套接字的引用計數值為0時,在會被真正的清理和釋放資源。換句話說呼叫clese函式不會直接出發四次斷開機制,也就是伺服器 74 不會主動傳送FIN。如果你真的要主動傳送FIN就要使用shutdown()函式。不過通常我們都使用close。 75 """ 76 c.close()
客戶端
1 #!/usr/bin/python 2 # -*- coding: UTF-8 -*- 3 4 import socket 5 6 """ 7 這裡和伺服器端的設定是一樣的,建立一個主動未開啟的套接字。 8 """ 9 s = socket.socket() 10 11 """ 12 這裡有些人可能看不懂,網上內多套接字程式設計客戶端根本不用bind()函式啊。沒錯的確不用,但是你不寫並不代表核心不用,如果你不寫 13 核心會幫你找一個當前系統使用的IP並隨機產生一個埠。如果你寫一個固定的也沒錯。如果你看了前面服務端對bind函式的說明你就 14 知道為什麼一定要顯式或隱式的呼叫bind()函式了。客戶端程式不明確呼叫這個函式只是因為在C/S模式中作為客戶端的一方不需要被別人 15 主動連線過來所以它bind可以使用本機任意可以和外面通訊的IP以及一個隨機埠。 16 """ 17 s.bind(("127.0.0.1", 9999)) 18 19 """ 20 connect()函式是用來與伺服器端建立TCP連線使用的,成功返回0否則返回-1。呼叫該函式會觸發TCP的三次握手。當收到SYN_ACK時該函式返回。 21 """ 22 s.connect(("127.0.0.1", 12345)) 23 print("使用:", s.getsockname(), " 連線遠端伺服器。") 24 # 接收資料 25 data = s.recv(1024) 26 print(data.decode(encoding="utf-8")) 27 s.close()
執行結果
這時候是阻塞的模式一次只能允許一個客戶端連線過來。