一、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 接口:抓指定网卡
- -i any:抓所有接口
- -i lo:抓环口
- -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连接建立与通信和连接断开过程,图片有点长,我就放关键的了:

我将按照从左往右开始介绍:
- 127.0.0.1.48712 > 127.0.0.1.8080:客户端向服务器发起请求
- Flags [S]:SYN 被置位
- Flags [S.]:SYN 和 ACK 被置位
- Flags [.]:ACK 被置位
- seq:初始化序列号
- ack:确认号,对端序列号 + 1
- win:接收窗口的最大值
如果这里没加 -S 最后的报文的 ack 会是1:
我这里为了方便演示,就继续加上-S。
当我发送数据一个字符 a 后(我裁了一下,否则太难看了):

- lenngth:数据负载
- 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;
}

