本文轉載自:http://ticktick.blog.51cto.com/823160/779866
今天與同學爭執一個話題:由於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;
- };
- 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; //本地端口(主機字節序)。
- …………
- }
問題二: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函數,原型如下:
- /* 參數:sockfd 監聽套接字,即服務器端創建的用於listen的socket描述符。
- * 參數:addr 這是一個結果參數,它用來接受一個返回值,這返回值指定客戶端的地址
- * 參數:len 描述 addr 的長度
- */
- 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和端口,因此可以鑑別)。