Python Socket 網路程式設計

海風林影發表於2016-08-18

Socket 是程式間通訊的一種方式,它與其他程式間通訊的一個主要不同是:它能實現不同主機間的程式間通訊,我們網路上各種各樣的服務大多都是基於 Socket 來完成通訊的,例如我們每天瀏覽網頁、QQ 聊天、收發 email 等等。要解決網路上兩臺主機之間的程式通訊問題,首先要唯一標識該程式,在 TCP/IP 網路協議中,就是通過 (IP地址,協議,埠號) 三元組來標識程式的,解決了程式標識問題,就有了通訊的基礎了。

本文主要介紹使用 Python 進行 TCP Socket 網路程式設計,假設你已經具有初步的網路知識及 Python 基本語法知識。

TCP 是一種面向連線的傳輸層協議,TCP Socket 是基於一種 Client-Server 的程式設計模型,服務端監聽客戶端的連線請求,一旦建立連線即可以進行傳輸資料。那麼對 TCP Socket 程式設計的介紹也分為客戶端和服務端:

客戶端程式設計

建立 socket

首先要建立 socket,用 Python 中 socket 模組的函式 socket 就可以完成:

函式 socket.socket 建立一個 socket,返回該 socket 的描述符,將在後面相關函式中使用。該函式帶有兩個引數:

  • Address Family:可以選擇 AF_INET(用於 Internet 程式間通訊) 或者 AF_UNIX(用於同一臺機器程式間通訊)
  • Type:套接字型別,可以是 SOCKET_STREAM(流式套接字,主要用於 TCP 協議)或者 SOCKET_DGRAM(資料包套接字,主要用於 UDP 協議)

注:由於本文主要概述一下 Python Socket 程式設計的過程,因此不會對相關函式引數、返回值進行詳細介紹,需要了解的可以檢視相關手冊

錯誤處理

如果建立 socket 函式失敗,會丟擲一個 socket.error 的異常,需要捕獲:

那麼到目前為止已成功建立了 socket,接下來我們將用這個 socket 來連線某個伺服器,就連 www.google.com 吧。

連線伺服器

本文開始也提到了,socket 使用 (IP地址,協議,埠號) 來標識一個程式,那麼我們要想和伺服器進行通訊,就需要知道它的 IP地址以及埠號。

獲得遠端主機的 IP 地址

Python 提供了一個簡單的函式 socket.gethostbyname 來獲得遠端主機的 IP 地址:

現在我們知道了伺服器的 IP 地址,就可以使用連線函式 connect 連線到該 IP 的某個特定的埠上了,下面例子連線到 80 埠上(是 HTTP 服務的預設埠):

執行該程式:

傳送資料

上面說明連線到 www.google.com 已經成功了,接下面我們可以向伺服器傳送一些資料,例如傳送字串 GET / HTTP/1.1rnrn,這是一個 HTTP 請求網頁內容的命令。

傳送完資料之後,客戶端還需要接受伺服器的響應。

接收資料

函式 recv 可以用來接收 socket 的資料:

一起執行的結果如下:

關閉 socket

當我們不想再次請求伺服器資料時,可以將該 socket 關閉,結束這次通訊:

小結

上面我們學到了如何:

  1. 建立 socket
  2. 連線到遠端伺服器
  3. 傳送資料
  4. 接收資料
  5. 關閉 socket

當我們開啟 www.google.com 時,瀏覽器所做的就是這些,知道這些是非常有意義的。在 socket 中具有這種行為特徵的被稱為CLIENT,客戶端主要是連線遠端系統獲取資料。

socket 中另一種行為稱為SERVER,伺服器使用 socket 來接收連線以及提供資料,和客戶端正好相反。所以 www.google.com 是伺服器,你的瀏覽器是客戶端,或者更準確地說,www.google.com 是 HTTP 伺服器,你的瀏覽器是 HTTP 客戶端。

那麼上面介紹了客戶端的程式設計,現在輪到伺服器端如果使用 socket 了。

伺服器端程式設計

伺服器端主要做以下工作:

  • 開啟 socket
  • 繫結到特定的地址以及埠上
  • 監聽連線
  • 建立連線
  • 接收/傳送資料

上面已經介紹瞭如何建立 socket 了,下面一步是繫結。

繫結 socket

函式 bind 可以用來將 socket 繫結到特定的地址和埠上,它需要一個 sockaddr_in 結構作為引數:

繫結完成之後,接下來就是監聽連線了。

監聽連線

函式 listen 可以將 socket 置於監聽模式:

該函式帶有一個引數稱為 backlog,用來控制連線的個數。如果設為 10,那麼有 10 個連線正在等待處理,此時第 11 個請求過來時將會被拒絕。

接收連線

當有客戶端向伺服器傳送連線請求時,伺服器會接收連線:

執行該程式的,輸出結果如下:

此時,該程式在 8888 埠上等待請求的到來。不要關掉這個程式,讓它一直執行,現在客戶端可以通過該埠連線到 socket。我們用 telnet 客戶端來測試,開啟一個終端,輸入 telnet localhost 8888

這時服務端輸出會顯示:

我們觀察到客戶端已經連線上伺服器了。在建立連線之後,我們可以用來與客戶端進行通訊。下面例子演示的是,伺服器建立連線之後,接收客戶端傳送來的資料,並立即將資料傳送回去,下面是完整的服務端程式:

在一個終端中執行這個程式,開啟另一個終端,使用 telnet 連線伺服器,隨便輸入字串,你會看到:

客戶端(telnet)接收了伺服器的響應。

我們在完成一次響應之後伺服器立即斷開了連線,而像 www.google.com 這樣的伺服器總是一直等待接收連線的。我們需要將上面的伺服器程式改造成一直執行,最簡單的辦法是將 accept 放到一個迴圈中,那麼就可以一直接收連線了。

保持服務

我們可以將程式碼改成這樣讓伺服器一直工作:

現在在一個終端下執行上面的伺服器程式,再開啟三個終端,分別用 telnet 去連線,如果一個終端連線之後不輸入資料其他終端是沒辦法進行連線的,而且每個終端只能服務一次就斷開連線。這從程式碼上也是可以看出來的。

這顯然也不是我們想要的,我們希望多個客戶端可以隨時建立連線,而且每個客戶端可以跟伺服器進行多次通訊,這該怎麼修改呢?

處理連線

為了處理每個連線,我們需要將處理的程式與主程式的接收連線分開。一種方法可以使用執行緒來實現,主服務程式接收連線,建立一個執行緒來處理該連線的通訊,然後伺服器回到接收其他連線的邏輯上來。

再次執行上面的程式,開啟三個終端來與主伺服器建立 telnet 連線,這時候三個客戶端可以隨時接入,而且每個客戶端可以與主伺服器進行多次通訊。

telnet 終端下可能輸出如下:

要結束 telnet 的連線,按下 Ctrl-] 鍵,再輸入 close 命令。

伺服器終端的輸出可能是這樣的:

總結

到目前為止,我們學習了 Python 下基本的 socket 程式設計。

參考資料

相關文章