TCP 套接字--服务器相关

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
    • 让系统自动选择与 domaintype 匹配的协议。
    • 对于 AF_INET + SOCK_STREAM,默认选择 TCP
  • 其他常见值:
    • IPPROTO_TCP(显式指定 TCP,但通常用 0 即可)。
    • IPPROTO_UDP(用于 SOCK_DGRAM)。

返回值

  • 成功 :返回一个 非负整数 ,即 套接字描述符server_sockfd)。
  • 失败 :返回 -1,并设置 errno(如 EMFILEENFILEEACCES 等)。

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;
}
相关推荐
一个向上的运维者1 分钟前
详谈OSI七层模型和TCP/IP四层模型以及tcp与udp为什么是4层,http与https为什么是7层
网络·网络协议
大新新大浩浩1 小时前
ubuntu22.04.4锁定内核应对海光服务器升级内核无法启动问题
运维·服务器
RainbowSea1 小时前
购买服务器 + 项目部署上线详细步骤说明
java·服务器·后端
RainbowSea2 小时前
用户中心项目部署上线03
linux·服务器·spring boot
sagima_sdu2 小时前
银河麒麟安装软件商店方法
linux·运维·服务器
Tipriest_3 小时前
ubuntu 多网络路由优先级问题
网络·ubuntu·路由·多网络
云飞云共享云桌面3 小时前
制造工厂高效出图新技术——共享云桌面
运维·服务器·网络·3d·自动化·制造
程序员JerrySUN8 小时前
Linux 内核基础统简全解:Kbuild、内存分配和地址映射
java·linux·运维·服务器·嵌入式硬件·缓存·文件系统
SunTecTec11 小时前
IDEA 类上方注释 签名
服务器·前端·intellij-idea
.YYY12 小时前
网络--VLAN技术
网络·计算机网络