socket的accept函式解析以及伺服器和多個客戶端的埠問題

luckyone906發表於2017-06-06


                      

今天與同學爭執一個話題:由於socket的accept函式在有客戶端連線的時候產生了新的socket用於服務該客戶端,那麼,這個新的socket到底有沒有佔用一個新的埠?

    討論完後,才發現,自己雖然熟悉socket的程式設計套路,但是卻並不是那麼清楚socket的原理,今天就趁這個機會,把有關socket程式設計的幾個疑問給搞清楚吧。

   先給出一個典型的TCP/IP通訊示意圖。



    問題一:socket結構體物件究竟是怎樣定義的?

   我們知道,在使用socket程式設計之前,需要呼叫socket函式建立一個socket物件,該函式返回該socket物件的描述符

int socket(int domain, int type, int protocol);

   那麼,這個socket物件究竟是怎麼定義的呢?它記錄了哪些資訊呢?只記錄了本機IP及埠、還是目的IP及埠、或者都記錄了?

   關於這個問題,大家可以在核心原始碼裡面找,也可以參考這篇文章《struct 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;  
};   


    其中,structsock 包含有一個 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和埠。

     問題二:connect函式究竟做了些什麼操作?

    在TCP客戶端,首先呼叫一個socket()函式,得到一個socket描述符socketfd,然後通過connect函式對伺服器進行連線,連線成功後,就可以利用這個socketfd描述符使用send/recv函式收發資料了。

   關於connect函式和send函式的原型如下:


int connect( int sockfd, const struct sockaddr* server_addr, socklen_t addrlen) 
 
int send( int sockfd, const void *msg,int len,int flags); 


   那麼,現在的困惑是,為什麼send函式僅僅傳入sockfd就可以知道伺服器的ip和埠號?

   其實,由“問題一”中的答案我們已經很清楚了,sockfd描述符所描述的socket物件不僅包含了本地IP和埠,同時也包含了伺服器的IP和埠,這樣,才能使得send函式只需要傳入sockfd即可知道該把資料發向什麼地方。而程式碼中,目的IP和埠只是在connect函式中出現過,因此,肯定是connect函式在成功建立連線後,將目的IP和埠寫入了sockfd描述符所描述的socket物件中。

    問題三:accept函式產生的socket有沒有佔用新的埠?

   首先,回顧一下accept函式,原型如下

 
int accept(int sockfd, struct sockaddr* addr, socklen_t* len) 

   accept函式主要用於伺服器端,一般位於listen函式之後,預設會阻塞程式,直到有一個客戶請求連線,建立好連線後,它返回的一個新的套接字socketfd_new ,此後,伺服器端即可使用這個新的套接字socketfd_new與該客戶端進行通訊,而sockfd則繼續用於監聽其他客戶端的連線請求。

   至此,我的困惑產生了,這個新的套接字socketfd_new 與監聽套接字sockfd 是什麼關係?它所代表的socket物件包含了哪些資訊?socketfd_new是否佔用了新的埠與客戶端通訊?

   先簡單分析一番,由於網站的伺服器也是一種TCP伺服器,使用的是80埠,並不會因客戶端的連線而產生新的埠給客戶端服務,該客戶端依然是向伺服器端的80埠傳送資料,其他客戶端依然向80埠申請連線。因此,可以判斷,socketfd_new並沒有佔用新的埠與客戶端通訊,依然使用的是與監聽套接字socketfd_new一樣的埠號。

   那這麼說,難道一個埠可以被兩個socket物件繫結?當客戶端傳送資料過來的時候,究竟是與哪一個socket物件通訊呢?
 

 
  我是這麼理解的(歡迎拍磚)。

   首先,一個埠肯定只能繫結一個socket。我認為,伺服器端的埠在bind的時候已經繫結到了監聽套接字socetfd所描述的物件上,accept函式新建立的socket物件其實並沒有進行埠的佔有,而是複製了socetfd的本地IP和埠號,並且記錄了連線過來的客戶端的IP和埠號。

   那麼,當客戶端傳送資料過來的時候,究竟是與哪一個socket物件通訊呢?

    客戶端傳送過來的資料可以分為2種,一種是連線請求,一種是已經建立好連線後的資料傳輸。

   由於TCP/IP協議棧是維護著一個接收和傳送緩衝區的。在接收到來自客戶端的資料包後,伺服器端的TCP/IP協議棧應該會做如下處理:如果收到的是請求連線的資料包,則傳給監聽著連線請求埠的socetfd套接字,進行accept處理;如果是已經建立過連線後的客戶端資料包,則將資料放入接收緩衝區。這樣,當伺服器端需要讀取指定客戶端的資料時,則可以利用socketfd_new套接字通過recv或者read函式到緩衝區裡面去取指定的資料(因為socketfd_new代表的socket物件記錄了客戶端IP和埠,因此可以鑑別)。


關於問題三的再次深探究:

那麼新建立的連線使用的埠號是否和listen所用埠號相同呢?以前我一直以為伺服器會隨機分配一個新的埠號來使用,後來發現錯了。

因為1、現在使用多路IO複用epoll等,配置好點的伺服器可以支援數十萬個併發連線,埠號為16位,最多才2^16-1,且加上一些常用的埠號不能使用,可用的埠號都沒那麼多。2、現在伺服器大多使用防火牆,防火牆只對特定埠開放。如果accept隨機分配埠號,會不能通過防火牆。

TCP/IP協議中,IP協議是端到端的協議,它只是負責把把資料傳送到端,交付給上層而已。運輸層TCP、UDP加上了埠號,目的是區分不同的應用。其中TCP還實現了流量控制、可靠傳輸等,而UDP只是應該是沒有對IP層資料進行處理了。

在以往的知識中,我知道一個應用程式只能使用一個埠號,如果accept返回的控制程式碼還是使用listen的埠號,那麼怎麼實現通訊呢?如果建立多個連線,應用程式怎麼區收到的資訊來自哪個客戶端呢?

現在才理解到accept返回的控制程式碼建立的連線包括四部分:源IP、源埠號、目的IP、目的埠號。這樣在一個應用程式中,就算和多個客戶端建立連線,在收到資料後,應用程式通過目的IP和目的埠號也能區分是哪一條連線。

通過一個echo伺服器來驗證一下,client和server都在同一臺機器上:

伺服器監聽8000埠,在未建立連線時,可以看到在監聽8000


在通過一個客戶端建立連線後,可以看到建立了一條連線,伺服器端的埠號是8000,監聽的還是8000。


在連線一個客戶端,可以看到建立了兩條連線,伺服器端都是使用8000,監聽的還是8000。

相關文章