文章目录
0. 概述
TCP网络编程是网络编程中常见的一种方式,广泛应用于服务器端和客户端开发。为了提高TCP网络程序的性能,需要对程序进行优化。本文将介绍一些常见的TCP网络编程性能优化点。
1、修改select recv缓存区大小
在TCP网络编程中,通常使用select函数来监控多个套接字的读写事件。当有数据可读时,select函数会返回。然后,程序就可以使用recv函数从套接字中读取数据。
默认情况下,recv函数的缓冲区大小为4096字节。如果每次读取的数据量小于4096字节,则会导致多次系统调用,降低程序的性能。
char tmp[8192];
while (1) {
int ret = select(FD_SETSIZE, &fds, NULL, NULL, &timeout);
if (ret == 0) {
break;
}
for (int i = 0; i < FD_SETSIZE; ++i) {
if (FD_ISSET(i, &fds)) {
ret = recv(i, tmp, sizeof(tmp), 0);
if (ret == -1) {
// 处理错误
} else if (ret == 0) {
// 处理连接断开
} else {
// 处理接收到的数据
}
}
}
}
2、应用程序级修改缓冲区大小
除了修改recv函数的缓冲区大小之外,还可以通过应用程序级的代码来修改缓冲区大小。例如,可以使用setsockopt函数来设置套接字的发送缓冲区和接收缓冲区大小
- 参考代码:
int sock, sock_buf_size;
sock = socket(AF_INET, SOCK_STREAM, 0);
sock_buf_size = 65536;
setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (char *)&sock_buf_size, sizeof(sock_buf_size));
setsockopt(sock, SOL_SOCKET, SO_RCVBUF, (char *)&sock_buf_size, sizeof(sock_buf_size));
修改缓冲区大小可以提高程序的性能,但需要注意的是,缓冲区大小不能设置太大,否则可能会导致内存溢出。
参考文章:Socket缓冲区大小修改与系统设置
3、端口复用
在高并发情况下,传统的单进程或单线程服务器可能会成为瓶颈。因为每个连接都需要占用一个进程或线程,这会导致大量的系统资源消耗。
端口复用是一种可以提高服务器性能的技术。它允许一个进程或线程同时处理多个连接。
在Linux系统中,可以使用SO_REUSEPORT选项来启用端口复用。
-
配置修改参考: c/c++:端口复用(setsockopt)、io多路转接(select、 poll、epoll)
调用setsockopt函数:
#include <sys/types.h>
#include <sys/socket.h>
// 设置套接字属性
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
// 参数:
// - sockfd: 要操作的文件描述符
// - level: 级别 -> SOL_SOCKET (端口复用的级别)
// - optname: 端口复用的级别(二选一,随便选,对于端口复用都可以)
// - SO_REUSEADDR
// - SO_REUSEPORT
// - optval: 端口复用-> 对应的是整形数
// - 1: 可以复用
// - 0: 不能复用
// - optlen: optval参数对应的内存大小
// 设置端口复用, 设置的时机: 服务器绑定端口之前, 设置端口复用。例如:
setsockopt();
bind();
4、内核参数修改
Linux内核提供了许多可以用来调整TCP/IP性能的参数。可以通过修改这些参数来优化TCP网络程序的性能。
-
修改linux系统文件句柄限制
执行
echo -e "* soft nofile 40960 \n* hard nofile 65536" >> /etc/security/limits.conf
修改linux系统文件句柄限制cat /etc/security/limits.conf 将在limits.conf中添加以下参数 * soft nofile 40960 * hard nofile 65536
参数
* 代表所有用户有效 soft:软限制,超过会报warn hard:实际限制 nofile:文件句柄参数 number:最大文件句柄数
使用
ulimit -n
查看open files 参数是否改变。 -
TCP/IP内核参数优化,建议参数如下:
/etc/sysctl.conf添加以下代码,可以执行sysctl -p命令或者重启后永久生效
net.core.rmem_default = 8388608 net.core.rmem_max = 16777216 net.core.wmem_default = 8388608 net.core.wmem_max = 16777216 net.core.netdev_max_backlog = 2000 net.core.somaxconn = 2048 net.core.optmem_max = 81920 net.ipv4.tcp_mem = 131072 262144 524288 net.ipv4.tcp_rmem = 8760 8388608 16777216 net.ipv4.tcp_wmem = 8760 8388608 16777216 net.ipv4.tcp_keepalive_time = 60 net.ipv4.tcp_keepalive_probes = 3 net.ipv4.tcp_keepalive_intvl = 20 vm.overcommit_memory=1
5、设置TcpNodelay和TcpKeepAlive
-
TCPNoDelay和TCPkeepalive都是常用的TCP选项,前者的作用是禁用Nagle 算法6,避免连续发包出现延迟,这对编写低延迟网络服务很重要。后者的作用是定期探查TCP连接是否还存在。一般来说如果有应用层心跳的话,TCPkeepalive不是必需的。
-
TCPNoDelay设置参考代码:
int sock, flag, ret; /* Create new stream socket */ sock = socket( AF_INET, SOCK_STREAM, 0 ); /* Disable the Nagle (TCP No Delay) algorithm */ flag = 1; ret = setsockopt( sock, IPPROTO_TCP, TCP_NODELAY, (char *)&flag, sizeof(flag) ); if (ret == -1) { printf("Couldn't setsockopt(TCP_NODELAY)\n"); exit(-1); }
-
TCPkeepalive内核中设置参考上面第4点,应用程序中设置参考如下:
int keepAlive = 1; // 开启keepalive属性 int keepIdle = 60; // 如该连接在60秒内没有任何数据往来,则进行探测 int keepInterval = 5; // 探测时发包的时间间隔为5 秒 int keepCount = 3; // 探测尝试的次数.如果第1次探测包就收到响应了,则后2次的不再发. setsockopt(rs, SOL_SOCKET, SO_KEEPALIVE, (void *)&keepAlive, sizeof(keepAlive)); setsockopt(rs, SOL_TCP, TCP_KEEPIDLE, (void*)&keepIdle, sizeof(keepIdle)); setsockopt(rs, SOL_TCP, TCP_KEEPINTVL, (void *)&keepInterval, sizeof(keepInterval)); setsockopt(rs, SOL_TCP, TCP_KEEPCNT, (void *)&keepCount, sizeof(keepCount));
-
见《Linux多线程服务编程》8.9节
6、使用std::vector作为数据缓冲区
使用 std::vector 作为数据缓冲区的优化方案。该方案的核心在于动态内存分配,这可能会让人联想到内存的存放位置以及性能影响。
-
内存分配位置:
std::vector 内部维护了一个动态数组,用于存储元素。该动态数组的内存空间位于堆上,而非栈上。原因分析:
栈内存通常用于存储函数局部变量和临时数据,其空间大小由编译器预先分配,并随着函数的调用和销毁而自动管理。
std::vector 的元素数量和大小可能在运行时发生变化,因此无法预先分配固定大小的栈空间。
堆内存可以动态分配和释放,满足 std::vector 动态管理内存的需求。 -
性能影响:
使用 std::vector 作为数据缓冲区可能会带来一定的性能开销,主要体现在以下方面:
内存分配/释放: 动态分配和释放内存空间需要额外的系统调用和操作,相比栈内存的自动管理,会带来一定的性能损耗。
内存碎片: 频繁的内存分配和释放可能会导致内存碎片,降低内存的使用效率,并增加额外的内存管理开销。
数据复制: 当 std::vector 的容量不足时,需要重新分配内存空间并复制原有数据,这也会带来额外的性能开销。
具体请参考https://gitee.com/liudegui/finger_server/blob/master/common/Buffer.cpp
7、对于tcp 客户端
# 1、修改/etc/sysctl.conf文件,增加下面的几个参数
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_max_tw_buckets = 10000
优化完内核参数后,可以执行sysctl -p命令,来激活上面的设置永久生效
# 2、解释
# 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;
net.ipv4.tcp_tw_reuse = 1
# 表示系统同时保持TIME_WAIT套接字的最大数量,(默认是18000).
net.ipv4.tcp_max_tw_buckets = 10000
# 参考:https://zhuanlan.zhihu.com/p/567088021