第二十七章 网络编程入门:写一个能聊天的程序

← 返回目录

本章目标:理解 socket 编程模型、用 POSIX API 写 TCP 客户端/服务器、认识 asio 等现代方案。 学完你会写出"本机可跑"的回显服务器和客户端。

27.1 网络编程全景(30 秒入门)

TCP 模型(人话):

和每个客户端收发数据

术语:

socket  网络通信的"文件描述符"(像文件一样读写)
bind    把 socket 绑到端口
listen  开始监听(等别人来连)
accept  接受一个连接(阻塞等待)
connect 主动连接
recv/send 收发数据

27.2 最简 TCP 回显服务器(完整可运行)

// server.cpp —— 收到什么就回什么
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <cstring>
#include <print>

int main() {
    int fd = ::socket(AF_INET, SOCK_STREAM, 0);
    if (fd < 0) { std::println("创建 socket 失败"); return 1; }

    sockaddr_in addr{};
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);   // 绑定所有网卡
    addr.sin_port = htons(8888);                // 端口 8888

    if (::bind(fd, (sockaddr*)&addr, sizeof(addr)) < 0) {
        std::println("绑定端口失败(可能已被占用)");
        return 1;
    }
    ::listen(fd, 5);                            // 开始监听

    std::println("服务器监听 8888 端口...");
    while (true) {
        int client = ::accept(fd, nullptr, nullptr);  // 等一个连接
        if (client < 0) break;
        char buf[1024];
        ssize_t n = ::recv(client, buf, sizeof(buf), 0);
        if (n > 0) {
            std::println("收到: {}", std::string_view(buf, n));
            ::send(client, buf, n, 0);          // 原样回发
        }
        ::close(client);
    }
    ::close(fd);
}

编译:clang++ -std=c++26 server.cpp -o server (Termux 需要:pkg install 不需要额外包,socket 是系统调用)

27.3 客户端(配套测试)

// client.cpp
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <print>

int main() {
    int fd = ::socket(AF_INET, SOCK_STREAM, 0);
    sockaddr_in addr{};
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = inet_addr("127.0.0.1");  // 本机
    addr.sin_port = htons(8888);

    if (::connect(fd, (sockaddr*)&addr, sizeof(addr)) < 0) {
        std::println("连接失败");
        return 1;
    }
    const char* msg = "你好,服务器!";
    ::send(fd, msg, std::strlen(msg), 0);
    char buf[1024];
    ssize_t n = ::recv(fd, buf, sizeof(buf), 0);
    std::println("服务器回复: {}", std::string_view(buf, n));
    ::close(fd);
}

27.4 阻塞、缓冲区与错误处理(概念)

  1. 多线程:每连接一个线程
  2. select/poll/epoll:监听多个 fd,谁有数据处理谁
  3. 非阻塞 + 异步(asio 协程风格)

要写循环直到收完(这引出了"消息边界"问题)

工程上现代方案:直接用 asio / Boost.Asio 或 C++26 网络 TS 草案实现——手写 POSIX 只用于理解原理。

27.5 多客户端服务器(每连接一线程)

while (true) {
    int client = ::accept(fd, nullptr, nullptr);
    if (client < 0) continue;
    std::thread([client] {
        char buf[1024];
        while (true) {
            ssize_t n = ::recv(client, buf, sizeof(buf), 0);
            if (n <= 0) break;              // 连接断开
            ::send(client, buf, n, 0);
        }
        ::close(client);
    }).detach();                            // 线程自主运行
}

注意:detach 的线程访问 client(栈变量)——这里 client 是 accept 循环的局部变量,每个迭代是独立副本,安全。

27.6 实战:本机聊天(测试文件实现)

测试文件实现一个简化版:

这是"端到端"测试,跑通说明你的网络栈理解正确。

27.7 陷阱清单

  1. 忘 htons/htonl(字节序转换)→ 端口/地址错乱
  2. 端口被占用 → bind 失败(换个端口)
  3. 客户端连错地址 → connect 失败
  4. recv 假设"一次收完" → 丢数据(要循环)
  5. send 假设"一次发完" → 对方收不全
  6. 忘 close → 句柄泄漏(fd 是有限的!)
  7. 服务器单线程 accept 后处理慢 → 其他客户端排队
  8. 多线程共享 client fd 不做同步

本章小结

练习题(配套测试:测试_第27章_网络编程.cpp)


  1. 运行服务器和客户端,观察回显。
  2. 把回显改成"大写回复"。
  3. 写一个每连接一线程的服务器。
  4. 测试客户端连接不存在的端口,观察 connect 失败。
  5. 让客户端连续发 10 条消息,服务器逐条回。