2019年8月7日 星期三

Socket-Programming-HOWTO翻譯

Socket Programming HOWTO(翻譯)

官方文件

Author: Gordon McMillan

Abstract
Sockets的應用無所不在,但卻是最被誤會的技術之一。這只是關於sockets的概述。它並不是一個真正的教程,你仍然需要下點工夫。它並沒有涵蓋的很精確(絕大部份如此),但我希望可以給你足夠的背景來正確的使用它。

Sockets

我只想談談INET(i.e. IPv4)sockets,但它們佔了將近99%使用中的sockets。並且我將只談STREAM(i.e. TCP)sockets,除了你真的知道你在做什麼(這種情況下這個HOWTO並不適合你),你將從STREAM socket得到比其它模型更好的做法及效能。

理解這些事的部份麻煩在於,socket可以代表許多不同細微的東西,這取決於應用上下文。首先,我們先區分client端的socket-一個會話的端點,與server端的socket,它更像是一個交換機的操作。client應用程式(如,瀏灠器)單純的使用client sockets;而與它談的web server則同時使用著server sockets與client sockets。

History

各種形式的IPC中,sockets是目前為止最受歡迎的。在任何給定平台上,可能其它形式的IPC是更快的,但對於跨平台通信,sockets是唯一的選擇。

它們在Berkeley發明,做為Unix BSD的一部份。他們在網路上迅速傳播。有充份的理由,sockets與INET的結合使得與世界各地的任何機器交談變的異常的簡單(至少與其它方案相比)

Creating a Socket

大致而言,當你點擊連結,帶你到這個網頁,你的瀏灠器做了下面事情:

# create an INET, STREAMing socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# now connect to the web server on port 80 - the normal http port
s.connect(("www.python.org", 80))

connect完成的時候,socket-s可以用來發送對頁面文字的請求。相同的socket將會讀取回覆,然後被銷毀。沒錯,被銷毀了。Client sockets通常只用於一次交換(或一小組序列交換)

在web server上發生的事有點複雜。首先,web server建立一個server socket

# create an INET, STREAMing socket
serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# bind the socket to a public host, and a well-known port
serversocket.bind((socket.gethostname(), 80))
# become a server socket
serversocket.listen(5)

有幾點需要注意:我們使用socket.gethostname(),這樣外部就可以看到socket。如果我們使用s.bind(('localhost', 80))s.bind(('127.0.0.1', 80)),我們依然會有一個server socket,但那只會在相同機器上可以看的見。s.bind(('', 80))指定socket是可以被機器碰巧有的位址訪問。

第二件要注意的事:數值較小的port通常保留給"已知"的服務(HTTP、SNMP等)。如果你只是玩玩,請記得用四位數以上的port號。

最後,listen的參數告訴socket library,我們希望它在拒絕外部連線之前,隊列內應該要有五個連線(正常最大值)。如果其餘的程式碼正確的話,那應該是足夠的。

現在,我們擁有server socket,監聽80port,我們可以進入web server的主要迴圈:

while True:
    # accept connections from outside
    (clientsocket, address) = serversocket.accept()
    # now do something with the clientsocket
    # in this case, we'll pretend this is a threaded server
    ct = client_thread(clientsocket)
    ct.run()

實際上,有三種通用方法是這個迴圈內能做的事-調度一個線程處理clientsocket,建立一個進程處理clientsocket,或重構這個應用程式,使用非阻塞式socket,以及使用select在我們的server socket及任一活動的clientstockets進行多工。稍後會詳細介紹。現在需要理解的一個重點是:這就是server socket做的全部的事了。它沒有發送任何的資料。它沒有接收任何的資料。它只生成client sockets。每個clientsocket都是為了回應某些其它client socket執行connect()到我們所綁定的主機與port而建立。一但我們建立clientsocket,我們就會回頭監聽更多的連線。兩個clients可以自由交握-它們使用一些動態分配的port,當對話結束的時候,這些port會被回收。

IPC

如果你需要在一台機器上的兩個進程之間快速的IPC,你應該調查pipes或共享記憶體。如果你決定使用AF_INETsockets,那就綁定server socket到localhost。在多數平台上,這將圍繞兩層網絡程式碼走一條捷徑,而且速度要快得多。

See also multiprocessing整合跨平台IPC到高階API中。

Using a Socket

首先要注意的是web browser的client socket與web server的client socket是完全相同的。意思是,這是點對點的通訊。或者換種方式,做為設計人員,你必須決定通訊的禮儀規則。通常,連接socket通過發送一個需求或登錄來啟動通訊。但它是設計決定-它並不是sockets的規則。

現在有兩組動詞用於通訊。你可以使用sendrecv,或首可以轉換你的client socket變為file-like,然後使用readwrite。後者是Java呈現socket的方式。除了警告你需要在socket上使用flush之外,我並不會在這邊討論它。這些是緩衝檔案,常見錯誤是寫入一些東西,然後讀取回覆。如果沒有flush,你也許會一直等待回覆,因為request依然在你的輸出緩衝區中。

現在我們回到socket的主要癥結-sendrecv操作在網路緩衝區中。它們並不需要處理所有你傳送給它們的位元組,因為它們主要關注的是處理網路緩衝區。通常,它們在相關的網路緩衝被填滿(send)或清空(recv)的時候回傳。然後它們會告訴你它們處理多少位元組。在你的訊息完全的被處理之前,你有責任再次調用它們。

recv回傳0位元組的時候,這代表另一端已經關閉(或者正在關閉)連線。你將不會在這連線上再接收到任何資料。曾經,你也許能夠成功的傳送資料,稍後我會再詳細討論這點。

像HTTP這樣的協定使用socket只能進行一次傳輸。client送出一個request,然後讀取回覆。就這樣。然後socket就被丟掉。這意味著client可以透過接收到0位元組來偵測回覆的結束。

但是,如果你計劃重新使用你的socket做進一步的傳輸,你必需意識到,socket沒有EOT(End-of-Transmission)。再說一遍,如果socket在處理0位元組之後sendrecv回傳,那這個連線已經斷了。如果連線沒有斷,你也許會一直等著recv,因為socket不會告訴你(現在)沒東西可讀了。現在,如果你稍微思考一下,你就會瞭解sockets的基本原理:訊息必需固定長度、或是被分割、或者指定它們的長度、或通過關閉連線結束。這都取決於你的決定(但有些方法比其它方法更正確)。

假設你沒有要斷開連線,最簡單的解決方式就是固定訊息長度:

class MySocket:
    """demonstration class only
      - coded for clarity, not efficiency
    """

    def __init__(self, sock=None):
        if sock is None:
            self.sock = socket.socket(
                            socket.AF_INET, socket.SOCK_STREAM)
        else:
            self.sock = sock

    def connect(self, host, port):
        self.sock.connect((host, port))

    def mysend(self, msg):
        totalsent = 0
        while totalsent < MSGLEN:
            sent = self.sock.send(msg[totalsent:])
            if sent == 0:
                raise RuntimeError("socket connection broken")
            totalsent = totalsent + sent

    def myreceive(self):
        chunks = []
        bytes_recd = 0
        while bytes_recd < MSGLEN:
            chunk = self.sock.recv(min(MSGLEN - bytes_recd, 2048))
            if chunk == b'':
                raise RuntimeError("socket connection broken")
            chunks.append(chunk)
            bytes_recd = bytes_recd + len(chunk)
        return b''.join(chunks)

這裡的發送程式碼幾乎可以用於任何消息傳遞方式-在Python你發送字串,你可以使用len()來確認它的長(即使它包含0個字元)。主要是接收程式碼變得更複雜了(在C語言中,它並不會更糟,除非你不能使用strlen)。

最簡單的增強方式就是讓訊息的第一個字元做為訊息的指示器,並且確定長度。現在你有兩個recvs-第一個取得(最少)第一個字元,你可以查詢長度,第二個在迴圈中取得其餘的字元。如果你決定使用分離路由,你會接收到一些任意chunk size,(4096或8192通常很適合網路緩衝區大小),並掃描你接收到的分隔符號。

需要注意的一個複雜因素是:如果你的通訊協定允許多個訊息被回送(back to back)(不需某種回覆),然後你傳遞recv任意chunk size,你可能最終會讀取到後面的訊息的起始字元。你需要把它放一邊並保持住,直到需要它為止。

訊息前綴長度(假設是五個字元)變的更複雜,因為(信不信由你)你可能無法一次recv得到五個字元。在遊戲中你可以擺脫它,但在高網路負載中,除非你可以使用兩個recv迴圈,否則你的程式碼會很快中斷-第一個迴圈用於確定長度,第二個用於取得訊息的資料部份。雅打。這也是當你發現send並不總是可以在一次傳輸處理所有事情的時候。儘管已經讀過這篇文章了,你最終還是會被它反咬一口。

Binary Data

通過socket發送二進制資料是完全有可能的。主要的問題是並不是所有的機器都可以使用相同格式的二進制資料。舉例來說,一個Motorola芯片將代表一個十六位元整數,值為1,即兩個十六進制位元組00 01,Intel與DEC,然而,位元組是倒轉的,相同的1,其十六進制為01 00。socket套件要求轉換十六與三十二位元整數-ntohl, htonl, ntohs, htonsn代表networkh代表hosts代表shortl代表long。如果網路順序是host順序,那它們什麼也不會做,但如果是位元組反轉的話,那它們會適當的交換位元組。

在現在32bit的機器中,二進制資料的ascii表示通常小於二進制表示。這是因為驚人的時間量,所有這些長整數型的值都是0,或者可能是1。字串0是2bytes,而二進制是4bytes。當然,這不是適固定長度的訊息。

Disconnecting

嚴格來說,你應該在closesocket之前先執行shutdownshutdown是給另一端socket的提示。取決於你傳送的參數,它可是"我沒有要傳送資料了,但我依然監聽中",或"我沒監聽了,清除!"。然而,多數的socket libraries都是習慣程式設計師忽略這規則,通常closeshutdown()相同。close()。因此,多數情況下不需要直接執行shutdown

一種有效使用shutdown的方法是在HTTP-like exchange。client端發送一個request,然後執行shutdown(1)。這告訴server端"這個client完成發送,但依然可以接收"。server可以透過接收0位元組來偵測"EOF"。它可以假設它已經完成request。server發送一個回覆。如果send成功完成,那實際上client依然在接收。

Python將自動關閉更進一步,並表明當socket被垃圾回收時,如果需要,它將自動執行close。但依賴這個是一個壞習慣。如果你的socket沒有close情況下消失,那另一端的socket也許會無限期的掛著,你還會以為只是變慢了。當你完成的時候請記得一定要close你的sockets。

When Sockets Die

使用blocking sockets最糟糕的事情,就是另一邊掛掉的時候會發生什麼事(沒有執行close)。你的socket會掛著。TCP是一種可靠的協議,在放棄連線之前它會等待非常長的一段時間。如果你使用thread,那基本上整個線程已經掛了。對此,你無能為力。只要你沒有做一些很愚蠢的事情,像是在blocking read的時候拿著鎖,那thread並不會消耗太多資源。不要試著去終止thread-threads比processes更有效率部份原因在於它們避免資源自動回收的開銷。換句話說,如果你試著去終止thread,你的整個過程很可能被搞砸。

Non-blocking Sockets

如果你有瞭解上述內容,那你已經瞭解使用sockets機制的大致內容。你仍然會使用大致相同的方式調用。就是這樣,如果你做對了,那你的應用程式幾乎是內而外。

在Python,你使用socket.setblocking(0)來使它non-blocking。在C,它會更複雜,(首先,你需要在BSD風格O_NONBLOCK與幾乎無法區分的Posix風格O_NDELAY之間做個選擇,這與TCP_NODELAY完全不同)這是完全相同的概念。在建立socket之後執行這個操作, 但在使用它之前。(事實上,如果你瘋了,你可以來回切換。)

主要的機械式差異在於sendrecvconnectaccept可以在不做任何事情況下回傳。你有(當然)多種選擇。你可以檢查回傳的程式碼與異常程式碼,這通常讓你抓狂。如果你不相信我,試一下。你的應用程式會變的愈來愈大、愈來愈多蟲,吸光CPU。因此,讓我們跳過腦死的解決方案,把它做對吧。

使用select

在C,寫一個select非常複雜。在Python,輕而易舉,但是它非常接近C的版本,如果你瞭解Python的select,那在C裡面你幾乎不會有任何困擾:

ready_to_read, ready_to_write, in_error = \
               select.select(
                  potential_readers,
                  potential_writers,
                  potential_errs,
                  timeout)

你傳給select三個lists:第一個包含了所有你想試著讀取的sockets;第二個是你想試著寫入的sockets,最後一個(通常空的)是你想檢查的錯誤。你應該注意到,socket可以進入多個lists。select的調用是blocking,但你可以設置timeout,這通常是一個明智的作法-給它一個很長的timeout(假設一分鐘),除非你有一個很好的理由不這麼做。

回傳的部份也將得到三個lists。他們包含了實際可讀、可寫以及錯誤的sockets。這些清單中的每一個都是你傳入的相對應清單的子集(可能是空的)。

如果socket在可讀的list中,你可以盡可能的像我們在這業務中所得的那樣,以便在該socket上的recv將回傳某些東西。對可寫list是相同的想法,你可以發送一些東西。這也許不是你想要的,但有一些東西總比沒東西好。(事實上,任何正常的socket都將以可寫模式回傳-這意味著輸出網路緩衝空間是可用的)。

如果你有一個server socket,把它放進potential_readers。如果它出現在可讀的list中,你的accept(幾乎確定)會有作用。如果你已經建立新的socket,並且connect到其它人身上了,把它放到potential_writers list。如果它出現在可寫的list中,那就有機會它是已經連接的了。

事實上,即使使用blocking sockets,select依然是非常方便的。這是一個確認你是否阻塞的方法-當有些東西在緩衝區的時候,socket回傳為可讀。然而,這對判斷另一端是否完成是沒有幫助的,或者它只是忙著其它事情。

跨平台警示:
在Unix上,select可以同時適用於sockets與files。不要在Windows上這麼做。在Windows上,select僅支援sockets。還有C語言,socket很多進階選項在Windows上是不同的。事實上,在Windows我通常使用執行緒(執行狀況非常、非常好)。