ServerSocket接口客戶端請求端口問題

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

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

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

問題一:socket結構體對象到底是怎樣定義的?dom

   咱們知道,在使用socket編程以前,須要調用socket函數建立一個socket對象,該函數返回該socket對象的描述符socket

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

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

關於這個問題,你們能夠在內核源碼裏面找,也能夠參考這篇文章《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和端口。spa

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

在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所用端口號相同呢?之前我一直覺得服務器會隨機分配一個新的端口號來使用,後來發現錯了。

由於一、如今使用多路IO複用epoll等,配置好點的服務器能夠支持數十萬個併發鏈接,端口號爲16位,最多才2^16-1,且加上一些經常使用的端口號不能使用,可用的端口號都沒那麼多。二、如今服務器大多使用防火牆,防火牆只對特定端口開放。若是accept隨機分配端口號,會不能經過防火牆。

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

在以往的知識中,我知道一個應用程序只能使用一個端口號,若是accept返回的句柄仍是使用listen的端口號,那麼怎麼實現通訊呢?若是創建多個鏈接,應用程序怎麼區收到的信息來自哪一個客戶端呢?

如今才理解到accept返回的句柄創建的鏈接包括四部分:源IP、源端口號、目的IP、目的端口號。這樣在一個應用程序中,就算和多個客戶端創建鏈接,在收到數據後,應用程序經過目的IP和目的端口號也能區分是哪一條鏈接。

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

服務器監聽8000端口,在未創建鏈接時,能夠看到在監聽8000

在經過一個客戶端創建鏈接後,能夠看到創建了一條鏈接,服務器端的端口號是8000,監聽的仍是8000。

在鏈接一個客戶端,能夠看到創建了兩條鏈接,服務器端都是使用8000,監聽的仍是8000。

相關文章
相關標籤/搜索