上篇文章:C/C++ Linux网络编程12 - 传输层UDP协议详解-CSDN博客
代码仓库:橘子真甜 (yzc-YZC) - Gitee.com
TCP是传输层协议,特点是:保证可靠传输,面向字节流,有连接。
目录
一. TCP报头格式
二. TCP 面向字节流
1.1 面向字节流理解
1.2 粘包问题
三. TCP 有连接⭐
3.1 连接准备
3.2 三次握手
3.4 四次挥手
3.3 思考总结⭐⭐
a 如果出现了大量的close_wait状态怎么办?
b 为什么要有time_wait状态?
c time_wait的危害是什么?如何解决?
四. TCP可靠性(下篇文章详解)
一. TCP报头格式
首先我们来了解TCP报头格式
可以看到,TCP报头前20字节(160位)是固定的。如果需要数据和选项,可以增加。
既然这样,TCP报头总长度并不是固定的。如果获取TCP报头的大小呢?
可以看到TCP首部有一个字段4位首部长度,这个就是用于计算的。
TCP首部长度 = 4位首部长 * 4 (字节)。
假如 4位首部长是 1111,那么该TCP首部长是 1111(15) * 4 = 60字节
可以看到,TCP首部最长就是60字节,最短是固定首部长20字节
和UDP一样,TCP报文也是一个结构化数据
二. TCP 面向字节流
1.1 面向字节流理解
TCP是面向字节流的,使用socket构建tcp的套接字时候会创建发送缓冲区和接受缓冲区。我们调用接口 recv/send/read/write时候会先将数据拷贝到对应的接收缓冲区中。
而什么时候发送缓冲区发送数据,发送多少。什么时候接受缓冲区接受数据,接受多少。丢包了怎么办。都由底层自己完成。
而我们的用户只需要向缓冲区拷贝/读取数据即可,还可以支持全双工。
1.2 粘包问题
由于TCP是面向字节流的,所以我们读取数据的时候并不能保证读取的数据是一个完整的报文(有可能多,有可能少)这就是粘包问题。
为了解决这种问题,我们要明确每一个报文的边界。
常用的解决方式如下:
1 通信双方规定一个报文是定长的,每次读取定长的数据
2 通过特殊字符对报文进行划分,比如\r\n
3 报头定义报文的长度,每一次去读取标明的长度数据
三. TCP 有连接⭐
TCP保持连接的目的是为了可靠性做基础。有了连接方便支持可靠性的实现。
3.1 连接准备
TCP的连接不是直接就能连接的,连接前需要做一些准备。
被动接收连接方(一般为服务端):
1首先要socket创建套接字(构建fd和tcp控制块)
2 bind绑定端口(完善tcp控制块)
3 isten创建半连接队列syc_queue和全连接队列 accept_queue 将自己设置为LISTEN状态(只有这个状态才能获取网络的连接)。
我们的listen函数( listen(int fd, int backlog) )的第二个参数就全连接队列的长度
主动连接方(一般为客户端):
1 socket创建套接字(构建fd和tcp控制块)
2 connect 连接远方服务器(一般os会自动帮助我们bind一个端口)。
注意:connect是三次握手的开始
3.2 三次握手
这里假设是:server-client
双方准备好连接后(此时server处于LISTEN状态,client处于close状态)
1 client调用connect开始连接,首先client向server发送一个SYN = x,表明自己想要建立连接。
2 server接收syn后如果判断可以连接就会向client发送 ACK = x + 1(表示同意连接请求) 和 自己的SYN = y(表示确认连接请求)。此时该连接也进入了半连接队列。
3 然后client接收到 ACK = x + 1 和 SYN = y 之后,如果确认同意连接就发送一个 ACK = y + 1表示同意确立连接请求。此时连接仍处于半连接队列。
4 当server接收到来自client的ACK = y + 1 之后三次握手就完成了(理论是完成了)。此时连接由半连接队列进入全连接队列。
然后当server调用accpet获取这个连接之后双方就能正常send/recv通信了。
流程如下图:
思考
三次握手中有哪些api调用?
connect发起三次握手,listen为三次握手做准备,accpet最后接收三次握手建立的连接。
Tcp第三次握手之后,如何从半连接队列中拿出匹配的连接放入全连接队列?
服务端通过TCP五元组找到半连接队列放入全连接队列。
3.4 四次挥手
网络编程中:调用close fd,当返回0说明这个连接就断开了。调用close之后,双方是如何处理的呢?其实是通过四次挥手处理的。
1 调用close前,双方处于ESTABLISHED状态。
2 主动断开方首先调用close发送fin(表示需要断开连接)并进入fin_wait1状态。
3 被动断开方接收fin后进入close_wait状态并发送一个ack(表示同意你的断开请求)然后等待上层调用close,调用close后发送fin(表示我也要断开连接)进入last_ack状态。
4 主动断开方接收ack之后进入fin_wait2状态等待对方的fin,接收到对方的fin后发送ack(表示我也同意你的断开请求)并进入time_wait状态(表示等待对方接收完毕,超时进入closed状态)
5 被动界的收到ack之后进入closed状态。
至此四次挥手就完毕了(一般是主动关闭方超时2MSL进入closed状态结束)
流程图如下:
3.3 思考总结⭐⭐
由四次挥手可知:
主动断开方最后进入time_wait状态。被动断开方不调用close会一直处于close_wait状态
a如果出现了大量的close_wait状态怎么办?
这种情况一般是服务器压力过大没时间close或者有bug无法正确close。前者想要解决只能等待压力减轻后更换设备或者更换方案了,后者bug需要修改代码即可。
b为什么要有time_wait状态?
time_wait状态大量出现其实是正常状态作用是:
防止主动断开方的最后一个ack丢包,被动断开方一直超时重传 fin 无法从 last_ack 进入closed。time_wait超时2 MSL(最大报文生存时间)期间可以保证对方接受ack。
防止网络中有延迟数据没有被接收导致的数据错误
ctime_wait的危害是什么?如何解决?
服务器关闭后,由于我们的端口处于time_wait此时重启会bind失败。
这个问题的危害:由于time_wait持续的时间是2 MSL这个期间我们的服务是停止的,这是一个巨大的损失。比如双11,如图淘宝两分钟无法服务会造成巨大的影响。
解决这个问题可以使用setsockopt来设置端口复用
正常来说,一个五元组只能bind一个端口。而使用了setsockopt可以保证多个五元组bind同一个端口并且保证不出错。这样就能保证我们的服务器关闭后可以快速重启。
代码如下:(我截取自己服务器的代码)
void initServer() { // 1.创建套接字,使用tcp协议 _listensockfd = socket(AF_INET, SOCK_STREAM, 0); if (_listensockfd < 0) { LogMessage(FATAL, "creat socket error"); exit(SOCKET_ERR); } LogMessage(NORMAL, "creat listensocket success:%d", _listensockfd); // 1.2 设置地址复用 int opt = 1; setsockopt(_listensockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt)); // 2.bind绑定自己的网络信息,sockfd与IP和port struct sockaddr_in local; memset(&local, 0, sizeof(local)); local.sin_family = PF_INET; // AF_INET就是PF_INET local.sin_port = htons(_port); local.sin_addr.s_addr = INADDR_ANY; if (bind(_listensockfd, (sockaddr *)&local, sizeof(local)) < 0) { LogMessage(FATAL, "server bind error"); exit(BIND_ERR); } LogMessage(NORMAL, "server bind success"); // 3. tcp需要建立连接! 设置监听状态,获取新连接 if (listen(_listensockfd, gbacklog) == -1) { LogMessage(FATAL, "server listen error"); exit(LISTEN_ERR); } LogMessage(NORMAL, "server listen success"); }参数说明:
SO_REUSEADDR:允许重用处于 TIME_WAIT 状态的地址,或同一IP的不同服务复用。它解决的是 TIME_WAIT 和地址冲突问题。(如果完全冲突的两个服务都活跃是无法bind的,time_wait状态就是一种不活跃的状态所以可以bind)
SO_REUSEPORT:允许多个独立套接字绑定到完全相同的 IP:端口。用于多进程/线程同时监听同一端口,实现高性能。(完全冲突的两个服务都也是可以bind的)
四. TCP可靠性(下篇文章详解)
可靠性内容较多,下篇文章详解TCP可靠性包含TCP 报头各个字段作用,确认应答,超时重传,连接管理(本文已有),流量控制,滑动窗口,拥塞控制。