Socket 編程實戰

本文原發於我的博客html

Socket 在英文中的含義爲「(鏈接兩個物品的)凹槽」,像the eye socket,意爲「眼窩」,此外還有「插座」的意思。在計算機科學中,socket 一般是指一個鏈接的兩個端點,這裏的鏈接能夠是同一機器上的,像unix domain socket,也能夠是不一樣機器上的,像network socketpython

本文着重介紹如今用的最多的 network socket,包括其在網絡模型中的位置、API 的編程範式、常見錯誤等方面,最後用 Python 語言中的 socket API 實現幾個實際的例子。Socket 中文通常翻譯爲「套接字」,不得不說這是個讓人摸不着頭腦的翻譯,我也沒想到啥「信達雅」的翻譯,因此本文直接用其英文表述。本文中全部代碼都可在 socket.py 倉庫中找到。linux

概述

Socket 做爲一種通用的技術規範,首次是由 Berkeley 大學在 1983 爲 4.2BSD Unix 提供的,後來逐漸演化爲 POSIX 標準。Socket API 是由操做系統提供的一個編程接口,讓應用程序能夠控制使用 socket 技術。Unix 哲學中有一條一切皆爲文件,因此 socketfile 的 API 使用很相似:能夠進行readwriteopenclose等操做。git

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

圖片描述

每層上都有其相應的協議,socket API 不屬於TCP/IP協議簇,只是操做系統提供的一個用於網絡編程的接口,工做在應用層與傳輸層之間:shell

圖片描述

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

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

  1. 地址,由 ip 與 端口組成,像192.168.0.1:80瀏覽器

  2. 協議,socket 所是用的傳輸協議,目前有三種:TCPUDPraw IP網絡

地址與協議能夠肯定一個socket;一臺機器上,只容許存在一個一樣的socket。TCP 端口 53 的 socket 與 UDP 端口 53 的 socket 是兩個不一樣的 socket。

根據 socket 傳輸數據方式的不一樣(使用協議不一樣),能夠分爲如下三種:

  1. Stream sockets,也稱爲「面向鏈接」的 socket,使用 TCP 協議。實際通訊前須要進行鏈接,傳輸的數據沒有特定的結構,因此高層協議須要本身去界定數據的分隔符,但其優點是數據是可靠的。

  2. Datagram sockets,也稱爲「無鏈接」的 socket,使用 UDP 協議。實際通訊前不須要鏈接,一個優點時 UDP 的數據包自身是可分割的(self-delimiting),也就是說每一個數據包就標示了數據的開始與結束,其劣勢是數據不可靠。

  3. Raw sockets,一般用在路由器或其餘網絡設備中,這種 socket 不通過TCP/IP協議簇中的傳輸層(transport layer),直接由網絡層(Internet layer)通向應用層(Application layer),因此這時的數據包就不會包含 tcp 或 udp 頭信息。

圖片描述

Python socket API

Python 裏面用(ip, port)的元組來表示 socket 的地址屬性,用AF_*來表示協議類型。
數據通訊有兩組動詞可供選擇:send/recvread/writeread/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_一、FIN_WAIT_二、CLOSING、TIME_WAIT。

此外,因爲 TCP 是可靠的傳輸協議,因此每次發送一個數據包後,都須要獲得對方的確認(ACK),有了上面這兩個知識後,再來看下面的圖:(圖片來源

tcpclosesimul.png

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

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

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

  4. 這時主動關閉端不會馬上轉爲 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

<html>
<meta http-equiv="refresh" content="0;url=http://www.baidu.com/">
</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.pyuds_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 差很少,主要區別是可以進行過濾(經過stateexclude關鍵字)。

$ 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)

這兩個命令更多用法能夠參考:

總結

咱們的生活已經離不開網絡,平時的開發也充斥着各類複雜的網絡應用,從最基本的數據庫,到各類分佈式系統,不論其應用層怎麼複雜,其底層傳輸數據的的協議簇是一致的。Socket 這一律念咱們不多直接與其打交道,可是當咱們的系統出現問題時,每每是對底層的協議認識不足形成的,但願這篇文章能對你們編程網絡方面的程序有所幫助。

參考

相關文章
相關標籤/搜索