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

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

作者头像
KINGYT
修改于 2019-06-11 08:11:13
修改于 2019-06-11 08:11:13
2.6K00
代码可运行
举报
运行总次数:0
代码可运行

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

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

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 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
代码运行次数:0
运行
AI代码解释
复制
// 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
代码运行次数:0
运行
AI代码解释
复制
// 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
代码运行次数:0
运行
AI代码解释
复制
// 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
代码运行次数:0
运行
AI代码解释
复制
// 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
代码运行次数:0
运行
AI代码解释
复制
// 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
代码运行次数:0
运行
AI代码解释
复制
// 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
代码运行次数:0
运行
AI代码解释
复制
// 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
代码运行次数:0
运行
AI代码解释
复制
// 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
代码运行次数:0
运行
AI代码解释
复制
// 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
代码运行次数:0
运行
AI代码解释
复制
// 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
代码运行次数:0
运行
AI代码解释
复制
// 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
代码运行次数:0
运行
AI代码解释
复制
// 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
代码运行次数:0
运行
AI代码解释
复制
// 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
代码运行次数:0
运行
AI代码解释
复制
// 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
代码运行次数:0
运行
AI代码解释
复制
// 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 删除。

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

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
能将三次握手理解到这个深度,面试官拍案叫绝!
在后端相关岗位的入职面试中,三次握手的出场频率非常的高,甚至说它是必考题也不为过。一般的答案都是说客户端如何发起 SYN 握手进入 SYN_SENT 状态,服务器响应 SYN 并回复 SYNACK,然后进入 SYN_RECV,...... , 吧啦吧啦诸如此类。
开发内功修炼
2022/03/24
4800
能将三次握手理解到这个深度,面试官拍案叫绝!
Linux tcp/ip 源码分析 - connection termination
前两篇文章中我们讲到,shutdown和close方法会发送fin消息给对方,开始tcp连接的关闭流程,现在我们从源码角度看下tcp连接关闭的具体过程,以及中间发送的消息和涉及到的各种状态。
KINGYT
2019/06/02
2K0
Linux系统研究 - 操作系统是如何管理tcp连接的 (2)
接上一篇文章 Linux系统研究 - 操作系统是如何管理tcp连接的 (1),我们再来继续讲。
KINGYT
2019/11/07
3.3K1
tcp rst报文_TCP报文格式
对于TCP客户端,在发送完SYN报文之后,如果接收到的回复报文同时设置了ACK和RST标志,在检查完ACK的合法性之后,处理RST标志,关闭套接口。对于ACK确认序号,其应当大于第一个未确认序号(snd_una),并且,确认序号不应大于未发送数据的序号(snd_nxt)。
全栈程序员站长
2022/11/10
1.7K0
Linux tcp/ip 源码分析 - connect
由第一篇文章可以知道,sock->ops->connect指向的方法为inet_stream_connect。
KINGYT
2019/05/30
2.1K0
从linux源码看socket的close
笔者一直觉得如果能知道从应用到框架再到操作系统的每一处代码,是一件Exciting的事情。上篇博客讲了socket的阻塞和非阻塞,这篇就开始谈一谈socket的close(以tcp为例且基于linux-2.6.24内核版本)
无毁的湖光-Al
2018/08/14
5.7K0
从linux源码看socket的close
解Bug之路-dubbo流量上线时的非平滑问题
笔者最近解决了一个困扰了业务系统很久的问题。这个问题只在发布时出现,每次只影响一两次调用,相较于其它的问题来说,这个问题有点不够受重视。由于种种原因,使得这个问题到了业务必须解决的程度,于是就到了笔者的手上。
呆呆
2021/05/21
5780
Linux tcp/ip 源码分析 - socket
Linux下的tcp编程中,第一步就是要创建socket,本文将从源码角度看下socket是如何被创建的。
KINGYT
2019/05/30
6.2K0
解Bug之路-记一次调用外网服务概率性失败问题的排查
和外部联调一直是令人困扰的问题,尤其是一些基础环境配置导致的问题。笔者在一次偶然情况下解决了一个调用外网服务概率性失败的问题。在此将排查过程发出来,希望读者遇到此问题的时候,能够知道如何入手。
无毁的湖光-Al
2019/10/22
1.9K2
解Bug之路-记一次调用外网服务概率性失败问题的排查
Linux tcp/ip 源码分析 - accept
3. 将sock->type赋值给newsock->type,type值为SOCK_STREAM。
KINGYT
2019/06/03
2K0
Linux内核代码审计之CVE-2018-5703
由于第一篇文章是针对网络子系统的,因此这篇也还是找一个网络子系统的漏洞去审计。本文同样是学习 Linux 内核漏洞时的记录,从草稿箱中翻出,稍加修改后在此分享(灌水)一下。
evilpan
2023/02/12
5610
Linux内核代码审计之CVE-2018-5703
Linux tcp/ip 源码分析 - listen
// net/socket.c SYSCALL_DEFINE2(listen, int, fd, int, backlog) { struct socket *sock; ... int somaxconn; sock = sockfd_lookup_light(fd, &err, &fput_needed); if (sock) { somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn; if ((unsigned int)backlog > somaxconn) backlog = somaxconn; ... if (!err) err = sock->ops->listen(sock, backlog); ... } return err; }
KINGYT
2019/05/30
1.6K0
图解 | 深入理解高性能网络开发路上的绊脚石 - 同步阻塞网络 IO
在网络开发模型中,有一种非常易于开发同学使用的方式,那就是同步阻塞的网络 IO(在 Java 中习惯叫 BIO)。
用户6543014
2021/03/25
5470
深入解析常见三次握手异常
在后端接口性能指标中一类重要的指标就是接口耗时。具体包括平均响应时间 TP90、TP99 耗时值等。这些值越低越好,一般来说是几毫秒,或者是几十毫秒。如果响应时间一旦过长,比如超过了 1 秒,在用户侧就能感觉到非常明显的卡顿。如果长此以往,用户可能就直接用脚投票,卸载我们的 App 了。
开发内功修炼
2022/03/24
1.1K0
深入解析常见三次握手异常
Linux tcp/ip 源码分析 - read
上一篇文章我们介绍了write是如何实现tcp写的,现在我们来看下read是如何实现tcp读的。
KINGYT
2019/06/02
3.3K0
Linux tcp/ip 源码分析 - shutdown
之前的文章已经分析了tcp的建立过程以及tcp读和写,下面我们继续看下shutdown方法。
KINGYT
2019/06/03
1.8K0
TCP源码分析 - 三次握手之 connect 过程
本文主要分析 TCP 协议的实现,但由于 TCP 协议比较复杂,所以分几篇文章进行分析,这篇主要介绍 TCP 协议建立连接时的三次握手过程。
用户7686797
2021/03/16
2.2K0
TCP源码分析 - 三次握手之 connect 过程
Linux tcp/ip 源码分析 - close
该方法先通过fd找到对应的file,再调用filp_close方法对file进行close。
KINGYT
2019/06/03
2.4K0
深入理解TCP/IP协议的实现之三次握手(基于linux1.2.13)
上篇我们分析了accept函数,他是消费者,这篇我们看看生产者是怎么实现的。我们从tcp_rcv函数开始,这个函数是一个分发器。当接收到一个tcp包的时候,底层就会调这个函数交给tcp层处理。
theanarkh
2020/03/12
6440
Linux内核代码审计之CVE-2018-9568(WrongZone)
在上一篇文章说到代码审计是漏洞挖掘的一个重要方法,因此本文就尝试用这种方法“挖”出一个知名漏洞。
evilpan
2023/02/12
5370
Linux内核代码审计之CVE-2018-9568(WrongZone)
相关推荐
能将三次握手理解到这个深度,面试官拍案叫绝!
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验