目录
[三. TCP 保证可靠性与高性能的策略:](#三. TCP 保证可靠性与高性能的策略:)
[四.TCP/IP 工作原理(IPV4):](#四.TCP/IP 工作原理(IPV4):)
[五.TCP 连接生命周期与状态流转:](#五.TCP 连接生命周期与状态流转:)
一.前言:
什么是TCP/IP协议:
传输控制协议/因特网互联协议 (Transmission Control Protocol/Internet Protocol)的简写,是Internet最基本的协议,也是国际互联网络的基础 。他们定义了电子设备如何连入因特网,以及数据如何传输的标准。
TCP 是一种面向连接的、可靠的、基于字节流的通信协议。即TCP 一旦发现传输过程中存在问题(如对端没有应答),就会做出对应的处理,比如重发 、流量控制等。
IP 定义了如何将数据包从源主机发送到目的主机,但不保证可靠性、顺序性或完整性。
讲解顺序:
- TCP/IP 基础使用与每个接口的作用。
- 简述 TCP/IP传输数据的流程。
- 了解TCP/IP 在传输数据上高可靠性 与高性能的原因。
- 深入理解 TCP/IP 运行原理。
- TCP 连接生命周期与状态流转。
- 常见问题。
界限说明:
- 本文章限基于 linux-7.3 与glibc 的分析
二.TCP/IP网络通信相关接口与结构:
socket:
cpp
extern int socket (int __domain, int __type, int __protocol) __THROW;
作用:是 Linux 下的系统调用,作用是在指定的协议域中创建某个类型的套接字
参数:
- __domain:协议域,用来选择IPV4 、IPV6 或是本地进程间通信等一系列协议。
- __type: 协议类型,用来设置使用 TCP 或是UDP 等。
- __protocol:自主选择协议,输入0可根据前面两个参数,自动选择最合适的协议链。
返回值:成功返回非负整数(文件描述符),失败返回 -1。
sockaddr_in:
cpp
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[sizeof (struct sockaddr)
- __SOCKADDR_COMMON_SIZE
- sizeof (in_port_t)
- sizeof (struct in_addr)];
};
作用:记录 ip 与端口号,并兼容不同地址承载类之间强转。
成员属性:
- __SOCKADDR_COMMON :unsigned short int 类型,地址族 ,表明是什么类型的地址(IPV4 ,IPV6)。
- sin_port :端口号,uint16_t 类型。
- sin_addr :内含 uint32_t 类型的结构体,存储 IPv4 的IP 地址。
- sin_zero :占位符,保证能够与 struct sockaddr之间完成安全强转。
bind:
cpp
extern int bind (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len);
作用:将地址绑定到套接字上。
参数:
- __fd :操作的套接字描述符。
- __addr :const struct sockaddr* 类型,指向你要绑定的目标地址的指针。
- __len :__addr的长度。
返回值:绑定成功返回0,绑定失败返回负数。
注:
为什么要用 __addr 和 __len :早期 C 语言不存在面向对象 的说法,也没有函数重载 ,想要 bind 函数包含到所有类型的地址绑定上,需要利用到 指针 + 长度 的设计模式。
listen:
cpp
extern int listen (int __fd, int __n) __THROW;
作用:切换等待其他设备主动连接的状态,基于传入的套接字准备接收连接。
参数:
- __fd:想要监听的描述符
- __n:全连接队列最大长度
返回值:成功返回0,失败返回负数。
connect:
cpp
int connect (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len);
作用:主动连接目标,发出三次握手请求,握手完成后返回。
参数:
- __fd:使用的文件描述符。
- __addr:指向你要绑定的目标地址信息的指针。
- __len:__addr 结构的长度。
返回值:成功返回0,失败返回负数。
accept:
cpp
int accept (int __fd, __SOCKADDR_ARG __addr,socklen_t *__restrict __addr_len);
作用:在套接字 FD 上等待连接的到来。
参数:
- __fd:被监听的套接字。
- __addr:承载连接发起方的地址。
- __addr_len:__addr长度。
返回值:返回新文件描述符(大于0),小于 0 失败。
三. TCP 保证可靠性与高性能的策略:
可靠性策略:
- 校验和 :将除校验位(2字节)以外的其他部分,按2字节为单位进行划分,如果总字节数为基数则在数据末尾逻辑补上一个全零字节,最后将所有部分相加取反。校验和匹配不上的数据报直接丢弃。
- 序列号 :32位长度,在 TCP 握手 时,交换生成的随机基数 ,随后发数据从基数开始按照字节数叠加,到达上限从零开始重新计算。
- 确认应答 :客户端收到数据报以后,会返回去一个 ACK 应答包。
- 超时重发:发送方在超时或者判断某段数据丢失后重发未确认的数据。
- 流量控制 :TCP 在握手阶段会交换双方缓冲区大小等,用滑动窗口 控制当前有效确认区与发送的数据。即根据接收端的处理能⼒,来决定 发送端的发送速度。
- 拥塞控制 :当对方响应由于网络条件差等原因而超时或是连接刚建立,此时就会进入慢启动模式,先发少量的数据,然后逐渐加快发送速度。
高性能策略:
- 滑动窗⼝: 在流量控制与拥塞窗口中,以滑动窗口的形式控制需要发送的数据,使其遍历一遍就能够完成发送,无需对每一个数据段都进行等待确认,大幅提高了网络的吞吐率。
- 快速重传: 在对端接收数据报时,可能会因为中间缺了一帧,导致应答序列号 多次处于同一个序号,此时连续三次检测到ACK 序列号在同一帧,视为丢包,立刻触发重传。
- 延迟应答: 对端在接收到数据报后,会等待一段时间再发送 ACK 应答,再此期间如果收到下一个数据报,就会连带新数据报一起完成应答。
- 捎带应答: 在双向通信中,如果接收端在发送ACK确认应答的同时,刚好也有数据需要发送给对方,此时就可以一起发送。
四.TCP/IP 工作原理(IPV4):
套接字的创建:
调用 socket() 创建套接字时,会先走到 __sys_socket 陷入内核态,执行压栈、切换内核标志位、查表等操作(这里不展开)。随后调用 sock_alloc() 分配内存并初始化套接字属性,确认各项属性无误后,由 pf->create() 正式填充协议内容------此时套接字在内核层面已经可以使用。但内核还做了一件事:调用 sock_map_fd() 创建一个新的**struct file** 对象,用 fd_install() 把 file 与刚分配的fd 编号在进程 fd 表中关联起来,最后返回这个文件描述符。至此,后续的网络操作都基于这个 fd 进行。
套接字的绑定:
调用 bind() 传入目标fd以及绑定的地址,会走到 _syscall_bind 触发中断,陷入内核态,检查,查表等等,最后根据 socket 中的方法表,在 **__inet_bind()**函数中完成绑定,对应代码如下:
inet->inet_rcv_saddr = inet->inet_saddr = addr->sin_addr.s_addr;
套接字的监听:
调用 listen() ,中断、进入内核、跳转等等解释过了,这里不解释,最终会执行 inet_csk_listen_start() 函数,开辟半连接 与全连接 队列,结束后将套接字修改成 TCP_LISTEN状态, 对应代码如下:
cpp
//开辟全连接与半连接队列
reqsk_queue_alloc(&icsk->icsk_accept_queue);
sk->sk_ack_backlog = 0;
inet_csk_delack_init(sk);
//修改状态
inet_sk_state_store(sk, TCP_LISTEN);
套接字连接发起:
中间调用链依旧不解释,直接看发起连接核心流程:
cpp
//第一次握手:
//申请空白数据报
buff = tcp_stream_alloc_skb(sk, sk->sk_allocation, true);
//填充数据报头
tcp_init_nondata_skb(buff, sk, tp->write_seq, TCPHDR_SYN);
//放入发送队列
tcp_connect_queue_skb(sk, buff);
//设置拥塞控制
tcp_ecn_send_syn(sk, buff);
//放入红黑树
tcp_rbtree_insert(&sk->tcp_rtx_queue, buff);
在 TCP 层,内核需要申请内存、封装 TCP 首部,放入 TCP 协议维护的结构中,此后将数据报发送到 IP 层封装:
cpp
//向后偏移协议头位置
skb_push(skb, sizeof(struct iphdr) + (inet_opt ? inet_opt->opt.optlen : 0));
//提取指针并强转
skb_reset_network_header(skb);
iph = ip_hdr(skb)
//填充ip协议
*((__be16 *)iph) = htons((4 << 12) | (5 << 8) | (tos & 0xff));
iph->ttl = ip_select_ttl(inet, &rt->dst);
iph->protocol = sk->sk_protocol;
ip_copy_addrs(iph, fl4);
ip_select_ident_segs(net, skb, sk, skb_shinfo(skb)->gso_segs ?: 1);
//向下一层发送
ip_local_out(net, sk, skb);
当 ip_local_out 被调用,数据包就正式走出了 IP 封装的主逻辑。此后的防火墙、ARP寻路并缓存、MAC 传输等等,这里不解释。
套接字数据接收与处理:
IP 层会首先拿到数据,此时先对数据进行解析验证合法性,如果通过验证,开始判断是否发给本机的,是否需要拦截等等。核心代码如下:
cpp
//判断数据报是否能够解析
ip_rcv_core(skb, net);
//判断是否是发给本地的,是否需要拦截等等
NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
net, NULL, skb, dev, NULL,
ip_rcv_finish);
TCP层随后拿到数据:
第一次握手:分配并初始化半连接请求,构造第二次握手数据报,放入发送队列,维护重传,最终执行发送;核心代码流程如下:
cpp
//分配半连接维护对象
inet_reqsk_alloc(rsk_ops, sk, true);
//初始化各项参数
tcp_rsk(req)->af_specific = af_ops;
tcp_rsk(req)->ts_off = 0;
tcp_rsk(req)->req_usec_ts = false;
tcp_rsk(req)->txhash = net_tx_rndhash();
tcp_clear_options(&tmp_opt);
tmp_opt.mss_clamp = af_ops->mss_clamp;
tmp_opt.user_mss = READ_ONCE(tp->rx_opt.user_mss);
tcp_parse_options(sock_net(sk), skb, &tmp_opt, 0, NULL);
tmp_opt.tstamp_ok = tmp_opt.saw_tstamp;
INDIRECT_CALL_INET(af_ops->init_seq_and_ts_off,
tcp_v6_init_seq_and_ts_off,
tcp_v4_init_seq_and_ts_off,
net, skb);
tcp_ecn_create_request(req, skb, sk, dst);
tcp_rsk(req)->snt_isn = isn; // 记录发送出去的序列号
tcp_rsk(req)->syn_tos = TCP_SKB_CB(skb)->ip_dsfield;
tcp_openreq_init_rwin(req, sk, dst);
sk_rx_queue_set(req_to_sk(req), skb)
//初始化半连接对象
tcp_openreq_init(req, &tmp_opt, skb, sk);
inet_rsk(req)->no_srccheck = inet_test_bit(TRANSPARENT, sk);
inet_rsk(req)->ir_iif = inet_request_bound_dev_if(sk, skb);
//挂载半连接队列
inet_csk_reqsk_queue_hash_add(sk, req);
//开始发送
af_ops->send_synack(sk, dst, &fl, req, &foc, TCP_SYNACK_NORMAL, skb);
第三次握手:会查询第二次握手完成的半连接对象,判断是否要完成套接字连接;核心代码如下:
cpp
//提取半连接对象
struct request_sock *req = inet_reqsk(sk);
//合成全新的套接字对象
tcp_child_process(sk, nsk, skb);
对于连接成功后的套接字接收普通数据,会先去除数据中附带的 TCP 头部,放入接收队列,更新接收状态,最后唤醒阻塞在该套接字上等待读取数据的进程(如调用 recv / read / epoll_wait 的进程);核心代码如下:
cpp
//获取纯净数据
__skb_pull(skb, tcp_header_len);
tcp_ecn_received_counters(sk, skb, len - tcp_header_len);
//放入接收队列
tcp_queue_rcv(sk, skb, &fragstolen);
//更新接收状态
tcp_event_data_recv(sk, skb);
//换醒进程
tcp_data_ready(sk);
注意: 对于接收但顺序不对的数据,内核不会丢弃,而是将其放入乱序队列(现代 Linux 中使用红黑树实现)中暂存,并触发对端的快速重传,等待填补空洞。
五.TCP 连接生命周期与状态流转:
流程图:

状态解释:
- CLOSED:套接字关闭状态,当前没有任何活动连接。
- LISTEN:服务端监听状态,被动等待客户端发起连接。
- SYN_SENT:客户端已发送 SYN 报文,等待服务端回复。
- NEW_SYN_RECV(或 SYN_RECV):服务端已回复 SYN-ACK,等待客户端最后一次 ACK(处于半连接队列)。
- ESTABLISHED:三次握手完成,连接已成功建立,双方可正常传输数据。
- FIN_WAIT_1:主动关闭方发出 FIN 报文,等待对方的 ACK 确认。
- FIN_WAIT_2:主动关闭方收到 ACK,进入半关闭状态,等待对方发出 FIN 报文。
- CLOSE_WAIT:被动关闭方收到 FIN 并回复了 ACK,等待应用层程序处理完毕并调用
close()。 - LAST_ACK:被动关闭方发出自己的 FIN 报文,等待对方最后一次 ACK 确认。
- TIME_WAIT:主动关闭方回复最终 ACK,等待 2MSL(报文最大生存时间两倍),确保对端收到并防止旧报文串扰,随后彻底关闭。
六.常见问题:
为什么一定是三次握手:
- 避免历史连接。
- 同步双方初始信息,如序列号、接收缓冲区等。
- 避免资源浪费。