小白程序員|Socket通信原理詳解

小白程序員|Socket通信原理詳解

什麼是Socket?

Socket的中文翻譯過來就是“套接字”。套接字是什麼,我們先來看看它的英文含義:插座。

Socket就像一個電話插座,負責連通兩端的電話,進行點對點通信,讓電話可以進行通信,端口就像插座上的孔,端口不能同時被其他進程佔用。而我們建立連接就像把插頭插在這個插座上,創建一個Socket實例開始監聽後,這個電話插座就時刻監聽著消息的傳入,誰撥通我這個“IP地址和端口”,我就接通誰。

實際上,Socket是在應用層和傳輸層之間的一個抽象層,它把TCP/IP層複雜的操作抽象為幾個簡單的接口,供應用層調用實現進程在網絡中的通信。Socket起源於UNIX,在Unix一切皆文件的思想下,進程間通信就被冠名為文件描述符(file desciptor),Socket是一種“打開—讀/寫—關閉”模式的實現,服務器和客戶端各自維護一個“文件”,在建立連接打開後,可以向文件寫入內容供對方讀取或者讀取對方內容,通訊結束時關閉文件。

另外我們經常說到的Socket所在位置如下圖:

小白程序員|Socket通信原理詳解

Socket通信過程

Socket保證了不同計算機之間的通信,也就是網絡通信。對於網站,通信模型是服務器與客戶端之間的通信。兩端都建立了一個Socket對象,然後通過Socket對象對數據進行傳輸。通常服務器處於一個無限循環,等待客戶端的連接。

一圖勝千言,下面是面向連接的TCP時序圖

小白程序員|Socket通信原理詳解

客戶端過程:

客戶端的過程比較簡單,創建Socket,連接服務器,將Socket與遠程主機連接(注意:只有TCP才有“連接”的概念,一些Socket比如UDP、ICMP和ARP沒有“連接”的概念),發送數據,讀取響應數據,直到數據交換完畢,關閉連接,結束TCP對話。

import socketimport sysif __name__ == '__main__': sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 創建Socket連接 sock.connect(('127.0.0.1', 8001)) # 連接服務器 while True: data = input('Please input data:') if not data: break try: sock.sendall(data) except socket.error as e: print('Send Failed...', e) sys.exit(0) print('Send Successfully') res = sock.recv(4096) # 獲取服務器返回的數據,還可以用recvfrom()、recv_into()等 print(res) sock.close()

sock.sendall(data)

這裡也可用send()方法:不同在於sendall()在返回前會嘗試發送所有數據,並且成功時返回None,而send()則返回發送的字節數量,失敗時都拋出異常。

服務端過程:

咱再來聊聊服務端的過程,服務端先初始化Socket,建立流式套接字,與本機地址及端口進行綁定,然後通知TCP,準備好接收連接,調用accept()阻塞,等待來自客戶端的連接。如果這時客戶端與服務器建立了連接,客戶端發送數據請求,服務器接收請求並處理請求,然後把響應數據發送給客戶端,客戶端讀取數據,直到數據交換完畢。最後關閉連接,交互結束。

import socketimport sysif __name__ == '__main__': sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 創建Socket連接(TCP) print('Socket Created') try: sock.bind(('127.0.0.1', 8001)) # 配置Socket,綁定IP地址和端口號 except socket.error as e: print('Bind Failed...', e) sys.exit(0) sock.listen(5) # 設置最大允許連接數,各連接和Server的通信遵循FIFO原則 while True: # 循環輪詢Socket狀態,等待訪問 conn, addr = sock.accept() try: conn.settimeout(10) # 獲得一個連接,然後開始循環處理這個連接發送的信息 # 如果要同時處理多個連接,則下面的語句塊應該用多線程來處理 while True: data = conn.recv(1024) print('Get value ' + data, end='') if not data: print('Exit Server', end='') break conn.sendall('OK') # 返回數據 except socket.timeout: # 建立連接後,該連接在設定的時間內沒有數據發來,就會引發超時 print('Time out') conn.close() # 當一個連接監聽循環退出後,連接可以關掉 sock.close()

conn, addr = sock.accept()

調用accept()時,Socket會進入“waiting”狀態。客戶請求連接時,方法建立連接並返回服務器。accept()返回一個含有兩個元素的元組(conn, addr)。第一個元素conn是新的Socket對象,服務器必須通過它與客戶通信;第二個元素addr是客戶的IP地址及端口。

data = conn.recv(1024)

接下來是處理階段,服務器和客戶端通過send()和recv()通信(傳輸數據)。

服務器調用send(),並採用字符串形式向客戶發送信息,send()返回已發送的字符個數。

服務器調用recv()從客戶接收信息。調用recv()時,服務器必須指定一個整數,它對應於可通過本次方法調用來接收的最大數據量。recv()在接收數據時會進入“blocked”狀態,最後返回一個字符串,用它表示收到的數據。如果發送的數據量超過了recv()所允許的,數據會被截短。多餘的數據將緩衝於接收端,以後調用recv()時,多餘的數據會從緩衝區刪除(以及自上次調用recv()以來,客戶可能發送的其它任何數據)。傳輸結束,服務器調用Socket的close()關閉連接。

TCP三次握手的Socket過程:

小白程序員|Socket通信原理詳解

  • 服務器調用socket()、bind()、listen()完成初始化後,調用accept()阻塞等待;
  • 客戶端Socket對象調用connect()向服務器發送了一個SYN並阻塞;
  • 服務器完成了第一次握手,即發送SYN和ACK應答;
  • 客戶端收到服務端發送的應答之後,從connect()返回,再發送一個ACK給服務器;
  • 服務器Socket對象接收客戶端第三次握手ACK確認,此時服務端從accept()返回,建立連接。

接下來就是兩個端的連接對象互相收發數據。

TCP四次揮手的Socket過程:

小白程序員|Socket通信原理詳解

  • 某個應用進程調用close()主動關閉,發送一個FIN;
  • 另一端接收到FIN後被動執行關閉,併發送ACK確認;
  • 之後被動執行關閉的應用進程調用close()關閉Socket,並也發送一個FIN;
  • 接收到這個FIN的一端向另一端ACK確認。

上面的代碼是簡單的演示Socket的基本函數使用,其實不管有多複雜的網絡程序,這些基本函數都會用到。上面的服務端代碼只有處理完一個客戶端請求才會去處理下一個客戶端的請求,這樣的服務器處理能力很弱,而實際中服務器都需要有併發處理能力,為了達到併發處理,服務器就需要fork一個新的進程或者線程去處理請求。


分享到:


相關文章: