深度理解TCP(backlog、连接机制、抓包实践)

一、backlog

1.1 介绍

在使用 listen 时,它的第二个参数是 backlog,相信大家在学习的时候,大概就只是知道它是等待 accept 队列的最大长度。并没有深入的理解。现在带大家来理解一下它到底有什么用。

1.2 实例

为了看到效果,我会将 default_backlog 设置为1,并将 Start() 中 while 循环里的代码给注释掉,相当于就是不拿已经建立好的连接,就让它一直在队列中(测试代码放到了文章末尾。因为我是在一台机器上测试的,它会有服务端到客户端,也有客户端到服务端,不过我会标注好,不必担心)。

首先,先让一个客户端连接服务器:

通过 netstat 查看如下:

可以看到 TCP 连接已经建立好了。画红线的是服务器创建的监听套接字。然后在让一个客户端连接,这时候该客户端就连接不上了,因为 backlog 是1。

可以看到居然还能连接上,如果在添加一个客户端呢?

之前存在的连接就不画了,大家看末尾,这个客户端端处于 SYN_SENT 状态,这是客户端发送了 SYN 报文,服务器没有返回报文时所处的状态。这其实很好理解,因为队列已经满了,服务器不在接收新的连接,所以就不做出应答。另外队列可以存的最大数量是 backlog 加1!

1.3 结论

该队列叫全连接队列,用来存放三次握手成功的连接,最大数量是 backlog 加1,这样的话即使backlog 为0也可以接受连接。该过程与 accept 是无关的,accept 只是从该队列中拿连接而已。

全连接队列的最大数量,并不是服务器可以处理连接的最大数量。因为只要服务器不是特别忙就会从全连接队列中拿去连接,除非是服务器特别忙。backlog 的值不宜很大,因为这会在一定程度上浪费内存空间,如果服务器特别忙完全可以先告诉用户让他一会儿在来访问,没必要一直干等,并且用户可能也不会很长时间等。

其实从本质上来看全连接队列就是生产消费者模型。

补充:还有一种叫半连接队列,它会存储三次握手还没有完成的连接(如果长时间没有收到后续报文会强制把该连接清除)。大概过程就是当一个 SYN 报文来时会先将报文放到半连接队列中,当三次握手结束后再将连接添加到全连接队列中。如果半连接满了,服务器会丢弃 SYN 报文,不做出应答。如果全连接满了,默认行为是会假装没收到这个 ACK,该连接依然在半连接队列中。

二、连接机制

2.1 引入

上面一直在说连接,那么连接到底是什么东西?其实它就是一种数据结构!

首先我们知道PCB它是进程的控制块,它里面可以找到 struct file,在 struct file 中有 private_data 它是用于给设备驱动(比如 tty、网络 socket)用的私有数据的空指针。有它就可以找到 struct socket ,它是通用套接字层结构(关于这个的理解,先继续向下看)。

口说无凭,下面来带大家去源码看看(我用的是linux-2.6.26版本,这是下载链接:2.2.26版源码)。下面是PCB的部分内容,他里面有文件描述符表指针(该指针是进程打开所有文件的集合):

cpp 复制代码
struct task_struct {
    //...
    /* open file information */
    struct files_struct *files;
    //...
};

之后跳转到 struct files_struct 的定义可以看到:

cpp 复制代码
struct files_struct {
    //...
    //文件描述符表
	struct file * fd_array[NR_OPEN_DEFAULT];
};

之后跳转到 struct file 的定义可以找到:

cpp 复制代码
struct file {
    //...
	/* needed for tty driver, and maybe others */
	void			*private_data;
    //...
};

关于为什么要这么设计,那就不得不提 linux 的设计核心:"一切皆文件"!它会把底层的细节都给隐藏掉,只向上提供统一的接口。

下面是 struct socket 的部分内容:

cpp 复制代码
struct socket {
    //...
	const struct proto_ops	*ops;
    //...
	struct file		*file;
	struct sock		*sk;
	short			type;
};

可以知道通过它也可以找到 struct file。const struct proto_ops 是函数指针数组,用来存放相关的接口,具体如下:

2.2 真实的连接

加入在全连接队列中的连接存的是 struct sock 它的本质上是 struct tcp_sock(先继续向下看) ,struct tcp_sock 它里面存的就是 TCP 的专属字段像:窗口、拥塞控制等:

cpp 复制代码
struct tcp_sock {
	/* inet_connection_sock has to be the first member of tcp_sock */
	struct inet_connection_sock	inet_conn;
    
    //...

    u32	snd_wl1;	/* Sequence for window update		*/
	u32	snd_wnd;	/* The window we expect to receive	*/
	u32	max_window;	/* Maximal window ever seen from peer	*/
	u32	mss_cache;	/* Cached effective mss, not including SACKS */

    //...

/*
 *	Slow start and congestion control (see also Nagle, and Karn & Partridge)
 */
 	u32	snd_ssthresh;	/* Slow start size threshold		*/
 	u32	snd_cwnd;	/* Sending congestion window		*/
	u32	snd_cwnd_cnt;	/* Linear increase counter		*/
	u32	snd_cwnd_clamp; /* Do not allow snd_cwnd to grow above this */
	u32	snd_cwnd_used;
	u32	snd_cwnd_stamp;
    //...
};

上图中就是控制滑动窗口和拥塞控制字段。struct inet_connection_sock 中它里面存储全连接队列和半连接队列等其他字段:

cpp 复制代码
struct inet_connection_sock {
	/* inet_sock has to be the first member! */
	struct inet_sock	  icsk_inet;
	struct request_sock_queue icsk_accept_queue; // 一个外壳结构,同时包含了全连接队列和半连接队列
    //...
};


struct request_sock_queue {
	struct request_sock	*rskq_accept_head; // 全连接队列头
	struct request_sock	*rskq_accept_tail; // 全连接队列尾
	rwlock_t		syn_wait_lock;
	u8			rskq_defer_accept;
	/* 3 bytes hole, try to pack */
	struct listen_sock	*listen_opt; // 半连接队列
};

上面我说全连接队列存的是 struct sock 下面就让咱来看看,这是request_sock的定义:

看到了吗?节点就是 struct sock。

struct inet_sock 它主要是存储 IP 、端口号、协议等信息:

cpp 复制代码
struct inet_sock {
	/* sk and pinet6 has to be the first two members of inet_sock */
	struct sock		sk;

    //...

	/* Socket demultiplex comparisons on incoming packets. */
	__be32			daddr; // 目的 IP 地址
	__be16			dport; // 目的端口号
	__u16			num;   // 协议号
	__be32			saddr; // 源 IP 地址
	__be16			sport; // 源端口号

    //...
};

struct sock 是网络套接字的本体,它里面存放的有接收缓冲区和发送缓冲区、套接字状态(LISTEN、ESTABLISHED、CLOSE_WAIT...)等:

cpp 复制代码
struct sock {
    struct sock_common	__sk_common; // 存放协议族、状态、引用计数等公共信息
#define sk_state		__sk_common.skc_state // 套接字状态
    //...

    struct sk_buff_head	sk_receive_queue; // 接受缓冲区
	struct sk_buff_head	sk_write_queue;   // 发送缓冲区
    //...
};

注意在 struct socket 中有一个字段是 struct sock *sk 发现什么了吗?sk 是指向底层真正的内核套接字,本质上struct sock就是一个通用基类指针。只要对它进行类型转换就可以拿到具体的协议结构,判断是何种类型通过 type 字段即可。

补充:对于无连接的 UDP 协议,它不需要连接管理,因此内核结构体最底层只有struct inet_sock。

2.3 总结

struct socket 是 VFS 与协议栈之间的桥梁,向上看是"一切皆文件",向下看是网络协议栈操作。它主要是提供函数和向内核操作的句柄等,当用户在上层调用 read 时最后会到 ops 调用。

分配 fd 的过程:当一个连接(tcp_sock)被 accept 时,OS 会创建 struct file、struct socket 然后去申请一个 fd 将 struct file的指针放到文件描述符表中,最后返回 fd。如果申请 fd 失败会直接发送 RST报文断开连接,并将创建好的资源全部释放。

三、抓包实践

3.1 抓指定 ip

抓包用的是 tcpdump 命令,通过 tcpdump --version 可以看主机中是否存在,如果不存在就用sudo apt-get install tcpdump(ubuntu系统)。

下面的命令可以抓所有的 TCP 报文:

bash 复制代码
sudo tcpdump -i any tcp

-i 接口:抓指定网卡

  1. -i any:抓所有接口
  2. -i lo:抓环口
  3. -i eth0:抓物理网卡

如果想抓特定源或目标 IP 地址的 TCP 报文:

  • 指定源 IP 地址
bash 复制代码
sudo tcpdump src host x.x.x.x and tcp
  • 指定目的 IP 地址
bash 复制代码
sudo tcpdump dst host x.x.x.x and tcp
  • 同时指定源和目的 IP 地址
bash 复制代码
sudo tcpdump src host x.x.x.x and dst host x.x.x.x and tcp

3.2 抓指定端口号

bash 复制代码
sudo tcpdump -S -n -i lo port x and tcp

-n:不解析域名

-S:显示原始的 Tcp 序列号

下面是TCP连接建立与通信和连接断开过程,图片有点长,我就放关键的了:

我将按照从左往右开始介绍:

  1. 127.0.0.1.48712 > 127.0.0.1.8080:客户端向服务器发起请求
  2. Flags [S]:SYN 被置位
  3. Flags [S.]:SYN 和 ACK 被置位
  4. Flags [.]:ACK 被置位
  5. seq:初始化序列号
  6. ack:确认号,对端序列号 + 1
  7. win:接收窗口的最大值

如果这里没加 -S 最后的报文的 ack 会是1:

我这里为了方便演示,就继续加上-S。

当我发送数据一个字符 a 后(我裁了一下,否则太难看了):

  1. lenngth:数据负载
  2. Flags [P.]:PUSH 和 ACK 被置位

发送数据时的ack确认序号是2789723769,这是因为它上次的报文只是单纯的 ack 报文,确认号不变 。如果上次的报文是SYN、FIN之类的报文不带有数据时确认号=seq+1。如果上次的报文带有数据,确认号=seq**+数据长度。**

当关闭连接时:

可以看到,只有3个报文,不应该是4次挥手吗?实际上当用户关闭连接的时候,这时服务器看到了,服务器一看自己没有什么要对客户端要发送的了,直接也对客服端发送了 FIN 正好成了捎带应答,所以看到的就是3次。想看到4次其实也简单,在连接关闭的时候 sleep(1) 即可:

3.3 补充

对于抓包的信息是可以放到文件中加上:-w 文件名.pcap。对于该文件的内容直接读是乱码,必须tcpdump -r 文件名.pcap。

四、代码

4.1 TcpClient.cc

cpp 复制代码
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>

int main(int argc, char **argv)
{
    if (argc != 3)
    {
        std::cerr << "\nUsage: " << argv[0] << " serverip serverport\n"
                  << std::endl;
        return 1;
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    int clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (clientSocket < 0)
    {
        std::cerr << "socket failed" << std::endl;
        return 1;
    }

    sockaddr_in serverAddr;
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(serverport);                  // 替换为服务器端口
    serverAddr.sin_addr.s_addr = inet_addr(serverip.c_str()); // 替换为服务器IP地址

    int result = connect(clientSocket, (struct sockaddr *)&serverAddr, sizeof(serverAddr));
    if (result < 0)
    {
        std::cerr << "connect failed" << std::endl;
        ::close(clientSocket);
        return 1;
    }
    while (true)
    {
        std::string message;
        std::cout << "Please Enter@ ";
        std::getline(std::cin, message);
        if (message.empty())
            continue;
        send(clientSocket, message.c_str(), message.size(), 0);

        char buffer[1024] = {0};
        int bytesReceived = recv(clientSocket, buffer, sizeof(buffer) - 1, 0);
        if (bytesReceived > 0)
        {
            buffer[bytesReceived] = '\0'; // 确保字符串以 null 结尾
            std::cout << "Received from server: " << buffer << std::endl;
        }
        else
        {
            std::cerr << "recv failed" << std::endl;
        }
    }
    ::close(clientSocket);
    return 0;
}

4.2 TcpServer.cc

cpp 复制代码
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <unistd.h>

const static int default_backlog = 6;

enum
{
    Usage_Err = 1,
    Socket_Err,
    Bind_Err,
    Listen_Err
};

#define CONV(addr_ptr) ((struct sockaddr *)addr_ptr)

class TcpServer
{
public:
    TcpServer(uint16_t port) : _port(port), _isrunning(false)
    {
    }
    // 都是固定套路
    void Init()
    {
        // 1. 创建socket, file fd, 本质是文件
        _listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensock < 0)
        {
            exit(0);
        }
        int opt = 1;
        setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));

        // 2. 填充本地网络信息并bind
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = htonl(INADDR_ANY);

        // 2.1 bind
        if (bind(_listensock, CONV(&local), sizeof(local)) != 0)
        {
            exit(Bind_Err);
        }

        // 3. 设置socket为监听状态,tcp特有的
        if (listen(_listensock, default_backlog) != 0)
        {
            exit(Listen_Err);
        }
    }
    void ProcessConnection(int sockfd, struct sockaddr_in &peer)
    {
        uint16_t clientport = ntohs(peer.sin_port);
        std::string clientip = inet_ntoa(peer.sin_addr);
        std::string prefix = clientip + ":" + std::to_string(clientport);
        std::cout << "get a new connection, info is : " << prefix << std::endl;
        while (true)
        {
            char inbuffer[1024];
            ssize_t s = ::read(sockfd, inbuffer, sizeof(inbuffer)-1);
            if(s > 0)
            {
                inbuffer[s] = 0;
                std::cout << prefix << "# " << inbuffer << std::endl;
                std::string echo = inbuffer;
                echo += "[tcp server echo message]";
                write(sockfd, echo.c_str(), echo.size());
            }
            else
            {
                std::cout << prefix << " client quit" << std::endl;
                break;
            }
        }
    }
    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            // 4. 获取连接
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int sockfd = accept(_listensock, CONV(&peer), &len);
            if (sockfd < 0)
            {
                continue;
            }
            ProcessConnection(sockfd, peer);
        }
    }
    ~TcpServer()
    {
    }

private:
    uint16_t _port;
    int _listensock; // TODO
    bool _isrunning;
};

using namespace std;

void Usage(std::string proc)
{
    std::cout << "Usage : \n\t" << proc << " local_port\n"
              << std::endl;
}
// ./tcp_server 8888
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        return Usage_Err;
    }
    uint16_t port = stoi(argv[1]);
    std::unique_ptr<TcpServer> tsvr = make_unique<TcpServer>(port);
    tsvr->Init();
    tsvr->Start();

    return 0;
}
相关推荐
苍煜11 小时前
Docker容器网络详解+端口映射原理(系列第二篇:实战核心)
网络·docker·容器
初願致夕霞12 小时前
基于系统调用的Linux网络编程——UDP与TCP
linux·网络·c++·tcp/ip·udp
数智化精益手记局13 小时前
什么是设备维护管理?设备维护管理包含哪些内容?
大数据·网络·人工智能·安全·信息可视化
salipopl16 小时前
FPGA中AXI-FIFO主机接口的自定义实现与versal读写工程分析
网络·fpga开发
会周易的程序员17 小时前
aiDgeScanner 工业设备网络扫描与管理工具
网络·c++·物联网·架构·electron·node.js·iot
CableTech_SQH18 小时前
F5G 全光网,赋能智慧校园数字化建设
大数据·网络·5g·运维开发·信息与通信
hellojackjiang201118 小时前
socket长连接在手游场景下的技术实践
网络·网络协议·tcp/ip·架构·网络编程
精益数智小屋18 小时前
设备维护方案核心功能拆解:一套好的设备维护方案如何解决设备突发故障
大数据·运维·网络·数据库·人工智能·面试·自动化