前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Linux tcp/ip 源码分析 - three way handshake

Linux tcp/ip 源码分析 - three way handshake

作者头像
KINGYT
修改2019-06-11 16:11:13
2.4K0
修改2019-06-11 16:11:13
举报

在上一篇文章中我们讲到,connect方法会发送syn消息给服务端,之后客户端会进入TCP_SYN_SENT状态。

这就是tcp三次握手的第一步。

接下来我们看下,服务端收到syn消息后的处理逻辑。

首先,ip层在收到消息后,会调用tcp_v4_rcv方法将消息转给tcp层。

代码语言:javascript
复制
// net/ipv4/tcp_ipv4.c
int tcp_v4_rcv(struct sk_buff *skb)
{
  ...
  const struct iphdr *iph;
  const struct tcphdr *th;
  ...
  struct sock *sk;
  int ret;
  ...
  th = (const struct tcphdr *)skb->data;
  iph = ip_hdr(skb);
  ...
  sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
             th->dest, sdif, &refcounted);
  ...
  if (sk->sk_state == TCP_LISTEN) {
    ret = tcp_v4_do_rcv(sk, skb);
    goto put_and_return;
  }
  ...
  return ret;
  ...
}

方法描述

1. 调用__inet_lookup_skb方法,根据ip、端口等信息在全局的hashtable中找到对应的sock。

2. 此时sk->sk_state应该是TCP_LISTEN状态,所以会继续调用tcp_v4_do_rcv方法。

代码语言:javascript
复制
// net/ipv4/tcp_ipv4.c
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
  struct sock *rsk;
  ...
  if (tcp_rcv_state_process(sk, skb)) {
    ...
    goto reset;
  }
  return 0;
  ...
}
EXPORT_SYMBOL(tcp_v4_do_rcv);

该方法会继续调用tcp_rcv_state_process方法。

代码语言:javascript
复制
// net/ipv4/tcp_input.c
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
  ...
  struct inet_connection_sock *icsk = inet_csk(sk);
  const struct tcphdr *th = tcp_hdr(skb);
  ...
  switch (sk->sk_state) {
  ...
  case TCP_LISTEN:
    ...
    if (th->syn) {
      ...
      acceptable = icsk->icsk_af_ops->conn_request(sk, skb) >= 0;
      ...
      return 0;
    }
  ...
  }
  ...
}
EXPORT_SYMBOL(tcp_rcv_state_process);

因为客户端发来的是syn消息,而服务端此时是TCP_LISTEN状态,所以该方法最终会调用icsk->icsk_af_ops->conn_request(sk, skb)方法。

由第一篇文章我们可以知道,icsk->icsk_af_ops字段的值为&ipv4_specific,所以icsk->icsk_af_ops->conn_request指向的方法为tcp_v4_conn_request。

代码语言:javascript
复制
// net/ipv4/tcp_ipv4.c
int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
{
  ...
  return tcp_conn_request(&tcp_request_sock_ops,
        &tcp_request_sock_ipv4_ops, sk, skb);
  ...
}
EXPORT_SYMBOL(tcp_v4_conn_request);

该方法又调用了tcp_conn_request方法,其中前两个参数为两个结构体,内容如下

代码语言:javascript
复制
// net/ipv4/tcp_ipv4.c
struct request_sock_ops tcp_request_sock_ops __read_mostly = {
  .family    =  PF_INET,
  .obj_size  =  sizeof(struct tcp_request_sock),
  .rtx_syn_ack  =  tcp_rtx_synack,
  .send_ack  =  tcp_v4_reqsk_send_ack,
  .destructor  =  tcp_v4_reqsk_destructor,
  .send_reset  =  tcp_v4_send_reset,
  .syn_ack_timeout =  tcp_syn_ack_timeout,
};

static const struct tcp_request_sock_ops tcp_request_sock_ipv4_ops = {
  .mss_clamp  =  TCP_MSS_DEFAULT,
#ifdef CONFIG_TCP_MD5SIG
  .req_md5_lookup  =  tcp_v4_md5_lookup,
  .calc_md5_hash  =  tcp_v4_md5_hash_skb,
#endif
  .init_req  =  tcp_v4_init_req,
#ifdef CONFIG_SYN_COOKIES
  .cookie_init_seq =  cookie_v4_init_sequence,
#endif
  .route_req  =  tcp_v4_route_req,
  .init_seq  =  tcp_v4_init_seq,
  .init_ts_off  =  tcp_v4_init_ts_off,
  .send_synack  =  tcp_v4_send_synack,
};

tcp_conn_request方法如下

代码语言:javascript
复制
// net/ipv4/tcp_input.c
int tcp_conn_request(struct request_sock_ops *rsk_ops,
         const struct tcp_request_sock_ops *af_ops,
         struct sock *sk, struct sk_buff *skb)
{
  ...
  struct request_sock *req;
  ...
  req = inet_reqsk_alloc(rsk_ops, sk, !want_cookie);
  ...
  tcp_rsk(req)->af_specific = af_ops;
  ...
  if (fastopen_sk) {
    ...
  } else {
    ...
    if (!want_cookie)
      inet_csk_reqsk_queue_hash_add(sk, req,
        tcp_timeout_init((struct sock *)req));
    af_ops->send_synack(sk, dst, &fl, req, &foc,
            !want_cookie ? TCP_SYNACK_NORMAL :
               TCP_SYNACK_COOKIE);
    ...
  }
  ...
  return 0;
  ...
}
EXPORT_SYMBOL(tcp_conn_request);

方法描述

1. 调用inet_reqsk_alloc方法,分配并初始化一个struct request_sock实例,用于存储连接请求数据。

2. 将tcp_rsk(req)->af_specific的值设置为af_ops,即&tcp_request_sock_ipv4_ops。

3. 调用inet_csk_reqsk_queue_hash_add方法,将连接请求实例req添加到全局hashtable中。

4. 调用af_ops->send_synack发送synack消息给客户端。

我们再来看下inet_reqsk_alloc方法

代码语言:javascript
复制
// net/ipv4/tcp_input.c
struct request_sock *inet_reqsk_alloc(const struct request_sock_ops *ops,
              struct sock *sk_listener,
              bool attach_listener)
{
  struct request_sock *req = reqsk_alloc(ops, sk_listener,
                 attach_listener);

  if (req) {
    struct inet_request_sock *ireq = inet_rsk(req);
    ...
    ireq->ireq_state = TCP_NEW_SYN_RECV;
    ...
  }

  return req;
}
EXPORT_SYMBOL(inet_reqsk_alloc);

该方法中要注意的就是,ireq->ireq_state的值被设置为TCP_NEW_SYN_RECV,这个后面会用到。

该方法有调用了reqsk_alloc方法,继续看下。

代码语言:javascript
复制
// include/net/request_sock.h
static inline struct request_sock *
reqsk_alloc(const struct request_sock_ops *ops, struct sock *sk_listener,
      bool attach_listener)
{
  struct request_sock *req;

  req = kmem_cache_alloc(ops->slab, GFP_ATOMIC | __GFP_NOWARN);
  ...
  if (attach_listener) {
    ...
    req->rsk_listener = sk_listener;
  }
  req->rsk_ops = ops;
  req_to_sk(req)->sk_prot = sk_listener->sk_prot;
  ...
  return req;
}

该方法需要注意的是

1. req->rsk_listener字段被设置为sk_listener,即listen sock。

2. req->rsk_ops被设置为ops,即&tcp_request_sock_ops。

到这里tcp_conn_request方法就算分析结束了。

至此,tcp三次握手的第二步结束。

我们再来看下客户端在收到synack消息时是如何处理的。

还是回到tcp_v4_rcv方法,此时sock的状态为TCP_SYN_SENT。

代码语言:javascript
复制
// net/ipv4/tcp_ipv4.c
int tcp_v4_rcv(struct sk_buff *skb)
{
  ...
  const struct tcphdr *th;
  struct sock *sk;
  ...
  th = (const struct tcphdr *)skb->data;
  ...
  sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
             th->dest, sdif, &refcounted);
  ...
  ret = 0;
  if (!sock_owned_by_user(sk)) {
    ret = tcp_v4_do_rcv(sk, skb);
  } else if (tcp_add_backlog(sk, skb)) {
    ...
  }
  ...
  return ret;
  ...
}

方法描述

1. 调用__inet_lookup_skb方法,根据ip、端口等信息,从全局hashtable中找到对应的sock。

2. 调用tcp_v4_do_rcv方法,继续处理。

代码语言:javascript
复制
// net/ipv4/tcp_ipv4.c
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
  struct sock *rsk;
  ...
  if (tcp_rcv_state_process(sk, skb)) {
    ...
    goto reset;
  }
  return 0;
  ...
}
EXPORT_SYMBOL(tcp_v4_do_rcv);

该方法又调用了tcp_rcv_state_process方法。

代码语言:javascript
复制
// net/ipv4/tcp_input.c
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
  ...
  const struct tcphdr *th = tcp_hdr(skb);
  ...
  switch (sk->sk_state) {
  ...
  case TCP_SYN_SENT:
    ...
    queued = tcp_rcv_synsent_state_process(sk, skb, th);
    ...
    return 0;
  }
  ...
}
EXPORT_SYMBOL(tcp_rcv_state_process);

因为客户端此时的状态为TCP_SYN_SENT,所以该方法最终会调用tcp_rcv_synsent_state_process方法。

代码语言:javascript
复制
// net/ipv4/tcp_input.c
static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,
           const struct tcphdr *th)
{
  struct inet_connection_sock *icsk = inet_csk(sk);
  ...
  if (th->ack) {
    ...
    if (!th->syn)
      goto discard_and_undo;
    ...
    tcp_finish_connect(sk, skb);
    ...
    if (!sock_flag(sk, SOCK_DEAD)) {
      sk->sk_state_change(sk);
      ...
    }
    ...
    if (sk->sk_write_pending ||
        icsk->icsk_accept_queue.rskq_defer_accept ||
        icsk->icsk_ack.pingpong) {
      ...
    } else {
      tcp_send_ack(sk);
    }
    return -1;
  }
  ...
}

方法描述

1. 如果此ack消息里没有syn标志,则丢弃消息。

2. 调用tcp_finish_connect方法,将sk->sk_state设置为TCP_ESTABLISHED,表示tcp建立连接成功。

代码语言:javascript
复制
// net/ipv4/tcp_input.c
void tcp_finish_connect(struct sock *sk, struct sk_buff *skb)
{
  ...
  tcp_set_state(sk, TCP_ESTABLISHED);
  ...
}

3. 调用sk->sk_state_change方法,通知sock状态发生变化。

在上一篇文章中我们讲到,如果socket没设置nonblock状态,则当connect时,会阻塞等待状态变化,而这里的sk->sk_state_change方法,正是用于告知等待线程sock状态变化了,可以从阻塞状态退出了。

4. 调用tcp_send_ack方法发送三次握手的最终的ack消息。

我们再来看下,服务端在收到这个ack消息后是如何处理的。

还是看tcp_v4_rcv方法

代码语言:javascript
复制
// net/ipv4/tcp_ipv4.c
int tcp_v4_rcv(struct sk_buff *skb)
{
  ...
  const struct tcphdr *th;
  ...
  struct sock *sk;
  ...
  th = (const struct tcphdr *)skb->data;
  ...
  sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
             th->dest, sdif, &refcounted);
  ...
  if (sk->sk_state == TCP_NEW_SYN_RECV) {
    struct request_sock *req = inet_reqsk(sk);
    struct sock *nsk;

    sk = req->rsk_listener;
    ...
    nsk = NULL;
    if (!tcp_filter(sk, skb)) {
      ...
      nsk = tcp_check_req(sk, skb, req, false);
    }
    ...
    if (nsk == sk) {
      ...
    } else if (tcp_child_process(sk, nsk, skb)) {
      ...
    } else {
      ...
      return 0;
    }
  }
  ...
}

方法描述

1. 调用__inet_lookup_skb方法,根据ip、端口等信息找到对应的sock。

2. 因为sk->sk_state为TCP_NEW_SYN_RECV,所以将sk转成struct request_sock类型,并赋值给req。

3. 将sk值设置为req->rsk_listener,即listen sock。

4. 调用tcp_check_req方法,创建一个新的struct sock实例newsk,把newsk->sk_state状态设置为TCP_SYN_RECV,之后把struct request_sock实例中的数据赋值到newsk中,再之后把newsk放到全局的hashtable中,最后把newsk放到listen sock的icsk_accept_queue队列中,这样后面的accept方法就能从这个队列获取这个sock了。

由于涉及到的代码太多,这里就不贴代码了,只说下相关方法

// net/ipv4/tcp_ipv4.c tcp_v4_syn_recv_sock // net/ipv4/inet_connection_sock.c inet_csk_complete_hashdance

5. 调用tcp_child_process将nsk->sk_state设置为TCP_ESTABLISHED,表示连接建立成功。

代码语言:javascript
复制
// net/ipv4/tcp_minisocks.c
int tcp_child_process(struct sock *parent, struct sock *child,
          struct sk_buff *skb)
{
  int ret = 0;
  int state = child->sk_state;
  ...
  if (!sock_owned_by_user(child)) {
    ret = tcp_rcv_state_process(child, skb);
    ...
  } else {
    ...
  }
  ...
  return ret;
}
EXPORT_SYMBOL(tcp_child_process);

该方法调用了tcp_rcv_state_process方法,此时参数child的sk_state为TCP_SYN_RECV。

代码语言:javascript
复制
// net/ipv4/tcp_input.c
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
  ...
  switch (sk->sk_state) {
  case TCP_SYN_RECV:
    ...
    tcp_set_state(sk, TCP_ESTABLISHED);
    sk->sk_state_change(sk);
    ...
    break;
  ...
  }
  ...
  return 0;
}
EXPORT_SYMBOL(tcp_rcv_state_process);

6. return 0 给上层,表示成功。

至此,tcp三次握手全部结束,tcp连接建立成功。

完。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-03-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Linux内核及JVM底层相关技术研究 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档