【Linux网络#17】TCP全连接队列与tcpdump抓包_tcpdump tcp 消息体里 包含某些字符
📃个人主页:island1314
🔥个人专栏:Linux—登神长阶
⛺️ 欢迎关注:👍点赞 👂🏽留言 😍收藏 💞 💞 💞
- 生活总是不会一帆风顺,前进的道路也不会永远一马平川,如何面对挫折影响人生走向 – 《人民日报》
🔥 目录
-
- 一、TCP 相关实验 测试
-
- 1. Listen 的第二个参数
- 2. 出现 SYN_SENT 原因
- 3. 理解全连接队列
- 二、全连接队列满
- 三、TCP dump 抓包
-
- 1. 安装 tcpdump
- 2. 常见使用
- 3. 抓包验证三次握手和四次挥手的过程
一、TCP 相关实验 测试
1. Listen 的第二个参数
LISTEN(2) Linux Programmer\'s Manual NAME listen - listen for connections on a socketSYNOPSIS #include /* See NOTES */ #include int listen(int sockfd, int backlog);
- 基于封装的
TcpSocket
实现以下测试代码 - 对于服务器,
listen
的第二个参数设置为 1, 并且不调用accept
- 测试代码如下:
Makefile
.PHONY:allall:tcp_server tcp_clienttcp_server:TcpServer.ccg++ -o $@ $^ -std=c++14tcp_client:TcpClient.ccg++ -o $@ $^ -std=c++14.PHONY:cleanclean:rm -rf tcp_server tcp_client
TcpClient.cc
#include #include #include #include #include #include #include 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 < 0) { buffer[bytesReceived] = \'\\0\'; // 确保字符串以 null 结尾 std::cout << \"Received from server: \" << buffer << std::endl; } else { std::cerr << \"recv failed\" << std::endl; } } ::close(clientSocket); return 0;}
TcpServer.cc
#include #include #include #include #include #include #include #include #include #include #include #include const static int default_backlog = 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 < 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) { sleep(1); // 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 8888int main(int argc, char *argv[]){ if (argc != 2) { Usage(argv[0]); return Usage_Err; } uint16_t port = stoi(argv[1]); std::unique_ptr tsvr = make_unique(port); tsvr->Init(); tsvr->Start(); return 0;}
【案例】:对测试代码进行修改,如下:
🌿将 backlog 的值修改为 1,并且注释掉 accept
代码
此时启动 1 个客户端同时连接服务器, 用 netstat 查看服务器状态, 一切正常,如下:
此时可以看到两条链接,由于 TCP 是全双工的,因此当我们客户端和服务端在同一台主机上,建立连接启动之后,netstat 就能查到两个,服务端 到 客户端,客户端 到 服务端
从这个例子我们可以知道,即使我们的上层不调用 accept , 客户端依然可以连接
- 因此我们可以知道 三次握手 建立连接的过程与三次握手无关,其会在底层自动把链接建立好
- 那么 accept 的本质其实就是把一个建立好的连接 通过 accept 以文件描述符 fd 的方式给用户返回,拿上来就行了
然后启动 2 个客户端同时连接服务器, 用 netstat 查看服务器状态, 仍然一切正常,但是启动第 3 个客户端时, 发现服务器对于第 3 个连接的状态存在问题了,如下:
我们可以看到 正常连接的前两台主机,都有两个连接(客户端 服务端),而且状态均是 ESTABLISHED
,而第三台服务端出现了 SYN_SENT
,并且连接也只有一个,说明连接建立失败
2. 出现 SYN_SENT 原因
之前已经讲过了三次握手的基本流程,如下:
- 第一次握手 :客户端发送
SYN
标志,表示发起连接请求; - 第二次握手 :服务器收到请求后,发送
SYN+ACK
,表示接受请求并同步; - 第三次握手 :客户端确认连接,发送
ACK
。
三次握手完成后,连接进入全连接队列,但此时应用层可能尚未处理这个连接请求
💻 那么为什么上面客户端状态正常,但是服务器端出现了 SYN_RECV
状态,而不是 ESTABLISHED
状态
原因:Linux 内核协议栈为一个 tcp 连接管理使用两个队列:
- 半连接队列(syn queue): 存储已发送
SYN
的连接,但尚未完成三次握手的连接。每当收到SYN
请求,服务器将这个连接放入半连接队列,直到三次握手完成或超时失败。(用来保存处于SYN_SENT
和SYN_RECV
状态的请求) - 全连接队列(accpetd 队列): 三次握手完成后,连接进入全连接队列,等待被应用层(通常是通过 accept() 函数)处理。如果全连接队列已满,新连接将被丢弃。 (用来保存处于
ESTABLISHED
状态, 但是应用层没有调用accept
取走的请求)- 🔗 而全连接队列的长度会受到 listen 第二个参数(
backlog
)的影响,全连接队列满了的时候,就无法继续让当前连接的状态进入ESTABLISHED
状态了.
- 🔗 而全连接队列的长度会受到 listen 第二个参数(
这个队列的长度通过上述实验可知,是 listen 的第二个参数(backlog
) + 1
结论:
listen
的第二个参数的本质是当服务器压力很大或者来不及获取(accept)新连接的时候,操作系统就会在底层(tcp
层)为我们维护一个全连接队列,这个队列会把新到来的连接维护起来,当我们未来需要的时候再把新连接获取上去,这个队列的最大长度叫做backlog + 1
3. 理解全连接队列
💻 在操作系统中的传输层中有一个接收队列 accept_queue
,建立连接时需要进行三次握手。操作系统中用户访问的网站多种多样,并且会并发的运行,所以在操作系统内部一定是要通过数据结构来进行管理的!
- 连接本质就是操作系统内核中的一批数据结构!
在传输层中将这个数据结构放入队列中进行管理!应用层会调用 accept
获取连接,传输层就会返回给一个 文件描述符fd 供应用层使用,通过这个文件描述符,应用层就可以进行通信!这个队列就是 全连接队列 !
🦈 当应用层非常忙,来不及 accept
的时候,那么此时 全连接队列 就会挤压连接,这个总数不能超过 backlog
!这个并不代表服务端只能同时处理 backlog + 1
个连接。全连接队列中的连接表示连接成功,但是来不及及时处理的连接!
全连接队列的本质就是一组 生产消费者模型,应用层从其中获取资源,传输层向其中放入资源!
- 这个队列保证了在应用层较忙时无法获取连接时,可以先将一些连接维护起来,等待应用层调用。
- 这样可以大大提升效率,提高连接吞吐量!减少服务端闲置率,增加给用户提供服务的效率和体验!这也是为什么全连接队列不能为空,也不能太长的原因
看了上面,我们再从 内核 的角度 来理解 全连接队列
🍺 当服务器启动时,本质上是启动一个进程,那么就会有对应的 task_struct
。在这个结构体中都会有 struct files_struct
!其中包含文件描述符表 struct file*fd_array[]
,每个元素都指向文件结构体 struct file
当创建网络套接字时,会创建一个 struct socket
结构体!在内核中时这样一个结构:
struct socket {socket_state tate; unsigned longflags; const struct proto_ops *ops; struct fasync_struct*fasync list; struct_file*file; struct sock*sk; wait_queue_head_t wait; shorttype;};
此时可以看到 struct socket
结构体内部有一个 struct file
结构体,但是未来我们是想通过文件描述符找到对应的套接字,然后进行读取数据。可是现在是 struct socket
结构体内部有一个 struct file
结构体,如果通过 struct file
结构体找到套接字呢?
🐑 在 struct file 结构体有一个指针 void* private_data
, 这个指针指向 struct socket
结构体。这样两个结构体就联系起来了!
📐 struct socket
结构体是 网络 Socket
的入口,其内部还包含一个 const struct proto_ops
结构体
- 这是一个方法集,集合了 bind,connect… 一系列的函数指针!
虽然我们 struct socket
结构体是内核中的套接字结构,但建立连接时真实的数据结构是 struct socket
结构体中 struct sock *sk
所指向的 tcp_sock
结构体!
🛜 这是 TCP 套接字,其中包含了慢启动算法阈值,拥塞窗口大小,关联进程… 一系列 TCP 协议中的对应字段!这个 tcp_sock 就是三次握手时候建立的结构体!其中的第一个成员 struct inet_connection_sock
是复制连接属性的! 这里就包含连接的相关信息。全连接队列就在这个结构体中!
结构体如下:
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 inet_bind_bucket*icsk_bind_hash; unsigned longicsk_timeout; struct timer_listicsk_retransmit_timer; struct timer_listicsk_delack_timer; __u32icsk_rto; __u32icsk_pmtu_cookie; const struct tcp_congestion_ops *icsk_ca_ops; const struct inet_connection_sock_af_ops *icsk_af_ops; unsigned int (*icsk sync mss)(struct sock *sk, u32 pmtu); __u8icsk_ca_state; __u8icsk_retransmits; __u8icsk_pending; __u8icsk_backoff; __u8icsk_syn_retries; __u8icsk_probes_out; __u16icsk_ext_hdr_len; struct{__u8pending; /* ACK is pending*/__u8quick;/*Scheduled number of quick acks*/__u8pingpong; /* The session is interactive*/__u8blocked; /* Delayed ACK was blocked by socket lock */__u32ato;/* Predicted tick of soft clock*/ unsigned longtimeout;/* Currently scheduled timeout*/ __u32lrcvtime;/* timestamp of last received data packet */ __u16last_seg_size;/* Size of last incoming segment*/ __u16rcv_mss;/* MSS used for delayed Ack decisions*/}icsk_ack; ...}
这里有超时重传的触发时间,TCP
连接的状态,握手失败重试次数,全连接队列…等数据。
request_accept_queue
(全连接队列)结构体如下:
struct request_sock_queue { struct request_sock *rskq accept head; struct request_sock *rskg accept tail; rwlock_tsyn_wait_lock; u8rskq_defer_accept; /* 3 bytes hole, try to pack */ struct listen_sock*listen opt; };
struct inet_connection_sock
中的第一个成员是 struct inet_sock
结构体,这是网络层的结构体。
🏡 struct inet_sock
结构体其中包含了 目的端口号,源端口号,目的 IP 地址 和 源 IP 地址 等数据!更重要的是其中第一个成员是 struct sock 结构体,里面包含着报文的一些属性 。
- 这是整个
tcp_sock
中最底层的结构体,其中有两个字段:接收队列 和 发送队列
struct sk_buff_head sk_receive_queue;struct sk_buff_head sk_write_queue;
- 这两个队列对于网络通信至关重要,因为它们直接参与了数据的接收和发送过程
然后我们再来看看 sock 结构体,如下:
我们再回过来看 struct socket,其中有一个结构体指针 struct sock* sk
,这个指针可以指向 tcp_sock
中最底层的 struct sock
结构体,然后可以通过类型转换,最终读取到整个 tcp_sock 结构体!也就是说,这个指针指向了 tcp_sock 结构体!
- 通过强制类型转换 这个指针可以直接指向
struct sock
、struct inet_sock
、struct ine_connection_sock
访问对应的数据
通过结构体嵌套的方式,使用公共指针指向结构体头部对象的方式 这就是 C风格的多态! 此时 struct sock 就是基类!
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_twait; shorttype;}
- 同样的创建 UDP 套接字时,
udp_sock
的第一个成员是struct inet_sock
结构体(因为 udp 不需要连接所以没有包含连接属性结构体)。那么最终也是一个struct sock
结构体,所以也可以通过C风格的多态实现!
通过 基类 struct socket
,我们可以进行 tcp 和 udp 的通信,所以说他是网络 socket
的入口。
二、全连接队列满
如果全连接队列已满,新的完成握手的连接会被丢弃,客户端可能收到ECONNREFUSED
错误或超时
原因如下:
- 应用处理速度不足:
- 应用程序调用
accept()
的速度跟不上新连接的到达速度
- 应用程序调用
- 队列长度设置过小:
- 操作系统默认的全连接队列长度(如Linux的
net.core.somaxconn
)不足以应对高并发
- 操作系统默认的全连接队列长度(如Linux的
- 突发流量冲击:
- 瞬时大量连接请求导致队列快速填满
- 资源限制:
- 文件描述符(FD)耗尽或内存不足,无法接收新连接
采取的优化措施如下:
① 调整队列长度,适用于 默认配置不足时
-
修改内核参数(以Linux为例):
# 查看当前全连接队列最大值sysctl net.core.somaxconn# 临时修改(重启失效)sysctl -w net.core.somaxconn=1024# 永久修改(写入配置文件)echo \"net.core.somaxconn=1024\" >> /etc/sysctl.confsysctl -p
-
应用程序设置:确保
listen(fd, backlog)
中的backlog
参数与somaxconn
一致。
listen(sockfd, 1024);
② 提升应用处理能力,适用于高并发且应用处理慢
- 多线程/进程模型:为每个连接分配独立线程/进程处理。
- 异步I/O(如epoll/kqueue):使用事件驱动模型(如Nginx、Node.js)高效处理并发。
- 连接批处理:单次
accept()
调用接收多个连接,减少系统调用开销。
③ 负载均衡与横向扩展,适用于单机资源瓶颈
- 多服务器分发:通过负载均衡器(如Nginx、HAProxy)将请求分散到多台服务器。
- 自动扩缩容:云环境下根据流量自动增加服务器实例(如Kubernetes HPA)。
④ 监控与告警,适用于预防性维护
-
监控队列状态:
# 查看全连接队列溢出情况(Linux)netstat -s | grep \"times the listen queue of a socket overflowed\"ss -lnt | grep \"Recv-Q\"
-
告警阈值:当溢出次数持续增长时触发告警,及时扩容或优化。
⑤ 优雅降级与限流
- 队列满时拒绝新连接:返回
503 Service Unavailable
或自定义错误页面。 - 限流措施:使用令牌桶或漏桶算法限制新连接速率(如
iptables
限速)。
⑥ 资源优化,适用于资源不足导致队列溢出
-
增加文件描述符限制:
# 修改系统级和进程级FD限制echo \"fs.file-max=100000\" >> /etc/sysctl.confulimit -n 65535
-
内存与TCP参数调优:
# 调整TCP缓冲区大小sysctl -w net.ipv4.tcp_mem=\"9437184 12582912 16777216\"sysctl -w net.ipv4.tcp_rmem=\"4096 87380 6291456\"sysctl -w net.ipv4.tcp_wmem=\"4096 16384 4194304\"
三、TCP dump 抓包
TCPDump 是一款强大的网络分析工具,主要用于捕获和分析网络上传输的数据包。
- 这里我们使用其 进行抓包,分析 TCP 过程
1. 安装 tcpdump
🎁 tcpdump 通常已经预装在大多数 Linux 发行版中。 如果没有安装, 可以使用包管理器进行安装。
例如 Ubuntu, 可以使用以下命令安装:
sudo apt-get updatesudo apt-get install tcpdump
- 而在 Red Hat 或 CentOS 系统中, 可以使用以下命令:
sudo yum install tcpdump
2. 常见使用
① 捕获所有网络接口上的 TCP 报文
① 使用以下命令可以捕获所有网络接口上传输的 TCP 报文:
$ sudo tcpdump -i any tcp
注意: -i any 指定捕获所有网络接口上的数据包, tcp 指定捕获 TCP 协议的数据包。 i 可以理解成为 interface 的意思
② 捕获指定网络接口上的 TCP 报文
- 如果你只想捕获某个特定网络接口(如 eth0) 上的 TCP 报文, 可以使用以下命令:
$ ifconfigeth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500inet 172.18.45.153 netmask 255.255.192.0 broadcast172.18.63.255inet6 fe80::216:3eff:fe03:959b prefixlen 64 scopeid0x20<link>ether 00:16:3e:03:95:9b txqueuelen 1000 (Ethernet)RX packets 34367847 bytes 9360264363 (9.3 GB)RX errors 0 dropped 0 overruns 0 frame 0TX packets 34274797 bytes 6954263329 (6.9 GB)TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0$ sudo tcpdump -i eth0 tcp
③ 捕获特定源或目的 IP 地址的 TCP 报文
- 使用
host
关键字可以指定 源或目的 IP 地址。 例如, 要捕获源 IP 地址为 192.168.1.100 的 TCP 报文, 可以使用以下命令:
$ sudo tcpdump src host 192.168.1.100 and tcp
【案例】
- 要捕获目的 IP 地址为 192.168.1.200 的 TCP 报文, 可以使用以下命令:
$ sudo tcpdump dst host 192.168.1.200 and tcp
- 同时指定源和目的 IP 地址, 可以使用 and 关键字连接两个条件:
$ sudo tcpdump src host 192.168.1.100 and dst host 192.168.1.200 and tcp
④ 捕获特定端口的 TCP 报文
- 使用 port 关键字可以指定端口号。 例如, 要捕获端口号为 80 的 TCP 报文(通常是HTTP 请求) , 可以使用以下命令:
$ sudo tcpdump port 80 and tcp
⑤ 保存捕获的数据包到文件
- 使用 -w 选项可以将捕获的数据包保存到文件中, 以便后续分析。 例如:
$ sudo tcpdump -i eth0 port 80 -w data.pcap
这将把捕获到的 HTTP 流量保存到名为 data.pcap 的文件中。
- 了解: pcap 后缀的文件通常与 PCAP(Packet Capture) 文件格式相关, 这是一种用于捕获网络数据包的文件格式
⑥ 从文件中读取数据包进行分析
🍑 使用 -r 选项可以从文件中读取数据包进行分析。 例如:
tcpdump -r data.pcap
这将读取 data.pcap
文件中的数据包并进行分析
注意事项
- 使用
tcpdump
时, 请确保你有足够的权限来捕获网络接口上的数据包。 通常, 你需要以 root 用户身份运行tcpdump
。- 使用
tcpdump
的时候, 有些主机名会被云服务器解释成为随机的主机名, 如果不想要, 就用 -n 选项
主机观察三次握手的第三次握手, 不占序号
当我们带上 -n 时,会如下:
在上面我们也可以看到,此时我们抓到了 Flags 为 S 的 SYN 报文
3. 抓包验证三次握手和四次挥手的过程
此时需要用到上面的测试代码进行验证:
- 将服务端和客户端同时启动, 可以看到下面三个就是
SYN
、SYN + ACK
、ACK
, 可以看到第二个ACK
就是对第一个SYN
的确认序号:
第三次的 ACK 就自动置 1 了, 双方开始正常通信
此时我们直接 CTRL + C 杀掉客户端 可以发现抓取到 FIN 标识位 和 ACK 但是为什么只有两次挥手呢?
- 此时我们有理由相信我们代码中是有
bug
的!! 很有可能没有关闭文件描述符!!!
查看代码,发现我们雀氏没有关闭 fd ,如下:
但是由于客户端和服务器关闭连接几乎是同时的,此时就造成了捎带应答!!!,如下:
而如果我们是让服务器 sleep 1 秒再退出,结果会咋样呢?
结果如下:
- 此时就能看到标准的 四次挥手了!!!