accept 是服务器端从监听队列里取出一个已经建立好的连接,并为这个客户端创建一条专属通信通道。
cpp
int connfd = accept(_sockfd, (struct sockaddr*)&client_addr, &len);
它做了两件事
1. 从队列里取一个连接
之前 listen 已经建好了等待队列,客户端完成三次握手后就在里面排队。accept 就是从队列里取出一个,如果队列为空,accept 会阻塞,一直等到有客户端连进来。
2. 返回一个新的文件描述符
这是最关键的一点。accept 返回的 connfd 和原来的 _sockfd 是两个不同的东西:
表格
| 描述符 | 作用 | 生命周期 |
|---|---|---|
_sockfd |
监听套接字,只负责接受新连接 | 服务器启动到关闭,一直存在 |
connfd |
通信套接字,专门和这个客户端收发数据 | 这个客户端断开后就关闭 |
为什么需要两个描述符?
想象一个餐厅:
_sockfd是门口的迎宾员,只管接待新客人,把客人领进来;connfd是专属服务员,只服务这一桌客人,点菜、上菜、结账都靠它。
迎宾员不能同时服务所有桌子,所以需要为每桌客人分配一个专属服务员。
如果服务器只用 _sockfd 和客户端通信,那当它正在和客户端 A 聊天时,客户端 B、C 的新连接就没人处理了。有了 connfd,_sockfd 可以立刻回到 accept 继续接待下一个客人,而 connfd 专门负责和当前客户端通信。
典型服务器循环
cpp
while (true) {
int connfd = accept(_sockfd, ...); // 等待新客户端,返回专属通道
// 用 connfd 和这个客户端收发数据
read(connfd, buf, ...);
write(connfd, resp, ...);
close(connfd); // 这个客户端走了,关闭专属通道
}
_sockfd 始终在循环里反复 accept,每来一个客户端就产生一个新的 connfd。
参数里的 client_addr
cpp
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int connfd = accept(_sockfd, (struct sockaddr*)&client_addr, &len);
调用后 client_addr 里就填好了客户端的 IP 和端口,你可以知道是谁连进来了。如果不需要,这两个参数可以传 NULL。
一句话总结
accept 就是:从监听队列里拿出一个已完成握手的连接,返回一个专属的文件描述符用来和这个客户端通信,同时让监听套接字继续接待下一个。