|->tcp_connect_init 初始化tcp socket|->tcp_transmit_skb 发送SYN包|->inet_csk_reset_xmit_timer 设置SYN重传定时器
tcp_connect_init初始化了一大堆TCP相关的设置,例如mss_cache/rcv_mss等一大堆。而且如果开启了TCP窗口扩大选项的话,其窗口扩大因子也在此函数里进行计算:
tcp_connect_init
|->
tcp_select_initial_window
int
tcp_select_initial_window
(...)
{......(*rcv_wscale) =
0;
if (wscale_ok) {
/* Set window scaling on max possible window
* See RFC1323 for an explanation of the limit to 14
*/
space =
max_t(u32, sysctl_tcp_rmem[
2], sysctl_rmem_max);
space =
min_t(u32, space, *window_clamp);
while (space >
65535 && (*rcv_wscale) <
14) {
space >>=
1;
(*rcv_wscale)++;}}......}
如上面代码所示,窗口扩大因子取决于Socket最大可允许的读缓冲大小和window_clamp(最大允许滑动窗口大小,动态调整)。搞完了一票初始信息设置后,才开始真正的三次握手。 在tcp_transmit_skb中才真正发送SYN包,同时在紧接着的inet_csk_reset_xmit_timer里设置了SYN超时定时器。如果对端一直不发送SYN_ACK,将会返回-ETIMEDOUT。
重传的超时时间和
/proc/sys/net/ipv4/tcp_syn_retries
息息相关,Linux默认设置为5,建议设置成3,下面是不同设置的超时时间参照图。
在设置了SYN超时重传定时器后,tcp_connnect就返回,并一路返回到最初始的inet_stream_connect。在这里我们就等待对端返回SYN_ACK或者SYN定时器超时。
int __inet_stream_connect(
struct socket *sock,...,)
{
// 如果设置了O_NONBLOCK则timeo为0
timeo = sock_sndtimeo(sk, flags & O_NONBLOCK);
......
// 如果timeo=0即O_NONBLOCK会立刻返回
// 否则等待timeo时间
if (!timeo || !inet_wait_for_connect(sk, timeo, writebias))
goto out;
}
Linux本身提供一个SO_SNDTIMEO来控制对connect的超时,不过Java并没有采用这个选项。而是采用别的方式进行connect的超时控制。仅仅就C语言的connect系统调用而言,不设置SO_SNDTIMEO,就会将对应用户进程进行睡眠,直到SYN_ACK到达或者超时定时器超时才将次用户进程唤醒。
如果是NON_BLOCK的话,则是通过select/epoll等多路复用机制去捕获超时或者连接成功事件。
对端SYN_ACK到达
在Server端SYN_ACK到达之后会按照下面的代码路径传递,并唤醒用户态进程:
tcp_v4_rcv
|->tcp_v4_do_rcv|->tcp_rcv_state_process|->tcp_rcv_synsent_state_process|->tcp_finish_connect|->tcp_init_metrics 初始化度量统计|->tcp_init_congestion_control 初始化拥塞控制|->tcp_init_buffer_space 初始化buffer空间|->inet_csk_reset_keepalive_timer 开启包活定时器|->sk_state_change(sock_def_wakeup) 唤醒用户态进程|->tcp_send_ack 发送三次握手的最后一次握手给Server端|->tcp_set_state(sk, TCP_ESTABLISHED) 设置为ESTABLISHED状态