Socket編程實戰

來源:互聯網
上載者:User
Socket 在英文中的含義為“(串連兩個物品的)凹槽”,像the eye socket,意為“眼窩”,此外還有“插座”的意思。在電腦科學中,socket 通常是指一個串連的兩個端點,這裡的串連可以是同一機器上的,像unix domain socket,也可以是不同機器上的,像network socket。

本文著重介紹現在用的最多的 network socket,包括其在網路模型中的位置、API 的編程範式、常見錯誤等方面,最後用 Python 語言中的 socket API 實現幾個實際的例子。Socket 中文一般翻譯為“通訊端”,不得不說這是個讓人摸不著頭腦的翻譯,我也沒想到啥“信達雅”的翻譯,所以本文直接用其英文表述。本文中所有代碼均可在 socket.py 倉庫中找到。

概述

Socket 作為一種通用的技術規範,首次是由 Berkeley 大學在 1983 為 4.2BSD Unix 提供的,後來逐漸演化為 POSIX 標準。Socket API 是由作業系統提供的一個編程介面,讓應用程式可以控制使用 socket 技術。Unix 哲學中有一條一切皆為檔案,所以 socket 和file 的 API 使用很類似:可以進行read、write、open、close等操作。

現在的網路系統是分層的,理論上有OSI模型,工業界有TCP/IP協議簇。其對比如下:

每層上都有其相應的協議,socket API 不屬於TCP/IP協議簇,只是作業系統提供的一個用於網路編程的介面,工作在應用程式層與傳輸層之間:

我們平常瀏覽網站所使用的http協議,收發郵件用的smtp與imap,都是基於 socket API 構建的。

一個 socket,包含兩個必要組成部分:

地址,由 ip 與 連接埠組成,像192.168.0.1:80。

協議,socket 所是用的傳輸協議,目前有三種:TCP、UDP、raw IP。

地址與協議可以確定一個socket;一台機器上,只允許存在一個同樣的socket。TCP 通訊埠 53 的 socket 與 UDP 連接埠 53 的 socket 是兩個不同的 socket。

根據 socket 傳輸資料方式的不同(使用協議不同),可以分為以下三種:

Stream sockets,也稱為“連線導向”的 socket,使用 TCP 協議。實際通訊前需要進行串連,傳輸的資料沒有特定的結構,所以高層協議需要自己去界定資料的分隔字元,但其優勢是資料是可靠的。

Datagram sockets,也稱為“無串連”的 socket,使用 UDP 協議。實際通訊前不需要串連,一個優勢時 UDP 的資料包自身是可分割的(self-delimiting),也就是說每個資料包就標示了資料的開始與結束,其劣勢是資料不可靠。

Raw sockets,通常用在路由器或其他網路裝置中,這種 socket 不經過TCP/IP協議簇中的傳輸層(transport layer),直接由網路層(Internet layer)通嚮應用層(Application layer),所以這時的資料包就不會包含 tcp 或 udp 頭資訊。

Python socket API

Python 裡面用(ip, port)的元組來表示 socket 的地址屬性,用AF_*來表示協議類型。

資料通訊有兩組動詞可供選擇:send/recv 或 read/write。read/write 方式也是 Java 採用的方式,這裡不會對這種方式進行過多的解釋,但是需要注意的是:

read/write 操作的具有 buffer 的“檔案”,所以在進行讀寫後需要調用flush方法去真正發送或讀取資料,否則資料會一直停留在緩衝區內。

TCP socket

TCP socket 由於在通向前需要建立串連,所以其模式較 UDP socket 負責些。具體如下:

每個API 的具體含義這裡不在贅述,可以查看手冊,這裡給出 Python 語言的實現的 echo server。

# echo_server.py # coding=utf8 import socket  sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 設定 SO_REUSEADDR 後,可以立即使用 TIME_WAIT 狀態的 socket sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(('', 5500)) sock.listen(5)
def handler(client_sock, addr):     print('new client from %s:%s' % addr)     msg = client_sock.recv(1024)     client_sock.send(msg)     client_sock.close()     print('client[%s:%s] socket closed' % addr)  if __name__ == '__main__':     while 1:         client_sock, addr = sock.accept()         handler(client_sock, addr)
# echo_client.py # coding=utf8 import socket  sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(('', 5500)) sock.send('hello socket world') print sock.recv(1024)

上面簡單的echo server 代碼中有一點需要注意的是:server 端的 socket 設定了SO_REUSEADDR為1,目的是可以立即使用處於TIME_WAIT狀態的socket,那麼TIME_WAIT又是什麼意思呢?後面在講解 tcp 狀態變更圖時再做詳細介紹。

UDP socket

UDP socket server 端代碼在進行bind後,無需調用listen方法。

# udp_echo_server.py # coding=utf8 import socket  sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 設定 SO_REUSEADDR 後,可以立即使用 TIME_WAIT 狀態的 socket sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(('', 5500)) # 沒有調用 listen  if __name__ == '__main__':     while 1:         data, addr = sock.recvfrom(1024)          print('new client from %s:%s' % addr)         sock.sendto(data, addr)  # udp_echo_client.py # coding=utf8 import socket  udp_server_addr = ('', 5500)  if __name__ == '__main__':     sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)     data_to_sent = 'hello udp socket'     try:         sent = sock.sendto(data_to_sent, udp_server_addr)         data, server = sock.recvfrom(1024)         print('receive data:[%s] from %s:%s' % ((data,) + server))     finally:         sock.close()

常見陷阱

忽略傳回值

本文中的 echo server 樣本因為篇幅限制,也忽略了傳回值。網路通訊是個非常複雜的問題,通常無法保障通訊雙方的網路狀態,很有可能在發送/接收資料時失敗或部分失敗。所以有必要對發送/接收函數的傳回值進行檢查。本文中的 tcp echo client 發送資料時,正確寫法應該如下:

total_send = 0 content_length = len(data_to_sent) while total_send < content_length:     sent = sock.send(data_to_sent[total_send:])     if sent == 0:         raise RuntimeError("socket connection broken")     total_send += total_send + sent

send/recv操作的是網路緩衝區的資料,它們不必處理傳入的所有資料。

一般來說,當網路緩衝區填滿時,send函數就返回了;當網路緩衝區被清空時,recv 函數就返回。

當 recv 函數返回0時,意味著對端已經關閉。

可以通過下面的方式設定緩衝區大小。

s.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, buffer_size)

認為 TCP 具有 framing

TCP 不提供 framing,這使得其很適合於傳輸資料流。這是其與 UDP 的重要區別之一。UDP 是一個面向訊息的協議,能保持一條訊息在寄件者與接受者之間的完備性。

程式碼範例參考:framing_assumptions

TCP 的狀態機器

在前面echo server 的樣本中,提到了TIME_WAIT狀態,為了正式介紹其概念,需要瞭解下 TCP 從產生到結束的狀態機器。(圖片來源)

這個狀圖轉移圖非常非常關鍵,也比較複雜,我自己為了方便記憶,對這個圖進行了拆解,仔細分析這個圖,可以得出這樣一個結論,串連的開啟與關閉都有被動(passive)與主動(active)兩種,主動關閉時,涉及到的狀態轉移最多,包括FIN_WAIT_1、FIN_WAIT_2、CLOSING、TIME_WAIT。

此外,由於 TCP 是可靠的傳輸協議,所以每次發送一個資料包後,都需要得到對方的確認(ACK),有了上面這兩個知識後,再來看下面的圖:

在主動關閉串連的 socket 調用 close方法的同時,會向被動關閉端發送一個 FIN

對端收到FIN後,會向主動關閉端發送ACK進行確認,這時被動關閉端處於 CLOSE_WAIT 狀態

當被動關閉端調用close方法進行關閉的同時向主動關閉端發送 FIN 訊號,接收到 FIN 的主動關閉端這時就處於 TIME_WAIT 狀態

這時主動關閉端不會立刻轉為 CLOSED 狀態,而是需要等待 2MSL(max segment life,一個資料包在網路傳輸中最大的生命週期),以確保被動關閉端能夠收到最後發出的 ACK。如果被動關閉端沒有收到最後的 ACK,那麼被動關閉端就會重新發送 FIN,所以處於TIME_WAIT的主動關閉端會再次發送一個 ACK 訊號,這麼一來(FIN來)一回(ACK),正好是兩個 MSL 的時間。如果等待的時間小於 2MSL,那麼新的socket就可以收到之前串連的資料。

前面 echo server 的樣本也說明了,處於 TIME_WAIT 並不是說一定不能使用,可以通過設定 socket 的 SO_REUSEADDR 屬性以達到不用等待 2MSL 的時間就可以複用socket 的目的,當然,這僅僅適用於測試環境,正常情況下不要修改這個屬性。

實戰

HTTP UA

http 協議是如今全球資訊網的基石,可以通過 socket API 來簡單類比一個瀏覽器(UA)是如何解析 HTTP 協議資料的。

#coding=utf8 import socket  sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) baidu_ip = socket.gethostbyname('baidu.com') sock.connect((baidu_ip, 80)) print('connected to %s' % baidu_ip)  req_msg = [     'GET / HTTP/1.1',     'User-Agent: curl/7.37.1',     'Host: baidu.com',     'Accept: */*', ] delimiter = '\r\n'  sock.send(delimiter.join(req_msg)) sock.send(delimiter) sock.send(delimiter)  print('%sreceived%s' % ('-'*20, '-'*20)) http_response = sock.recv(4096) print(http_response)

運行上面的代碼可以得到下面的輸出

--------------------received-------------------- HTTP/1.1 200 OK Date: Tue, 01 Nov 2016 12:16:53 GMT Server: Apache Last-Modified: Tue, 12 Jan 2010 13:48:00 GMT ETag: "51-47cf7e6ee8400" Accept-Ranges: bytes Content-Length: 81 Cache-Control: max-age=86400 Expires: Wed, 02 Nov 2016 12:16:53 GMT Connection: Keep-Alive Content-Type: text/html    

http_response是通過直接調用recv(4096)得到的,萬一真正的返回大於這個值怎麼辦?我們前面知道了 TCP 協議是面向流的,它本身並不關心訊息的內容,需要應用程式自己去界定訊息的邊界,對於應用程式層的 HTTP 協議來說,有幾種情況,最簡單的一種時通過解析傳回值頭部的Content-Length屬性,這樣就知道body的大小了,對於 HTTP 1.1版本,支援Transfer-Encoding: chunked傳輸,對於這種格式,這裡不在展開講解,大家只需要知道, TCP 協議本身無法區分訊息體就可以了。對這塊感興趣的可以查看 CPython 核心模組 http.client

Unix_domain_socket

UDS 用於同一機器上不同進程通訊的一種機制,其API適用與 network socket 很類似。只是其串連地址為本地檔案而已。

程式碼範例參考:uds_server.py、uds_client.py

ping

ping 命令作為檢測網路聯通性最常用的工具,其適用的傳輸協議既不是TCP,也不是 UDP,而是 ICMP,利用 raw sockets,我們可以適用純 Python 代碼來實現其功能。

程式碼範例參考:ping.py

netstat vs ss

netstat 與 ss 是類 Unix 系統上查看 Socket 資訊的命令。netstat 是比較老牌的命令,我常用的選擇有

-t,只顯示 tcp 串連

-u,只顯示 udp 串連

-n,不用解析hostname,用 IP 顯示主機,可以加快執行速度

-p,查看串連的進程資訊

-l,只顯示監聽的串連

ss 是新興的命令,其選項和 netstat 差不多,主要區別是能夠進行過濾(通過state與exclude關鍵字)。

$ ss -o state time-wait -n | head Recv-Q Send-Q             Local Address:Port               Peer Address:Port 0      0                 10.200.181.220:2222              10.200.180.28:12865  timer:(timewait,33sec,0) 0      0                      127.0.0.1:45977                 127.0.0.1:3306   timer:(timewait,46sec,0) 0      0                      127.0.0.1:45945                 127.0.0.1:3306   timer:(timewait,6.621ms,0) 0      0                 10.200.181.220:2222              10.200.180.28:12280  timer:(timewait,12sec,0) 0      0                 10.200.181.220:2222              10.200.180.28:35045  timer:(timewait,43sec,0) 0      0                 10.200.181.220:2222              10.200.180.28:42675  timer:(timewait,46sec,0) 0      0                      127.0.0.1:45949                 127.0.0.1:3306   timer:(timewait,11sec,0) 0      0                      127.0.0.1:45954                 127.0.0.1:3306   timer:(timewait,21sec,0) 0      0               ::ffff:127.0.0.1:3306           ::ffff:127.0.0.1:45964  timer:(timewait,31sec,0)

這兩個命令更多用法可以參考:

SS Utility: Quick Intro

10 basic examples of linux netstat command

總結

我們的生活已經離不開網路,平時的開發也充斥著各種複雜的網路應用,從最基本的資料庫,到各種分布式系統,不論其應用程式層怎麼複雜,其底層傳輸資料的的協議簇是一致的。Socket 這一概念我們很少直接與其打交道,但是當我們的系統出現問題時,往往是對底層的協議認識不足造成的,希望這篇文章能對大家編程網路方面的程式有所協助。

  • 聯繫我們

    該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

    如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

    A Free Trial That Lets You Build Big!

    Start building with 50+ products and up to 12 months usage for Elastic Compute Service

    • Sales Support

      1 on 1 presale consultation

    • After-Sales Support

      24/7 Technical Support 6 Free Tickets per Quarter Faster Response

    • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.