前置知识: C

C++网络编程

4 min中级

Socket编程与网络协议

前置知识

学习目标

  • 掌握「概述」的核心机制、典型用法与常见陷阱
  • 掌握「基础概念」的核心机制、典型用法与常见陷阱
  • 掌握「快速上手」的核心机制、典型用法与常见陷阱
  • 掌握「详细用法」的核心机制、典型用法与常见陷阱
  • 掌握「常见场景」的核心机制、典型用法与常见陷阱

概述

C++ 网络编程是使用 C++ 通过网络协议进行数据通信的技术。最基础的方式是使用操作系统提供的 Socket API,它允许程序通过网络发送和接收数据。更高级的方式是使用第三方库如 Boost.Asio、libcurl 等,它们封装了底层细节,提供了更易用的接口。

为什么需要网络编程?几乎所有现代应用都需要网络通信。聊天应用需要收发消息,游戏需要同步玩家状态,Web 服务需要处理 HTTP 请求。C++ 网络编程让你能够构建高性能的网络应用,从简单的 TCP 客户端到高并发的服务器。

基础概念

Socket:网络通信的端点,由 IP 地址和端口号标识。可以理解为网络中的”插座”,程序通过它发送和接收数据。

TCP:传输控制协议,提供可靠的、有序的数据传输。适合需要确保数据完整性的场景,如文件传输、Web 请求。

UDP:用户数据报协议,提供不可靠但快速的数据传输。适合对实时性要求高但允许丢包的场景,如视频流、游戏。

字节序:多字节数据在内存中的存储顺序。网络传输使用大端序(Network Byte Order),而 x86 架构使用小端序。需要使用 htonl、ntohl 等函数转换。

阻塞与非阻塞:阻塞模式下,网络操作会等待完成才返回;非阻塞模式下,操作立即返回,需要轮询或使用事件通知机制。

快速上手

TCP 客户端

#include <iostream>
#include <string>
#include <cstring>

#ifdef _WIN32
    #include <winsock2.h>
    #include <ws2tcpip.h>
    #pragma comment(lib, "ws2_32.lib")
#else
    #include <sys/socket.h>
    #include <arpa/inet.h>
    #include <unistd.h>
    #include <netdb.h>
#endif

// 跨平台初始化和清理
class SocketInit {
public:
    SocketInit() {
#ifdef _WIN32
        WSADATA wsaData;
        WSAStartup(MAKEWORD(2, 2), &wsaData);
#endif
    }
    ~SocketInit() {
#ifdef _WIN32
        WSACleanup();
#endif
    }
};

int main() {
    SocketInit socketInit;  // 自动初始化和清理

    // 创建 TCP Socket
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) {
        std::cerr << "创建 Socket 失败" << std::endl;
        return -1;
    }

    // 设置服务器地址
    sockaddr_in serverAddr{};
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(8080);  // 端口号,转换为网络字节序
    inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);  // IP 地址

    // 连接服务器
    if (connect(sock, (sockaddr*)&serverAddr, sizeof(serverAddr)) < 0) {
        std::cerr << "连接服务器失败" << std::endl;
        return -1;
    }

    std::cout << "已连接到服务器" << std::endl;

    // 发送数据
    std::string message = "你好,服务器!";
    send(sock, message.c_str(), message.size(), 0);

    // 接收数据
    char buffer[1024] = {};
    int bytesReceived = recv(sock, buffer, sizeof(buffer) - 1, 0);
    if (bytesReceived > 0) {
        buffer[bytesReceived] = '\0';
        std::cout << "收到回复: " << buffer << std::endl;
    }

    // 关闭 Socket
#ifdef _WIN32
    closesocket(sock);
#else
    close(sock);
#endif

    return 0;
}

TCP 服务器

#include <iostream>
#include <cstring>
#include <vector>

int main() {
    SocketInit socketInit;

    // 创建监听 Socket
    int serverSock = socket(AF_INET, SOCK_STREAM, 0);
    if (serverSock < 0) {
        std::cerr << "创建 Socket 失败" << std::endl;
        return -1;
    }

    // 设置地址复用(避免重启时端口被占用)
    int opt = 1;
    setsockopt(serverSock, SOL_SOCKET, SO_REUSEADDR, (const char*)&opt, sizeof(opt));

    // 绑定地址和端口
    sockaddr_in serverAddr{};
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = INADDR_ANY;  // 监听所有网络接口
    serverAddr.sin_port = htons(8080);

    if (bind(serverSock, (sockaddr*)&serverAddr, sizeof(serverAddr)) < 0) {
        std::cerr << "绑定端口失败" << std::endl;
        return -1;
    }

    // 开始监听,backlog 为等待连接的队列长度
    if (listen(serverSock, 5) < 0) {
        std::cerr << "监听失败" << std::endl;
        return -1;
    }

    std::cout << "服务器已启动,等待连接..." << std::endl;

    while (true) {
        // 接受客户端连接
        sockaddr_in clientAddr{};
        int clientAddrLen = sizeof(clientAddr);
        int clientSock = accept(serverSock, (sockaddr*)&clientAddr, &clientAddrLen);

        if (clientSock < 0) {
            std::cerr << "接受连接失败" << std::endl;
            continue;
        }

        // 获取客户端地址
        char clientIP[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &clientAddr.sin_addr, clientIP, INET_ADDRSTRLEN);
        std::cout << "客户端连接: " << clientIP << ":" << ntohs(clientAddr.sin_port) << std::endl;

        // 接收数据
        char buffer[1024] = {};
        int bytesReceived = recv(clientSock, buffer, sizeof(buffer) - 1, 0);
        if (bytesReceived > 0) {
            buffer[bytesReceived] = '\0';
            std::cout << "收到消息: " << buffer << std::endl;

            // 发送回复
            std::string reply = "服务器已收到你的消息";
            send(clientSock, reply.c_str(), reply.size(), 0);
        }

        // 关闭客户端连接
#ifdef _WIN32
        closesocket(clientSock);
#else
        close(clientSock);
#endif
    }

    return 0;
}

详细用法

UDP 通信

// UDP 服务器
void udpServer() {
    int sock = socket(AF_INET, SOCK_DGRAM, 0);  // SOCK_DGRAM 表示 UDP

    sockaddr_in serverAddr{};
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = INADDR_ANY;
    serverAddr.sin_port = htons(9090);

    bind(sock, (sockaddr*)&serverAddr, sizeof(serverAddr));

    std::cout << "UDP 服务器已启动" << std::endl;

    while (true) {
        char buffer[1024] = {};
        sockaddr_in clientAddr{};
        int clientAddrLen = sizeof(clientAddr);

        // 接收数据(同时获取发送方地址)
        int bytesReceived = recvfrom(sock, buffer, sizeof(buffer) - 1, 0,
            (sockaddr*)&clientAddr, &clientAddrLen);

        if (bytesReceived > 0) {
            buffer[bytesReceived] = '\0';
            std::cout << "收到: " << buffer << std::endl;

            // 向发送方回复
            std::string reply = "UDP 回复";
            sendto(sock, reply.c_str(), reply.size(), 0,
                (sockaddr*)&clientAddr, clientAddrLen);
        }
    }
}

// UDP 客户端
void udpClient() {
    int sock = socket(AF_INET, SOCK_DGRAM, 0);

    sockaddr_in serverAddr{};
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(9090);
    inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);

    // UDP 不需要连接,直接发送
    std::string message = "UDP 你好";
    sendto(sock, message.c_str(), message.size(), 0,
        (sockaddr*)&serverAddr, sizeof(serverAddr));

    // 接收回复
    char buffer[1024] = {};
    recvfrom(sock, buffer, sizeof(buffer) - 1, 0, nullptr, nullptr);
    std::cout << "收到: " << buffer << std::endl;
}

使用 Boost.Asio

Boost.Asio 是 C++ 最流行的异步网络库:

#include <boost/asio.hpp>
#include <iostream>

using boost::asio::ip::tcp;

// 同步 TCP 客户端
void syncClient() {
    boost::asio::io_context ioContext;

    // 解析主机名和端口
    tcp::resolver resolver(ioContext);
    auto endpoints = resolver.resolve("127.0.0.1", "8080");

    // 连接服务器
    tcp::socket socket(ioContext);
    boost::asio::connect(socket, endpoints);

    std::cout << "已连接到服务器" << std::endl;

    // 发送数据
    std::string message = "你好,Boost.Asio!";
    boost::asio::write(socket, boost::asio::buffer(message));

    // 接收数据
    char reply[1024] = {};
    size_t replyLength = socket.read_some(boost::asio::buffer(reply));
    std::cout << "收到: " << std::string(reply, replyLength) << std::endl;
}

// 异步 TCP 服务器
class Session : public std::enable_shared_from_this<Session> {
    tcp::socket socket_;
    char buffer_[1024];

public:
    Session(tcp::socket socket) : socket_(std::move(socket)) {}

    void start() {
        doRead();  // 开始异步读取
    }

private:
    void doRead() {
        auto self = shared_from_this();
        socket_.async_read_some(boost::asio::buffer(buffer_),
            [this, self](boost::system::error_code ec, size_t length) {
                if (!ec) {
                    std::cout << "收到: " << std::string(buffer_, length) << std::endl;
                    doWrite(length);  // 回显数据
                }
            });
    }

    void doWrite(size_t length) {
        auto self = shared_from_this();
        boost::asio::async_write(socket_, boost::asio::buffer(buffer_, length),
            [this, self](boost::system::error_code ec, size_t) {
                if (!ec) {
                    doRead();  // 继续读取
                }
            });
    }
};

class AsyncServer {
    boost::asio::io_context& ioContext_;
    tcp::acceptor acceptor_;

public:
    AsyncServer(boost::asio::io_context& ioContext, short port)
        : ioContext_(ioContext)
        , acceptor_(ioContext, tcp::endpoint(tcp::v4(), port))
    {
        doAccept();  // 开始接受连接
    }

private:
    void doAccept() {
        acceptor_.async_accept(
            [this](boost::system::error_code ec, tcp::socket socket) {
                if (!ec) {
                    // 为每个连接创建一个 Session
                    std::make_shared<Session>(std::move(socket))->start();
                }
                doAccept();  // 继续接受下一个连接
            });
    }
};

// 使用异步服务器
int main() {
    boost::asio::io_context ioContext;
    AsyncServer server(ioContext, 8080);
    std::cout << "异步服务器已启动" << std::endl;
    ioContext.run();  // 运行事件循环
    return 0;
}

HTTP 请求(使用 libcurl)

#include <curl/curl.h>
#include <iostream>
#include <string>

// 回调函数:处理接收到的数据
size_t writeCallback(void* contents, size_t size, size_t nmemb, std::string* userp) {
    size_t totalSize = size * nmemb;
    userp->append((char*)contents, totalSize);
    return totalSize;
}

// 发送 GET 请求
std::string httpGet(const std::string& url) {
    CURL* curl = curl_easy_init();
    std::string response;

    if (curl) {
        curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback);
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
        curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);  // 超时 10 秒

        CURLcode res = curl_easy_perform(curl);
        if (res != CURLE_OK) {
            std::cerr << "请求失败: " << curl_easy_strerror(res) << std::endl;
        }

        curl_easy_cleanup(curl);
    }

    return response;
}

// 发送 POST 请求
std::string httpPost(const std::string& url, const std::string& data) {
    CURL* curl = curl_easy_init();
    std::string response;

    if (curl) {
        curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
        curl_easy_setopt(curl, CURLOPT_POST, 1L);
        curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str());
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback);
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);

        // 设置请求头
        struct curl_slist* headers = nullptr;
        headers = curl_slist_append(headers, "Content-Type: application/json");
        curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);

        curl_easy_perform(curl);
        curl_slist_free_all(headers);
        curl_easy_cleanup(curl);
    }

    return response;
}

int main() {
    curl_global_init(CURL_GLOBAL_DEFAULT);

    // GET 请求
    std::string response = httpGet("https://httpbin.org/get");
    std::cout << "GET 响应: " << response << std::endl;

    // POST 请求
    std::string postResponse = httpPost("https://httpbin.org/post",
        "{\"name\":\"张三\",\"age\":25}");
    std::cout << "POST 响应: " << postResponse << std::endl;

    curl_global_cleanup();
    return 0;
}

常见场景

简单的聊天服务器

// 多客户端聊天服务器(使用 select 多路复用)
#include <set>

void chatServer() {
    int serverSock = socket(AF_INET, SOCK_STREAM, 0);

    sockaddr_in addr{};
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    addr.sin_port = htons(8080);

    bind(serverSock, (sockaddr*)&addr, sizeof(addr));
    listen(serverSock, 5);

    std::set<int> clients;  // 所有已连接的客户端

    while (true) {
        fd_set readFds;
        FD_ZERO(&readFds);
        FD_SET(serverSock, &readFds);  // 监听服务器 Socket

        int maxFd = serverSock;
        for (int client : clients) {
            FD_SET(client, &readFds);
            maxFd = std::max(maxFd, client);
        }

        // 等待任一 Socket 有数据可读
        select(maxFd + 1, &readFds, nullptr, nullptr, nullptr);

        // 检查是否有新连接
        if (FD_ISSET(serverSock, &readFds)) {
            int clientSock = accept(serverSock, nullptr, nullptr);
            clients.insert(clientSock);
            std::cout << "新客户端连接,当前人数: " << clients.size() << std::endl;
        }

        // 检查各客户端是否有消息
        char buffer[1024];
        for (auto it = clients.begin(); it != clients.end(); ) {
            if (FD_ISSET(*it, &readFds)) {
                int bytes = recv(*it, buffer, sizeof(buffer) - 1, 0);
                if (bytes <= 0) {
                    // 客户端断开
                    it = clients.erase(it);
                    continue;
                }
                buffer[bytes] = '\0';

                // 广播给所有其他客户端
                for (int other : clients) {
                    if (other != *it) {
                        send(other, buffer, bytes, 0);
                    }
                }
            }
            ++it;
        }
    }
}

注意事项

字节序转换:网络传输多字节整数时必须使用 htonl/ntohl/htons/ntohs 进行字节序转换,否则在不同架构的机器间通信会出错。

粘包问题:TCP 是流式协议,不保证一次 recv 对应一次 send。需要在应用层定义消息边界,如使用长度前缀或分隔符。

资源释放:Socket 是系统资源,使用完毕后必须关闭。建议使用 RAII 封装 Socket 的生命周期。

跨平台差异:Windows 和 Linux 的 Socket API 有细微差异(如 closesocket vs close,SOCKADDR vs sockaddr)。使用条件编译或跨平台库处理。

并发安全:多线程环境下操作同一 Socket 需要加锁。推荐使用 Boost.Asio 的异步模型,避免多线程竞争。

进阶用法

使用 io_uring(Linux 5.1+)

// io_uring 是 Linux 的高性能异步 I/O 接口
// 比 epoll 更高效,适合高并发网络服务

#include <liburing.h>

void ioUringServer() {
    struct io_uring ring;
    io_uring_queue_init(256, &ring, 0);

    // 创建监听 Socket
    int serverSock = socket(AF_INET, SOCK_STREAM, 0);
    // ... bind, listen ...

    // 提交 accept 请求
    struct io_uring_sqe* sqe = io_uring_get_sqe(&ring);
    io_uring_prep_accept(sqe, serverSock, nullptr, nullptr, 0);
    io_uring_submit(&ring);

    // 等待完成
    struct io_uring_cqe* cqe;
    io_uring_wait_cqe(&ring, &cqe);
    int clientSock = cqe->res;
    io_uring_cqe_seen(&ring, cqe);

    // 提交 recv 请求
    char buffer[1024];
    sqe = io_uring_get_sqe(&ring);
    io_uring_prep_recv(sqe, clientSock, buffer, sizeof(buffer), 0);
    io_uring_submit(&ring);

    io_uring_queue_exit(&ring);
}

POSIX Socket 基础

基本写法:创建套接字 int socket(<域>, <类型>, <协议>)

#include <sys/socket.h>
// 创建 TCP 套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) { perror("socket"); }
// AF_INET    IPv4
// AF_INET6   IPv6
// SOCK_STREAM TCP
// SOCK_DGRAM  UDP

基本写法:绑定地址 bind(<fd>, <地址指针>, <地址长度>)

#include <netinet/in.h>
struct sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);          // 端口转网络字节序
addr.sin_addr.s_addr = INADDR_ANY;    // 监听所有地址
bind(fd, (struct sockaddr*)&addr, sizeof(addr));

基本写法:监听 listen(<fd>, <等待队列长度>)

// 开始监听
listen(fd, 5); // backlog=5

基本写法:接受连接 accept(<fd>, <地址>, <长度>)

// 接受客户端连接(阻塞)
struct sockaddr_in client{};
socklen_t len = sizeof(client);
int conn = accept(fd, (struct sockaddr*)&client, &len);

基本写法:连接服务器 connect(<fd>, <地址>, <长度>)

// 客户端连接
struct sockaddr_in srv{};
srv.sin_family = AF_INET;
srv.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &srv.sin_addr);
connect(fd, (struct sockaddr*)&srv, sizeof(srv));

基本写法:收发数据 send/recv 或 read/write

// TCP 收发
char buf[1024];
ssize_t n = recv(conn, buf, sizeof(buf), 0);
send(conn, "hello", 5, 0);
// UDP 用 sendto/recvfrom
sendto(fd, "hi", 2, 0, (struct sockaddr*)&srv, sizeof(srv));
recvfrom(fd, buf, sizeof(buf), 0, nullptr, nullptr);

地址转换

基本写法:IP 字符串与二进制互转 inet_pton(<域>, <字符串>, <二进制>)

#include <arpa/inet.h>
// 字符串转二进制
struct in_addr addr;
inet_pton(AF_INET, "192.168.1.1", &addr);
// 二进制转字符串
char str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &addr, str, sizeof(str));

基本写法:getaddrinfo 解析 getaddrinfo(<主机>, <服务>, <提示>, <结果>)

#include <netdb.h>
struct addrinfo hints{}, *res;
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
getaddrinfo("example.com", "80", &hints, &res);
// 使用 res 链表
freeaddrinfo(res);

套接字选项

基本写法:设置选项 setsockopt(<fd>, <级别>, <选项>, <值>, <长度>)

// 地址重用
int yes = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
// 接收超时
struct timeval tv{.tv_sec = 5, .tv_usec = 0};
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));

I/O 复用

基本写法:select 多路复用 select(<最大fd+1>, <读集>, <写集>, <异常集>, <超时>)

#include <sys/select.h>
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
struct timeval tv{5, 0};
int n = select(fd + 1, &readfds, nullptr, nullptr, &tv);
if (n > 0 && FD_ISSET(fd, &readfds)) { /* 可读 */ }

基本写法:poll 多路复用 poll(<数组>, <数量>, <超时>)

#include <poll.h>
struct pollfd fds[2];
fds[0].fd = fd1; fds[0].events = POLLIN;
fds[1].fd = fd2; fds[1].events = POLLIN;
int n = poll(fds, 2, 5000); // 超时 5 秒
for (int i = 0; i < 2; ++i)
    if (fds[i].revents & POLLIN) { /* 可读 */ }

基本写法:epoll(Linux) epoll_create / epoll_ctl / epoll_wait

#include <sys/epoll.h>
int ep = epoll_create1(0);
struct epoll_event ev{.events = EPOLLIN, .data.fd = fd};
epoll_ctl(ep, EPOLL_CTL_ADD, fd, &ev);
struct epoll_event events[10];
int n = epoll_wait(ep, events, 10, -1); // -1 阻塞

非阻塞 I/O

基本写法:设置非阻塞 fcntl(<fd>, F_SETFL, O_NONBLOCK)

#include <fcntl.h>
// 设为非阻塞
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);

简易 TCP 服务器框架

基本写法:服务器流程 socket → bind → listen → accept → recv/send

// TCP echo 服务器骨架
int srv = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
addr.sin_addr.s_addr = INADDR_ANY;
bind(srv, (struct sockaddr*)&addr, sizeof(addr));
listen(srv, 5);
while (true) {
    int conn = accept(srv, nullptr, nullptr);
    char buf[1024];
    ssize_t n = recv(conn, buf, sizeof(buf), 0);
    send(conn, buf, n, 0); // 回显
    close(conn);
}

C++ 封装

基本写法:RAII 套接字 struct Socket { int fd; ~Socket() { close(fd); } };

// RAII 管理套接字生命周期
struct Socket {
    int fd;
    Socket(int f) : fd(f) {}
    ~Socket() { if (fd >= 0) close(fd); }
    Socket(const Socket&) = delete;
    Socket& operator=(const Socket&) = delete;
    Socket(Socket&& o) noexcept : fd(o.fd) { o.fd = -1; }
};