跳转至

理解Linux如何看待连接以及TCP全连接队列

约 1317 个字 341 行代码 1 张图片 预计阅读时间 9 分钟

理解Linux如何看待连接

在前面不论是编写UDP服务器和编写TCP服务器第一步都需要创建套接字,而这个套接字本质还是一个文件描述符,那么文件描述符是如何与套接字产生关联的?

为了解决这个问题,首先需要看struct file结构,该结构定义如下:

C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
struct file 
{
    union {
        struct list_head    fu_list;
        struct rcu_head     fu_rcuhead;
    } f_u;
    struct dentry       *f_dentry;
    struct vfsmount         *f_vfsmnt;
    const struct file_operations    *f_op;
    atomic_t        f_count;
    unsigned int        f_flags;
    mode_t          f_mode;
    loff_t          f_pos;
    struct fown_struct  f_owner;
    unsigned int        f_uid, f_gid;
    struct file_ra_state    f_ra;

    unsigned long       f_version;
    void            *f_security;

    /* needed for tty driver, and maybe others */
    void            *private_data;

#ifdef CONFIG_EPOLL
    /* Used by fs/eventpoll.c to link all the hooks to this file */
    struct list_head    f_ep_links;
    spinlock_t      f_ep_lock;
#endif /* #ifdef CONFIG_EPOLL */
    struct address_space    *f_mapping;
};

在该结构中有一个指针private_data,这个指针的类型是void *,所以可以指向任意类型的数据。在网络部分,一旦创建了套接字,那么就会创建一个struct socket结构,这个是网络套接字的入口结构,该结构定义如下:

C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct socket 
{
    socket_state        state;
    unsigned long       flags;
    const struct proto_ops  *ops;
    struct fasync_struct    *fasync_list;
    struct file     *file;
    struct sock     *sk;
    wait_queue_head_t   wait;
    short           type;
};

而要实现文件描述符与套接字的关联,就需要将struct socket结构体对象的地址赋值给struct file结构的private_data指针,另外该结构中也存在一个回指指针struct file *file,通过这个指针也可以找到对应的struct file对象

接着,在struct socket结构中,存在着一个成员struct sock *sk,这个成员表示具体的某一个套接字类型,具体类型由struct socketshort type成员决定,既可以是UDP套接字也可以是TCP套接字,该类型的定义如下:

C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
struct sock 
{
    struct sock_common  __sk_common;
#define sk_family       __sk_common.skc_family
#define sk_state        __sk_common.skc_state
#define sk_reuse        __sk_common.skc_reuse
#define sk_bound_dev_if     __sk_common.skc_bound_dev_if
#define sk_node         __sk_common.skc_node
#define sk_bind_node        __sk_common.skc_bind_node
#define sk_refcnt       __sk_common.skc_refcnt
#define sk_hash         __sk_common.skc_hash
#define sk_prot         __sk_common.skc_prot
    unsigned char       sk_shutdown : 2,
                sk_no_check : 2,
                sk_userlocks : 4;
    unsigned char       sk_protocol;
    unsigned short      sk_type;
    int         sk_rcvbuf;
    socket_lock_t       sk_lock;
    wait_queue_head_t   *sk_sleep;
    struct dst_entry    *sk_dst_cache;
    struct xfrm_policy  *sk_policy[2];
    rwlock_t        sk_dst_lock;
    atomic_t        sk_rmem_alloc;
    atomic_t        sk_wmem_alloc;
    atomic_t        sk_omem_alloc;
    struct sk_buff_head sk_receive_queue;
    struct sk_buff_head sk_write_queue;
    struct sk_buff_head sk_async_wait_queue;
    int         sk_wmem_queued;
    int         sk_forward_alloc;
    gfp_t           sk_allocation;
    int         sk_sndbuf;
    int         sk_route_caps;
    int         sk_gso_type;
    int         sk_rcvlowat;
    unsigned long       sk_flags;
    unsigned long           sk_lingertime;

    struct {
        struct sk_buff *head;
        struct sk_buff *tail;
    } sk_backlog;
    struct sk_buff_head sk_error_queue;
    struct proto        *sk_prot_creator;
    rwlock_t        sk_callback_lock;
    int         sk_err,
                sk_err_soft;
    unsigned short      sk_ack_backlog;
    unsigned short      sk_max_ack_backlog;
    __u32           sk_priority;
    struct ucred        sk_peercred;
    long            sk_rcvtimeo;
    long            sk_sndtimeo;
    struct sk_filter        *sk_filter;
    void            *sk_protinfo;
    struct timer_list   sk_timer;
    struct timeval      sk_stamp;
    struct socket       *sk_socket;
    void            *sk_user_data;
    struct page     *sk_sndmsg_page;
    struct sk_buff      *sk_send_head;
    __u32           sk_sndmsg_off;
    int         sk_write_pending;
    void            *sk_security;
    void            (*sk_state_change)(struct sock *sk);
    void            (*sk_data_ready)(struct sock *sk, int bytes);
    void            (*sk_write_space)(struct sock *sk);
    void            (*sk_error_report)(struct sock *sk);
    int         (*sk_backlog_rcv)(struct sock *sk,
                          struct sk_buff *skb);  
    void                    (*sk_destruct)(struct sock *sk);
};

在这个结构中就存在着UDP和TCP需要用到的缓冲区成员sk_receive_queue(接收缓冲区)和sk_write_queue(发送缓冲区),这两个缓冲区在序列化和反序列化与网络计算机一节已经有所提及,此处不再赘述

如果看得到UDP套接字和TCP套接字的相关结构定义:

C
1
2
3
4
5
6
struct udp_sock 
{
    /* inet_sock has to be the first member */
    struct inet_sock inet;
    // ...
};
C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
struct inet_connection_sock 
{
    /* inet_sock has to be the first member! */
    struct inet_sock      icsk_inet;
    // ...
};

struct tcp_sock 
{
    /* inet_connection_sock has to be the first member of tcp_sock */
    struct inet_connection_sock inet_conn;
    // ...
}

可以发现,在tcp_sock结构和udp_sock中都存在着一个成员struct inet_sock inet,在这个类型中:

C
1
2
3
4
5
struct inet_sock {
    /* sk and pinet6 has to be the first two members of inet_sock */
    struct sock     sk;
    // ...
};

第一个成员就是struct sock类型,以TCP为例,所以可以得出下图:

对比udp_socktcp_sock结构可以推出:TCP的连接管理就是通过struct inet_connection_sock结构实现的,而UDP没有这一个结构,也就没有对应的连接管理

这就是在网络套接字部分实现的继承和多态,这一点在前面操作系统管理System V标准中三种资源的方式也有类似的结构形式

理解TCP全连接队列

在使用TCP的listen接口时,可以看到listen的第二个参数backlog,这个参数实际上描述的就是TCP全连接队列,所谓全连接就是指的是三次握手成功,但是还没有被上层进行accept的连接。对应的还有半连接,指的就是还没有建立完三次握手的连接。下面主要考虑全连接

本次实验使用下面的代码进行测试:

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#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;
}
C++
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
#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 = 1; // 全连接队列设置为1

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;
}

编译运行上面的代码可以发现,连接两个客户端(建立三次握手)没有问题,但是一旦连接两个以上的客户端时,就会发现服务端无法处理新的连接。出现这个问题的主要原因就是服务端已经到达了能够处理的最大连接个数(backlog值+1),而因为上层一直没有accept,所以此时其他的连接就无法再正常进行三次握手从而导致进入了半连接,而已经建立三次握手的连接则进入了全连接队列,等待上层进行accept

所以,在设置backlog值时,不能让backlog值为0,这就会导致如果一个连接还没有被上层进行accept,那么后续到来的连接就会被丢弃,如果连接很多,就会导致大量的连接请求丢失,如果连接很少,也会增加服务器的闲置率;同样,如果backlog值设置的很大,一旦上层来不及accept,那么此时全连接队列就会堆积非常多的连接,那么新到来的连接可能就排到非常后面导致用户体验感差

抓包

抓包是指捕获网络中传输的数据包,并对其进行分析的过程。通过抓包,我们可以观察网络通信的细节,包括协议头、数据内容、通信时序等信息。这对于网络故障排查、安全分析、性能优化和协议研究都非常有价值

在Linux中一般会自带tcpdump抓包工具,使用方法如下:

Bash
1
sudo tcpdump -i any tcp

上面的命令表示查看所有网络接口上的TCP数据包

如果想要查看指定端口的TCP数据包,可以使用下面的命令:

Bash
1
sudo tcpdump -i any port 端口号 and tcp

如果想要查看指定IP地址的TCP数据包,可以使用下面的命令:

Bash
1
sudo tcpdump -i any host IP地址 and tcp

如果想要查看指定IP地址和端口的TCP数据包,可以使用下面的命令:

Bash
1
sudo tcpdump -i any host IP地址 and port 端口号 and tcp

如果想要查看指定IP地址和端口的TCP数据包,并将其保存到文件中,可以使用下面的命令:

Bash
1
sudo tcpdump -i any host IP地址 and port 端口号 and tcp -w 文件名

一般建议将文件名以.pcap结尾,这样可以方便后续的分析