C++ 实战小项目:从零实现简易网络通信工具

内容分享2周前发布
0 0 0

一、项目概述

本项目将基于 C++ 标准库及跨平台网络编程接口(Winsock/BSD Socket),实现一个支持 TCP 协议的简易网络通信工具,包含服务端客户端两部分。服务端支持多客户端连接(基于多线程),可接收客户端消息并广播给所有在线客户端;客户端可连接服务端,实现消息的发送与接收。

通过该项目,你将掌握:

跨平台网络编程的核心概念(Socket、TCP 连接、端口/IP 等);C++ 多线程编程(处理多客户端并发);网络数据的收发与简单解析;跨平台编译适配(Windows/Linux)。

技术栈

编程语言:C++11 及以上网络接口:Winsock2(Windows)/BSD Socket(Linux/macOS)多线程:std::thread(C++11 标准)编译工具:g++(Linux)/MSVC(Windows)

二、核心原理

1. TCP 网络通信模型

TCP 是面向连接、可靠的字节流协议,通信流程遵循“三次握手建立连接→数据传输→四次挥手断开连接”:

服务端:创建 Socket → 绑定端口(bind)→ 监听连接(listen)→ 接受连接(accept)→ 收发数据(recv/send);客户端:创建 Socket → 连接服务端(connect)→ 收发数据(recv/send)。

2. 多线程处理多客户端

单线程服务端只能处理一个客户端,因此本项目通过主线程监听连接,每接收到一个新客户端连接,就创建一个子线程专门处理该客户端的消息收发,实现并发通信。

3. 跨平台适配

Windows 下使用 Winsock2 库,需初始化 WSA 环境;Linux/macOS 下使用 BSD Socket,无需初始化,直接调用接口。通过条件编译(
#ifdef _WIN32
)实现代码跨平台兼容。

三、项目实现步骤

步骤 1:环境准备

Windows 环境

编译器:Visual Studio 2019/2022 或 MinGW;依赖:Winsock2 库(编译时需链接
ws2_32.lib
)。

Linux/macOS 环境

编译器:g++(需支持 C++11,命令加
-std=c++11
);依赖:无(系统自带 BSD Socket)。

步骤 2:封装跨平台 Socket 工具类

首先封装一个
SocketUtil
类,统一跨平台的 Socket 操作,避免重复代码。


#include <iostream>
#include <string>
#include <cstring>
#include <vector>
#include <thread>
#include <mutex>
#include <atomic>

// 跨平台适配
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib") // 链接Winsock库
#define SOCKET_ERROR_CODE SOCKET_ERROR
#define INVALID_SOCKET_FD INVALID_SOCKET
typedef SOCKET SocketFD;
#else
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#define SOCKET_ERROR_CODE -1
#define INVALID_SOCKET_FD -1
typedef int SocketFD;
#endif

// 全局互斥锁,保护客户端列表操作
std::mutex client_mutex;
// 在线客户端列表
std::vector<SocketFD> client_list;
// 服务端运行状态
std::atomic<bool> server_running(false);

// Socket工具类
class SocketUtil {
public:
    // 初始化网络环境(仅Windows需要)
    static bool init() {
#ifdef _WIN32
        WSADATA wsaData;
        int ret = WSAStartup(MAKEWORD(2, 2), &wsaData);
        if (ret != 0) {
            std::cerr << "WSA初始化失败:" << ret << std::endl;
            return false;
        }
#endif
        return true;
    }

    // 清理网络环境(仅Windows需要)
    static void cleanup() {
#ifdef _WIN32
        WSACleanup();
#endif
    }

    // 创建TCP Socket
    static SocketFD create_tcp_socket() {
        SocketFD fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        if (fd == INVALID_SOCKET_FD) {
            std::cerr << "创建Socket失败:" << get_error() << std::endl;
        }
        return fd;
    }

    // 获取错误码
    static int get_error() {
#ifdef _WIN32
        return WSAGetLastError();
#else
        return errno;
#endif
    }

    // 关闭Socket
    static void close_socket(SocketFD fd) {
        if (fd == INVALID_SOCKET_FD) return;
#ifdef _WIN32
        closesocket(fd);
#else
        close(fd);
#endif
    }

    // 设置Socket地址(IPv4)
    static void set_sockaddr(sockaddr_in& addr, const std::string& ip, uint16_t port) {
        memset(&addr, 0, sizeof(addr));
        addr.sin_family = AF_INET;
        addr.sin_port = htons(port); // 端口转网络字节序
        if (ip.empty()) {
            addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定所有网卡
        } else {
            inet_pton(AF_INET, ip.c_str(), &addr.sin_addr); // IP转网络字节序
        }
    }
};

步骤 3:实现服务端核心逻辑

服务端包含以下核心功能:

启动服务,绑定端口并监听;循环接受客户端连接,为每个客户端创建处理线程;接收客户端消息,广播给所有在线客户端;处理客户端断开连接,清理资源。


// 服务端类
class TCPServer {
private:
    SocketFD server_fd;
    uint16_t port;

    // 处理单个客户端的消息(子线程函数)
    void handle_client(SocketFD client_fd) {
        char buffer[1024] = {0};
        std::cout << "客户端[" << client_fd << "] 已连接" << std::endl;

        // 将客户端加入列表
        {
            std::lock_guard<std::mutex> lock(client_mutex);
            client_list.push_back(client_fd);
        }

        // 循环接收客户端消息
        while (server_running) {
            int ret = recv(client_fd, buffer, sizeof(buffer) - 1, 0);
            if (ret <= 0) {
                // 客户端断开或出错
                std::cerr << "客户端[" << client_fd << "] 断开连接:" << SocketUtil::get_error() << std::endl;
                break;
            }

            // 打印接收到的消息
            std::string msg(buffer, ret);
            std::cout << "收到客户端[" << client_fd << "]消息:" << msg << std::endl;

            // 广播消息给所有客户端
            broadcast_message(client_fd, msg);

            // 清空缓冲区
            memset(buffer, 0, sizeof(buffer));
        }

        // 清理客户端资源
        {
            std::lock_guard<std::mutex> lock(client_mutex);
            // 从列表中移除客户端
            for (auto it = client_list.begin(); it != client_list.end(); ++it) {
                if (*it == client_fd) {
                    client_list.erase(it);
                    break;
                }
            }
        }
        SocketUtil::close_socket(client_fd);
        std::cout << "客户端[" << client_fd << "] 资源已清理" << std::endl;
    }

    // 广播消息给所有客户端(排除发送者)
    void broadcast_message(SocketFD sender_fd, const std::string& msg) {
        std::lock_guard<std::mutex> lock(client_mutex);
        for (SocketFD fd : client_list) {
            if (fd == sender_fd) continue; // 不回发给自己
            send(fd, msg.c_str(), msg.size(), 0);
        }
    }

public:
    TCPServer(uint16_t port) : port(port), server_fd(INVALID_SOCKET_FD) {}

    // 启动服务端
    bool start() {
        // 创建服务端Socket
        server_fd = SocketUtil::create_tcp_socket();
        if (server_fd == INVALID_SOCKET_FD) return false;

        // 设置服务端地址
        sockaddr_in server_addr;
        SocketUtil::set_sockaddr(server_addr, "", port);

        // 绑定端口
        if (bind(server_fd, (sockaddr*)&server_addr, sizeof(server_addr)) == SOCKET_ERROR_CODE) {
            std::cerr << "绑定端口失败:" << SocketUtil::get_error() << std::endl;
            SocketUtil::close_socket(server_fd);
            return false;
        }

        // 监听连接(最大等待队列10)
        if (listen(server_fd, 10) == SOCKET_ERROR_CODE) {
            std::cerr << "监听失败:" << SocketUtil::get_error() << std::endl;
            SocketUtil::close_socket(server_fd);
            return false;
        }

        server_running = true;
        std::cout << "服务端启动成功,监听端口:" << port << std::endl;

        // 循环接受客户端连接
        while (server_running) {
            sockaddr_in client_addr;
            socklen_t client_addr_len = sizeof(client_addr);
            SocketFD client_fd = accept(server_fd, (sockaddr*)&client_addr, &client_addr_len);
            if (client_fd == INVALID_SOCKET_FD) {
                if (server_running) {
                    std::cerr << "接受连接失败:" << SocketUtil::get_error() << std::endl;
                }
                continue;
            }

            // 创建线程处理客户端
            std::thread(&TCPServer::handle_client, this, client_fd).detach();
        }

        return true;
    }

    // 停止服务端
    void stop() {
        server_running = false;
        SocketUtil::close_socket(server_fd);
        std::cout << "服务端已停止" << std::endl;

        // 关闭所有客户端连接
        std::lock_guard<std::mutex> lock(client_mutex);
        for (SocketFD fd : client_list) {
            SocketUtil::close_socket(fd);
        }
        client_list.clear();
    }

    ~TCPServer() {
        stop();
    }
};

步骤 4:实现客户端核心逻辑

客户端包含以下核心功能:

连接指定 IP 和端口的服务端;创建线程接收服务端消息;从控制台输入消息,发送给服务端;处理断开连接逻辑。


// 客户端类
class TCPClient {
private:
    SocketFD client_fd;
    std::atomic<bool> client_running;

    // 接收服务端消息(子线程函数)
    void recv_message() {
        char buffer[1024] = {0};
        while (client_running) {
            int ret = recv(client_fd, buffer, sizeof(buffer) - 1, 0);
            if (ret <= 0) {
                std::cerr << "与服务端断开连接:" << SocketUtil::get_error() << std::endl;
                client_running = false;
                break;
            }
            std::string msg(buffer, ret);
            std::cout << "
收到服务端广播:" << msg << std::endl;
            std::cout << "请输入消息:";
            std::cout.flush(); // 刷新输出缓冲区
            memset(buffer, 0, sizeof(buffer));
        }
    }

public:
    TCPClient() : client_fd(INVALID_SOCKET_FD), client_running(false) {}

    // 连接服务端
    bool connect_server(const std::string& ip, uint16_t port) {
        // 创建客户端Socket
        client_fd = SocketUtil::create_tcp_socket();
        if (client_fd == INVALID_SOCKET_FD) return false;

        // 设置服务端地址
        sockaddr_in server_addr;
        SocketUtil::set_sockaddr(server_addr, ip, port);

        // 连接服务端
        if (connect(client_fd, (sockaddr*)&server_addr, sizeof(server_addr)) == SOCKET_ERROR_CODE) {
            std::cerr << "连接服务端失败:" << SocketUtil::get_error() << std::endl;
            SocketUtil::close_socket(client_fd);
            client_fd = INVALID_SOCKET_FD;
            return false;
        }

        client_running = true;
        std::cout << "成功连接服务端 [" << ip << ":" << port << "]" << std::endl;

        // 创建线程接收消息
        std::thread(&TCPClient::recv_message, this).detach();

        // 循环发送消息
        std::string msg;
        while (client_running) {
            std::cout << "请输入消息(输入exit退出):";
            std::getline(std::cin, msg);
            if (msg == "exit") {
                stop();
                break;
            }
            if (msg.empty()) continue;

            // 发送消息
            int ret = send(client_fd, msg.c_str(), msg.size(), 0);
            if (ret == SOCKET_ERROR_CODE) {
                std::cerr << "发送消息失败:" << SocketUtil::get_error() << std::endl;
                stop();
                break;
            }
        }

        return true;
    }

    // 停止客户端
    void stop() {
        client_running = false;
        SocketUtil::close_socket(client_fd);
        std::cout << "客户端已断开连接" << std::endl;
    }

    ~TCPClient() {
        stop();
    }
};

步骤 5:实现主函数(入口)

主函数通过命令行参数区分服务端/客户端模式,解析参数后启动对应逻辑。


// 打印使用帮助
void print_help(const std::string& prog_name) {
    std::cout << "简易网络通信工具使用说明:" << std::endl;
    std::cout << "服务端模式:" << prog_name << " -s <端口>" << std::endl;
    std::cout << "客户端模式:" << prog_name << " -c <服务端IP> <端口>" << std::endl;
    std::cout << "示例:" << std::endl;
    std::cout << "  启动服务端:" << prog_name << " -s 8888" << std::endl;
    std::cout << "  启动客户端:" << prog_name << " -c 127.0.0.1 8888" << std::endl;
}

int main(int argc, char* argv[]) {
    // 初始化网络环境
    if (!SocketUtil::init()) {
        return -1;
    }

    // 解析命令行参数
    if (argc < 3) {
        print_help(argv[0]);
        SocketUtil::cleanup();
        return -1;
    }

    std::string mode = argv[1];
    if (mode == "-s") {
        // 服务端模式
        uint16_t port = atoi(argv[2]);
        if (port == 0 || port > 65535) {
            std::cerr << "无效的端口号:" << argv[2] << std::endl;
            SocketUtil::cleanup();
            return -1;
        }

        TCPServer server(port);
        if (!server.start()) {
            SocketUtil::cleanup();
            return -1;
        }
    } else if (mode == "-c") {
        // 客户端模式
        if (argc < 4) {
            print_help(argv[0]);
            SocketUtil::cleanup();
            return -1;
        }
        std::string ip = argv[2];
        uint16_t port = atoi(argv[3]);
        if (port == 0 || port > 65535) {
            std::cerr << "无效的端口号:" << argv[3] << std::endl;
            SocketUtil::cleanup();
            return -1;
        }

        TCPClient client;
        if (!client.connect_server(ip, port)) {
            SocketUtil::cleanup();
            return -1;
        }
    } else {
        print_help(argv[0]);
        SocketUtil::cleanup();
        return -1;
    }

    // 清理网络环境
    SocketUtil::cleanup();
    return 0;
}

四、编译与运行

1. Linux/macOS 编译

使用 g++ 编译,需指定 C++11 标准:


g++ -std=c++11 -o net_tool net_tool.cpp -pthread

2. Windows 编译(MinGW)


g++ -std=c++11 -o net_tool.exe net_tool.cpp -lws2_32

3. Windows 编译(Visual Studio)

创建空项目,添加代码文件;项目属性 → 链接器 → 输入 → 附加依赖项,添加
ws2_32.lib
;编译生成可执行文件。

4. 运行测试

启动服务端

# Linux/macOS
./net_tool -s 8888

# Windows
net_tool.exe -s 8888

输出:
服务端启动成功,监听端口:8888

启动客户端(多个)

# Linux/macOS
./net_tool -c 127.0.0.1 8888

# Windows
net_tool.exe -c 127.0.0.1 8888

输出:
成功连接服务端 [127.0.0.1:8888]

测试消息收发

在任意客户端输入消息,服务端会接收并广播给所有客户端,例如:

客户端1输入:
Hello World!
服务端输出:
收到客户端[5]消息:Hello World!
客户端2/3 输出:
收到服务端广播:Hello World!

输入
exit
可退出客户端,服务端会清理该客户端连接。

五、功能扩展建议

本项目实现了基础的 TCP 通信功能,可进一步扩展:

消息格式优化:添加消息头(如发送者ID、时间戳、消息长度),支持结构化消息;断线重连:客户端检测到断开后,自动重试连接服务端;UDP 协议支持:补充 UDP 通信逻辑,实现无连接的消息传输;日志系统:将消息记录到文件,便于调试和审计;权限验证:客户端连接时需输入密码,服务端验证通过后才允许通信;图形界面:使用 Qt/GTK 封装界面,替代控制台输入输出;大文件传输:支持分片传输大文件,实现断点续传。

六、常见问题与解决

1. Windows 编译提示
ws2_32.lib
找不到

确认项目属性中已添加
ws2_32.lib
;MinGW 编译时需加
-lws2_32
参数。

2. 客户端连接服务端失败

检查服务端是否已启动,端口是否正确;检查防火墙是否放行对应端口;服务端绑定
0.0.0.0
(INADDR_ANY),客户端需使用服务端的实际 IP(非 127.0.0.1 需确保网络互通)。

3. 多客户端连接后消息广播异常

确认客户端列表操作已加互斥锁(避免线程安全问题);检查 Socket 关闭逻辑,确保断开的客户端从列表中移除。

4. 中文乱码

统一编码格式(如 UTF-8),Windows 控制台需设置编码:
chcp 65001

七、总结

本项目从零实现了一个跨平台的简易网络通信工具,核心围绕 TCP Socket 编程和多线程并发处理展开。通过封装跨平台工具类,解决了 Windows/Linux 网络接口差异的问题;通过多线程和互斥锁,实现了服务端对多客户端的并发处理;通过广播机制,实现了客户端之间的消息互通。

该项目覆盖了 C++ 网络编程的核心知识点,代码结构清晰、易于扩展,是入门网络编程的优质实战案例。掌握本项目后,可进一步学习更复杂的网络框架(如 Boost.Asio、libevent),实现高性能的网络应用。

© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
none
暂无评论...