Socket中的一些見解

本文轉載自:http://ticktick.blog.51cto.com/823160/779866

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

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

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

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

    我們知道,在使用socket編程之前,需要調用socket函數創建一個socket對象,該函數返回該socket對象的描述符。

  1. 函數原型:int socket(int domain, int type, int protocol); 

    那麼,這個socket對象究竟是怎麼定義的呢?它記錄了哪些信息呢?只記錄了本機IP及端口、還是目的IP及端口、或者都記錄了?

    關於這個問題,大家可以在內核源碼裏面找,也可以參考這篇文章《struct socket 結構詳解》,我們可以看到 socket  結構體的定義如下:   

  1. struct socket   
  2. {   
  3.     socket_state              state;   
  4.     unsigned long             flags;   
  5.     const struct proto_ops    *ops;   
  6.     struct fasync_struct      *fasync_list;   
  7.     struct file               *file;   
  8.     struct sock               *sk;   
  9.     wait_queue_head_t         wait;   
  10.     short                     type;   
  11. };  
其中,struct sock 包含有一個 sock_common 結構體,而sock_common結構體又包含有struct inet_sock 結構體,而struct inet_sock 結構體的部分定義如下:
  1. struct inet_sock   
  2. {   
  3.     struct sock     sk;   
  4. #if defined(CONFIG_IPV6) || defined(CONFIG_IPV6_MODULE)   
  5.     struct ipv6_pinfo   *pinet6;   
  6. #endif   
  7.     __u32           daddr;          //IPv4的目的地址。   
  8.     __u32           rcv_saddr;      //IPv4的本地接收地址。   
  9.     __u16           dport;          //目的端口。   
  10.     __u16           num;            //本地端口(主機字節序)。  
  11.     
  12. …………      
  13. }
由此,我們清楚了,socket結構體不僅僅記錄了本地的IP和端口號,還記錄了目的IP和端口。

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

     在TCP客戶端,首先調用一個socket()函數,得到一個socket描述符socketfd,然後通過connect函數對服務器進行連接,連接成功後,就可以利用這個socketfd描述符使用send/recv函數收發數據了。

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

  1. int connect( int sockfd, const struct sockaddr* server_addr, socklen_t addrlen)  
  2.  
  3. 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函數,原型如下:

  1. /* 參數:sockfd 監聽套接字,即服務器端創建的用於listen的socket描述符。  
  2.  * 參數:addr  這是一個結果參數,它用來接受一個返回值,這返回值指定客戶端的地址  
  3.  * 參數:len 描述 addr 的長度  
  4.  */ 
  5. int accept(int sockfd, struct sockaddr* addr, socklen_t* len)  
  6.  

    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和端口,因此可以鑑別)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章