TCP連接中客戶端的端口號(hào)是如何確定的?

在 TCP 連接中,客戶端在發(fā)起連接請(qǐng)求前會(huì)先確定一個(gè)客戶端端口,然后用這個(gè)端口去和服務(wù)器端進(jìn)行握手建立連接。那么在 Linux 上,客戶端的端口到底是如何被確定下來(lái)的呢?

事實(shí)上很多我們平時(shí)遇到的問(wèn)題都和這個(gè)端口選擇過(guò)程相關(guān),如果能深度理解這個(gè)過(guò)程,將有助于我們對(duì)這些問(wèn)題的深刻理解。

  • Cannot assign requested address 報(bào)錯(cuò)是怎么回事?
  • 一個(gè)客戶端端口可以同時(shí)用在兩條 TCP 連接上嗎?

還是讓我們借助一段簡(jiǎn)單到只有兩句的代碼,從這個(gè)來(lái)講起!

int?main(){
?fd?=?socket(AF_INET,SOCK_STREAM,?0);
?connect(fd,?...);
?...
}

一、創(chuàng)建 socket

客戶端在發(fā)起連接的時(shí)候,需要事先創(chuàng)建一個(gè) socket。在 c 語(yǔ)言中,就是調(diào)用 socket 函數(shù),例如 socket(AF_INET,SOCK_STREAM, 0) 這句。

socket 函數(shù)執(zhí)行完畢后,在用戶層視角我們是看到返回了一個(gè)文件描述符 fd。但在內(nèi)核中其實(shí)是一套內(nèi)核對(duì)象組合,大體結(jié)構(gòu)如下。

從上圖我們看到,socket 在內(nèi)核里并不是一個(gè)內(nèi)核對(duì)象。而是包含 file、socket、sock 等多個(gè)相關(guān)內(nèi)核對(duì)象構(gòu)成,每個(gè)內(nèi)核對(duì)象還定義了 ops 操作函數(shù)集合。在后面的內(nèi)核源碼執(zhí)行過(guò)程中,我們需要時(shí)不時(shí)回頭來(lái)看這些內(nèi)核對(duì)象,這里先簡(jiǎn)單了解一下就行。

這些內(nèi)核對(duì)象都是在 socket 系統(tǒng)調(diào)用執(zhí)行過(guò)程中創(chuàng)建出來(lái)的。為了避免喧賓奪主,這里只列出入口代碼,詳細(xì)過(guò)程就不展開(kāi)介紹了。

//file:?net/socket.c
SYSCALL_DEFINE3(socket,?int,?family,?int,?type,?int,?protocol)
{
?//創(chuàng)建?socket、sock?等內(nèi)核對(duì)象,并初始化
?sock_create(family,?type,?protocol,?&sock);

?//創(chuàng)建?file?內(nèi)核對(duì)象,申請(qǐng)?fd
?sock_map_fd(sock,?flags?&?(O_CLOEXEC?|?O_NONBLOCK));

?......
}

二、connect 發(fā)起連接

接下來(lái)我們就進(jìn)入到 connect 函數(shù)的執(zhí)行過(guò)程中來(lái)。由于這個(gè)過(guò)程比較長(zhǎng),所以我們分成幾個(gè)小節(jié)來(lái)進(jìn)行討論。

2.1 connect 調(diào)用鏈展開(kāi)

當(dāng)我們?cè)诳蛻舳藱C(jī)上調(diào)用 connect 函數(shù)的時(shí)候,事實(shí)上會(huì)進(jìn)入到內(nèi)核的系統(tǒng)調(diào)用源碼中進(jìn)行執(zhí)行。

//file:?net/socket.c
SYSCALL_DEFINE3(connect,?int,?fd,?struct?sockaddr?__user?*,?uservaddr,
??int,?addrlen)
{
?struct?socket?*sock;

?//根據(jù)用戶?fd?查找內(nèi)核中的?socket?對(duì)象
?sock?=?sockfd_lookup_light(fd,?&err,?&fput_needed);

?//進(jìn)行?connect
?err?=?sock->ops->connect(sock,?(struct?sockaddr?*)&address,?addrlen,
?????sock->file->f_flags);
?...
}

這段代碼首先根據(jù)用戶傳入的 fd(文件描述符)來(lái)查詢對(duì)應(yīng)的 socket 內(nèi)核對(duì)象。在第一節(jié)中我們看了 socket 內(nèi)核對(duì)象結(jié)構(gòu),據(jù)此可以知道接下來(lái) sock->ops->connect 其實(shí)調(diào)用的是 inet_stream_connect 函數(shù)。

//file:?ipv4/af_inet.c
int?inet_stream_connect(struct?socket?*sock,?...){?
?...
?__inet_stream_connect(sock,?uaddr,?addr_len,?flags);
}

int?__inet_stream_connect(struct?socket?*sock,?...)
{
?struct?sock?*sk?=?sock->sk;

?switch?(sock->state)?{
??case?SS_UNCONNECTED:
???err?=?sk->sk_prot->connect(sk,?uaddr,?addr_len);
???sock->state?=?SS_CONNECTING;
???break;
?}
?...
}

剛創(chuàng)建完畢的 socket 的狀態(tài)就是 SS_UNCONNECTED,所以在 __inet_stream_connect 中的 switch 判斷會(huì)進(jìn)入到 case SS_UNCONNECTED 的處理邏輯中。

上述代碼中 sk 取的是 sock 對(duì)象。繼續(xù)回顧第一節(jié)中 socket 的內(nèi)核數(shù)據(jù)結(jié)構(gòu)圖,可以得知 sk->sk_prot->connect 實(shí)際上對(duì)應(yīng)的是 tcp_v4_connect 方法。

我們來(lái)看 tcp_v4_connect 的代碼,它位于 net/ipv4/tcp_ipv4.c。

//file:?net/ipv4/tcp_ipv4.c
int?tcp_v4_connect(struct?sock?*sk,?struct?sockaddr?*uaddr,?int?addr_len){
?//設(shè)置?socket?狀態(tài)為?TCP_SYN_SENT
?tcp_set_state(sk,?TCP_SYN_SENT);

?//動(dòng)態(tài)選擇一個(gè)端口
?err?=?inet_hash_connect(&tcp_death_row,?sk);

?//函數(shù)用來(lái)根據(jù) sk 中的信息,構(gòu)建一個(gè)完成的 syn 報(bào)文,并將它發(fā)送出去。
?err?=?tcp_connect(sk);
}

在 tcp_v4_connect 中我們終于看到了選擇端口的函數(shù),那就是 inet_hash_connect。

2.2 選擇可用端口

我們找到 inet_hash_connect 的源碼,我們來(lái)看看到底端口是如何選擇出來(lái)的。

//file:net/ipv4/inet_hashtables.c
int?inet_hash_connect(struct?inet_timewait_death_row?*death_row,????????struct?sock?*sk){
?return?__inet_hash_connect(death_row,?sk,?inet_sk_port_offset(sk),
???__inet_check_established,?__inet_hash_nolisten);
}

這里需要提一下在調(diào)用 __inet_hash_connect 時(shí)傳入的兩個(gè)重要參數(shù)。

  • inet_sk_port_offset(sk):這個(gè)函數(shù)是根據(jù)要連接的目的 IP 和端口等信息生成一個(gè)隨機(jī)數(shù)。
  • __inet_check_established:檢查是否和現(xiàn)有 ESTABLISH 的連接是否沖突的時(shí)候用的函數(shù)

了解了這兩個(gè)參數(shù)后,讓我們進(jìn)入 __inet_hash_connect。這個(gè)函數(shù)比較長(zhǎng),為了方便理解,我們先看前面這一段。

//file:net/ipv4/inet_hashtables.c
int?__inet_hash_connect(...)
{
?//是否綁定過(guò)端口
?const?unsigned?short?snum?=?inet_sk(sk)->inet_num;

?//獲取本地端口配置
?inet_get_local_port_range(&low,?&high);
??remaining?=?(high?-?low)?+?1;

?if?(!snum)?{
??//遍歷查找
??for?(i?=?1;?i?<=?remaining;?i++)?{
???port?=?low?+?(i?+?offset)?%?remaining;
???...
??}
?}
}

在這個(gè)函數(shù)中首先判斷了 inet_sk(sk)->inet_num,如果我們調(diào)用過(guò) bind,那么這個(gè)函數(shù)會(huì)選擇好端口并設(shè)置在 inet_num 上。這個(gè)我們后面專門(mén)分一小節(jié)介紹。這里我們假設(shè)沒(méi)有調(diào)用過(guò) bind,所以 snum 為 0。

接著調(diào)用 inet_get_local_port_range,這個(gè)函數(shù)讀取的是 net.ipv4.ip_local_port_range 這個(gè)內(nèi)核參數(shù)。來(lái)讀取管理員配置的可用的端口范圍。

該參數(shù)的默認(rèn)值是 32768 61000,意味著端口總可用的數(shù)量是 61000 - 32768 = 28232 個(gè)。如果你覺(jué)得這個(gè)數(shù)字不夠用,那就修改你的 net.ipv4.ip_local_port_range 內(nèi)核參數(shù)。

接下來(lái)進(jìn)入到了 for 循環(huán)中。其中offset 是我們前面說(shuō)的,通過(guò) inet_sk_port_offset(sk) 計(jì)算出的隨機(jī)數(shù)。那這段循環(huán)的作用就是從某個(gè)隨機(jī)數(shù)開(kāi)始,把整個(gè)可用端口范圍來(lái)遍歷一遍。直到找到可用的端口后停止。

那么我們接著來(lái)看,如何來(lái)確定一個(gè)端口是否可以使用呢?

//file:net/ipv4/inet_hashtables.c
int?__inet_hash_connect(...)
{
?for?(i?=?1;?i?<=?remaining;?i++)?{
??port?=?low?+?(i?+?offset)?%?remaining;

??//查看是否是保留端口,是則跳過(guò)
??if?(inet_is_reserved_local_port(port))
???continue;

??//?查找和遍歷已經(jīng)使用的端口的哈希鏈表
??head?=?&hinfo->bhash[inet_bhashfn(net,?port,
????hinfo->bhash_size)];
??inet_bind_bucket_for_each(tb,?&head->chain)?{

???//如果端口已經(jīng)被使用
???if?(net_eq(ib_net(tb),?net)?&&
???????tb->port?==?port)?{

????????????????//通過(guò)?check_established?繼續(xù)檢查是否可用
????if?(!check_established(death_row,?sk,
???????port,?&tw))
?????goto?ok;
???}
??}

??//未使用的話,直接?ok
??goto?ok;
?}

?return?-EADDRNOTAVAIL;
ok:?
?...??
}

首先判斷的是 inet_is_reserved_local_port,這個(gè)很簡(jiǎn)單就是判斷要選擇的端口是否在 net.ipv4.ip_local_reserved_ports 中,在的話就不能用。

如果你因?yàn)槟撤N原因不希望某些端口被內(nèi)核使用,那么就把它們寫(xiě)到 ip_local_reserved_ports 這個(gè)內(nèi)核參數(shù)中就行了。

整個(gè)系統(tǒng)中會(huì)維護(hù)一個(gè)所有使用過(guò)的端口的哈希表,它就是 hinfo->bhash。接下來(lái)的代碼就會(huì)在這里進(jìn)行查找。如果在哈希表中沒(méi)有找到,那么說(shuō)明這個(gè)端口是可用的。至此端口就算是找到了。

遍歷完所有端口都沒(méi)找到合適的,就返回 -EADDRNOTAVAIL,你在用戶程序上看到的就是 Cannot assign requested address 這個(gè)錯(cuò)誤。怎么樣,是不是很眼熟,你見(jiàn)過(guò)它的對(duì)吧,哈哈!

/*?Cannot?assign?requested?address?*/
#define?EADDRNOTAVAIL?99?

以后當(dāng)你再遇到 Cannot assign requested address 錯(cuò)誤,你應(yīng)該想到去查一下 net.ipv4.ip_local_port_range 中設(shè)置的可用端口的范圍是不是太小了。

2.3 端口被使用過(guò)怎么辦

回顧剛才的 __inet_hash_connect, 為了描述簡(jiǎn)單我們之前跳過(guò)了已經(jīng)在 bhash 中存在時(shí)候的判斷。這是由于其一這個(gè)過(guò)程比較長(zhǎng),其二這段邏輯很有價(jià)值,所以飛哥單獨(dú)拉一小節(jié)出來(lái)。

//file:net/ipv4/inet_hashtables.c
int?__inet_hash_connect(...)
{
?for?(i?=?1;?i?<=?remaining;?i++)?{
??port?=?low?+?(i?+?offset)?%?remaining;

??...
??//如果端口已經(jīng)被使用
??if?(net_eq(ib_net(tb),?net)?&&
???????tb->port?==?port)?{
???//通過(guò)?check_established?繼續(xù)檢查是否可用
???if?(!check_established(death_row,?sk,?port,?&tw))
????goto?ok;
??}
?}
}

port 已經(jīng)在 bhash 中如果已經(jīng)存在,就表示有其它的連接使用過(guò)該端口了。請(qǐng)注意,如果 check_established 返回 0,該端口仍然可以接著使用!。

這里可能會(huì)讓很多同學(xué)困惑了,一個(gè)端口怎么可以被用多次呢?

回憶下四元組的概念,兩對(duì)兒四元組中只要任意一個(gè)元素不同,都算是兩條不同的連接。以下的兩條 TCP 連接完全可以同時(shí)存在(假設(shè) 192.168.1.101 是客戶端,192.168.1.100 是服務(wù)端)

  • 連接1:192.168.1.101 5000 192.168.1.100 8090
  • 連接2:192.168.1.101 5000 192.168.1.100 8091

check_established 作用就是檢測(cè)現(xiàn)有的 TCP 連接中是否四元組和要建立的連接四元素完全一致。如果不完全一致,那么該端口仍然可用!??!

這個(gè) check_established 是由調(diào)用方傳入的,實(shí)際上使用的是 __inet_check_established。我們來(lái)看它的源碼。

//file:?net/ipv4/inet_hashtables.c
static?int?__inet_check_established(struct?inet_timewait_death_row?*death_row,
????????struct?sock?*sk,?__u16?lport,
????????struct?inet_timewait_sock?**twp)
{
?//找到hash桶
?struct?inet_ehash_bucket?*head?=?inet_ehash_bucket(hinfo,?hash);

?//遍歷看看有沒(méi)有四元組一樣的,一樣的話就報(bào)錯(cuò)
?sk_nulls_for_each(sk2,?node,?&head->chain)?{
??if?(sk2->sk_hash?!=?hash)
???continue;
??if?(likely(INET_MATCH(sk2,?net,?acookie,
??????????saddr,?daddr,?ports,?dif)))
???goto?not_unique;
?}

unique:
?//要用了,記錄,返回?0?(成功)
?return?0;
not_unique:
?return?-EADDRNOTAVAIL;?
}

該函數(shù)首先找到 inet_ehash_bucket,這個(gè)和 bhash 類似,只不過(guò)是所有 ESTABLISH 狀態(tài)的 socket 組成的哈希表。然后遍歷這個(gè)哈希表,使用 INET_MATCH 來(lái)判斷是否可用。

這里 INET_MATCH 源碼如下:

//?include/net/inet_hashtables.h
#define?INET_MATCH(__sk,?__net,?__cookie,?__saddr,?__daddr,?__ports,?__dif)?\
?((inet_sk(__sk)->inet_portpair?==?(__ports))?&&??\
??(inet_sk(__sk)->inet_daddr?==?(__saddr))?&&??\
??(inet_sk(__sk)->inet_rcv_saddr?==?(__daddr))?&&??\
??(!(__sk)->sk_bound_dev_if?||????\
????((__sk)->sk_bound_dev_if?==?(__dif)))??&&??\??net_eq(sock_net(__sk),?(__net)))

在 INET_MATCH 中將 __saddr、__daddr、__ports 都進(jìn)行了比較。當(dāng)然除了 ip 和端口,INET_MATCH還比較了其它一些東東,所以 TCP 連接還有五元組、七元組之類的說(shuō)法。為了統(tǒng)一,咱們還沿用四元組的說(shuō)法。

如果 MATCH,就是說(shuō)就四元組完全一致的連接,所以這個(gè)端口不可用。也返回 -EADDRNOTAVAIL。

如果不 MATCH,哪怕四元組中有一個(gè)元素不一樣,例如服務(wù)器的端口號(hào)不一樣,那么就 return 0,表示該端口仍然可用于建立新連接。

所以一臺(tái)客戶端機(jī)最大能建立的連接數(shù)并不是 65535。只要 server 足夠多,單機(jī)發(fā)出百萬(wàn)條連接沒(méi)有任何問(wèn)題。

2.4 發(fā)起 syn 請(qǐng)求

再回到 tcp_v4_connect,這時(shí)我們的 inet_hash_connect 已經(jīng)返回了一個(gè)可用端口了。接下來(lái)就進(jìn)入到 tcp_connect,如下源碼所示。

//file:?net/ipv4/tcp_ipv4.c
int?tcp_v4_connect(struct?sock?*sk,?struct?sockaddr?*uaddr,?int?addr_len){
?......

?//動(dòng)態(tài)選擇一個(gè)端口
?err?=?inet_hash_connect(&tcp_death_row,?sk);

?//函數(shù)用來(lái)根據(jù) sk 中的信息,構(gòu)建一個(gè)完成的 syn 報(bào)文,并將它發(fā)送出去。
?err?=?tcp_connect(sk);
}

到這里其實(shí)就和本文要討論的主題沒(méi)有關(guān)系了,所以我們只是簡(jiǎn)單看一下。

//file:net/ipv4/tcp_output.c
int?tcp_connect(struct?sock?*sk){
?//申請(qǐng)并設(shè)置?skb
?buff?=?alloc_skb_fclone(MAX_TCP_HEADER?+?15,?sk->sk_allocation);
?tcp_init_nondata_skb(buff,?tp->write_seq++,?TCPHDR_SYN);

?//添加到發(fā)送隊(duì)列?sk_write_queue?上
?tcp_connect_queue_skb(sk,?buff);

?//實(shí)際發(fā)出?syn
?err?=?tp->fastopen_req???tcp_send_syn_data(sk,?buff)?:
???????tcp_transmit_skb(sk,?buff,?1,?sk->sk_allocation);

?//啟動(dòng)重傳定時(shí)器
?inet_csk_reset_xmit_timer(sk,?ICSK_TIME_RETRANS,
??????inet_csk(sk)->icsk_rto,?TCP_RTO_MAX);
}

tcp_connect 一口氣做了這么幾件事

  • 申請(qǐng)一個(gè) skb,并將其設(shè)置為 SYN 包
  • 添加到發(fā)送隊(duì)列上
  • 調(diào)用 tcp_transmit_skb 將該包發(fā)出
  • 啟動(dòng)一個(gè)重傳定時(shí)器,超時(shí)會(huì)重發(fā)

三、bind 時(shí)端口如何選擇

在 2.2 小節(jié)中,我們看到 connect 選擇端口之前先判斷了 inet_sk(sk)->inet_num 有沒(méi)有值。如果有的話就直接用這個(gè),而會(huì)跳過(guò)端口選擇過(guò)程。

那么這個(gè)值是從哪兒來(lái)的呢?不賣關(guān)子,它就是在對(duì) socket 使用 bind 時(shí)設(shè)置的。

不只是服務(wù)器端,哪怕是對(duì)于客戶端,也可以對(duì) socket 使用 bind 來(lái)綁定 IP 或者端口。如果使用了 bind,那么在 bind 的時(shí)候就會(huì)確定好端口,并設(shè)置到 inet_num 變量中。

一般非常不推薦在客戶端角色下使用 bind。因?yàn)檫@會(huì)打亂 connect 里的端口選擇過(guò)程。

bind 的時(shí)候,如果傳了端口,那么 bind 就會(huì)嘗試使用該端口。如果端口號(hào)傳的是 0 ,那么 bind 有一套獨(dú)立的選擇端口號(hào)的邏輯。

//file:?net/ipv4/af_inet.c
int?inet_bind(struct?socket?*sock,?struct?sockaddr?*uaddr,?int?addr_len){
?struct?sock?*sk?=?sock->sk;
?...

?//用戶傳入的端口號(hào)
?snum?=?ntohs(addr->sin_port);

?//不允許綁定?1024?以下的端口
?if?(snum?&&?snum?<?PROT_SOCK?&&
?????!ns_capable(net->user_ns,?CAP_NET_BIND_SERVICE))
??goto?out;

?//嘗試確定端口號(hào)
?if?(sk->sk_prot->get_port(sk,?snum))?{
??inet->inet_saddr?=?inet->inet_rcv_saddr?=?0;
??err?=?-EADDRINUSE;
??goto?out_release_sock;
?}

根據(jù)第一節(jié)中的 socket 內(nèi)核對(duì)象,能找到 sk->sk_prot->get_port 實(shí)際調(diào)用的是 inet_csk_get_port。該函數(shù)來(lái)嘗試確定端口號(hào),如果嘗試失敗,返回 EADDRINUSE。你的應(yīng)用程序?qū)?huì)顯示一條錯(cuò)誤信息 “Address already in use”。

#define?EADDRINUSE?226?/*?Address?already?in?use?*/

我們簡(jiǎn)單看一下如果用戶沒(méi)有傳入端口(傳入的為 0),bind 是怎么選擇端口的。

//file:?net/ipv4/inet_connection_sock.c
int?inet_csk_get_port(struct?sock?*sk,?unsigned?short?snum){
?...

?if?(!snum)?{
??inet_get_local_port_range(&low,?&high);
??remaining?=?(high?-?low)?+?1;
??smallest_rover?=?rover?=?net_random()?%?remaining?+?low;

??do?{
???if?(inet_is_reserved_local_port(rover))
????goto?next_nolock;

???head?=?&hashinfo->bhash[inet_bhashfn(net,?rover,
??????hashinfo->bhash_size)];
???inet_bind_bucket_for_each(tb,?&head->chain)

????//?沖突檢測(cè)
????if?(!inet_csk(sk)->icsk_af_ops->bind_conflict(sk,?tb,?false))?{
?????snum?=?rover;
?????goto?tb_found;
????}

??}?while?(--remaining?>?0);
?}
}

這段邏輯和 connect 很像,通過(guò) net_random 來(lái)從 net.ipv4.ip_local_port_range 指定的端口范圍內(nèi)一個(gè)隨機(jī)位置開(kāi)始遍歷。也會(huì)跳開(kāi) ip_local_reserved_ports 保留端口配置。通過(guò) inet_csk(sk)->icsk_af_ops->bind_conflict 進(jìn)行沖突檢測(cè)。

inet_csk_bind_conflict 這個(gè)函數(shù)整體比較復(fù)雜,不過(guò)我們只需要了解一點(diǎn)就好,該函數(shù)和 connect 中端口選擇邏輯不同的是,并不會(huì)到 ESTABLISH 的哈希表進(jìn)行可用檢測(cè),只在 bind 狀態(tài)的 socket 里查。所以默認(rèn)情況下,只要端口用過(guò)一次就不會(huì)再次使用。

四、結(jié)論

客戶端建立連接前需要確定一個(gè)端口,該端口會(huì)在兩個(gè)位置進(jìn)行確定。

第一個(gè)位置,也是最主要的確定時(shí)機(jī)是 connect 系統(tǒng)調(diào)用執(zhí)行過(guò)程。在 connect 的時(shí)候,會(huì)隨機(jī)地從 ip_local_port_range 選擇一個(gè)位置開(kāi)始循環(huán)判斷。找到可用端口后,發(fā)出 syn 握手包。如果端口查找失敗,會(huì)報(bào)錯(cuò) “Cannot assign requested address”。這個(gè)時(shí)候你應(yīng)該首先想到去檢查一下服務(wù)器上的 net.ipv4.ip_local_port_range 參數(shù),是不是可以再放的多一些。

如果你因?yàn)槟撤N原因不希望某些端口被使用到,那么就把它們寫(xiě)到 ip_local_reserved_ports 這個(gè)內(nèi)核參數(shù)中就行了,內(nèi)核在選擇的時(shí)候會(huì)跳過(guò)這些端口。

另外注意即使是一個(gè)端口是可以被用于多條 TCP 連接的。所以一臺(tái)客戶端機(jī)最大能建立的連接數(shù)并不是 65535。只要 server 足夠多,單機(jī)發(fā)出百萬(wàn)條連接沒(méi)有任何問(wèn)題。

我給大伙兒貼一下我實(shí)驗(yàn)時(shí)候在客戶機(jī)上實(shí)驗(yàn)時(shí)的實(shí)際截圖,來(lái)實(shí)際看一下一個(gè)端口號(hào)確實(shí)是被用在了多條連接上了。

截圖中左邊的 192 是客戶端,右邊的 119 是服務(wù)器的 ip??梢钥吹娇蛻舳说?10000 這個(gè)端口號(hào)是用在了多條連接上了的。

第二個(gè)位置,如果在 connect 之前使用了 bind,將會(huì)使得 connect 時(shí)的端口選擇方式無(wú)效。轉(zhuǎn)而使用 bind 時(shí)確定的端口。bind 時(shí)如果傳入了端口號(hào),會(huì)嘗試首先使用該端口號(hào),如果傳入了 0 ,也會(huì)自動(dòng)選擇一個(gè)。但默認(rèn)情況下一個(gè)端口只會(huì)被使用一次。所以對(duì)于客戶端角色的 socket,不建議使用 bind !

最后我再想多說(shuō)一句,上面的選擇端口的都是從 ip_local_port_range 范圍中的某一個(gè)隨機(jī)位置開(kāi)始循環(huán)的。如果可用端口很充足,則能快一點(diǎn)找到可用端口,那循環(huán)很快就能退出。假設(shè)實(shí)際中 ip_local_port_range 中的端口快被用光了,這時(shí)候內(nèi)核就大概率得把循環(huán)多執(zhí)行很多輪才能找到,這會(huì)導(dǎo)致 connect 系統(tǒng)調(diào)用的 CPU 開(kāi)銷的上漲。

所以,最好不要等到端口不夠用了才加大 ip_local_port_range 的范圍,而是事先就應(yīng)該保持一個(gè)充足的范圍。

喜歡的話別忘了?分享、點(diǎn)贊、收藏?三連~

歡迎關(guān)注公眾號(hào)?前端進(jìn)階體驗(yàn)?收獲更多優(yōu)質(zhì)文章~

本文使用 文章同步助手 同步

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容