tcp 客户端 发送syn

简介

sys_connect->inet_stream_connect->inet_stream_connect->tcp_v4_connect->tcp_connect
对于tcp,
inet_stream_connect()调用tcp_v4_connect发送三次握手的第一次syn请求, 并根据socket是否阻塞来决定是否调用inet_wait_for_connect来等待

tcp_v4_connect

  • 调用ip_route_connect和ip_route_newports创建或者获取路由缓存,并决定发送地址/设备, 下一跳
  • 更新状态机TCP_CLOSE->TCP_SYN_SENT
  • inet_hash_connect(&tcp_death_row, sk); 如果socket没有bind到特定端口,这里选择端口进行bind, 如果是reuseport判断能否recycle tw
  • tp->write_seq = secure_tcp_sequence_number() 生产初始seq序号
  • tcp_connect()发送握手包

 

/* This will initiate an outgoing connection. 
1. 检查socket的地址长度和使用的协议族。
2. 查找路由缓存。
3. 设置本端的IP。
4. 如果传输控制块已经被使用过了,则重新初始化相关变量。
5. 记录服务器端的IP和端口。
6. 把连接的状态更新为TCP_SYN_SENT。
7. 选取本地端口,可以是未被使用过的端口,也可以是允许重用的端口。
8. 把sock链入本地端口的使用者哈希队列,把sock链入ehash哈希表。
9. 如果源端口或者目的端口发生改变,则需要重新查找路由。
10. 根据四元组,设置本端的初始序列号。
11. 根据初始序号和当前时间,设置IP首部ID字段值。
12. 构造一个SYN段,并发送出去。
调用ip_route_connect和ip_route_newports创建或者获取路由缓存,并决定发送地址/设备, 下一跳
更新状态机TCP_CLOSE->TCP_SYN_SENT
inet_hash_connect(&tcp_death_row, sk); 如果socket没有bind到特定端口,这里选择端口进行bind, 如果是reuseport判断能否recycle tw
tp->write_seq = secure_tcp_sequence_number() 生产初始seq序号
tcp_connect()发送握手包
*/
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
    struct sockaddr_in *usin = (struct sockaddr_in *)uaddr;
    struct inet_sock *inet = inet_sk(sk);
    struct tcp_sock *tp = tcp_sk(sk);
    __be16 orig_sport, orig_dport;
    __be32 daddr, nexthop;
    struct flowi4 *fl4;
    struct rtable *rt;
    int err;
    struct ip_options_rcu *inet_opt;

    if (addr_len < sizeof(struct sockaddr_in))
        return -EINVAL;

    if (usin->sin_family != AF_INET)
        return -EAFNOSUPPORT;
//connect的时候s_addr里面对应的是目的地址,即对端ip地址
    nexthop = daddr = usin->sin_addr.s_addr;
    inet_opt = rcu_dereference_protected(inet->inet_opt,
                         lockdep_sock_is_held(sk));
    if (inet_opt && inet_opt->opt.srr) {
        if (!daddr)
            return -EINVAL;
        nexthop = inet_opt->opt.faddr;
    }

    orig_sport = inet->inet_sport;
    orig_dport = usin->sin_port;
    fl4 = &inet->cork.fl.u.ip4;
     /*根据fl4,查找或创建路由缓存
     * 调用ip_route_connect()根据下一跳地址等信息查找目的路由缓存项,如果路由查找命中,则生成一个相应的路由缓存项,这个缓存项不但
     * 可以用于当前待发送SYN段,而且对后续的所有数据包都可以起到一个加速路由查找的作用。
     */
    rt = ip_route_connect(fl4, nexthop, inet->inet_saddr,
                  RT_CONN_FLAGS(sk), sk->sk_bound_dev_if,
                  IPPROTO_TCP,
                  orig_sport, orig_dport, sk);
    if (IS_ERR(rt)) {
        err = PTR_ERR(rt);
        if (err == -ENETUNREACH)
            IP_INC_STATS(sock_net(sk), IPSTATS_MIB_OUTNOROUTES);
        return err;
    }
 /*TCP不能使用类型为组播或多播的路由缓存项。*/
    if (rt->rt_flags & (RTCF_MULTICAST | RTCF_BROADCAST)) { // tcp不支持多播和广播
        ip_rt_put(rt);
        return -ENETUNREACH;
    }
/* 如果没有启用源路由选项,则使用获取到路由缓存项中的目的地址。*/
    if (!inet_opt || !inet_opt->opt.srr)
        daddr = fl4->daddr;
  /* 如果还未设置传输控制块中的源地址,则使用路由缓存项中的源地址对其进行设置。*/
    //这里说明了客户端在连接的时候可以不用指明本地IP地址,由路由缓存找到对应目的IP的时候,就可以确定本地IP地址了。
    if (!inet->inet_saddr)
        inet->inet_saddr = fl4->saddr;
    sk_rcv_saddr_set(sk, inet->inet_saddr);
  /* 如果传输控制块中的时间戳和目的地址已被使用过,则说明该传输控制块之前已建立连接并进行过通信,需重新初始化相关成员。 */
    if (tp->rx_opt.ts_recent_stamp && inet->inet_daddr != daddr) {
        /* Reset inherited state */
        tp->rx_opt.ts_recent       = 0;
        tp->rx_opt.ts_recent_stamp = 0;
        if (likely(!tp->repair))
            tp->write_seq       = 0;
    }
/* 如果启用了sysctl_tw_recycle并接收过时间戳选项,从对端信息块中获取相应的值来初始化ts_recent_stamp和ts_recent。*/
    if (tcp_death_row.sysctl_tw_recycle &&
        !tp->rx_opt.ts_recent_stamp && fl4->daddr == daddr)
        tcp_fetch_timewait_stamp(sk, &rt->dst);

    inet->inet_dport = usin->sin_port;
    sk_daddr_set(sk, daddr);

    inet_csk(sk)->icsk_ext_hdr_len = 0;
    if (inet_opt)
        inet_csk(sk)->icsk_ext_hdr_len = inet_opt->opt.optlen;

    tp->rx_opt.mss_clamp = TCP_MSS_DEFAULT;

    /* Socket identity is still unknown (sport may be zero).
     * However we set state to SYN-SENT and not releasing socket
     * lock select source port, enter ourselves into the hash tables and
     * complete initialization after this.
     */
     /* 将TCP设置为SYN_SENT,动态绑定一个本地端口,并将传输控制块添加到ehash散列表中。由于在动态分配端口时,如果找到的是已使用的端口,则
     * 需在TIME_WAIT状态中进行相应的确认,因此调用inet_hash_connect()时需用timewait传输控制块和参数管理器tcp_death_row作为参数。*/
    tcp_set_state(sk, TCP_SYN_SENT);
    //bind local port,tw_recycle
    /*/没有bind端口,随机生成一个偏移,随机化端口分配过程*/
    err = inet_hash_connect(&tcp_death_row, sk);
    if (err)
        goto failure;

    sk_set_txhash(sk);

    rt = ip_route_newports(fl4, rt, orig_sport, orig_dport,
                   inet->inet_sport, inet->inet_dport, sk);
    if (IS_ERR(rt)) {
        err = PTR_ERR(rt);
        rt = NULL;
        goto failure;
    }
    /* OK, now commit destination to socket.  */
    sk->sk_gso_type = SKB_GSO_TCPV4;
    sk_setup_caps(sk, &rt->dst);

    /*
     * 如果write_seq字段值为零,则说明该传输控制块还
     * 未设置初始序号,因此需调用secure_tcp_sequence_number(),
     * 根据双方的地址、端口计算初始序列号,同时根据
     * 发送需要和当前时间得到用于设置IP首部ID域的值。
     */
    if (!tp->write_seq && likely(!tp->repair))
        tp->write_seq = secure_tcp_sequence_number(inet->inet_saddr,
                               inet->inet_daddr,
                               inet->inet_sport,
                               usin->sin_port);

    inet->inet_id = tp->write_seq ^ jiffies;

    err = tcp_connect(sk);

    rt = NULL;
    if (err)
        goto failure;

    return 0;

failure:
    /*
     * This unhashes the socket and releases the local port,
     * if necessary.
     */
    tcp_set_state(sk, TCP_CLOSE);
    ip_rt_put(rt);
    sk->sk_route_caps = 0;
    inet->inet_dport = 0;
    return err;
}
EXPORT_SYMBOL(tcp_v4_connect);
  1. 对于已经bind端口的socket
  • 判断是否有人reuseport, 如果只有自己bind到这个port, 则调用inet_ehash_nolisten(sk, NULL);插入ehash中
  • 如果有其他人bind到这个端口,则调用__inet_check_established,
    -确认其他人是否在ehash中,不在ehash中,则可以使用这个port
    -在ehash中,并存在满足五元组的timewait状态sk,则调用tcp_twsk_unique判断是否能被回收
    -在ehash中,但是满足五元组的sk不是timewait状态,则不能使用这个port来connect。 这个就说明了两个tcp connect(), 开启reuseport后bind到相同端口,bind()能成功,但是第二个connect会失败
  1. 对于没有bind端口的socket, 则需要尝试分配端口
    • inet_sk_port_offset随机生成一个port_offset, 通过port_offset来保证端口搜索区间的随机性, 遍历这个区间,尝试分配
    • 在bhash中查找是否有其他socket bind到这个端口上,没有则表示可以分配
    • 如果有其他socket在相同的bhash bucket上,调用__inet_check_established来确认是否能分配这个端口, 过程同上
    • 分配成功后inet_bind_hash,设置端口和bhash, 并调用inet_ehash_nolisten插入ehash中
    • 如果需要,还需要释放tw socket

 

/*
 * Bind a port for a connect operation and hash it.
 */
 /*
 * inet_hash_connect()主要用于在主动连接时动态绑定一个端口。
 * 1)在动态端口范围内,从通过源地址、目的地址和目的端口
 *    计算得到的偏移开始,确认一个可用的端口号
 * 2)如果该端口已使用,则进而确定该端口是否能使用,不能
 *    则递增端口号继续确认;能使用则可用端口已找到。
 * 3)如果该端口未使用,则可使用该端口
 * 4)最后完成绑定过程。
 */ 
 /* 动态绑定一个本地端口,并将传输控制块添加到ehash散列表中。由于在动态分配端口时,如果找到的是已使用的端口,则
     * 需在TIME_WAIT状态中进行相应的确认,因此调用inet_hash_connect()时需用timewait传输控制块和参数管理器tcp_death_row作为参数。*/
 //这里面会把sk添加到ehash中,虽然连接还没建立起来。该函数外的tcp_connect才是真正发送SYN报文的地方
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);
}



/*
从这个函数的实现可以看出,主要是由于可用的端口被占满了,所以找不到一个可用的端口,导致连接失败。 运行netstat可以发现确实存在很多TIME_WAIT状态的socket,这些socket将可用端口占满了。 netstat -n | awk '/^tcp/ {++state[$NF]} END {for(key in state) print key,"\t",state[key]}' TIME_WAIT 26837 ESTABLISHED 30 */ //参考:http://www.yunstorage.org/%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B/socket-connect-error-99cannot-assign-requested-address/ //如果快速回收TIME_WAIT状态的端口 int __inet_hash_connect(struct inet_timewait_death_row *death_row, struct sock *sk, u32 port_offset, int (*check_established)(struct inet_timewait_death_row *, struct sock *, __u16, struct inet_timewait_sock **)) { struct inet_hashinfo *hinfo = death_row->hashinfo; /* 通过tcp_death_row中的成员hashinfo,获取指向TCP中散列表管理器hashinfo。 */ struct inet_timewait_sock *tw = NULL; struct inet_bind_hashbucket *head; int port = inet_sk(sk)->inet_num; struct net *net = sock_net(sk); struct inet_bind_bucket *tb; u32 remaining, offset; int ret, i, low, high; static u32 hint; if (port) {//如果是应用程序bind的时候指定了端口,则无需端口复用检查。 head = &hinfo->bhash[inet_bhashfn(net, port, hinfo->bhash_size)]; tb = inet_csk(sk)->icsk_bind_hash; spin_lock_bh(&head->lock); if (sk_head(&tb->owners) == sk && !sk->sk_bind_node.next) {//也就是说只有自己bind到这个端口, 没有reuseport inet_ehash_nolisten(sk, NULL);//插入ehash spin_unlock_bh(&head->lock); return 0; } spin_unlock(&head->lock); /* No definite answer... Walk to established hash table */ //否则检查ehash,查看bind到相同端口的socket是否进入timewait,进入timewait则判断是否能recycle,否则就是说还在连接状态或是没在ehash中 ret = check_established(death_row, sk, port, NULL); local_bh_enable(); return ret; } inet_get_local_port_range(net, &low, &high); high++; /* [32768, 60999] -> [32768, 61000[ */ remaining = high - low; if (likely(remaining > 1)) remaining &= ~1U; offset = (hint + port_offset) % remaining; /* In first pass we try ports of @low parity. * inet_csk_get_port() does the opposite choice. */ offset &= ~1U; other_parity_scan: port = low + offset; for (i = 0; i < remaining; i += 2, port += 2) { if (unlikely(port >= high)) port -= remaining; if (inet_is_local_reserved_port(net, port)) continue; head = &hinfo->bhash[inet_bhashfn(net, port, hinfo->bhash_size)]; spin_lock_bh(&head->lock); /* Does not bother with rcv_saddr checks, because * the established check is already unique enough. */ inet_bind_bucket_for_each(tb, &head->chain) { if (net_eq(ib_net(tb), net) && tb->port == port) { if (tb->fastreuse >= 0 || tb->fastreuseport >= 0) goto next_port; WARN_ON(hlist_empty(&tb->owners)); if (!check_established(death_row, sk, port, &tw))//在ehash中查找timewait,如果满足五元组,并调用tcp_twsk_unique判断 goto ok; goto next_port; } } tb = inet_bind_bucket_create(hinfo->bind_bucket_cachep, net, head, port); if (!tb) { spin_unlock_bh(&head->lock); return -ENOMEM; } tb->fastreuse = -1; tb->fastreuseport = -1; goto ok; next_port: spin_unlock_bh(&head->lock); cond_resched(); } offset++; if ((offset & 1) && remaining > 1) goto other_parity_scan; return -EADDRNOTAVAIL; ok: hint += i + 2; /* Head lock still held and bh's disabled */ inet_bind_hash(sk, tb, port);//设置snum和tb if (sk_unhashed(sk)) { inet_sk(sk)->inet_sport = htons(port); inet_ehash_nolisten(sk, (struct sock *)tw);//删除tw,插入sk } if (tw) inet_twsk_bind_unhash(tw, hinfo);//删除tw的bind关系 spin_unlock(&head->lock); if (tw) inet_twsk_deschedule_put(tw);//回收tw local_bh_enable(); return 0; }

 

 

 

 

 

listen socket 的 struct sock 数据结构 inet_connection_sock。

  • 全连接队列和半连接队列最大长度: inet_connection_sock.icsk_inet.sock.sk_max_ack_backlog

  • 全连接队列: inet_connection_sock.icsk_accept_queue.rskq_accept_head

  • 当前全连接队列长度: inet_connection_sock.icsk_inet.sock.sk_ack_backlog

  • 半连接队列(哈希表): inet_hashinfo.inet_ehash_bucket

  • 当前半连接队列长度: inet_connection_sock.icsk_accept_queue.qlen

struct inet_connection_sock {
    /* inet_sock has to be the first member! */
    struct inet_sock      icsk_inet; //inet_connection_sock  common struct
    struct request_sock_queue icsk_accept_queue;  //tcp newsk 存放新的链接sock 等待accept 读取
    struct inet_bind_bucket      *icsk_bind_hash;//指向与之bind的信息
    unsigned long          icsk_timeout;//数据包超时时间-- 重传 tv_off    --通常为 jiffies+ icsk_rto 后 进行重传
     struct timer_list      icsk_retransmit_timer; // 通过icsk_pengding 来区分重传定时器和持续定时器
     struct timer_list      icsk_delack_timer;// 延时发送ack 定时器
    __u32              icsk_rto;// 重传超时时间 初始值为    TCP_TIMEOUT_INIT    根据网络情况动态计算
    __u32              icsk_pmtu_cookie; //最后一次更新的路径MTU 
    const struct tcp_congestion_ops *icsk_ca_ops;
    const struct inet_connection_sock_af_ops *icsk_af_ops;
    unsigned int          (*icsk_sync_mss)(struct sock *sk, u32 pmtu);
    __u8              icsk_ca_state:6,// 拥塞状态
                  icsk_ca_setsockopt:1,
                  icsk_ca_dst_locked:1;
    __u8              icsk_retransmits;// 超时重传的次数
    __u8              icsk_pending; //标志定时器事件     ICSK_TIME_EARLY_RETRANS  等可选值 表示 重传定时 持续定时器 保活定时器等
    __u8              icsk_backoff;// 计算持续定时器 下一个设定值的指数 退避算法指数 
    __u8              icsk_syn_retries; // 建立tcp 允许 重传 syn   syn+ack的次数
    __u8              icsk_probes_out;// 持续定时等  周期性发出未被确认的tcp seg 数目
    __u16              icsk_ext_hdr_len;
    struct {
        __u8          pending;     /* ACK is pending 标示 需要确认发送的 紧急程度 和状态               */
        __u8          quick;     /* Scheduled number of quick acks在快速发送确认模式中       */
        __u8          pingpong;     /* The session is interactive 启用禁用 快速确认模式    1 ---标示延时发送ack 0 标示快速发送ack       */
        __u8          blocked;     /* Delayed ACK was blocked by socket lock  软中断 用户进程 不能同时own sk
        如果sk 被 user  拥有, 延时ack 定时器被触发,此时不应该发送ack, 
        blocked 为1;标示 如果有机会就需要立即发送,所以当接收的数据被cp到user 后 就可以立即发送ack */
        __u32          ato;         /* Predicted tick of soft clock 延时确认的估值       */
        unsigned long      timeout;     /* Currently scheduled timeout当前延时确认时间 超时后立即发送ack           */
        __u32          lrcvtime;     /* timestamp of last received data packet 最近一次接收到数据包时间*/
        __u16          last_seg_size; /* Size of last incoming segment最后一个接收到段的长度 用来计算rcv_mss       */
        __u16          rcv_mss;     /* MSS used for delayed ACK decisions 由最近接收的段计算出MSS       */ 
    } icsk_ack; // 延时确认控制块
    struct {
        int          enabled;// 是否开启路径MTU

        /* Range of MTUs to search  */
        int          search_high;
        int          search_low;

        /* Information on the current probe当前mtu 探测的长度 用于判断mtu是否完成  初始值为0. */
        int          probe_size;

        u32          probe_timestamp;
    } icsk_mtup;
    u32              icsk_user_timeout;

    u64              icsk_ca_priv[64 / sizeof(u64)];
#define ICSK_CA_PRIV_SIZE      (8 * sizeof(u64))
};

 

posted @ 2019-07-01 23:17  codestacklinuxer  阅读(958)  评论(0编辑  收藏  举报