一、参考资料
9.2 I/O 多路复用:select/poll/epoll | 小林coding
二、相关介绍
1. TCP Socket
TCP Socket性能优化秘籍:掌握read、recv、readv、write、send、sendv的最佳实践-CSDN博客
2. socket buffer
Linux(程序设计):48—缓冲区大小(SO_RCVBUF、SO_SNDBUF套接字选项)-CSDN博客
何时调整buffer大小
在以下情况下,调整发送缓冲区大小可能是必要的:
- 高丢包率的网络:在不可靠的网络中,发送缓冲区需要保留未确认的数据包,较大的缓冲区可以减少阻塞1。
- 高吞吐量应用:对于需要高吞吐量的应用,增加缓冲区大小可以提高性能。
调整send buffer大小:
sysctl -w net.ipv4.tcp_wmem="4096 16384 8388608"
为了使更改永久生效,可以将设置添加到 /etc/sysctl.conf
文件中:
net.ipv4.tcp_wmem = 4096 16384 8388608
使生效:
sysctl -p
通过合理调整 net.ipv4.tcp_wmem
参数,可以优化 TCP 连接的性能,特别是在高负载或不可靠的网络环境中。
send buffer
查看send的默认buffer大小:/proc/sys/net/ipv4/tcp_wmem
。net.ipv4.tcp_wmem
是一个内核参数,用于配置 Linux 系统中 TCP 发送缓冲区的大小。它包含三个值,分别表示最小、默认和最大缓冲区大小。调整这些值可以优化网络性能,特别是在高吞吐量或不可靠网络环境中。
yoyo@yoyo:/proc/sys/net/ipv4$ cat tcp_wmem
4096 16384 4194304
或者:
yoyo@yoyo:/proc/sys/net/ipv4$ sysctl net.ipv4.tcp_wmem
net.ipv4.tcp_wmem = 4096 16384 4194304
输出显示最小值为 4096 字节(4KB),默认值为 16384 字节(16KB),最大值为 4194304 字节(4MB)。
recv buffer
查看recv的默认buffer大小:/proc/sys/net/ipv4/tcp_rmem
。
yoyo@yoyo:/proc/sys/net/ipv4$ cat tcp_rmem
4096 131072 6291456
或者:
yoyo@yoyo:/proc/sys/net/ipv4$ sysctl net.ipv4.tcp_rmem
net.ipv4.tcp_rmem = 4096 131072 6291456
一次发送完整buffer
C++ Socket编程中send()函数不能保证一次性发送所有缓冲区的数据-CSDN博客
第五篇:socket通讯中采取循环方式发送数据的优点-CSDN博客
3. 网络字节序
主机字节序(大端/小端) 和 网络字节序 - 52php - 博客园
4. 同步阻塞
同步socket通讯时,程序会阻塞在诸如(connect
、accept
、recv
、recvfrom
)等操作上,直到有事件发生时才会继续。
5. I/O多路复用
Linux下的异步TCP socket及实例_linux tcp服务器的异步模式-CSDN博客
Linux 五种 IO 模型和三种多路复用技术大详解-51CTO.COM
深入了解select、poll、epoll之间的区别 — [野火]嵌入式Linux基础与应用开发实战指南——基于i.MX6ULL开发板 文档
select
Linux网络设计之网络IO与select_linux 读取网口开发 select(devicemax,&deviceread, &device-CSDN博客
epoll
Linux网络设计之网络IO与epoll_linux 文件描述符监听到的事件类型是什么-CSDN博客
reactor网络模型
Linux网络设计之reactor网络模型及其应用-CSDN博客
Linux网络设计之Reactor与http静态服务器-CSDN博客
Linux网络设计之Reactor模型和百万级并发-CSDN博客
三、常用函数
1. 常用头文件
在 TCP 服务器程序中:
- 创建套接字:
socket()
(<sys/socket.h>
)。 - 绑定地址:
bind()
(<sys/socket.h>
+struct sockaddr_in
来自<netinet/in.h>
)。 - 监听连接:
listen()
(<sys/socket.h>
)。 - 接受连接:
accept()
(<sys/socket.h>
)。 - 处理数据:
read()
/write()
(<unistd.h>
)或send()
/recv()
(<sys/socket.h>
)。 - 关闭套接字:
close()
(<unistd.h>
)。
<sys/socket.h>
-
作用:套接字编程的核心头文件,提供套接字操作函数和数据结构。
-
主要内容:
-
函数:
socket()
(创建套接字)、bind()
(绑定地址)、listen()
(监听连接)、accept()
(接受连接)、connect()
(发起连接)、send()
/recv()
(数据传输)等。int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); int listen(int sockfd, int backlog); int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
数据结构:
struct sockaddr
(通用套接字地址结构)。 -
常量:
SOCK_STREAM
(TCP)、SOCK_DGRAM
(UDP)、AF_INET
(IPv4地址族)等。
-
-
用途:实现套接字通信的核心功能,适用于 TCP/UDP 客户端和服务器。
<unistd.h>
作用:提供 POSIX 操作系统 API 的系统调用。
主要内容:
- 函数:
close()
(关闭文件描述符或套接字)、read()
/write()
(低级 I/O 操作)、fork()
(创建子进程)、getpid()
(获取进程ID)等。
用途:关闭套接字、进程控制及低级文件操作。
<netinet/in.h>
-
作用:定义 IPv4/IPv6 地址结构和网络字节序转换函数。
-
主要内容:
-
数据结构:
-
struct sockaddr_in
(IPv4 地址结构,包含端口和地址)。struct sockaddr_in { // IPv4地址结构 sa_family_t sin_family; // AF_INET in_port_t sin_port; // 16位端口号 struct in_addr sin_addr; // 32位IP地址 unsigned char sin_zero[8];// 填充字段 }; struct in_addr { uint32_t s_addr; // 网络字节序IP地址 };
-
struct sockaddr_in6
(IPv6 地址结构)。
-
-
字节序函数:
htonl()
、htons()
(主机到网络字节序)、ntohl()
、ntohs()
(网络到主机字节序)。// 字符串IP与二进制转换 int inet_pton(int af, const char *src, void *dst); const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); // 字节序转换 uint32_t htonl(uint32_t hostlong); // 主机到网络长整型 uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); // 网络到主机 uint16_t ntohs(uint16_t netshort);
-
常量:
INADDR_ANY
(绑定所有接口)、IPPROTO_TCP
(TCP 协议号)。
-
-
用途:处理 IP 地址和端口号,确保数据在不同系统间的兼容性。
<arpa/inet.h>
- 作用:提供 IP 地址转换函数。
- 主要内容:
- 函数:
inet_pton()
(将字符串 IP 转换为二进制,支持 IPv4/IPv6)。inet_ntop()
(将二进制 IP 转换为字符串)。- 旧函数:
inet_addr()
(IPv4 字符串转二进制)、inet_ntoa()
(二进制转字符串)。
- 函数:
- 用途:在可读格式(如
"192.168.1.1"
)和二进制格式(网络字节序)之间转换 IP 地址。
2. socket()
-创建套接字
函数原型:
// 创建套接字
/* Create a new socket of type TYPE in domain DOMAIN, using
protocol PROTOCOL. If PROTOCOL is zero, one is chosen automatically.
Returns a file descriptor for the new socket, or -1 for errors. */
int socket(int domain, int type, int protocol);
参数解释:
domain
,表示地址域。AF_INET
,IPv4地址。
type
,表示套接字类型。SOCK_STREAM
,流式套接字。SOCK_DGRAM
,数据报套接字。
protocol
,表示协议类型,默认为0。流式套接字默认为TCP协议IPPROTO_TCP
,数据报套接字默认 UDP 协议IPPROTO_UDP
。
3. bind()
-为套接字绑定地址信息(ip、port)
bind()
函数既可以绑定IPV4版本协议,也可以绑定IPV6版本。两个版本的IP头结点的大小不同,定义 sockaddr
为了寻求接口的统一,都要使用 struct sockaddr
。
struct sockaddr_in; // IPV4
struct sockaddr_in6; // IPV6
struct sockaddr_in address{};
address.sin_family = AF_INET;
address.sin_port = htons("8081");
address.sin_addr.s_addr = INADDR_ANY;
参数解释:
INADDR_ANY
(值为0.0.0.0
)允许服务器监听所有网络接口,无论客户端从哪个网卡连接都能接受。
函数原型:
/* Give the socket FD the local address ADDR (which is LEN bytes long). */
int bind(int sockfd, struct sockaddr *addr,socklen_t addrlen);
参数解释:
sockfd
,套接字描述符。addr
,要绑定的地址信息。addrlen
,地址信息的长度。
4. listen()
-监听(服务端)
函数原型:
/* Prepare to accept connections on socket FD.
N connection requests will be queued before further requests are refused.
Returns 0 on success, -1 for errors. */
int listen(int sockfd, int backlog);
参数解释:
sockfd
,sockfd描述符。backlog
,表示等待连接队列的最大长度。当连接请求过多时,超过这个值的连接会被拒绝。
5. connect()
-连接请求(客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数解释:
sockfd
,客户端的socket描述符。addr
,要连接的服务端地址。addrlen
,要连接的服务端的socket结构体长度,单位为字节。
6. accept()
-接受连接请求(服务端)
accept()
函数是一个阻塞型的函数,连接成功队列中如果没有新的连接到来,那么就会一直阻塞直到有新的客户端连接到来。
函数原型:
/* Await a connection on socket FD.
When a connection arrives, open a new socket to communicate with it,
set *ADDR (which is *ADDR_LEN bytes long) to the address of the connecting
peer and *ADDR_LEN to the address's actual length, and return the
new socket's descriptor, or -1 for errors.
*/
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数解释:
sockfd
,服务端新创建的socket描述符。addr
,新创建连接的客户端地址信息。addrlen
,客户端的socket结构体的长度,单位为字节。
当服务端成功接受连接请求之后,会为新连接重新创建一个socket用于专门和这个连接的通信,而这个重新创建的socket是由 accept()
这个函数完成的。 而原来socket函数返回的socket只是用于接受连接,并不用来进行通信。
7. recv()
接收/send()
发送数据
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数解释:
sockfd
,连接成功的socket描述符。buf
,指定接收数据的存放位置。len
,指定希望接收的数据长度。flags
,默认为0,表示阻塞式接收。
8. close()
-关闭套接字
int close(int sockfd);
9. 简单示例
网络编程-一个简单的客户端与服务器程序(0),(看这篇就够了)-CSDN博客
tcpServer
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include<unistd.h>
int main() {
// 创建socket套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket error");
return -1;
}
// 定义sockaddr结构体
struct sockaddr_in ser;
ser.sin_family = AF_INET;
ser.sin_port = htons(atoi("8081"));
ser.sin_addr.s_addr = inet_addr("192.168.33.1");
// 绑定地址
socklen_t len = sizeof(struct sockaddr_in);
if (bind(sockfd, (struct sockaddr*)&ser, len) < 0) {
perror("bind error");
close(sockfd);
return -1;
}
// 监听
if (listen(sockfd, 5) < 0) {
perror("listen error");
close(sockfd);
return -1;
}
// 等待连接
while(1) {
int n_sockfd;
struct sockaddr_in cli;
// 接受连接
n_sockfd = accept(sockfd, (struct sockaddr*)&cli, &len);
if (n_sockfd < 0) {
perror("accept error");
continue;
}
// 循环接收消息
while(1) {
char buff[1024] = {0};
// 接收消息
int ret = recv(n_sockfd, buff, 1023, 0);
if (ret < 0) {
perror("recv error");
continue;
}
printf("client %s[%d]say: %s \n", inet_ntoa(cli.sin_addr), ntohs(cli.sin_port), buff);
// 发送消息
memset(buff, 0x00, 1024);
scanf("%s", buff);
send(n_sockfd, buff, sizeof(buff), 0);
}
close(n_sockfd);
}
// 关闭连接
close(sockfd);
}
tcpClient
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include<unistd.h>
int main(){
// 创建socket套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket error");
return -1;
}
// 定义sockaddr
struct sockaddr_in ser;
ser.sin_family = AF_INET;
ser.sin_port = htons(atoi("8081"));
ser.sin_addr.s_addr = inet_addr("192.168.33.1");
// 连接请求
socklen_t len = sizeof(struct sockaddr_in);
if(connect(sockfd, (struct sockaddr*)&ser, len) < 0) {
perror("connet error");
return -1;
}
// 循环接收消息
while (1) {
// 发送消息
char buff[1024] = {0};
scanf("%s", buff);
send(sockfd, buff, sizeof(buff), 0);
// 接收消息
int ret = recv(sockfd, buff, 1023, 0);
if (ret < 0) {
perror("recv error");
return -1;
}
printf("server %s[%d]say: %s \n", inet_ntoa(ser.sin_addr), ntohs(ser.sin_port), buff);
}
// 关闭套接字
close(sockfd);
}
四、相关经验
优化TCP服务
一个简单的TCP服务器(C++)的设计 - 田大叔 - 博客园
协议设计建议:
- 心跳机制:每30秒发送0x06指令保持连接。
- 数据校验:关键数据传输使用CRC32校验。
- 完善的错误处理。
- 多线程/异步处理。
- 分块传输:大文件分块传输(建议1024-4096字节/块)。
- 重传机制:增加ACK确认包(0x07指令),超时未确认则重传。
- 版本协商:固件更新前先交换版本信息。
- 加密传输:敏感数据使用AES加密载荷。
- 协议版本管理。
协议扩展性设计:
- 保留0x80-0xFF为自定义扩展指令
- 载荷前16字节保留为扩展字段
- 使用TLV(Type-Length-Value)格式承载复杂参数
完整实现需要考虑:
- 字节序统一(建议网络字节序)
- 粘包处理(建议头部定长+长度字段)
- 超时重传机制
- 数据完整性验证
- 多线程处理(收发分离)
- 流量控制(滑动窗口机制)
需要根据具体硬件性能和网络环境调整:
- 视频流的分辨率/码率
- 分块传输的大小
- 心跳间隔时间
- 超时重试次数
建议先用Wireshark抓包验证协议格式,再逐步实现完整功能。
NetAssist网络调试工具
Windows上网络调试助手NetAssist的使用-CSDN博客
下载地址:https://free.cmsoft.cn/download/cmsoft/assistant/netassist5.0.13.zip
TCP+多线程
TCP服务器的演变过程:揭秘使用多线程实现一对多的TCP服务器-CSDN博客
04.基于C++实现多线程TCP服务器与客户端通信-CSDN博客
TCP+多进程
TCP服务器的演变过程:多进程实现一对多的TCP服务器-CSDN博客
GitHub - WuMingrui98/TCP-Server-Client: C++多进程实现高并发TCP服务器与客户端
TCP+select
TCP服务器的演变过程:IO多路复用机制select实现TCP服务器-CSDN博客
TCP+epoll
【TCP服务器的演变过程】使用IO多路复用器epoll实现TCP服务器-CSDN博客
TCP+epoll+reactor
TCP服务器的演变过程:使用epoll构建reactor网络模型实现百万级并发(详细代码)-CSDN博客
TCP+libevent
TCP服务器的演变过程:C++使用libevent库开发服务器程序-CSDN博客
C++中使用 Protocol Buffers(从零开始掌握跨语言数据传输的利器) - 知乎
TCP+协程(推荐)
GitHub - pxzwxx/Coroutine: C++多线程+协程构建异步并发TCP服务器
NtyCo的实现 · wangbojing/NtyCo Wiki · GitHub
以同步的编程方式,实现异步的性能。
TCP+json
Linux下通过jsoncpp和socket解析和发送json数据的使用例程 - 代码先锋网
基于C++、JsonCpp、Muduo库实现的分布式RPC通信框架-腾讯云开发者社区-腾讯云
TCP+二进制协议
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-| 指令类型(1B) | 状态码(1B) | 序列号(2B) | 数据长度(4B) | 载荷(N Bytes) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
字段说明:
- 指令类型:1字节(0x01~0xFF)
- 0x01 拍照指令
- 0x02 开始录像
- 0x03 停止录像
- 0x04 实时预览
- 0x05 固件更新
- 0x06 心跳包
- 0x07 传输确认
- 状态码:1字节(0x00=成功,其他为错误码)
- 0x00 成功
- 0x01 无效指令
- 0x02 设备忙
- 0x03 存储空间不足
- 0x04 校验失败
- 0x05 固件版本过低
- 序列号:2字节(用于请求-响应匹配)
- 数据长度:4字节(大端字节序)
- 载荷:变长数据
TCP客户端
使用epoll实现异步的tcp请求客户端 - 步孤天 - 博客园
心跳检测
void heartbeat_check() {
while (running) {
std::this_thread::sleep_for(10s);
if (sendto(sockfd, "PING", 4, 0,
(struct sockaddr*)&addr, sizeof(addr)) < 0) {
g_socket_valid = false;
}
}
}