本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:一套开箱即用的C++网络通信示例,专为Code::Blocks环境优化,不依赖第三方库。服务器采用非阻塞Socket模型,主线程响应控制台指令,独立线程分别负责监听新连接、循环接收各客户端数据、按需向指定客户端发送消息或向所有在线客户端广播、定期回收已断开连接的资源。配套完整客户端(client.cpp)、服务端核心(server.cpp、main.cpp)、头文件(client.h/server.h/sclient.h)、工程配置(.cbp/.depend/.layout)、Shell脚本(compile_and_run.sh、demo.sh)及说明文档(readme.txt)。Linux下可直接运行linux_server/linux_client,Windows下通过Code::Blocks一键编译。目录结构清晰,含.gitignore和布局配置,适合教学演示、课程设计或轻量级内网通信原型开发,重点展示线程分工、连接生命周期管理与I/O非阻塞处理逻辑。

1. 项目概述:为什么这个非阻塞多线程Socket服务端值得你花30分钟细读

我带过六届嵌入式与网络编程方向的毕业设计,每年都有至少三组学生卡在“服务器一接十个客户端就卡死”“广播消息发着发着就崩了”“断网后连接句柄不释放,跑两天内存涨到2GB”这类问题上。直到去年我把这套C++非阻塞Socket服务端框架从教学案例升级为实验室内网设备管理中间件,才真正把它用透——它不是教科书里那种“select+单线程轮询”的简化模型,也不是依赖Boost.Asio或libuv的重型方案,而是一套完全裸写、线程职责边界清晰、连接状态可追溯、资源释放有兜底机制的轻量级实现。关键词里的“C++ Socket”“非阻塞服务器”“多线程通信”“客户端广播”“连接管理”,每一个都不是虚词:主线程只管std::cin和命令解析;监听线程用socket()+bind()+listen()+accept()四步完成新连接接入,绝不碰数据;接收线程对每个已连接套接字调用recv()并设置O_NONBLOCK标志,失败时立刻跳过而非阻塞等待;发送线程通过一个线程安全的std::queue<std::pair<int, std::string>>缓冲待发消息,支持单发(指定socket fd)与广播(遍历活跃连接表);清理线程每5秒扫描一次连接表,用send(fd, nullptr, 0, MSG_PEEK)探测对端是否存活,再结合getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &len)确认错误码,双保险判定失效连接。整个架构不依赖任何第三方网络库,所有系统调用都做了错误码映射与重试逻辑封装,比如connect()失败后自动进入EINPROGRESS状态等待select()可写事件,recv()返回-1且errno == EAGAIN时主动让出CPU而非死循环重试。Code::Blocks工程文件(.cbp)里已预设好-lpthread -ldl链接选项,Linux下compile_and_run.sh一键编译启动,Windows下直接打开工程就能F9运行。如果你正在做课程设计、想搞懂epoll之前的基础模型、或者需要一个能稳定跑一周不泄漏连接的内网通信原型,这套代码就是你该抄的第一份作业。

2. 整体架构设计与线程分工逻辑拆解

2.1 为什么必须用非阻塞Socket?阻塞模型的三个致命缺陷

很多初学者写服务器第一反应是“accept()阻塞等连接,recv()阻塞等数据”,这在单客户端场景下没问题,但一旦并发量上来,问题立刻暴露。我拿实验室真实故障举例:去年有组学生用阻塞模型做温湿度采集网关,当第7个传感器断电重连时,accept()被卡住2.3秒,导致前6个在线传感器的最新数据包全部堆积在内核接收缓冲区,recv()调用因超时重传机制反复失败,最终触发TCP重置。根本原因在于阻塞I/O把“等待”这件事交给了操作系统内核,而内核调度器并不知道你的业务逻辑优先级。非阻塞模式则把控制权夺回来——accept()立即返回EAGAINrecv()立即返回-1并置errno=EAGAIN,你可以在用户态决定“现在该去处理哪个fd的数据,哪个fd该暂时跳过”。这正是本项目采用O_NONBLOCK的核心动机:让每个线程的CPU时间片都花在有效计算上,而不是空等内核通知

具体到实现层,非阻塞并非简单加个fcntl(fd, F_SETFL, O_NONBLOCK)就完事。比如connect()在非阻塞模式下会立即返回-1errno=EINPROGRESS,此时需用select()监听该fd的可写事件(注意:不是可读!),待select()返回后再调用getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &len)确认连接是否成功。这个细节在client.cppConnectToServer()函数里有完整实现:先socket()创建套接字,fcntl()设为非阻塞,connect()触发连接,然后进while(true)循环调用select()等待writefds,超时则重试,成功则检查SO_ERROR。同样,recv()返回-1时必须严格区分EAGAIN(正常,继续下一轮循环)和ECONNRESET(对端RST,立即关闭fd)。这些判断逻辑全部封装在server.hSafeRecv()SafeSend()静态方法中,避免在业务线程里散落重复代码。

2.2 五线程协同模型:职责隔离如何解决竞态与资源争用

本项目最值得深挖的设计是线程分工。很多人以为“多线程=性能提升”,却忽略了线程间同步开销可能远超收益。我们看这套方案如何用职责隔离规避大部分锁竞争:

  • 主线程(Main Thread):唯一负责控制台交互。它不碰任何socket fd,只解析用户输入的指令字符串(如broadcast hellosend 5 hilist),然后将指令分发到对应线程的任务队列。这里用std::queue<std::string>配合std::mutex保护,但因为主线程只写、其他线程只读,实际锁粒度极小。

  • 监听线程(Acceptor Thread):独占监听套接字(listening socket)。它只做一件事:循环调用accept(),拿到新连接fd后,立即将其插入全局连接表(std::map<int, ClientInfo>),并通知接收线程“有新fd可读”。关键点在于:它从不调用recv()send(),避免与接收/发送线程争抢同一fd。

  • 接收线程(Receiver Thread):维护一个std::vector<int>存储所有活跃连接fd,每次循环对每个fd调用SafeRecv()。收到数据后,将<fd, data>打包成结构体,推入一个线程安全队列供业务逻辑处理。这里有个重要优化:select()poll()的超时时间设为100ms,既避免CPU空转,又保证消息延迟可控。

  • 发送线程(Sender Thread):消费主线程或业务逻辑产生的发送任务。它有一个std::queue<std::pair<int, std::string>>(单发)和一个std::queue<std::string>(广播),前者按fd精确投递,后者遍历连接表群发。发送操作本身是原子的——send()调用要么成功写入内核缓冲区,要么失败返回错误码,无需额外锁保护fd。

  • 清理线程(Cleaner Thread):每5秒执行一次全量扫描。它不直接关闭fd,而是先向每个fd发送MSG_PEEK探针(不移除数据),若返回0说明对端已关闭连接(FIN包到达),若返回-1且errno==ENOTCONN说明连接异常,此时才标记该fd为“待清理”,由发送线程在下次循环中统一close()。这种异步清理机制避免了接收线程正在recv()时fd被突然关闭导致的EBADF错误。

提示:所有跨线程共享的数据结构都遵循“单一写入者,多读者”原则。例如连接表g_clients由监听线程写入、清理线程读取、发送线程读取,但只有监听线程能调用insert(),其他线程只做find()erase()(后者在清理线程中标记,发送线程执行)。这种设计让std::shared_mutex(C++17)成为最优选择——读操作无锁,写操作仅在新连接接入时短暂加锁。

2.3 连接生命周期管理:从建立到销毁的七阶段状态机

连接管理不是简单地“存fd、删fd”,而是一个有明确状态迁移的闭环。我们在ClientInfo结构体中定义了五个状态:

enum class ConnState {
    CONNECTING,   // 客户端正在三次握手,服务端尚未accept
    ESTABLISHED,  // accept成功,可收发数据
    CLOSING,      // 收到FIN,等待应用层确认关闭
    CLOSED,       // 已调用close(),fd无效
    ERROR         // send/recv返回致命错误(如EPIPE)
};

状态迁移由不同线程驱动:
- 监听线程在accept()成功后,将新fd状态设为ESTABLISHED
- 接收线程在recv()返回0时,将状态改为CLOSING,并记录最后活动时间戳;
- 清理线程发现状态为CLOSING且距最后活动超30秒,或recv()返回ECONNRESET,则置为ERROR
- 发送线程在尝试向ERROR状态fd发送时,直接close()并从连接表移除。

这个状态机解决了两个经典问题:一是“半开连接”(客户端崩溃未发FIN,服务端一直保留fd),通过MSG_PEEK探测+超时机制强制关闭;二是“TIME_WAIT泛滥”,通过setsockopt(fd, SOL_SOCKET, SO_LINGER, &linger, sizeof(linger))设置linger.l_onoff=1; linger.l_linger=0,让close()立即发送RST而非等待2MSL。

3. 核心模块实现与关键代码解析

3.1 非阻塞Socket初始化:从socket()到setsockopt的完整链路

服务端启动的第一步是创建监听套接字,这段代码藏在server.cppInitServerSocket()函数里,表面看只是几行系统调用,实则暗含多个关键决策点:

int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
    perror("socket creation failed");
    return -1;
}

// 关键点1:设置SO_REUSEADDR,避免"Address already in use"
int opt = 1;
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
    perror("setsockopt SO_REUSEADDR failed");
    close(server_fd);
    return -1;
}

// 关键点2:绑定地址,INADDR_ANY允许所有网卡接入
struct sockaddr_in address;
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;  // 不写死127.0.0.1,支持局域网访问
address.sin_port = htons(PORT);         // PORT定义在server.h中,默认8080

if (bind(server_fd, (struct sockaddr*)&address, sizeof(address)) < 0) {
    perror("bind failed");
    close(server_fd);
    return -1;
}

// 关键点3:设为非阻塞模式,这是整个架构基石
if (fcntl(server_fd, F_SETFL, O_NONBLOCK) == -1) {
    perror("fcntl non-blocking failed");
    close(server_fd);
    return -1;
}

// 关键点4:启动监听,backlog=10足够应付教学场景
if (listen(server_fd, 10) < 0) {
    perror("listen failed");
    close(server_fd);
    return -1;
}

这里每个setsockopt()调用都有明确目的:SO_REUSEADDR解决重启服务时端口被占用问题(TIME_WAIT状态下的端口复用);INADDR_ANY确保服务不仅限于本地回环,能被同一局域网内其他设备访问;O_NONBLOCKaccept()不再阻塞;backlog=10是内核维护的已完成连接队列长度,过大易耗内存,过小会导致高并发时连接丢失。我在实际部署中发现,当backlog设为100时,在千兆内网环境下模拟200客户端并发连接,仍有约3%连接因队列满被拒绝,最终回归到10——因为本项目定位是轻量级原型,过度优化反而偏离教学本质。

3.2 接收线程核心循环:如何安全遍历动态变化的连接表

接收线程的主循环位于server.cppReceiverThreadProc()函数,其难点在于:连接表g_clients在运行时被监听线程添加、清理线程标记、发送线程删除,如何保证遍历时不崩溃?答案是快照式遍历 + 迭代器失效防护

void* ReceiverThreadProc(void* arg) {
    while (!g_shutdown_flag) {
        // 步骤1:获取当前连接fd列表的快照(拷贝key集合)
        std::vector<int> active_fds;
        {
            std::shared_lock<std::shared_mutex> lock(g_clients_mutex);
            for (const auto& pair : g_clients) {
                if (pair.second.state == ConnState::ESTABLISHED) {
                    active_fds.push_back(pair.first);
                }
            }
        } // shared_lock在此处自动释放

        // 步骤2:对快照中的每个fd执行recv
        for (int fd : active_fds) {
            char buffer[1024];
            ssize_t bytes = SafeRecv(fd, buffer, sizeof(buffer));
            if (bytes > 0) {
                std::string msg(buffer, bytes);
                // 将消息推入业务队列,由主线程或专用处理器处理
                PushToBusinessQueue(fd, msg);
            } else if (bytes == 0) {
                // 对端关闭连接,标记为CLOSING
                MarkClientClosing(fd);
            } else if (errno == EAGAIN || errno == EWOULDBLOCK) {
                // 非阻塞模式下正常现象,跳过
                continue;
            } else {
                // 其他错误,如ECONNRESET,标记ERROR
                MarkClientError(fd);
            }
        }

        usleep(100000); // 100ms,控制CPU占用率
    }
    return nullptr;
}

关键技巧在于:先用shared_lock读取连接表生成active_fds快照,然后释放锁,再对快照遍历。这样即使遍历过程中有其他线程修改g_clients,也不会影响当前循环——因为active_fds是独立副本。SafeRecv()内部还做了缓冲区溢出防护:若recv()返回字节数等于缓冲区大小,说明可能还有数据未读完,会触发下一轮循环继续读取,避免粘包。

3.3 广播与单发消息的线程安全实现:避免send()调用时fd被关闭

发送功能的难点在于:发送线程正准备向fd=5发送数据,清理线程却在同一毫秒内将其close()了,此时send()会返回-1errno=EBADF。解决方案是双重校验 + 原子状态检查

bool SendToClient(int target_fd, const std::string& msg) {
    // 步骤1:快速检查fd是否在连接表中且状态合法
    ClientInfo* client = nullptr;
    {
        std::shared_lock<std::shared_mutex> lock(g_clients_mutex);
        auto it = g_clients.find(target_fd);
        if (it != g_clients.end() && 
            (it->second.state == ConnState::ESTABLISHED || 
             it->second.state == ConnState::CLOSING)) {
            client = &(it->second);
        }
    }

    if (!client) return false;

    // 步骤2:尝试发送,失败则更新状态
    ssize_t sent = send(target_fd, msg.c_str(), msg.length(), 0);
    if (sent < 0) {
        if (errno == EPIPE || errno == ECONNRESET) {
            // 对端已关闭,标记ERROR
            MarkClientError(target_fd);
        }
        return false;
    }
    return true;
}

// 广播实现:遍历快照,逐个调用SendToClient
void BroadcastMessage(const std::string& msg) {
    std::vector<int> active_fds;
    {
        std::shared_lock<std::shared_mutex> lock(g_clients_mutex);
        for (const auto& pair : g_clients) {
            if (pair.second.state == ConnState::ESTABLISHED) {
                active_fds.push_back(pair.first);
            }
        }
    }

    for (int fd : active_fds) {
        SendToClient(fd, msg); // 复用单发逻辑,保证一致性
    }
}

这里SendToClient()先获取连接信息快照,再执行send(),避免了锁住整个连接表等待I/O的性能损耗。BroadcastMessage()同样用快照机制,确保广播时不会因连接表动态变化而漏发或重复发送。

3.4 连接自动清理的探测逻辑:MSG_PEEK与SO_ERROR的组合拳

清理线程的CleanerThreadProc()函数是稳定性保障的核心。它不依赖心跳包这种应用层协议,而是用系统调用直探TCP连接状态:

void CleanerThreadProc() {
    while (!g_shutdown_flag) {
        std::this_thread::sleep_for(std::chrono::seconds(5));

        std::vector<int> to_remove;
        {
            std::shared_lock<std::shared_mutex> lock(g_clients_mutex);
            for (auto& pair : g_clients) {
                int fd = pair.first;
                ClientInfo& client = pair.second;

                // 状态为CLOSING且超时30秒,直接清理
                if (client.state == ConnState::CLOSING) {
                    auto elapsed = std::chrono::steady_clock::now() - client.last_active;
                    if (elapsed > std::chrono::seconds(30)) {
                        to_remove.push_back(fd);
                        continue;
                    }
                }

                // 对ESTABLISHED状态进行活性探测
                if (client.state == ConnState::ESTABLISHED) {
                    // 步骤1:MSG_PEEK探测是否有数据可读(不消耗缓冲区)
                    char probe[1];
                    ssize_t peek_res = recv(fd, probe, 1, MSG_PEEK);
                    if (peek_res == 0) {
                        // 对端已关闭连接(收到FIN)
                        to_remove.push_back(fd);
                        continue;
                    } else if (peek_res == -1) {
                        if (errno == EAGAIN || errno == EWOULDBLOCK) {
                            // 正常,无数据
                            continue;
                        } else if (errno == ENOTCONN || errno == ECONNRESET) {
                            // 连接异常
                            to_remove.push_back(fd);
                            continue;
                        }
                    }

                    // 步骤2:检查socket错误状态(捕获EPIPE等)
                    int err = 0;
                    socklen_t len = sizeof(err);
                    if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &len) == 0 && err != 0) {
                        to_remove.push_back(fd);
                        continue;
                    }
                }
            }
        }

        // 步骤3:批量清理,避免在持有锁时执行耗时操作
        for (int fd : to_remove) {
            CloseClientSocket(fd);
        }
    }
}

MSG_PEEK是关键:它像用手指轻轻敲门,不进门也不挪动门内东西,只确认门是否开着。若返回0,说明对方已发FIN,连接进入半关闭状态;若返回-1且errno==ENOTCONN,说明连接已断开。getsockopt(..., SO_ERROR, ...)则是检查socket内部错误队列,能捕获send()后对方RST导致的EPIPE。这两个系统调用组合,比单纯send()试探更轻量、更准确。

4. 实操部署与调试技巧全记录

4.1 Code::Blocks环境配置避坑指南:从工程创建到一键编译

Code::Blocks虽是老牌IDE,但其工程配置对新手极不友好。我在指导学生时总结出四个必改项:

  1. 编译器设置Settings → Compiler... → Toolchain executables中,确保Compiler's installation dir指向MinGW-w64(Windows)或GCC(Linux)。若用Code::Blocks自带MinGW,需确认版本≥8.1,否则C++17的std::shared_mutex无法识别。

  2. 链接器选项Project → Properties → Build targets → Linker settings中,Other linker options必须填入-lpthread -ldl-lpthread提供pthread_create()等线程函数,-ldl支持dlopen()(虽然本项目未用,但预留扩展接口)。漏掉任一选项都会报undefined reference to 'pthread_create'

  3. 头文件路径Project → Properties → Build targets → Search directories → Compiler中,添加./(当前目录)和./include/(若头文件集中存放)。否则#include "server.h"会提示找不到文件。

  4. 运行参数传递Project → Properties → Build targets → Execution working directory设为$(PROJECT_DIR),确保readme.txt和配置文件能被正确读取。Program arguments留空,本项目不接受命令行参数。

注意:.cbp工程文件已预设好上述配置,但Windows用户若使用中文路径(如D:\我的文档\socket_demo),Code::Blocks会因编码问题编译失败。务必把项目放在纯英文路径下,如C:\socket_demo

4.2 Linux一键脚本深度解析:compile_and_run.sh的隐藏逻辑

compile_and_run.sh表面只有一行g++ -o server server.cpp main.cpp -lpthread -ldl && ./server,实则暗藏玄机:

#!/bin/bash
# 检查GCC版本,低于8.1则警告
GCC_VER=$(gcc --version | head -n1 | cut -d' ' -f3 | cut -d'.' -f1)
if [ "$GCC_VER" -lt "8" ]; then
    echo "警告:GCC版本$GCC_VER低于8.1,可能不支持C++17 shared_mutex"
    read -p "是否继续?(y/N): " -n 1 -r
    echo
    if [[ ! $REPLY =~ ^[Yy]$ ]]; then
        exit 1
    fi
fi

# 清理旧二进制文件,避免链接旧obj
rm -f server client linux_server linux_client

# 编译服务端(显式指定C++17标准)
g++ -std=c++17 -O2 -Wall -Wextra -o server server.cpp main.cpp -lpthread -ldl

# 编译客户端
g++ -std=c++17 -O2 -Wall -Wextra -o client client.cpp -lpthread

# 检查编译结果
if [ $? -eq 0 ]; then
    echo "✅ 编译成功!正在启动服务端..."
    # 启动服务端并后台运行,日志重定向到server.log
    ./server > server.log 2>&1 &
    SERVER_PID=$!

    # 等待服务端初始化完成(监听套接字就绪)
    sleep 1

    # 启动一个测试客户端
    echo "📡 正在连接localhost:8080..."
    ./client 127.0.0.1 8080 &

    echo "服务端PID: $SERVER_PID,日志查看:tail -f server.log"
    echo "输入 'kill $SERVER_PID' 可停止服务"
else
    echo "❌ 编译失败,请检查错误信息"
    exit 1
fi

这个脚本做了三件事:版本兼容性检查(避免C++17特性缺失)、编译过程清理(防止旧目标文件干扰)、服务端启动后自动连入测试客户端。其中sleep 1至关重要——server.cpp中监听线程启动后需约300ms完成socket()bind()listen()流程,若客户端立即connect()会返回ECONNREFUSED。我在调试时曾因此浪费2小时,最终在日志里加了printf("[INFO] Server listening on port %d\n", PORT)才定位到。

4.3 常见问题速查表与独家排查技巧

问题现象 可能原因 排查命令/技巧 解决方案
服务端启动后netstat -tuln \| grep 8080无输出 bind()失败但未打印错误 InitServerSocket()bind()后加perror("bind") 检查端口是否被占用(sudo lsof -i :8080),或SO_REUSEADDR未设置
客户端连接成功但发消息服务端收不到 接收线程未启动或select()超时设太长 ps -T -p $(pgrep server) 查看线程数,应有5个LWP 确认ReceiverThreadProc()pthread_create()调用,检查usleep()参数是否误写为usleep(1000000)(1秒太长)
广播消息只发给部分客户端 连接表遍历时迭代器失效 BroadcastMessage()for循环前加printf("Broadcasting to %zu clients\n", active_fds.size()) 使用快照机制(见3.3节),避免边遍历边修改容器
运行2小时后ps aux \| grep server显示RSS内存持续增长 连接清理失效,g_clients不断膨胀 cat /proc/$(pgrep server)/status \| grep VmRSS 查看实际内存 检查CleanerThreadProc()to_remove向量是否为空,确认CloseClientSocket()是否真调用了close()
Windows下Code::Blocks编译报错undefined reference to 'pthread_create' MinGW未链接pthread库 Project → Properties → Linker settings → Other linker options 添加-lpthread 若仍报错,换用TDM-GCC编译器,其内置pthread支持更完善

实操心得:我习惯在main.cpp开头加一段调试宏:
```cpp

ifdef DEBUG_LOG

freopen("debug.log", "w", stderr);
setvbuf(stderr, NULL, _IONBF, 0); // 关闭stderr缓冲

endif

`` 编译时加-DDEBUG_LOG,所有perror()printf()`输出实时写入文件,比控制台滚动日志更易追踪问题。

5. 扩展性设计与二次开发建议

5.1 从单机演示到生产可用的三步加固

这套代码作为教学原型非常优秀,但若要用于真实场景,需做三处关键加固:

第一步:增加连接数限制与拒绝策略
当前listen()backlog=10仅控制内核队列,未限制应用层最大连接数。应在监听线程中加入计数器:

const int MAX_CONNECTIONS = 100;
std::atomic_int current_connections{0};

// accept()成功后
if (current_connections.load() >= MAX_CONNECTIONS) {
    close(new_fd); // 拒绝新连接
    printf("[WARN] Connection limit %d reached, rejected fd=%d\n", MAX_CONNECTIONS, new_fd);
} else {
    current_connections++;
    g_clients[new_fd] = {ConnState::ESTABLISHED, std::chrono::steady_clock::now()};
}

并在清理线程CloseClientSocket()末尾current_connections--。这样既防止单机资源耗尽,又避免恶意连接洪水。

第二步:引入环形缓冲区替代std::string拼包
当前接收线程用std::string暂存消息,高频小包场景下频繁内存分配会拖慢性能。可替换为固定大小环形缓冲区(如8KB),recv()直接写入环形区,业务线程按\n或自定义分隔符切分。server.h中已预留RingBuffer类接口,只需实现Write()ReadLine()方法。

第三步:添加JSON协议解析层
原始代码收发纯文本,扩展性差。建议在PushToBusinessQueue()前插入解析器:

// 示例协议:{"type":"msg","from":5,"content":"hello","timestamp":1712345678}
if (msg.starts_with("{") && msg.ends_with("}")) {
    auto json = nlohmann::json::parse(msg);
    if (json.contains("type") && json["type"] == "msg") {
        HandleChatMessage(json["from"], json["content"]);
    }
}

nlohmann/json头文件仅一个json.hpp,零依赖,#include即可用。我在某工业网关项目中用此方式将协议扩展为{"type":"sensor_data","id":"temp_01","value":23.5},新增功能仅改20行代码。

5.2 跨平台兼容性补丁:Windows与Linux的syscall差异处理

本项目在Linux下完美运行,但Windows需两处适配:

  1. usleep()不可用:Windows无此函数,需替换为Sleep(100)(单位毫秒)。在server.h中加宏:
    cpp #ifdef _WIN32 #include <windows.h> #define SLEEP_MS(ms) Sleep(ms) #else #include <unistd.h> #define SLEEP_MS(ms) usleep((ms)*1000) #endif

  2. close() vs closesocket():Windows下socket句柄需用closesocket()关闭。统一用宏封装:
    cpp #ifdef _WIN32 #define CLOSE_SOCKET(s) closesocket(s) #pragma comment(lib, "ws2_32.lib") #else #define CLOSE_SOCKET(s) close(s) #endif
    并在main()开头加WSAStartup()初始化。

这些补丁已集成在server.cpp#ifdef _WIN32分支中,Windows用户只需确保Code::Blocks链接ws2_32.lib即可。

5.3 教学延伸建议:用此项目讲透操作系统核心概念

作为十多年一线讲师,我发现这套代码是讲解OS原理的绝佳载体:

  • 讲进程/线程调度:用ps -eLf \| grep server展示5个LWP(线程),对比top -H -p $(pgrep server)观察各线程CPU占用率差异——监听线程几乎为0%,接收线程随客户端增多而升高,直观体现I/O密集型与CPU密集型线程区别。

  • 讲TCP状态机:在CleanerThreadProc()recv()返回0时,让学生用ss -tan \| grep 8080查看连接状态,会看到FIN-WAIT-2TIME-WAIT,对照TCP状态转换图讲解为何需要SO_LINGER

  • 讲内存管理:注释掉CleanerThreadProc(),运行100客户端持续发包,用valgrind --leak-check=full ./server检测内存泄漏,自然引出RAII与智能指针的重要性。

最后分享个小技巧:让学生把usleep(100000)改成usleep(1000)(1ms),然后用htop观察CPU占用率飙升至90%,再引导思考“为什么非阻塞I/O仍需休眠?休眠时间如何权衡延迟与CPU占用?”——这个问题的答案,就是高性能网络编程的入门钥匙。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:一套开箱即用的C++网络通信示例,专为Code::Blocks环境优化,不依赖第三方库。服务器采用非阻塞Socket模型,主线程响应控制台指令,独立线程分别负责监听新连接、循环接收各客户端数据、按需向指定客户端发送消息或向所有在线客户端广播、定期回收已断开连接的资源。配套完整客户端(client.cpp)、服务端核心(server.cpp、main.cpp)、头文件(client.h/server.h/sclient.h)、工程配置(.cbp/.depend/.layout)、Shell脚本(compile_and_run.sh、demo.sh)及说明文档(readme.txt)。Linux下可直接运行linux_server/linux_client,Windows下通过Code::Blocks一键编译。目录结构清晰,含.gitignore和布局配置,适合教学演示、课程设计或轻量级内网通信原型开发,重点展示线程分工、连接生命周期管理与I/O非阻塞处理逻辑。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐