C/C++ Linux网络编程13 - 传输层TCP协议详解(面向字节流和有连接)

上篇文章:C/C++ Linux网络编程12 - 传输层UDP协议详解-CSDN博客

代码仓库:橘子真甜 (yzc-YZC) - Gitee.com

TCP是传输层协议,特点是:保证可靠传输,面向字节流,有连接。

目录

[一. TCP报头格式](#一. TCP报头格式)

[二. TCP 面向字节流](#二. TCP 面向字节流)

[1.1 面向字节流理解](#1.1 面向字节流理解)

[1.2 粘包问题](#1.2 粘包问题)

[三. TCP 有连接⭐](#三. TCP 有连接⭐)

[3.1 连接准备](#3.1 连接准备)

[3.2 三次握手](#3.2 三次握手)

[3.4 四次挥手](#3.4 四次挥手)

[3.3 思考总结⭐⭐](#3.3 思考总结⭐⭐)

[a 如果出现了大量的close_wait状态怎么办?](#a 如果出现了大量的close_wait状态怎么办?)

[b 为什么要有time_wait状态?](#b 为什么要有time_wait状态?)

[c time_wait的危害是什么?如何解决?](#c time_wait的危害是什么?如何解决?)

[四. TCP可靠性(下篇文章详解)](#四. 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。

防止网络中有延迟数据没有被接收导致的数据错误

c time_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 报头各个字段作用,确认应答,超时重传,连接管理(本文已有),流量控制,滑动窗口,拥塞控制。

相关推荐
嘻哈baby2 小时前
systemd服务管理深入实践从入门到自定义服务
linux·服务器·网络
lightqjx2 小时前
【算法】双指针
c++·算法·leetcode·双指针
qq_5470261792 小时前
Docker 搭建Nexus3私服
运维·docker·容器
历程里程碑2 小时前
C++ 7vector:动态数组的终极指南
java·c语言·开发语言·数据结构·c++·算法
June`2 小时前
SSH连接原理与守护进程实战
linux·运维·服务器
JH灰色2 小时前
【大模型】-LangChain--stream流式同步异步
服务器·前端·langchain
专业开发者2 小时前
近距离检测功能亮点
网络·tcp/ip·安全
水天需0102 小时前
Grep 例程大全
linux