[TOC]
今天與同學(xué)爭執(zhí)一個話題:由于socket的accept函數(shù)在有客戶端連接的時候產(chǎn)生了新的socket用于服務(wù)該客戶端,那么,這個新的socket到底有沒有占用一個新的端口?
討論完后,才發(fā)現(xiàn),自己雖然熟悉socket的編程套路,但是卻并不是那么清楚socket的原理,今天就趁這個機(jī)會,把有關(guān)socket編程的幾個疑問給搞清楚吧。
先給出一個典型的TCP/IP通信示意圖。

問題一:socket結(jié)構(gòu)體對象究竟是怎樣定義的?
我們知道,在使用socket編程之前,需要調(diào)用socket函數(shù)創(chuàng)建一個socket對象,該函數(shù)返回該socket對象的描述符
int socket(int domain, int type, int protocol);
那么,這個socket對象究竟是怎么定義的呢?它記錄了哪些信息呢?只記錄了本機(jī)IP及端口、還是目的IP及端口、或者都記錄了?
關(guān)于這個問題,大家可以在內(nèi)核源碼里面找,也可以參考這篇文章《struct socket 結(jié)構(gòu)詳解》,我們可以看到socket 結(jié)構(gòu)體的定義如下:
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 結(jié)構(gòu)體,而sock_common結(jié)構(gòu)體又包含有struct inet_sock結(jié)構(gòu)體,而struct inet_sock 結(jié)構(gòu)體的部分定義如下:
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; //本地端口(主機(jī)字節(jié)序)。
…………
}
由此,我們清楚了,socket結(jié)構(gòu)體不僅僅記錄了本地的IP和端口號,還記錄了目的IP和端口。
問題二:connect函數(shù)究竟做了些什么操作?
在TCP客戶端,首先調(diào)用一個socket()函數(shù),得到一個socket描述符socketfd,然后通過connect函數(shù)對服務(wù)器進(jìn)行連接,連接成功后,就可以利用這個socketfd描述符使用send/recv函數(shù)收發(fā)數(shù)據(jù)了。
關(guān)于connect函數(shù)和send函數(shù)的原型如下:
int connect( int sockfd, const struct sockaddr* server_addr, socklen_t addrlen)
int send( int sockfd, const void *msg,int len,int flags);
那么,現(xiàn)在的困惑是,為什么send函數(shù)僅僅傳入sockfd就可以知道服務(wù)器的ip和端口號?
其實,由“問題一”中的答案我們已經(jīng)很清楚了,sockfd描述符所描述的socket對象不僅包含了本地IP和端口,同時也包含了服務(wù)器的IP和端口,這樣,才能使得send函數(shù)只需要傳入sockfd即可知道該把數(shù)據(jù)發(fā)向什么地方。而代碼中,目的IP和端口只是在connect函數(shù)中出現(xiàn)過,因此,肯定是connect函數(shù)在成功建立連接后,將目的IP和端口寫入了sockfd描述符所描述的socket對象中。
問題三:accept函數(shù)產(chǎn)生的socket有沒有占用新的端口?
首先,回顧一下accept函數(shù),原型如下:
int accept(int sockfd, struct sockaddr* addr, socklen_t* len)
accept函數(shù)主要用于服務(wù)器端,一般位于listen函數(shù)之后,默認(rèn)會阻塞進(jìn)程,直到有一個客戶請求連接,建立好連接后,它返回的一個新的套接字socketfd_new ,此后,服務(wù)器端即可使用這個新的套接字socketfd_new與該客戶端進(jìn)行通信,而sockfd則繼續(xù)用于監(jiān)聽其他客戶端的連接請求。
至此,我的困惑產(chǎn)生了,這個新的套接字socketfd_new 與監(jiān)聽套接字sockfd 是什么關(guān)系?它所代表的socket對象包含了哪些信息?socketfd_new是否占用了新的端口與客戶端通信?
先簡單分析一番,由于網(wǎng)站的服務(wù)器也是一種TCP服務(wù)器,使用的是80端口,并不會因客戶端的連接而產(chǎn)生新的端口給客戶端服務(wù),該客戶端依然是向服務(wù)器端的80端口發(fā)送數(shù)據(jù),其他客戶端依然向80端口申請連接。因此,可以判斷,socketfd_new并沒有占用新的端口與客戶端通信,依然使用的是與監(jiān)聽套接字socketfd_new一樣的端口號。
那這么說,難道一個端口可以被兩個socket對象綁定?當(dāng)客戶端發(fā)送數(shù)據(jù)過來的時候,究竟是與哪一個socket對象通信呢?
我是這么理解的(歡迎拍磚)。
首先,一個端口肯定只能綁定一個socket。我認(rèn)為,服務(wù)器端的端口在bind的時候已經(jīng)綁定到了監(jiān)聽套接字socetfd所描述的對象上,accept函數(shù)新創(chuàng)建的socket對象其實并沒有進(jìn)行端口的占有,而是復(fù)制了socetfd的本地IP和端口號,并且記錄了連接過來的客戶端的IP和端口號。
那么,當(dāng)客戶端發(fā)送數(shù)據(jù)過來的時候,究竟是與哪一個socket對象通信呢?
客戶端發(fā)送過來的數(shù)據(jù)可以分為2種,一種是連接請求,一種是已經(jīng)建立好連接后的數(shù)據(jù)傳輸。
由于TCP/IP協(xié)議棧是維護(hù)著一個接收和發(fā)送緩沖區(qū)的。在接收到來自客戶端的數(shù)據(jù)包后,服務(wù)器端的TCP/IP協(xié)議棧應(yīng)該會做如下處理:如果收到的是請求連接的數(shù)據(jù)包,則傳給監(jiān)聽著連接請求端口的socetfd套接字,進(jìn)行accept處理;如果是已經(jīng)建立過連接后的客戶端數(shù)據(jù)包,則將數(shù)據(jù)放入接收緩沖區(qū)。這樣,當(dāng)服務(wù)器端需要讀取指定客戶端的數(shù)據(jù)時,則可以利用socketfd_new套接字通過recv或者read函數(shù)到緩沖區(qū)里面去取指定的數(shù)據(jù)(因為socketfd_new代表的socket對象記錄了客戶端IP和端口,因此可以鑒別)。
現(xiàn)在使用多路IO復(fù)用epoll等,配置好點的服務(wù)器可以支持?jǐn)?shù)十萬個并發(fā)連接,端口號為16位,最多才2^16-1,且加上一些常用的端口號不能使用,可用的端口號都沒那么多。2、現(xiàn)在服務(wù)器大多使用防火墻,防火墻只對特定端口開放。如果accept隨機(jī)分配端口號,會不能通過防火墻。