LISTEN(2) Linux Programmer's Manual
NAME
listen - listen for connections on a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
TcpSocket
实现以下测试代码listen
的第二个参数设置为 1, 并且不调用 accept
Makefile
.PHONY:all
all:tcp_server tcp_client
tcp_server:TcpServer.cc
g++ -o $@ $^ -std=c++14
tcp_client:TcpClient.cc
g++ -o $@ $^ -std=c++14
.PHONY:clean
clean:
rm -rf tcp_server tcp_client
TcpClient.cc
#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;
}
TcpServer.cc
#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;
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)
{
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 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
代码
此时启动 1 个客户端同时连接服务器, 用 netstat 查看服务器状态, 一切正常,如下:
此时可以看到两条链接,由于 TCP 是全双工的,因此当我们客户端和服务端在同一台主机上,建立连接启动之后,netstat 就能查到两个,服务端 到 客户端,客户端 到 服务端
从这个例子我们可以知道,即使我们的上层不调用 accept , 客户端依然可以连接
然后启动 2 个客户端同时连接服务器, 用 netstat 查看服务器状态, 仍然一切正常,但是启动第 3 个客户端时, 发现服务器对于第 3 个连接的状态存在问题了,如下:
我们可以看到 正常连接的前两台主机,都有两个连接(客户端 <==> 服务端),而且状态均是 ESTABLISHED
,而第三台服务端出现了 SYN_SENT
,并且连接也只有一个,说明连接建立失败
之前已经讲过了三次握手的基本流程,如下:
SYN
标志,表示发起连接请求;SYN+ACK
,表示接受请求并同步;ACK
。三次握手完成后,连接进入全连接队列,但此时应用层可能尚未处理这个连接请求
💻 那么为什么上面客户端状态正常,但是服务器端出现了 SYN_RECV
状态,而不是 ESTABLISHED
状态
原因:Linux 内核协议栈为一个 tcp 连接管理使用两个队列:
SYN
的连接,但尚未完成三次握手的连接。每当收到 SYN
请求,服务器将这个连接放入半连接队列,直到三次握手完成或超时失败。(用来保存处于 SYN_SENT
和 SYN_RECV
状态的请求)ESTABLISHED
状态, 但是应用层没有调用 accept
取走的请求) backlog
)的影响,全连接队列满了的时候,就无法继续让当前连接的状态进入 ESTABLISHED
状态了.这个队列的长度通过上述实验可知,是 listen 的第二个参数(backlog
) + 1
结论:
listen
的第二个参数的本质是当服务器压力很大或者来不及获取(accept)新连接的时候,操作系统就会在底层(tcp
层)为我们维护一个全连接队列,这个队列会把新到来的连接维护起来,当我们未来需要的时候再把新连接获取上去,这个队列的最大长度叫做backlog + 1
💻 在操作系统中的传输层中有一个接收队列 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 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
结构体,但是未来我们是想通过文件描述符找到对应的套接字,然后进行读取数据。可是现在是 struct socket
结构体内部有一个 struct file
结构体,如果通过 struct file
结构体找到套接字呢?
🐑 在 struct file 结构体有一个指针 void* private_data
, 这个指针指向 struct socket
结构体。这样两个结构体就联系起来了!
📐 struct socket
结构体是 网络 Socket
的入口,其内部还包含一个 const struct proto_ops
结构体
虽然我们 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 long icsk_timeout;
struct timer_list icsk_retransmit_timer;
struct timer_list icsk_delack_timer;
__u32 icsk_rto;
__u32 icsk_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);
__u8 icsk_ca_state;
__u8 icsk_retransmits;
__u8 icsk_pending;
__u8 icsk_backoff;
__u8 icsk_syn_retries;
__u8 icsk_probes_out;
__u16 icsk_ext_hdr_len;
struct{
__u8 pending; /* ACK is pending*/
__u8 quick; /*Scheduled number of quick acks*/
__u8 pingpong; /* The session is interactive*/
__u8 blocked; /* Delayed ACK was blocked by socket lock */
__u32 ato; /* Predicted tick of soft clock*/
unsigned longtimeout;/* Currently scheduled timeout*/
__u32 lrcvtime; /* timestamp of last received data packet */
__u16 last_seg_size;/* Size of last incoming segment*/
__u16 rcv_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_t syn_wait_lock;
u8 rskq_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_t wait;
short type;
}
udp_sock
的第一个成员是 struct inet_sock
结构体(因为 udp 不需要连接所以没有包含连接属性结构体)。那么最终也是一个 struct sock
结构体,所以也可以通过C风格的多态实现!通过 基类 struct socket
,我们可以进行 tcp 和 udp 的通信,所以说他是网络 socket
的入口。
如果全连接队列已满,新的完成握手的连接会被丢弃,客户端可能收到ECONNREFUSED
错误或超时
原因如下:
accept()
的速度跟不上新连接的到达速度net.core.somaxconn
)不足以应对高并发采取的优化措施如下:
① 调整队列长度,适用于 默认配置不足时
修改内核参数(以Linux为例):
# 查看当前全连接队列最大值
sysctl net.core.somaxconn
# 临时修改(重启失效)
sysctl -w net.core.somaxconn=1024
# 永久修改(写入配置文件)
echo "net.core.somaxconn=1024" >> /etc/sysctl.conf
sysctl -p
应用程序设置:确保listen(fd, backlog)
中的backlog
参数与somaxconn
一致。
listen(sockfd, 1024);
② 提升应用处理能力,适用于高并发且应用处理慢
accept()
调用接收多个连接,减少系统调用开销。③ 负载均衡与横向扩展,适用于单机资源瓶颈
④ 监控与告警,适用于预防性维护
监控队列状态:
# 查看全连接队列溢出情况(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.conf
ulimit -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"
TCPDump 是一款强大的网络分析工具,主要用于捕获和分析网络上传输的数据包。
🎁 tcpdump 通常已经预装在大多数 Linux 发行版中。 如果没有安装, 可以使用包管理器进行安装。
例如 Ubuntu, 可以使用以下命令安装:
sudo apt-get update
sudo apt-get install tcpdump
sudo yum install tcpdump
① 捕获所有网络接口上的 TCP 报文
① 使用以下命令可以捕获所有网络接口上传输的 TCP 报文:
$ sudo tcpdump -i any tcp
注意: -i any 指定捕获所有网络接口上的数据包, tcp 指定捕获 TCP 协议的数据包。 i 可以理解成为 interface 的意思
② 捕获指定网络接口上的 TCP 报文
$ ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.18.45.153 netmask 255.255.192.0 broadcast
172.18.63.255
inet6 fe80::216:3eff:fe03:959b prefixlen 64 scopeid
0x20<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 0
TX 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
【案例】
$ sudo tcpdump dst host 192.168.1.200 and tcp
$ sudo tcpdump src host 192.168.1.100 and dst host 192.168.1.200 and tcp
④ 捕获特定端口的 TCP 报文
$ sudo tcpdump port 80 and tcp
⑤ 保存捕获的数据包到文件
$ sudo tcpdump -i eth0 port 80 -w data.pcap
这将把捕获到的 HTTP 流量保存到名为 data.pcap 的文件中。
⑥ 从文件中读取数据包进行分析
🍑 使用 -r 选项可以从文件中读取数据包进行分析。 例如:
tcpdump -r data.pcap
这将读取 data.pcap
文件中的数据包并进行分析
注意事项
tcpdump
时, 请确保你有足够的权限来捕获网络接口上的数据包。 通常, 你需要以 root 用户身份运行 tcpdump
。 tcpdump
的时候, 有些主机名会被云服务器解释成为随机的主机名, 如果不想要, 就用 -n 选项
主机观察三次握手的第三次握手, 不占序号当我们带上 -n 时,会如下:
在上面我们也可以看到,此时我们抓到了 Flags 为 S 的 SYN 报文
此时需要用到上面的测试代码进行验证:
SYN
、 SYN + ACK
、 ACK
, 可以看到第二个 ACK
就是对第一个 SYN
的确认序号:第三次的 ACK 就自动置 1 了, 双方开始正常通信
此时我们直接 CTRL + C 杀掉客户端 可以发现抓取到 FIN 标识位 和 ACK 但是为什么只有两次挥手呢?
bug
的!! 很有可能没有关闭文件描述符!!!查看代码,发现我们雀氏没有关闭 fd ,如下:
但是由于客户端和服务器关闭连接几乎是同时的,此时就造成了捎带应答!!!,如下:
而如果我们是让服务器 sleep 1 秒再退出,结果会咋样呢?
结果如下:
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有