分类
linux network

TCP socket connect

TCP 在作为客户端时,创建完socket一般都会立即向服务端发起connect请求。如果用户没有事先调用bind 绑定源端口和源ip。则内核会选取一个源端口,并根据路由选择源IP。
TCP connect 的大致流程是,调用inet的connect函数,调用TCP的connect函数,在TCP的connect函数中,设置socket状态。发送syn报文,设置超时重发,将socket 添加到establish hash 表中,阻塞等待服务端发送syn/ack报文。等到syn/ack报文,回ack报文,并返回,或超时返回。
系统调用入口函数 __sys_connect 如下

int __sys_connect(int fd, struct sockaddr __user *uservaddr, int addrlen)
{
        struct socket *sock;
        struct sockaddr_storage address;
        int err, fput_needed;

        sock = sockfd_lookup_light(fd, &err, &fput_needed); //通过fd获取sock结构
        if (!sock)
                goto out;
        err = move_addr_to_kernel(uservaddr, addrlen, &address); //拷贝服务端地址
        if (err < 0)
                goto out_put;

        err =
            security_socket_connect(sock, (struct sockaddr *)&address, addrlen); //感觉像是selinux的hook。没仔细研究过,这块好多东西
        if (err)
                goto out_put;

        err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen,
                                 sock->file->f_flags); //调用 inet_stream_ops  inet_stream_connect。进行connect
out_put:
        fput_light(sock->file, fput_needed);
out:
        return err;
}

SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr,
                int, addrlen)
{
        return __sys_connect(fd, uservaddr, addrlen);
}

最后调用 __inet_stream_connect 这个函数。connect最后阻塞在这个函数里面。函数重要代码如下

int __inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,
                          int addr_len, int flags, int is_sendmsg)
{
    ......
            case SS_UNCONNECTED:
                err = -EISCONN;
                if (sk->sk_state != TCP_CLOSE)
                        goto out;

                if (BPF_CGROUP_PRE_CONNECT_ENABLED(sk)) { //连接前的 bpf操作,进去看了一下 如果没有bpf prog 很快出来
                        err = sk->sk_prot->pre_connect(sk, uaddr, addr_len);
                        if (err)
                                goto out;
                }

                err = sk->sk_prot->connect(sk, uaddr, addr_len); //执行具体协议的connect函数
                if (err < 0)
                        goto out;

                sock->state = SS_CONNECTING; //修改socket状态为connecting

                if (!err && inet_sk(sk)->defer_connect)
                        goto out;

                /* Just entered SS_CONNECTING state; the only
                 * difference is that return value in non-blocking
                 * case is EINPROGRESS, rather than EALREADY.
                 */
                err = -EINPROGRESS;
                break;
        }

        timeo = sock_sndtimeo(sk, flags & O_NONBLOCK); //获取超时时间,默认非常大。

        if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
                int writebias = (sk->sk_protocol == IPPROTO_TCP) &&
                                tcp_sk(sk)->fastopen_req &&
                                tcp_sk(sk)->fastopen_req->data ? 1 : 0;

                /* Error code is set above */
                if (!timeo || !inet_wait_for_connect(sk, timeo, writebias))
                    /*等待连接,连接超时(这个超时是TCP设置的)或者收到syn/ack。
                    *如果没有设置O_NOBLOCK则进程被阻塞。否则直接返回,直接返回后通过
                    *epoll等待连接事件。在某些版本的内核里面超时以后,这块行为会变成可读
                    *可写加超时error,因此用户态需要注意处理方式。踩过坑
                    */
                        goto out;

                err = sock_intr_errno(timeo);
                if (signal_pending(current))
                        goto out;
        }

        /* Connection was closed by RST, timeout, ICMP error
         * or another process disconnected us.
         */
        if (sk->sk_state == TCP_CLOSE) //查看tcp连接状态。如果TCP状态没有改变说明没有收到 syn/ack报文
                goto sock_error;
        /* sk->sk_err may be not zero now, if RECVERR was ordered by user
         * and error was received after socket entered established state.
         * Hence, it is handled normally after connect() return successfully.
         */

        sock->state = SS_CONNECTED; //修改状态为连接完成
        err = 0;
out:
        return err;

sock_error:
        err = sock_error(sk) ? : -ECONNABORTED;
        sock->state = SS_UNCONNECTED;
        if (sk->sk_prot->disconnect(sk, flags))
                sock->state = SS_DISCONNECTING;
        goto out;
}

sk->sk_prot->connect 在tcp中调用的是tcp_v4_connect。 这个函数的作用,按照代码顺序分为以下几个:
1.如果用户已经设置源IP,则直接使用设置的源IP
2.如果没有设置源IP则根据目的ip和路由查找源IP
3.如果用户已经设置源port,则直接使用源Port
4.如果没有设置源PORT 根据目的IP port 和当前系统的establish tcp hash表查找sport。
5.查找目的路由
6.将TCP sock 状态设置为TCP_SYN_SENT。
7.根据已经查找到的路由及路由出口设备的能力设置一些GSO TSO标志等。sk_setup_caps。
这个函数比较简单,这里就不列出来了。需要注意的是查找可用源PORT的函数,会在查找到源port后将sock添加到establish hash表中。这个时候sock状态还没有establish。个人认为 主要是为了区分listen。listen 五元组不全, establish 是全五元组。函数是inet_hash_connect。有兴趣可以进去看看。
tcp connect 关键函数是tcp_connect 函数体如下

int tcp_connect(struct sock *sk)
{       
        struct tcp_sock *tp = tcp_sk(sk);
        struct sk_buff *buff;
        int err;

        tcp_call_bpf(sk, BPF_SOCK_OPS_TCP_CONNECT_CB, 0, NULL);

        if (inet_csk(sk)->icsk_af_ops->rebuild_header(sk))
                return -EHOSTUNREACH; /* Routing failure or similar. */

        tcp_connect_init(sk);  //连接初始化,这个里面会设置一些超时时间等

        if (unlikely(tp->repair)) {
                tcp_finish_connect(sk, NULL);
                return 0;
        }

        buff = sk_stream_alloc_skb(sk, 0, sk->sk_allocation, true);
        if (unlikely(!buff))
                return -ENOBUFS;

        tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN);
        tcp_mstamp_refresh(tp);
        tp->retrans_stamp = tcp_time_stamp(tp);
        tcp_connect_queue_skb(sk, buff);        
        tcp_ecn_send_syn(sk, buff);
        tcp_rbtree_insert(&sk->tcp_rtx_queue, buff);

        /* Send off SYN; include data in Fast Open. */ 
        err = tp->fastopen_req ? tcp_send_syn_data(sk, buff) :
              tcp_transmit_skb(sk, buff, 1, sk->sk_allocation); //发送数据包
        if (err == -ECONNREFUSED)            
                return err;

        /* We change tp->snd_nxt after the tcp_transmit_skb() call
         * in order to make this packet get counted in tcpOutSegs.
         */
        tp->snd_nxt = tp->write_seq;
        tp->pushed_seq = tp->write_seq;
        buff = tcp_send_head(sk);
        if (unlikely(buff)) {   
                tp->snd_nxt     = TCP_SKB_CB(buff)->seq;
                tp->pushed_seq  = TCP_SKB_CB(buff)->seq;
        }
        TCP_INC_STATS(sock_net(sk), TCP_MIB_ACTIVEOPENS);

        /* Timer for repeating the SYN until an answer. */
        inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
                                  inet_csk(sk)->icsk_rto, TCP_RTO_MAX); //设置超时重发,使用的retrans机制。
        return 0;
}

在tcp连接中有三个定时器。比较重要
1.icsk_retransmit_timer 超时重发,handler tcp_write_timer
2.icsk_delack_timer 延迟回复ack。减少ack数量handler tcp_delack_timer
3.sk_timer keepalive_handler 这个不是tcp专有,连接保活用的。handler tcp_keepalive_timer

如果超时没有收到 服务器端返回的syn/ack 报文,当前设置为1秒,则会进入超时重发逻辑,进入tcp_write_timer_handler。 这个函数根据超时事件,进行不同的处。如果是超时重发则进入 tcp_retransmit_timer中处理,这个函数将重试发包。如果超过重试次数,则会进入 tcp_write_err函数,唤醒阻塞的实体处理,调用sk->sk_error_report (sock_def_error_report)。
tcp connect到这儿发送部分基本就完了,下节分析一下 收到syn/ack后,修改TCP状态到ESTABLISH。

发表评论

电子邮件地址不会被公开。 必填项已用*标注