深入理解socket程式設計的幾個函式和兩種fd
在開始之前希望大家都只到以下幾點:
首先,一個埠肯定只能繫結一個socket,當然這個socket可能會產生很多“socket連線”;
其次,只要伺服器效能好一個埠就可以繫結無數個“socket連線”;
再次,一個socket控制代碼代表兩個地址對“本地IP:埠”--“遠端IP:埠” 。
關於這一點我們多說幾句:我們知道在廣袤的網際網路中唯一標示一個程序(應用程式)需要三元組:IP地址、埠和協議。
此時我問你 描述兩個應用程序之間的端到端的通訊需要什麼?顯然需要五元組:本地IP、本地埠、遠端IP、遠端埠和協議。而接下來要將的套接字,它的本質就是涵蓋著五元組資訊的類/物件。其操作也大多圍繞這五元組資訊展開的。
一、socket()函式——“亞當夏娃”套接字的誕生
使用socket()函式建立套接字的時候,我們實際上是在核心裡面建立了一個數據結構(或者說是物件)。這個資料結構包括了上面所說的地址,此外還有協議等條目(只不過剛建立的時候這些條目還未指定具體值而已!)。socket()函式執行和成功的話返回一個int型的描述符,它指向前面那個被維護在核心裡的socket資料結構。我們的任何操作都是通過這個描述符而作用到那個資料結構上的。
socket結構究竟包括哪些內容呢?實際上 socket 結構體的定義如下:
struct socket { socket_state state; unsigned long flags; const struct proto_ops *ops; struct fasync_struct *fasync_list; struct file *file; struct sock *sk; wait_queue_head_t wait; short type; };
其中,struct sock 包含有一個 sock_common 結構體,而sock_common結構體又包含有struct inet_sock 結構體,而struct inet_sock 結構體的部分定義如下:
struct inet_sock { struct sock sk; #if defined(CONFIG_IPV6) || defined(CONFIG_IPV6_MODULE) struct ipv6_pinfo *pinet6; #endif __u32 daddr; //IPv4的目的地址。 __u32 rcv_saddr; //IPv4的本地接收地址。 __u16 dport; //目的埠。 __u16 num; //本地埠(主機位元組序)。 ………… }
由此,我們知道socket結構體不僅僅記錄了本地的IP:埠號,還記錄了目的IP:埠,即“本地地址”和“遠端地址”。
也就是說socket在新建的時候只是一個待填充的空的結構,後面的connect()和bind()函式實際上可以看做給核心裡邊的那個socket物件設定具體IP和埠等條目的資訊的過程。(注:此處沒有包括listen、accept,不過貌似也可以包括。)
注:這個描述符我們用來監聽用的稱為——監聽socket。
二、bind()和connect()函式——給套接字賦予“本地地址”(對客戶端是“遠端地址”)
依照建立套接字的目的不同,賦予套接字地址的方式有兩種:伺服器端使用bind,客戶端使用connect。
1、對於服務端而言, bind()函式的作用是將標註有socket地址資訊的資料結構(struct sockaddr)和socket()函式建立的那個套接字聯絡起來,即賦予這個套接字一個“本地地址”。
2、對於客戶端而言,connect()函式的作用是將客戶端socket()函式建立socket和其所期望連線的伺服器之間建立關係。只不過這個伺服器是用標註相應資訊的結構物件(struct sockaddr物件)來表徵的。注:(1)作為客戶端端,你想要連線的伺服器的地址肯定是已經知道了的。(2)在connect建立socket和socket地址兩者關係的同時,它也在嘗試著建立遠端的連線。
總的來說,就是將服務端、客戶端socket()建立的socket都同服務端的地址繫結。
三、listen()函式——開始監聽的開關
listen函式的作用可以理解成開啟監聽的開關,listen被執行之後開關被開啟,就開始監聽了。輪詢的時候判斷到有連線請求過來的時候呼叫accept接受即可。
四、accept函式——建立套接字連線
accept()接受一個客戶端的連線請求(該客戶端的所有資訊均來自“監聽socket”),並返回一個新的套接字。所謂“新的”就是說這個套接字與socket()返回的用於監聽和接受客戶端的連線請求的套接字不是同一個套接字。與本次接受的客戶端的通訊是通過在這個新的套接字上傳送和接收資料來完成的。再次呼叫accept()可以接受下一個客戶端的連線請求,並再次返回一個新的套接字(與socket()返回的套接字、之前accept()返回的套接字都不同的新的套接字)。
假設一共有三個客戶端連線到伺服器端。那麼在伺服器端就一共有4個套接字:第1個是socket()返回的、經過bind或connect填充“本地IP:埠”的用於監聽的套接字;其餘3個是分別呼叫3次accept()返回的包含“本地/遠端”雙重資訊的不同的套接字(他們之間遠端資訊不同)。
如果已經有客戶端連線到伺服器端,不再需要監聽和接受更多的客戶端連線的時候,可以關閉由socket()返回的套接字,而不會影響與客戶端之間的通訊。當某個客戶端斷開連線、或者是與某個客戶端的通訊完成之後,伺服器端需要關閉用於與該客戶端通訊的套接字。
由以上可知對服務端而言bind操作的只是那一個socket()返回、用於監聽的socket。其他幾個“socket連線”,他們在核心中應該也是有對應的物件的(猜測)。不過他們並不用於監聽,而是用於具體讀寫。這種“一對多”的關係可以這樣理解:一個僅有“本地IP:埠”的父親(監聽socket)將自己監聽到的資訊用於派生,派生除了一個又一個同時具備“本地IP:埠”和“遠端IP:埠”的新的socket(即socket連線)。注:(1)上述的“派生”的過程由accept函式完成的(2)這裡監聽socket和socket連線的結構應該是一樣的,只不過“派生”出的socket把“遠端IP:埠號”的資訊也分別填充了。(3)“遠端IP:埠”的資訊是來自監聽socket的。
至此為什麼只有這個新的套接字才能用於同這次接受的客戶端之間的通訊也就一目瞭然了,因為這個套接字才真正包含了“本地資訊”和“遠端資訊”,只有同時包含這兩重資訊才可以用於資料的收發。
五、accept返回的fd和listen的fd是什麼關係?
(1)首先他們肯定是不等的。一個socket是由一個五元組來唯一標示的,即(協議,server_ip, server_port, client_ip,client_port)。只要該五元組中任何一個值不同,則其代表的socket就不同。https://blog.csdn.net/cws1214/article/details/9671543
(2)那這樣算不算 一個監聽socket和若干socket連線共享埠呢?
答:準確的說兩者的概念有一點區別。“一個埠不能被多個socket共享”,應該理解成“彼此獨立的socket不能共享一個埠”。顯然“監聽socket”和由其產生的“socket連線”並不是彼此獨立的socket。也就是說他們確實共享埠了,但是此共享非彼共享。或者你可以理解成“本地IP:埠”是被他們所共有的,即都是用“監聽socket”的那份。
參考:
https://blog.csdn.net/mythicsr/article/details/44313893
https://blog.csdn.net/kingshown_wz/article/details/52103327
http://blog.51cto.com/ticktick/779866
https://blog.csdn.net/cws1214/article/details/9671543