深入理解TCP协议及其源代码
本次实验,我们来探究connect及bind、listen、accept背后的三次握手。
实验原理
首先简要回顾一下TCP三次握手的过程:
第一次握手:client向server发送SYN=1的数据报文表示请求连接,初始序列号(Sequence Number)字段为X。此时client端处于SYN-SENT状态。
第二次握手:server发送ACK=1, SYN=1的报文表示确认连接请求。ack序列号为X+1, 序列号字段置为Y。此时server处于SYN-RECEIVED状态。
第三次握手:client发送ACK=1的报文向server表示最后确认。ack序列号为Y+1,序列号为X+1。至此双方均进入ESTABLISHED状态,至此连接成功建立。
为何需要三次握手呢?这是因为我们TCP需要工作在不可靠的信道中。考虑两次握手:假设客户端发送的第一个 SYN 在网络中滞留了,客户端因此重发 SYN 并建立连接,直到释放。此时滞留的第一个 SYN 终于到了,根据两次握手的规则,服务端直接进入 ESTABLISHED
状态,而此时客户端根本没有发起新的连接,不会理会服务端发送的报文,白白浪费了服务端的资源。事实上,只要信道不可靠,双方永远都没有办法确认对方知道自己将要进入连接状态。例如三次握手,最后一次 ACK 如果丢失,则只有客户端进入连接状态。四次、五次、多少次握手都有类似问题,三次其实是理论和实际的一个权衡。
回顾之前的reply/hello通信程序:server依次调用了bind(), listen()以及accept()函数;而client在调用connect()之后,双方便可以发送/接收数据了。其中bind()和listen()函数用于设置服务端本机的端口绑定和监听。而accept()返回的正是client的套接字描述符,这意味着此时连接已经建立。不难推断出,三次握手的连接建立主要在client调用connect()以及server调用accept()的过程中完成。下面我们以这两个系统调用为核心,探究TCP三次握手在Linux内核中的实现。实验环境为:基于Linux-5.0.1的64为MenuOS。
实验过程
前面的实验已经表明connect()和accept()对应内核函数为 __sys_connect()
和 __sys_accept4()
。首先我们来看一看前者的实现:
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);
if (!sock)
goto out;
err = move_addr_to_kernel(uservaddr, addrlen, &address);
if (err < )
goto out_put;
err =
security_socket_connect(sock, (struct sockaddr *)&address, addrlen);
if (err)
goto out_put;
err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen,
sock->file->f_flags);
out_put:
fput_light(sock->file, fput_needed);
out:
return err;
}
可以看到,该方法的核心是对sock->ops->connect()的调用。同样的,__sys_accept4()
的核心仍然是对sock->ops->accept()的调用。这是两个函数指针,在TCP协议栈初始化后,指向的分别是tcp_v4_conncet和inet_csk_accept这两个函数,这些信息被记录在名叫tcp_prot结构体变量中。我们可以在GDB中设置断点验证一下:
可以看到,在依次执行server和client程序后,程序会停在相应的断点,被绑定的函数按预期被调用。
下面通过源码看一看这两个函数的实现,首先是tcp_v4_connect:
/* This will initiate an outgoing connection. */
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;
struct inet_timewait_death_row *tcp_death_row = &sock_net(sk)->ipv4.tcp_death_row;
if (addr_len < sizeof(struct sockaddr_in))
return -EINVAL;
if (usin->sin_family != AF_INET)
return -EAFNOSUPPORT;
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;
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;
}
if (rt->rt_flags & (RTCF_MULTICAST | RTCF_BROADCAST)) {
ip_rt_put(rt);
return -ENETUNREACH;
}
if (!inet_opt || !inet_opt->opt.srr)
daddr = fl4->daddr;
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 = ;
tp->rx_opt.ts_recent_stamp = ;
if (likely(!tp->repair))
tp->write_seq = ;
}
inet->inet_dport = usin->sin_port;
sk_daddr_set(sk, daddr);
inet_csk(sk)->icsk_ext_hdr_len = ;
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_set_state(sk, TCP_SYN_SENT);
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);
rt = NULL;
if (likely(!tp->repair)) {
if (!tp->write_seq)
tp->write_seq = secure_tcp_seq(inet->inet_saddr,
inet->inet_daddr,
inet->inet_sport,
usin->sin_port);
tp->tsoffset = secure_tcp_ts_off(sock_net(sk),
inet->inet_saddr,
inet->inet_daddr);
}
inet->inet_id = tp->write_seq ^ jiffies;
if (tcp_fastopen_defer_connect(sk, &err))
return err;
if (err)
goto failure;
err = tcp_connect(sk);
if (err)
goto failure;
return ;
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 = ;
inet->inet_dport = ;
return err;
}
tcp_v4_connect的主要作用就是建立连接。TCP是构建于IP之上的传输层协议,因此可以看到,该函数也调用了很多ip层提供的函数,比如:ip_route_connect(),ip_route_newports()等等。此处我们关注的重点是tcp_set_state(sk, TCP_SYN_SENT)
以及tcp_connect(sk)
这两行。前者将状态设置为TCP_SYN_SENT,即将数据报中的SYN字段置1;后者具体负责报文的发送,同样地,它也会调用更下层的服务。
接下来是inet_csk_accept:
/*
* This will accept the next outstanding connection.
*/
struct sock *inet_csk_accept(struct sock *sk, int flags, int *err, bool kern)
{
struct inet_connection_sock *icsk = inet_csk(sk);
struct request_sock_queue *queue = &icsk->icsk_accept_queue;
struct request_sock *req;
struct sock *newsk;
int error;
lock_sock(sk);
/* We need to make sure that this socket is listening,
* and that it has something pending.
*/
error = -EINVAL;
if (sk->sk_state != TCP_LISTEN)
goto out_err;
/* Find already established connection */
if (reqsk_queue_empty(queue)) {
long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);
/* If this is a non blocking socket don't sleep */
error = -EAGAIN;
if (!timeo)
goto out_err;
error = inet_csk_wait_for_connect(sk, timeo);
if (error)
goto out_err;
}
req = reqsk_queue_remove(queue, sk);
newsk = req->sk;
if (sk->sk_protocol == IPPROTO_TCP &&
tcp_rsk(req)->tfo_listener) {
spin_lock_bh(&queue->fastopenq.lock);
if (tcp_rsk(req)->tfo_listener) {
/* We are still waiting for the final ACK from 3WHS
* so can't free req now. Instead, we set req->sk to
* NULL to signify that the child socket is taken
* so reqsk_fastopen_remove() will free the req
* when 3WHS finishes (or is aborted).
*/
req->sk = NULL;
req = NULL;
}
spin_unlock_bh(&queue->fastopenq.lock);
}
out:
release_sock(sk);
if (req)
reqsk_put(req);
return newsk;
out_err:
newsk = NULL;
req = NULL;
*err = error;
goto out;
}
该函数的主要作用是从请求队列中取出数据进行处理。当队列为空时,则会调用inet_csk_wait_for_connect(),从名称不难看出,此时将进入阻塞状态直至有新的请求入队。后者通过执行无限循环实现等待,有新的连接则跳出循环结束等待。
至此我们对基于IPv4的TCP三次握手背后的两大核心函数做了简要介绍。最后贴一张TCP状态转换图感受一下TCP的复杂与精巧。
深入理解TCP协议及其源代码的更多相关文章
- 通俗大白话来理解TCP协议的三次握手和四次断开
from : https://blog.csdn.net/Neo233/article/details/72866230?locationNum=15&fps=1%20HTTP%E6%8F%A ...
- 通俗大白话来理解TCP协议的三次握手和四次分手
通俗理解: 但是为什么一定要进行三次握手来保证连接是双工的呢,一次不行么?两次不行么?我们举一个现实生活中两个人进行语言沟通的例子来模拟三次握手. 引用网上的一些通俗易懂的例子,虽然不太正确,后面会指 ...
- 理解tcp协议的3次握手和面向连接
1.tcp是有连接的, 这个不是说他有个实际的连接,这个是个虚拟的连接,连接的保持信息不是由连接的路线来保存的,他是由连接的两方来保存其状态信息,这就是面向连接的, 2.tcp要3次握手: 客户端发给 ...
- TCP协议理解
一.前言: TCP协议和UDP协议是网络编程里最重要的协议,很多新出的技术.新出的协议本质上都是基于这两个协议的,其中又以TCP协议居多:比如HTTP协议就是基于TCP协议的,应用程序和数据库交互也是 ...
- TCP协议疑难杂症全景解析
说明: 1).本文以TCP的发展历程解析容易引起混淆,误会的方方面面2).本文不会贴大量的源码,大多数是以文字形式描述,我相信文字看起来是要比代码更轻松的3).针对对象:对TCP已经有了全面了解的人. ...
- 【转载】TCP协议疑难杂症全景解析
说明: 1).本文以TCP的发展历程解析容易引起混淆,误会的方方面面2).本文不会贴大量的源码,大多数是以文字形式描述,我相信文字看起来是要比代码更轻松的3).针对对象:对TCP已经有了全面了解的人. ...
- 【转载】TCP协议要点和难点全解
说明: 1).本文以TCP的发展历程解析容易引起混淆,误会的方方面面 2).本文不会贴大量的源码,大多数是以文字形式描述,我相信文字看起来是要比代码更轻松的 3).针对对象:对TCP已经有了全面了解的 ...
- TCP协议要点和难点全解
转载自http://www.cnblogs.com/leetieniu2014/p/5771324.html TCP协议要点和难点全解 说明: 1).本文以TCP的发展历程解析容易引起混淆,误会的方方 ...
- 理论经典:TCP协议的3次握手与4次挥手过程详解
1.前言 尽管TCP和UDP都使用相同的网络层(IP),TCP却向应用层提供与UDP完全不同的服务.TCP提供一种面向连接的.可靠的字节流服务. 面向连接意味着两个使用TCP的应用(通常是一个客户和一 ...
随机推荐
- Strcpy,strcpy使用注意
一.char *strcpy(char *dest, const char *src) 参数 dest -- 指向用于存储复制内容的目标数组. src -- 要复制的字符串. 注意: 1.dest需要 ...
- sql server 2000安装程序配置服务器失败
第一种方法 今天安装SQL Server 2000遇到了个很BT的问题,提示出下: 安装程序配置服务器失败.参考服务器错误日志和C:\Windows\sqlstp.log了解更多信息. 以前进安装目录 ...
- 前端开发本地存储之localStorage和sessionStorage
1.localStorage 概念 HTML5 web 存储:HTML5 提供了两种在客户端存储数据的新方式:localStorage 和 sessionStorage ,两者都是仅在客户端(即浏览器 ...
- SaltStack(自动化运维工具)
SaltStack管理工具允许管理员对多个操作系统创建一个一致的管理系统,包括VMware vSphere环境.SaltStack作用于仆从和主拓扑.SaltStack与特定的命令结合使用可以在一个或 ...
- The mook jong
The mook jong Accepts: 506 Submissions: 1281 Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65 ...
- Outlets
Outlets Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)Total Sub ...
- 初识 ❤ TensorFlow |【一见倾心】
说明
- 六. jenkins部署springboot项目(3)--windows环境--远程windows server服务器
前提:jenkins服务器和windows server服务器不在一台机器上 对于jenkins服务器上编译好的jar或war包如何推送到windows server服务器上. 参照网上的,在wind ...
- 116、TensorFlow变量的版本
import tensorflow as tf v = tf.get_variable("v", shape=(), initializer=tf.zeros_initialize ...
- Linux ftp安装
ftp安装部分,操作步骤如下: 可以使用yum命令直接安装ftp # yum install vsftpd ftp服务的开启与关闭命令: 开启:# /bin/systemctl start vsftp ...