從內(nèi)核角度看怎么設(shè)置connect超時(shí)
我們?cè)诰帉?xiě)網(wǎng)絡(luò)程序時(shí),通常需要連接其他服務(wù)端(如微服務(wù)之間的通信),這時(shí)就需要通過(guò)調(diào)用 connect 函數(shù)來(lái)連接服務(wù)端。但我們發(fā)現(xiàn) connect 函數(shù)并沒(méi)有提供超時(shí)的設(shè)置,而在 Linux 系統(tǒng)中,connect 的默認(rèn)超時(shí)時(shí)間為75秒。所以,在連接不上服務(wù)端的情況下,我們需要等待75秒,這對(duì)我們不能接受的。
通過(guò) SO_SNDTIMEO 設(shè)置 connect 超時(shí)時(shí)間
雖然 connect 系統(tǒng)調(diào)用沒(méi)有提供超時(shí)的設(shè)置,但我們通過(guò)查閱 Linux 內(nèi)核代碼可以發(fā)現(xiàn),connect 系統(tǒng)調(diào)用的超時(shí)時(shí)間可以通過(guò) SO_SNDTIMEO 參數(shù)來(lái)設(shè)定的,而 SO_SNDTIMEO 參數(shù)可以通過(guò) setsockopt 系統(tǒng)調(diào)用來(lái)設(shè)置,如下代碼:
struct timeval tv;tv.tv_sec = 1; /* 設(shè)置1秒超時(shí) */tv.tv_usec = 0;setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
一般來(lái)說(shuō),SO_SNDTIMEO 參數(shù)是用來(lái)設(shè)置 socket 的發(fā)送超時(shí)時(shí)間,為什么在 Linux 中還能設(shè)置 connect 的超時(shí)時(shí)間呢?我們來(lái)查看一下 connect 系統(tǒng)調(diào)用的實(shí)現(xiàn):
// 調(diào)用鏈: connect() -> sys_connect() -> inet_stream_connect()int inet_stream_connect(struct socket *sock, struct sockaddr * uaddr,int addr_len, int flags){struct sock *sk = sock->sk;int err;long timeo;lock_sock(sk);...switch (sock->state) {...case SS_UNCONNECTED:...err = -EINPROGRESS;break;}timeo = sock_sndtimeo(sk, flags & O_NONBLOCK); // 獲取 connect 超時(shí)時(shí)間,如果是非阻塞會(huì)返回0if ((1<<sk->state)&(TCPF_SYN_SENT|TCPF_SYN_RECV)) {// 如果 socket 設(shè)置了非阻塞或者 connect 超時(shí)了// 跳到 out 處執(zhí)行, 并且返回 EINPROGRESS 錯(cuò)誤if (!timeo || !inet_wait_for_connect(sk, timeo))goto out;err = sock_intr_errno(timeo);if (signal_pending(current))goto out;}if (sk->state == TCP_CLOSE)goto sock_error;sock->state = SS_CONNECTED;err = 0;out:release_sock(sk);return err;...}
在 inet_stream_connect 函數(shù)中,首先調(diào)用了 sock_sndtimeo 獲取 socket 的 SO_SNDTIMEO 的值,我們來(lái)看看 sock_sndtimeo 函數(shù)的實(shí)現(xiàn):
static inline long sock_sndtimeo(struct sock *sk, int noblock){return noblock ? 0 : sk->sndtimeo; // 獲取socket的SO_SNDTIMEO的值,如果socket被設(shè)置了非阻塞,那么返回0}
sock_sndtimeo 函數(shù)只是簡(jiǎn)單的從 socket 對(duì)象中獲取 sndtimeo 字段的值,如果 socket 被設(shè)置了非阻塞,那么就返回0。
我們接著分析 inet_stream_connect 函數(shù),在獲取到 SO_SNDTIMEO 的值后,就調(diào)用 inet_wait_for_connect 函數(shù)等待 socket 連接返回。返回三種情況:
連接成功了。
連接超時(shí)了。
連接被中斷了。
如果連接成功,connect 會(huì)返回0;如果連接超時(shí),connect 會(huì)返回 EINPROGRESS 錯(cuò)誤;如果連接被中斷,connect 會(huì)返回 EINTR 錯(cuò)誤。
通過(guò)非阻塞與多路復(fù)用IO設(shè)置 connect 超時(shí)時(shí)間
從上面的分析可以看到,當(dāng)把 socket 設(shè)置為非阻塞時(shí),connect 系統(tǒng)調(diào)用會(huì)立刻返回 EINPROGRESS 錯(cuò)誤,這時(shí)我們可以把 socket 添加到多路復(fù)用 IO 中進(jìn)行監(jiān)聽(tīng),并且設(shè)置多路復(fù)用 IO 的超時(shí)時(shí)間即可達(dá)到設(shè)置 connect 超時(shí)時(shí)間的目的,如下代碼:
int connect_timeout(int sockfd, struct sockaddr *serv_addr, int addrlen, int timeout){int flags = fcntl(sockfd, F_GETFL, 0);fcntl(sockfd, F_SETFL, flags|O_NONBLOCK); // 設(shè)置為非阻塞int n = connect(sockfd, serv_addr, sizeof(*serv_addr)); // 連接服務(wù)端if (n < 0) {if (errno != EINPROGRESS && errno != EWOULDBLOCK)return -1;struct timeval tv;fd_set wset;tv.tv_sec = timeout/1000;tv.tv_usec = (timeout - tv.tv_sec*1000)*1000;FD_ZERO(&wset);FD_SET(sockfd, &wset); // 把socket添加到select中進(jìn)行監(jiān)聽(tīng)n = select(sockfd + 1, NULL, &wset, NULL, &tv);if (n < 0) {return -1; // 出錯(cuò)} else if (0 == n) {return 0; // 超時(shí)}}fcntl(fd,F_SETFL,flags & ~O_NONBLOCK); // 恢復(fù)為阻塞模式return 1;}
connect_timeout 函數(shù)實(shí)現(xiàn)了有超時(shí)機(jī)制的 connect,其主要步驟有:
通過(guò)調(diào)用
fcntl函數(shù)把 socket 設(shè)置為非阻塞。調(diào)用
connect函數(shù)進(jìn)行連接服務(wù)端。如果
connect函數(shù)返回EINPROGRESS或者EWOULDBLOCK錯(cuò)誤,表示連接還沒(méi)有建立,所以此時(shí)把 socket 添加到select中進(jìn)行監(jiān)聽(tīng),并且設(shè)置select的超時(shí)時(shí)間。判斷
select的返回值,如果返回值大于0,表示連接成功;如果返回值小于0,表示連接出錯(cuò);如果反正等于0,表示連接超時(shí)。最后把 socket 恢復(fù)到阻塞模式。
這種設(shè)置 connect 的超時(shí)時(shí)間的方式比前面設(shè)置 SO_SNDTIMEO 值的方式更為通用,因?yàn)樵诜?Linux 系統(tǒng)中,設(shè)置 SO_SNDTIMEO 值的方式不一定有效。
