Socket 通訊原理

什麼是 Socket?

Socket 的中文翻譯過來就是「套接字」。套接字是什麼,咱們先來看看它的英文含義:插座。python

Socket 就像一個電話插座,負責連通兩端的電話,進行點對點通訊,讓電話能夠進行通訊,端口就像插座上的孔,端口不能同時被其餘進程佔用。而咱們創建鏈接就像把插頭插在這個插座上,建立一個 Socket 實例開始監聽後,這個電話插座就時刻監聽着消息的傳入,誰撥通我這個「IP 地址和端口」,我就接通誰。服務器

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

另外咱們常常說到的Socket 所在位置以下圖:多線程

Socket 抽象層位置

Socket 通訊過程

Socket 保證了不一樣計算機之間的通訊,也就是網絡通訊。對於網站,通訊模型是服務器與客戶端之間的通訊。兩端都創建了一個 Socket 對象,而後經過 Socket 對象對數據進行傳輸。一般服務器處於一個無限循環,等待客戶端的鏈接。併發

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

TCP 時序圖

客戶端過程:

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

import socket
import sys

if __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 socket
import sys

if __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)  # 若是請求超過 10 秒沒有完成,就終止操做

            # 若是要同時處理多個鏈接,則下面的語句塊應該用多線程來處理
            while True:  # 得到一個鏈接,而後開始循環處理這個鏈接發送的信息
                data = conn.recv(1024)
                print('Get value ' + data, end='\n\n')
                if not data:
                    print('Exit Server', end='\n\n')
                    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 地址及端口。spa

data = conn.recv(1024)

接下來是處理階段,服務器和客戶端經過 send()recv() 通訊(傳輸數據)。
服務器調用 send(),並採用字符串形式向客戶端發送信息,send() 返回已發送的字符個數。
服務器調用 recv() 從客戶端接收信息。調用 recv() 時,服務器必須指定一個整數,它對應於可經過本次方法調用來接收的最大數據量。recv() 在接收數據時會進入blocked狀態,最後返回一個字符串,用它表示收到的數據。若是發送的數據量超過了 recv() 所容許的,數據會被截短。多餘的數據將緩衝於接收端,之後調用 recv() 時,會繼續讀剩餘的字節,若是有多餘的數據會從緩衝區刪除(以及自上次調用 recv() 以來,客戶端可能發送的其它任何數據)。傳輸結束,服務器調用 Socket 的 close() 關閉鏈接。

從 TCP 鏈接的視角看 Socket 過程:

TCP 三次握手的 Socket 過程:

Socket 三次握手

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

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

TCP 四次揮手的 Socket 過程:

Socket 四次揮手

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

總結:

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

相關文章
相關標籤/搜索