關於Socket API的設計?


首先反對 @姚冬 的說法。
socket 分為有連接和無連接(connectionless)通信模式,
只有前者才需要 listen/accept/connect,後者根本不存在這個說法。
因此「打電話」的比喻是以偏概全的。

然後我們來看 FreeBSD 手冊對 listen(2) 的解釋:

To accept connections, a socket is first created with socket(2), a willingness to accept incoming connections and a queue limit for incoming connections are specified with listen(), and then the connections are accepted with accept(2). The listen() system call applies only to sockets of type SOCK_STREAM or SOCK_SEQPACKET.

照這段話的意思,stream socket 剛剛創建之初是用來主動 connect 的,它沒有接收 connection 的意願。只有通過 listen() 系統調用才能完成轉變。
其次,該系統調用只能作用於 STREAM/SEQPACKET 類型的 socket,主要也就是 TCP(若作用於 DGRAM 類型的 socket 將得到 ENOTSUP 錯誤)。

到這裡我們已經可以猜測,這種設計的根源在於 TCP 本身。
感興趣的話可以去細讀 RFC 793,關鍵詞是 passive 和 active。
這裡只附一張 TCP 連接狀態圖:


引用Richard Stevens UNP中的一段話:

重點部分翻譯下:
1,創建好的socket相當於一部電話機。
2,bind相當於告訴別人你的電話號碼(其實我覺得更像是去電信公司開戶)。
3,listen相當於打開電話的響鈴,這樣人家來電話我才能聽到。
4,connect相當於對方知道的我電話號碼並向我打電話。
5,accept相當於我看到有人打電話過來,我拿起電話機,準備跟人家聊天。
6,DNS相當於號碼薄,可以通過號碼薄來根據人名查找電話號碼(其實我覺得是DNS+使用的協議)。

Stevens這裡還特別說明了socket api跟現實中電話不同的一點是,在socket模型中,accept返回之前我們是沒法知道,而打電話時,在沒有接聽之前我是能看到對方的電話號碼的,之後再選擇是不是要接聽。

所以我覺得 @姚冬大叔關於電話的類比還是比較靠譜的,很多人反對可能是因為不太清楚「listen是隨時準備接電話」的這個說明,其實跟Stevens大神的打開響鈴是一樣的意思啊。listen字面意思也就是,我洗乾淨耳朵聽著呢。


你可以 bind 之後不 listen 而是 connect(指定地址的客戶端)。


socket嘛.. 來自unix的那一套理論...
windows抄了一下搞出了winsock
這套理論也跑進POSIX里,自然linux也就是這樣了...

至於@l wiky提到的, 是不是沒有什麼設計, 當然這套東西最後是實現了TCP/IP協議的一套API, 但是首先人家設計的時候根本就不是這麼想的, 雖然最後成了這樣, 單不管怎麼說你不能說他沒有設計不是... TLI(ATT的那套理論), 會哭暈在廁所的...

socket本身是一種抽象, socket是為了提供一個"插座"(請允許我直接這樣寫), 這就帶來實際上我並不關心插在上面的是什麼. 本質上跑在上面的都是IPC(進程間通信).
所以實際上到bind之後, 不同的協議就要用不同的函數了... (關於socket抽象的部分其實到此為止辣, 某種意義上) 你總不能讓udp也干tcp的活吧, 實際上接下來的不同函數(比如tcp的 或者 udp的)的差異就是插在上面的東西導致的. 插座是什麼都能插, 但是插了不同的東西自然要干不同的活.

又因為只是插座, 所以socket本身又沒有對協議做出區分(create的時候要指明, 但是socket體現給你看的就是socket, 底層又是另一回事).

=========

至於accept, 一方面作為一個底層介面, 要儘可能把tcp連接中所有的狀態暴露給上層的程序員, 又要一定程度上確保你不能亂搞, 一切操作都要按照tcp協議的基本法. 而具體就表現在, 你可以listen而不accept, 或者accept一次, 但是你不能不listen而accept...
再或者, boost中, 你也是需要手動(或者它幫你)調用listen(儘管它屬於acceptor), 然後你看,你有兩種accept能用呢, async_accept, accept. 把accept和listen分開能提供更好的靈活度.

=========

然後為什麼不像boost那樣...我覺得你想關注的其實是這個.

boost並不是如此, 你可以看到它實際上是把acceptor和endpoint, socket分開了, 這個acceptor寫作acceptor讀作-&>listen-&>accept-&>... 嗯你的流程里漏了一個, 是socket-&>bind-&>listen-&>accept-&>... bind是endpoint描述, acceptor乾的.

當然看起來是socket -&> acceptor(endpoint)

粗略看來, 這樣只是一個簡單的劃分.

為什麼要這麼設計呢?

首先, asio裡面你既然都知道是tcp了, 封裝程度當然越高大家用著越happy. 那當然最好大家包裝成一個比如TCPSocket 把這些東西放一起, 那是最好的! (你看ruby就是這樣)

那socket為什麼不也包進去呢? 我估計這樣有具體實現的障礙, 不過我們來看另一個地方.
boost/asio/ip/tcp.hpp
boost/asio/ip/udp.hpp
比如說我們都知道tcp和udp有一些區別是吧.
他是如何體現的呢?

看這兩段代碼

/// The TCP socket type.
typedef basic_stream_socket& socket;

/// The TCP acceptor type.
typedef basic_socket_acceptor& acceptor;

/// The TCP resolver type.
typedef basic_resolver& resolver;

/// The TCP iostream type.
typedef basic_socket_iostream& iostream;

/// The UDP socket type.
typedef basic_datagram_socket& socket;

/// The UDP resolver type.
typedef basic_resolver& resolver;

簡單地說就是, tcp和udp的差異, 被描述成了所能提供的能力(tcp下的不同typedef)的不同, 而這些能力(acceptor, resolver, 不同的socket)又進行了抽象(basic_xxxxx), 最終反饋為底層的相應函數(你可以在basic_xxx裡面看到對底層的的調用).

也就是說, boost實際上是根據提供的能力, 來將socket的不同階段進行了劃分, 並加以抽象.

很顯然這是先有了socket的不同工作模式才有的結果, 能夠合併的東西都被儘可能的合併了.


其實我也一直覺得listen和accept分離的介面設計是不必要的。
唯一可能合理的解釋是:這麼設計是為了適配某些特殊的協議,而這些協議我們已經極少能見到了,畢竟socket能支持的協議是很多的。
當然,這只是我的猜測,並沒有什麼證據。


看到姚冬的回答進來跑個題。

其實……最早的時候電話機是人工接線的,要在一個機器後面坐一個接線員,聽到信號之後,按照要求 把相應的線插到 目標埠之後,電話才能通。嗯,之前用戶不是撥號,而是猛搖一陣手柄,拿起話筒喊,幫我接 某某某。

後來……這個步驟變成機械的了,可以根據電話號碼自動接過去
再後……這個變電子化,可以編程式控制制了。

而一個電話被呼叫的前提是接入電話公司……可以被認為是bind。
而listen那個工作,就是接線員幹得工作,接聽進來的呼叫,然後按客戶要求把接頭插過去 。 在unix里,就是accept 後create socket實例……


這個吧,推薦看下設計模式中的builder模式,當socket從申請資源到最後進入監聽態,有很多的配置參數,比如backlog,比如timeout,比如host,比如port,比如發送和接收緩衝區大小,眾多參數編程上如何處理呢?
這些參數涉及socket進入監聽態的各個階段,所以可以採用統一的類似構造函數的東西一次配置所有參數,並且完成所有狀態的轉移直到最後修成正果
還可以分階段通過類似set的方法設置不同階段的參數,一步一個腳印的完成…
你可以把它理解成激進型和保守型的區別啦,哈哈


最初是作為協議的gateway設計,從
unix管道思想而來,即一端入一端出再加上同步語義,最初不止為tcpip設計,這樣雖然介面麻煩些,但勝於為每個協議單獨設計。思想上也有unix"一切皆文件"的影子。想要做到"一切通信皆套接著字"。所以套接字應用範圍很廣,是unix進程間通信的重要手段。我自己就開發過幾組套接著字介面用於嵌入式設備通信。為什麼和tcp相似呢?這其實是一個近年的網路語意義學結論,即可靠通信至少要包含tcp語義,我一同事博士就做得這個。手機打字累不說了,有機會細說一下。


plan 9 裡面不是這樣設計的吧。。。


很好奇為何大家都和電話幹上了……多大仇


狀態機上增加多一步,邏輯上更嚴謹,說明你不能accept 沒有listen 的socket


介面定義的靈活點兒沒什麼不好。實在不喜歡socket的話各類平台上也有其他各種各樣api可供選擇。


bind 是通知操作系統 這個埠我要了,
listen 就是告訴os這個埠流量導給我。

類似於 alloc。init 兩者分開,分別是申請內存和初始化,這兩者為什麼沒寫在一起成為一個函數
能不能合併?可以(我猜)


但是可能是原教旨主義之類的原因,
複雜的東西好拆拆拆,什麼?不能拆?那就創造機會借口拆,能說服自己就好了。


booster提供的介面只不過是封裝了SOCKET,一個是素顏一個是化妝,沒有什麼可比性,SOCK這個提供的是最底層的介面,這是無法替代的,唯一的


不知道acceptor.查了它的構造函數:
basic_socket_acceptor::accept
可以bind-&>listen-&>accept, 也可以直接accept.
而省略bind和listen,顯然是在構造函數裡面幹了
Socket沒法這麼做,除非改用c++重新設計...

為什麼不像boost那樣提供介面,有哪些歷史原因或者設計上的考量

BSD Socket最初是設計用於通信
然而不是網路,是進程通信.
也就是UNIX Socket

Socket API之所以普及,重要的原因:它比較容易實現


推薦閱讀:

Linux 下 socket 編程有什麼需要注意的?
如何用 Nginx 配置透明 HTTP 和 HTTPS 代理?
Epoll的EPOLLOUT事件的一些疑問?
運維工程師必須掌握的基礎技能有哪些?

TAG:API | Linux | Socket | 網路編程 | TCPIP |