1.创建 TCP 套接字
int server_sockfd = socket(AF_INET,SOCK_STREAM, 0);
函数原型:
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
|------------|----------|--------------------|
| domain
| 协议族(地址族) | AF_INET
(IPv4) |
| type
| 套接字类型 | SOCK_STREAM
(TCP) |
| protocol
| 协议类型 | 0
(自动选择) |
(1) domain
(协议族)
AF_INET
:IPv4 地址族(最常用)。AF_INET6
:IPv6 地址族。AF_UNIX
(或AF_LOCAL
):本地进程间通信(UNIX 域套接字)。
(2) type
(套接字类型)
SOCK_STREAM
:- 面向连接的 TCP 套接字。
- 提供可靠、双向、基于字节流的通信。
SOCK_DGRAM
:- 无连接的 UDP 套接字。
- 提供不可靠、无边界的数据报服务。
SOCK_RAW
:- 原始套接字,用于自定义协议(如 ICMP)。
(3) protocol
(协议类型)
0
:- 让系统自动选择与
domain
和type
匹配的协议。 - 对于
AF_INET + SOCK_STREAM
,默认选择 TCP。
- 让系统自动选择与
- 其他常见值:
IPPROTO_TCP
(显式指定 TCP,但通常用0
即可)。IPPROTO_UDP
(用于SOCK_DGRAM
)。
返回值
- 成功 :返回一个 非负整数 ,即 套接字描述符 (
server_sockfd
)。 - 失败 :返回
-1
,并设置errno
(如EMFILE
、ENFILE
、EACCES
等)。
2.设置套接字选项 SO_REUSEADDR
的标准用法,用于控制套接字的行为
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse))
- 作用 :
- 允许套接字(
sockfd
)绑定到 处于TIME_WAIT
状态的地址(例如服务器重启时)。 - 避免
bind()
失败(EADDRINUSE
错误)。
- 允许套接字(
- 参数说明 :
sockfd
:目标套接字描述符。SOL_SOCKET
:表示操作套接字层选项(通用选项)。SO_REUSEADDR
:选项名称,允许地址重用。&reuse
:指向选项值的指针(int
类型,1
启用,0
禁用)。sizeof(reuse)
:选项值的大小。
3.TCP 服务器绑定(bind()
)操作
cpp
struct sockaddr_in server_sockaddr;
server_sockaddr.sin_family = AF_INET; // IPv4 协议族
server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定到所有本地接口(0.0.0.0) server_sockaddr.sin_port = htons(voice_SysParameter.port); // 绑定到指定端口(网络字节序)
// 绑定套接字
if (bind(server_sockfd, (struct sockaddr *)&server_sockaddr, sizeof(server_sockaddr)) == -1)
{
perror("bind"); // 输出错误信息
goto voice_tcp_Thread_TaskProcError; // 错误处理
}
- 关键点 :
AF_INET
:使用 IPv4 协议。INADDR_ANY
:绑定到所有本地网络接口(即0.0.0.0
),允许通过任何本地 IP 访问服务。htons()
和htonl()
:将主机字节序(小端或大端)转换为网络字节序(大端)。bind()
返回值:成功返回0
,失败返回-1
并设置errno
。
4.TCP 连接超时检测与清理
cpp
for (int i = 0; i < voice_MaxConnectNum; i++) {
// 检查条件:
// 1. ConnfdCurTime[i] > 0(记录过心跳时间)
// 2. 当前时间 - 最后一次心跳时间 > 允许的心跳超时时间(voice_SysParameter.heartBeat)
// 3. Connfd[i] > 0(连接有效)
if ((ConnfdCurTime[i] > 0) &&
(ConnfdCurTime[i] + voice_SysParameter.heartBeat < HeartBeatCnt) &&
(Connfd[i] > 0))
{
struct sockaddr_in sa = {0};
int len = sizeof(sa);
// 获取客户端的 IP 地址和端口信息
getpeername(Connfd[i], (struct sockaddr *)&sa, &len);
// 关闭超时连接,并打印日志(连接描述符 + 客户端 IP)
close(Connfd[i]);
printf("Connfd Tcp__closed=%d %s\n", Connfd[i], inet_ntoa(sa.sin_addr));
// 重置连接状态
Connfd[i] = 0;
ConnfdCurTime[i] = 0;
}
}
5.检查所有已记录的子设备(SubDev)状态
cpp
for (int j = 0; j < voice_SysParameter.connectNo; j++) {
// 如果子设备的 IP 地址为空,跳过处理
if (strcmp(voice_SysParameter.SubDevState.SubDevState[j].ip, "") == 0)
continue;
int i = 0;
// 遍历所有 TCP 连接,检查是否有连接匹配当前子设备的 IP
for (i = 0; i < voice_MaxConnectNum; i++) {
if (Connfd[i] == 0)
continue; // 跳过无效连接(Connfd[i]=0 表示空闲)
struct sockaddr_in sa = {0};
int len = sizeof(sa);
// 获取当前连接的客户端 IP 地址
getpeername(Connfd[i], (struct sockaddr *)&sa, &len);
// 如果当前连接的 IP 匹配子设备的 IP,跳出循环
if (strcmp(voice_SysParameter.SubDevState.SubDevState[j].ip, inet_ntoa(sa.sin_addr)) == 0)
break;
}
// 如果遍历完所有连接都没找到匹配的 IP(i == voice_MaxConnectNum)
if (i == voice_MaxConnectNum) {
// 清除子设备状态(标记为未连接)
voice_SysParameter.SubDevState.SubDevState[j].slaveNo = (char)-1;
strcpy(voice_SysParameter.SubDevState.SubDevState[j].ip, "");
voice_SysParameter.SubDevState.SubDevState[j].status = 0;
}
}
6.使用 select()
监听多个文件描述符(file descriptors)的可读事件
cpp
// 清空文件描述符集合
FD_ZERO(&rset);
FD_SET(server_sockfd, &rset); // 将服务器套接字加入监听集合
maxfd = server_sockfd; // 初始化最大文件描述符为服务器套接字
// 遍历所有客户端连接,更新监听集合和最大文件描述符
for (int i = 0; i < voice_MaxConnectNum; i++) {
maxfd = maxfd > Connfd[i] ? maxfd : Connfd[i]; // 更新最大文件描述符
if (Connfd[i] > 0) // 只监听有效的连接
FD_SET(Connfd[i], &rset); // 加入监听集合
}
// 调用 select() 监听 I/O 事件
nready = select(maxfd + 1, &rset, NULL, NULL, &timeout);
// 超时处理
if (nready == 0) {
continue; // select 超时,继续循环
}
- 核心功能 :使用
select()
监听服务器套接字和客户端连接的可读事件,实现 I/O 多路复用。 - 关键点 :
FD_ZERO()
+FD_SET()
初始化监听集合。select(maxfd + 1, &rset, ...)
阻塞等待 I/O 事件。FD_ISSET()
检查哪个文件描述符就绪。
- 改进方向 :
- 改用
epoll()
/kqueue()
提高性能。 - 增加错误处理和连接管理。
- 动态调整超时时间。
- 改用
7.处理 select()
返回的就绪文件描述符
cpp
for (int i = 0; i < voice_MaxConnectNum; i++) {
if (FD_ISSET(Connfd[i], &rset)) { // 检查 Connfd[i] 是否就绪(可读)
ConnfdCurTime[i] = HeartBeatCnt; // 更新心跳时间戳
if (voice_ConTask(Connfd[i]) == -1) { // 处理客户端数据,返回 -1 表示错误
struct sockaddr_in sa = {0};
int len = sizeof(sa);
// 1. 获取客户端 IP 地址(用于日志或调试)
getpeername(Connfd[i], (struct sockaddr *)&sa, &len);
// 2. 关闭连接并清理资源
close(Connfd[i]);
printf("Connfd Tcp__closed=%d %s\n", Connfd[i], inet_ntoa(sa.sin_addr));
Connfd[i] = 0; // 标记连接为无效
ConnfdCurTime[i] = 0; // 清空心跳时间戳
}
}
}
8.处理单个客户端连接的数据接收、解析和响应
cpp
int voice_ConTask(int sockfd) {
char buffer[2048]; // 接收数据的缓冲区
memset(buffer, 0, sizeof(buffer)); // 清空缓冲区
// 1. 接收客户端数据
int len = recv(sockfd, buffer, sizeof(buffer), 0);
// 2. 检查是否收到 "exit" 命令(客户端主动关闭)
if (strcmp(buffer, "exit") == 0) {
printf("sockfd: %d exited.1\n", sockfd);
return -1; // 返回 -1 表示连接需要关闭
}
// 3. 检查连接是否已关闭(len=0 表示客户端断开)
else if (len == 0) {
printf("sockfd: %d exited.2\n", sockfd);
return -1; // 返回 -1 表示连接需要关闭
}
// 4. 处理有效数据
else {
// 调用 voice_TaskCommand 处理数据,并返回响应长度
len = voice_TaskCommand(buffer, len);
if (len > 0) {
// 发送响应数据给客户端
send(sockfd, buffer, len, 0);
}
}
return 1; // 返回 1 表示处理成功,继续保持连接
}
9.TCP 服务器 处理 新客户端连接 的逻辑
cpp
// 1. 检查服务器 socket 是否可读(即是否有新连接到达)
if (FD_ISSET(server_sockfd, &rset)) {
struct sockaddr_in client_addr;
socklen_t length = sizeof(client_addr);
// 2. 接受新连接
int conn = accept(server_sockfd, (struct sockaddr*)&client_addr, &length);
if (conn < 0) {
perror("New client connect Error");
continue; // 接受失败,跳过本次循环
} else {
printf("new client accepted.\n");
}
// 3. 将新连接存入连接池(Connfd 数组)
for (int i = 0; i < voice_MaxConnectNum; i++) {
if (Connfd[i] == 0) { // 找到空闲位置
Connfd[i] = conn; // 存储新连接的文件描述符
printf("Connfd[%d]=%d\n\r", i, conn);
printf("IP:%s Connected...\n\r", inet_ntoa(client_addr.sin_addr));
break; // 存入后跳出循环
}
// 4. 处理连接数超限
if (i == voice_MaxConnectNum - 1) { // 遍历完所有位置
printf("New Conn Num limit to %d \n\r", voice_MaxConnectNum);
close(conn); // 关闭新连接(因为无法存储)
}
}
}
完整代码
cpp
/**
* @brief TCP 服务器线程处理函数
* @param p 线程参数(结构体指针,包含线程启动标志)
* @return void* 线程返回值(未使用)
*/
void* voice_tcp_Thread_TaskProc(void* p) {
// 1. 解析线程参数
struct voice_Thread_PARA_S *pstPara = (struct voice_Thread_PARA_S*)p;
// 2. 创建服务器 socket(IPv4 + TCP)
int server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (server_sockfd < 0) {
perror("socket creation failed");
goto voice_tcp_Thread_TaskProcError;
}
// 3. 初始化连接池和心跳时间记录数组
int Connfd[voice_MaxConnectNum] = {0}; // 存储客户端连接的文件描述符
int ConnfdCurTime[voice_MaxConnectNum] = {0}; // 记录最后一次心跳时间(秒)
// 4. 设置 SO_REUSEADDR 选项(避免端口占用)
int reuse = 1;
if (setsockopt(server_sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0) {
perror("Setting SO_REUSEADDR failed");
goto voice_tcp_Thread_TaskProcError;
}
// 5. 绑定服务器地址和端口
struct sockaddr_in server_sockaddr;
memset(&server_sockaddr, 0, sizeof(server_sockaddr));
server_sockaddr.sin_family = AF_INET;
server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有网卡
server_sockaddr.sin_port = htons(voice_SysParameter.port); // 转换端口为网络字节序
if (bind(server_sockfd, (struct sockaddr*)&server_sockaddr, sizeof(server_sockaddr)) == -1) {
perror("bind failed");
goto voice_tcp_Thread_TaskProcError;
}
printf("bind success.\n");
// 6. 开始监听(待处理连接队列长度为 20)
if (listen(server_sockfd, 20) == -1) {
perror("listen failed");
goto voice_tcp_Thread_TaskProcError;
}
printf("listen success.\n");
printf("Tcp Server: %s:%d\n", voice_SysParameter.ip, voice_SysParameter.port);
// 7. 主循环(处理客户端连接和数据)
while (pstPara->bThreadStart == 1) {
// 7.1 获取当前时间(用于心跳检测)
struct timeval tv;
gettimeofday(&tv, NULL);
int current_time = tv.tv_sec;
// 7.2 心跳检测:关闭超时未活动的连接
for (int i = 0; i < voice_MaxConnectNum; i++) {
if (Connfd[i] > 0 && ConnfdCurTime[i] > 0 &&
(current_time - ConnfdCurTime[i] > voice_SysParameter.heartBeat)) {
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
getpeername(Connfd[i], (struct sockaddr*)&client_addr, &len); // 获取客户端 IP
printf("Heartbeat timeout, close connection: fd=%d, IP=%s\n",
Connfd[i], inet_ntoa(client_addr.sin_addr));
close(Connfd[i]); // 关闭连接
Connfd[i] = 0; // 清空连接池位置
ConnfdCurTime[i] = 0;
}
}
// 7.3 检查设备状态(如果客户端断开,更新设备状态)
for (int j = 0; j < voice_SysParameter.connectNo; j++) {
if (strcmp(voice_SysParameter.SubDevState.SubDevState[j].ip, "") == 0) continue;
int i;
for (i = 0; i < voice_MaxConnectNum; i++) {
if (Connfd[i] <= 0) continue; // 跳过无效连接
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
getpeername(Connfd[i], (struct sockaddr*)&client_addr, &len);
if (strcmp(voice_SysParameter.SubDevState.SubDevState[j].ip,
inet_ntoa(client_addr.sin_addr)) == 0) {
break; // 找到匹配的连接
}
}
// 如果未找到匹配连接,清空设备状态
if (i == voice_MaxConnectNum) {
voice_SysParameter.SubDevState.SubDevState[j].slaveNo = -1;
strcpy(voice_SysParameter.SubDevState.SubDevState[j].ip, "");
voice_SysParameter.SubDevState.SubDevState[j].status = 0;
}
}
// 7.4 使用 select 监听 I/O 事件
fd_set rset;
FD_ZERO(&rset);
FD_SET(server_sockfd, &rset); // 监听服务器 socket(接受新连接)
int maxfd = server_sockfd;
for (int i = 0; i < voice_MaxConnectNum; i++) {
if (Connfd[i] > 0) {
FD_SET(Connfd[i], &rset); // 监听所有客户端连接
maxfd = (Connfd[i] > maxfd) ? Connfd[i] : maxfd; // 更新最大文件描述符
}
}
// 设置 select 超时时间(1 秒)
struct timeval timeout;
timeout.tv_sec = 1;
timeout.tv_usec = 0;
int nready = select(maxfd + 1, &rset, NULL, NULL, &timeout);
if (nready <= 0) continue; // 超时或错误,继续循环
// 7.5 处理客户端数据
for (int i = 0; i < voice_MaxConnectNum; i++) {
if (Connfd[i] > 0 && FD_ISSET(Connfd[i], &rset)) {
ConnfdCurTime[i] = current_time; // 更新心跳时间
// 调用业务逻辑处理函数(如解析协议、处理请求)
if (voice_ConTask(Connfd[i]) == -1) {
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
getpeername(Connfd[i], (struct sockaddr*)&client_addr, &len);
printf("Client disconnected or error, close connection: fd=%d, IP=%s\n",
Connfd[i], inet_ntoa(client_addr.sin_addr));
close(Connfd[i]);
Connfd[i] = 0;
ConnfdCurTime[i] = 0;
}
}
}
// 7.6 处理新连接
if (FD_ISSET(server_sockfd, &rset)) {
struct sockaddr_in client_addr;
socklen_t length = sizeof(client_addr);
int conn = accept(server_sockfd, (struct sockaddr*)&client_addr, &length);
if (conn < 0) {
perror("accept failed");
continue;
}
printf("New client accepted: fd=%d, IP=%s\n",
conn, inet_ntoa(client_addr.sin_addr));
// 将新连接存入连接池
for (int i = 0; i < voice_MaxConnectNum; i++) {
if (Connfd[i] == 0) {
Connfd[i] = conn;
ConnfdCurTime[i] = current_time; // 初始化心跳时间
printf("Connfd[%d]=%d, IP=%s Connected\n",
i, conn, inet_ntoa(client_addr.sin_addr));
break;
}
// 连接数超限
if (i == voice_MaxConnectNum - 1) {
printf("Connection limit reached (%d), reject new client\n", voice_MaxConnectNum);
close(conn); // 关闭新连接
}
}
}
}
// 8. 清理资源(线程退出时)
for (int i = 0; i < voice_MaxConnectNum; i++) {
if (Connfd[i] > 0) {
close(Connfd[i]);
Connfd[i] = 0;
}
}
close(server_sockfd);
return NULL;
voice_tcp_Thread_TaskProcError:
if (server_sockfd >= 0) close(server_sockfd);
return NULL;
}