通過linux源碼分析nodejs的keep-alive

之前已經分析過了keep-alive,最近在使用nodejs的keep-alive的時候發現了遺漏了一個內容。本文進行一個補充說明。我們先看一下nodejs中keep-alive的使用。

enable:是否開啓keep-alive,linux下默認是不開啓的。
initialDelay:多久沒有收到數據包就開始發送探測包。
接着我們看看這個api在libuv中的實現。

int uv__tcp_keepalive(int fd, int on, unsigned int delay) {
  if (setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &on, sizeof(on)))
    return UV__ERR(errno);
// linux定義了這個宏
#ifdef TCP_KEEPIDLE
  /*
	  on是1纔會設置,所以如果我們先開啓keep-alive,並且設置delay,
	  然後關閉keep-alive的時候,是不會修改之前修改過的配置的。
	  因爲這個配置在keep-alive關閉的時候是沒用的
  */
  if (on && setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, &delay, sizeof(delay)))
    return UV__ERR(errno);
#endif

  return 0;
}

我們看到libuv調用了同一個系統函數兩次。我們分別看一下這個函數的意義。參考linux2.6.13.1的代碼。

// net\socket.c
asmlinkage long sys_setsockopt(int fd, int level, int optname, char __user *optval, int optlen)
{
	int err;
	struct socket *sock;

	if ((sock = sockfd_lookup(fd, &err))!=NULL)
	{
		...
		if (level == SOL_SOCKET)
			err=sock_setsockopt(sock,level,optname,optval,optlen);
		else
			err=sock->ops->setsockopt(sock, level, optname, optval, optlen);
		sockfd_put(sock);
	}
	return err;
}

當level是SOL_SOCKET代表修改的socket層面的配置。IPPROTO_TCP是修改tcp層的配置(該版本代碼裏是SOL_TCP)。我們先看SOL_SOCKET層面的。

// net\socket.c -> net\core\sock.c -> net\ipv4\tcp_timer.c
int sock_setsockopt(struct socket *sock, int level, int optname,
		    char __user *optval, int optlen) {
	...
	case SO_KEEPALIVE:
			
			if (sk->sk_protocol == IPPROTO_TCP)
				tcp_set_keepalive(sk, valbool);
			// 設置SOCK_KEEPOPEN標記位1
			sock_valbool_flag(sk, SOCK_KEEPOPEN, valbool);
			break;
	...
}

sock_setcsockopt首先調用了tcp_set_keepalive函數,然後給對應socket的SOCK_KEEPOPEN字段打上標記(0或者1表示開啓還是關閉)。接下來我們看tcp_set_keepalive

void tcp_set_keepalive(struct sock *sk, int val)
{
	if ((1 << sk->sk_state) & (TCPF_CLOSE | TCPF_LISTEN))
		return;
	/*
		如果val是1並且之前是0(沒開啓)那麼就開啓計時,超時後發送探測包,
		如果之前是1,val又是1,則忽略,所以重複設置是無害的
	*/
	if (val && !sock_flag(sk, SOCK_KEEPOPEN))
		tcp_reset_keepalive_timer(sk, keepalive_time_when(tcp_sk(sk)));
	else if (!val)
		// val是0表示關閉,則清除定時器,就不發送探測包了
		tcp_delete_keepalive_timer(sk);
}

我們看看超時後的邏輯。

// 多久沒有收到數據包則發送第一個探測包
static inline int keepalive_time_when(const struct tcp_sock *tp)
{
	// 用戶設置的(TCP_KEEPIDLE)和系統默認的
    return tp->keepalive_time ? : sysctl_tcp_keepalive_time;
}
// 隔多久發送一個探測包
static inline int keepalive_intvl_when(const struct tcp_sock *tp)
{
    return tp->keepalive_intvl ? : sysctl_tcp_keepalive_intvl;
}

static void tcp_keepalive_timer (unsigned long data)
{
...
// 多久沒有收到數據包了
elapsed = tcp_time_stamp - tp->rcv_tstamp;
	// 是否超過了閾值
	if (elapsed >= keepalive_time_when(tp)) {
		// 發送的探測包個數達到閾值,發送重置包
		if ((!tp->keepalive_probes && tp->probes_out >= sysctl_tcp_keepalive_probes) ||
		     (tp->keepalive_probes && tp->probes_out >= tp->keepalive_probes)) {
			tcp_send_active_reset(sk, GFP_ATOMIC);
			tcp_write_err(sk);
			goto out;
		}
		// 發送探測包,並計算下一個探測包的發送時間(超時時間)
		tcp_write_wakeup(sk)
			tp->probes_out++;
			elapsed = keepalive_intvl_when(tp);
	} else {
		/*
			還沒到期則重新計算到期時間,收到數據包的時候應該會重置定時器,
			所以執行該函數說明的確是超時了,按理說不會進入這裏。
		*/
		elapsed = keepalive_time_when(tp) - elapsed;
	}

	TCP_CHECK_TIMER(sk);
	sk_stream_mem_reclaim(sk);

resched:
	// 重新設置定時器
	tcp_reset_keepalive_timer (sk, elapsed);
...

所以在SOL_SOCKET層面是設置是否開啓keep-alive機制。如果開啓了,就會設置定時器,超時的時候就會發送探測包。

對於定時發送探測包這個邏輯,tcp層定義了三個配置。
1 多久沒有收到數據包,則開始發送探測包。
2 開始發送,探測包之前,如果還是沒有收到數據(這裏指的是有效數據,因爲對端會回覆ack給探測包),每隔多久,再次發送探測包。
3 發送多少個探測包後,就斷開連接。
但是我們發現,SOL_SOCKET只是設置了是否開啓探測機制,並沒有定義上面三個配置的值,所以系統會使用默認值進行心跳機制(如果我們設置了開啓keep-alive的話)。這就是爲什麼libuv調了兩次setsockopt函數。第二次的調用設置了就是上面三個配置中的第一個(後面兩個也可以設置,不過libuv沒有提供接口,可以自己調用setsockopt設置)。那麼我們來看一下libuv的第二次調用setsockopt是做了什麼。我們直接看tcp層的實現。

// net\ipv4\tcp.c
int tcp_setsockopt(struct sock *sk, int level, int optname, char __user *optval,int optlen)
{
	...
	case TCP_KEEPIDLE:
		// 修改多久沒有收到數據包則發送探測包的配置
		tp->keepalive_time = val * HZ;
			// 是否開啓了keep-alive機制
			if (sock_flag(sk, SOCK_KEEPOPEN) &&
			    !((1 << sk->sk_state) &
			      (TCPF_CLOSE | TCPF_LISTEN))) {
			    // 當前時間減去上次收到數據包的時候,即多久沒有收到數據包了
				__u32 elapsed = tcp_time_stamp - tp->rcv_tstamp;
				// 算出還要多久可以發送探測包,還是可以直接發(已經觸發了)
				if (tp->keepalive_time > elapsed)
					elapsed = tp->keepalive_time - elapsed;
				else
					elapsed = 0;
				// 設置定時器
				tcp_reset_keepalive_timer(sk, elapsed);
			}	
		...
}

該函數首先修改配置,然後判斷是否開啓了keep-alive的機制,如果開啓了,則重新設置定時器,超時的時候就會發送探測包。

總結:keep-alive有兩個層面的內容,第一個是是否開啓,第二個是開啓後,使用的配置。nodejs的setKeepAlive就是做了這兩件事情。只不過他只支持修改一個配置。另外測試發現,window下,調用setKeepAlive設置的initialDelay,會修改兩個配置。分別是多久沒有數據包就發送探測包,隔多久發送一次這兩個配置。但是linux下只會修改多久沒有數據包就發送探測包這個配置。

最後我們看看linux下的默認配置。

include <stdio.h>
#include <netinet/tcp.h>     

int main(int argc, const char *argv[])
{
    int sockfd;
    int optval;
    socklen_t optlen = sizeof(optval);

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    getsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, &optval, &optlen);
    printf("默認是否開啓keep-alive:%d \n", optval);

    getsockopt(sockfd, SOL_TCP, TCP_KEEPIDLE, &optval, &optlen);
    printf("多久沒有收到數據包則發送探測包:%d seconds \n", optval);

    getsockopt(sockfd, SOL_TCP, TCP_KEEPINTVL, &optval, &optlen);
    printf("多久發送一次探測包:%d seconds \n", optval);

    getsockopt(sockfd, SOL_TCP, TCP_KEEPCNT, &optval, &optlen);
    printf("最多發送幾個探測包就斷開連接:%d \n", optval);
   
    return 0;
}

執行結果

我們看到linux下默認是不開啓keep-alive的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章