在Linux系统中,socket被抽象为一种特殊的文件描述符,应用进程通过它和内核网络协议栈交换数据。实现服务器与客户端对话的核心,是服务端先绑定地址并监听,客户端发起连接,双方建立一条虚拟的字节流管道,之后便可以反复调用读写函数传递消息。理解这个过程不能只停留在函数调用顺序,还要清楚内核为每个连接维护的发送与接收缓冲区,以及进程在读写时是被阻塞还是立即返回。

socket通信的基础流程与核心函数
一个典型的TCP对话程序分为服务端和客户端两条线。服务端依次调用socket创建监听套接字,bind绑定到本机IP和端口,listen将套接字设为被动监听状态,随后在循环里accept等待客户端连入。每接受一个连接,内核会生成一个新的已连接套接字,专门用于和这个客户端通信。客户端则调用socket创建套接字后,使用connect主动连接服务端地址,连接成功即可直接读写。
这些函数都声明在sys/socket.h和netinet/in.h中。需要注意struct sockaddr_in结构体的字段字节序:端口和IP地址必须用htons和htonl转为网络序,否则在大小端不同的机器上会出现连不上的情况。另外,accept返回的新的文件描述符和监听描述符是两个不同的东西,关闭时要分别处理,遗漏已连接套接字会造成文件描述符泄漏。
下面是一段最简服务端骨架代码,展示了从创建到接受连接的基础步骤。真实对话还要在accept之后进入读写循环,这里先给出结构:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main() {
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(listen_fd, 5);
// 单次接受,仅作流程演示
int conn_fd = accept(listen_fd, NULL, NULL);
printf("client connected, fd=%dn", conn_fd);
close(conn_fd);
close(listen_fd);
return 0;
}
阻塞模式下的双向对话实现
在默认的阻塞模式中,read或recv调用会一直等待,直到对端发送数据或者连接关闭才返回。这让我们写对话逻辑非常简单:服务端accept之后进入一个while循环,先读客户端发来的内容,处理后再写回响应,客户端也用类似方式接收回显。这种模型对于教学和理解非常直观,但缺点是一个服务端进程同一时间只能服务一个客户端,除非使用多进程或多线程。
为了支持多客户端,常见做法是每accept一个连接就fork一个子进程,或者创建一个线程去处理该连接的读写。这样每个执行流各自阻塞在自己的read上,互不干扰。不过当并发数达到上千时,线程或进程切换的开销会变得明显,而且共享数据需要加锁,复杂度上升。对于单纯学习对话机制,阻塞加多线程已经足够看清数据流动。
以下代码展示了客户端如何连接服务端并发送一条消息,然后接收服务端的回显。服务端只需在收到后原样写回即可形成对话:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main() {
int fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in srv;
memset(&srv, 0, sizeof(srv));
srv.sin_family = AF_INET;
srv.sin_port = htons(8080);
srv.sin_addr.s_addr = inet_addr("127.0.0.1");
connect(fd, (struct sockaddr*)&srv, sizeof(srv));
char buf[1024];
printf("input: ");
fgets(buf, sizeof(buf), stdin);
write(fd, buf, strlen(buf));
int n = read(fd, buf, sizeof(buf) - 1);
buf[n] = ' ';
printf("server echo: %s", buf);
close(fd);
return 0;
}
使用epoll提升服务端并发对话能力
当我们需要在单进程内同时和大量客户端保持对话,epoll是Linux下高效的事件通知机制。它通过内核维护一个兴趣列表,当某个套接字可读或可写时,epoll_wait会返回对应的事件,避免了在大量连接上盲目轮询。服务端把监听套接字和每个新接受的连接都加入epoll实例,在事件循环中根据events[i].events判断是新连接到来还是某个客户端发来了数据。
使用epoll时通常把套接字设为非阻塞,配合ET(边缘触发)模式可以减少重复通知,但要求一次事件必须把缓冲区数据读尽,否则可能丢失后续通知。对于对话程序,我们可以在可读事件里循环recv直到返回EAGAIN,再把收到的内容直接send回去。这样单线程就能轻松支撑数千并发连接,且代码逻辑仍是清晰的事件驱动结构。
下面的片段演示了epoll服务端的核心循环,省略了错误检查。它把监听fd加入epoll,在循环中处理新连接和已连接套接字的读写,实现了一个可扩展的回显对话服务:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#define MAX_EVENTS 1024
int main() {
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(listen_fd, 5);
int ep = epoll_create1(0);
struct epoll_event ev, events[MAX_EVENTS];
ev.events = EPOLLIN;
ev.data.fd = listen_fd;
epoll_ctl(ep, EPOLL_CTL_ADD, listen_fd, &ev);
while (1) {
int n = epoll_wait(ep, events, MAX_EVENTS, -1);
for (int i = 0; i < n; i++) {
if (events[i].data.fd == listen_fd) {
int conn = accept(listen_fd, NULL, NULL);
fcntl(conn, F_SETFL, O_NONBLOCK);
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = conn;
epoll_ctl(ep, EPOLL_CTL_ADD, conn, &ev);
} else {
char buf[1024];
int m = recv(events[i].data.fd, buf, sizeof(buf), 0);
if (m <= 0) {
close(events[i].data.fd);
} else {
send(events[i].data.fd, buf, m, 0);
}
}
}
}
return 0;
}
从基础阻塞模型到多线程,再到epoll事件驱动,Linux socket实现服务器与客户端对话的路径非常清晰。初学者建议先写通阻塞版单连接回显,理解read和write的字节流本质,再逐步引入并发方案。只要把握住文件描述符、内核缓冲区和事件循环这三个要点,就能应对绝大多数基于TCP的对话类应用开发。
linux_socketTCP通信epoll修改时间:2026-08-15 20:42:16