网络编程在做什么:客户端与服务器

网络编程的本质很朴素:两个程序各自在自己的机器上运行,通过网线把字节送来送去。一边叫服务器(先守在一个端口上等别人来连),一边叫客户端(主动发起连接去连服务器)。你平时打开网页,浏览器就是客户端,远端那台机器就是服务器。

所有这类通信都建立在一个操作系统抽象之上——socket(套接字)。你可以把它理解成一个"带编号的文件句柄":往里写字节就发出去了,从里读字节就收到对端的数据。收发网络和读写文件在 C++ 里的味道惊人地像。

socket 是操作系统替你管理的通信端点。网络编程那套 bind、listen、accept,其实就是围绕这个端点的几步仪式。
本文核心知识点
  • socket()创建一个通信端点,返回一个文件描述符。
  • bind/listen/accept服务器侧三步走,开始等待连接。
  • connect客户端主动连到服务器的地址与端口。
  • 阻塞与异步默认 recv 会卡住等数据;异步 I/O 让单线程处理大量连接。
C++ 网络编程配图:蓝调渐变背景上中央一个发光的服务器方块节点,四周通过发光连线连接多个圆形节点与小方块,连线上有数据包光点流动,背景点缀世界网格与光晕
一台中心服务器,连线通向四面八方,数据包在连线间流动——这就是网络世界的拓扑。

socket:操作系统给的通信端点

第一步永远是向操作系统要一个 socket:调用 socket(),指定用 IPv4 地址族、流式(TCP)协议,它返回一个整数描述符。后面所有操作都拿这个描述符去谈。

int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
    perror("socket");
    return 1;
}

三个参数记住大意即可:AF_INET 表示用 IPv4,SOCK_STREAM 表示要一条可靠的字节流(也就是 TCP),而不是无连接的 UDP。这个描述符和你读文件时拿到的句柄地位相当,用完一样要 close。

一次连接:bind、listen、accept、connect

服务器这边要做三步:先 bind 把 socket 绑到本机的某个端口,再 listen 宣布"开始排队等连接",最后 accept 从队列里取出一个真正到来的连接。客户端则只需 connect 到服务器的地址和端口。

// 服务器侧
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);          // 端口 8080
addr.sin_addr.s_addr = htonl(INADDR_ANY);

bind(fd, (struct sockaddr*)&addr, sizeof(addr));
listen(fd, 10);                        // 最多排 10 个待连接

int conn = accept(fd, NULL, NULL);     // 阻塞,直到有人来连

accept 返回的是一个新的 socket,专门用来和这一个客户端对话;原来那个监听 socket 继续等下一个连接。这就是"一个监听、多个会话"的模型雏形。

收发数据:send 与 recv

连接建立后,收发就是两个函数:send 把缓冲里的字节发给对端,recv 从对端收字节到缓冲里。

char buf[1024];
int n = recv(conn, buf, sizeof(buf) - 1, 0);  // 读到 n 字节
buf[n] = '\0';
send(conn, "hello client\n", 13, 0);
close(conn);

一个常被忽略的事实:TCP 是字节流,没有消息边界。你 send 两次,对端可能一次 recv 全收到,也可能分好几次。所以真实的协议层要自己约定"一个消息到哪算结束"——比如按固定长度、或在末尾加一个结束标记。

阻塞的问题:为什么需要异步 I/O

上面那版代码有个致命问题:accept 和 recv 默认是阻塞的——没连接时 accept 卡住,没数据时 recv 卡住。一个线程一次只能伺候一个客户端。来了一千个连接怎么办?老式办法是"每来一个连接就开一个线程",但线程很重,几千个线程会把内存和上下文切换吃光。

  • 阻塞模型:连接一多,线程数爆炸,系统扛不住。
  • 多线程模型:能并行,但每连接一线程开销大、难管理。
  • 异步 I/O:让一个线程盯着很多连接,哪个有数据就处理哪个,不用为每个连接养一个线程。

现代高并发服务器几乎都走异步路线——这正是 Redis、Nginx 能单机扛住数万连接的底层原因。

epoll 直觉:单线程处理很多连接

Linux 上这套机制叫 epoll。你把成千上万个连接 socket 都"登记"给 epoll,然后问它一句:现在哪些连接有数据就绪了?epoll 只把就绪的那几个返回给你,你逐个处理即可。一个线程就这样轮着伺候了所有连接,而不是死等某一个。

// 伪代码:epoll 的核心循环
int ep = epoll_create1(0);
epoll_ctl(ep, EPOLL_CTL_ADD, conn, &ev);   // 登记连接

while (running) {
    int n = epoll_wait(ep, events, MAX, -1); // 等到有连接就绪
    for (int i = 0; i < n; i++) {
        handle(events[i]);                   // 只处理就绪的
    }
}

你不必一开始就手写 epoll——现实中直接用成熟库(如 asio、libuv)即可。但理解了"登记一堆连接、只处理就绪的"这个思路,你就理解了所有现代异步服务器的骨架。