TCP 服务器整体流程
一个最基础的 TCP 服务器,需要经历以下步骤:
socket() bind() listen() accept() read()/write() close()
流程图可以理解为:
创建套接字 → 绑定端口 → 开始监听 → 等待客户端连接 → 收发数据 → 关闭连接
我们知道 TCP 是连接的、可靠的传输层协议,所以每一个客户端在访问服务器的时候都会建立连接(也就是三次握手)。在客户端没有申请建立连接的时候,服务器要始终保持监听状态(调用系统调用接口 listen),因为用户可能在任意时间对服务器进行访问。一旦客户端进行申请连接(调用系统调用接口 connect),服务器就得与客户端进行连接(调用系统调用接口 accept),然后与客户端进行通信,通信结束后进行关闭。
为什么有 listenfd 还要 accept 生成新的 sockfd?
这是一个关于 TCP 网络编程中的常见问题。我们通过 socket 已经创建了一个 listenfd,为什么还要系统调用 accept 再创建一个 sockfd 呢?
我们可以用生活中的例子来理解:商场的美食区派专人拉客(张三),吸引顾客进店后交给店内员工(李四)接待。张三继续出去拉客,李四负责服务。这样张三一直负责监听新顾客,李四负责具体服务。
这就是 TCP 的设计思想:
listenfd:专门负责接收新连接sockfd:专门负责和客户端通信
单进程版本
TCP 服务端
class Server {
public:
Server(uint16_t server_port, std::string server_ip) : server_port_(server_port), server_ip_(server_ip) { }
void init() {
listenfd_ = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd_ < 0) {
printf("listenfd_ create error!!!\n");
exit(1);
}
printf("listenfd_ create successful!!!, listenfd_:%d\n", listenfd_);
struct sockaddr_in server;
memset(&server, 0, sizeof server);
server.sin_family = AF_INET;
server.sin_port = htons(server_port_);
inet_pton(AF_INET, server_ip_.c_str(), &(server.sin_addr));
if (bind(listenfd_, (struct sockaddr *)&server, sizeof(server)) < 0) {
printf("bind fail !!!\n");
exit(2);
}
printf("bind successful!!!\n");
if (listen(listenfd_, 10) < 0) {
printf("listen fail!!!\n");
exit(3);
}
printf("listen successful!!!\n");
}
void service(int sockfd, std::string client_ip, uint16_t client_port) {
while (1) {
char buffer[1024];
ssize_t s = read(sockfd, buffer, sizeof(buffer) - 1);
if (s > 0) {
buffer[s] = 0;
printf("client say:%s\n", buffer);
std::string info = "server say:";
info += buffer;
write(sockfd, info.c_str(), info.size());
} else if (s == 0) {
printf("client quit !!!\n");
break;
} else {
printf("read error !!!\n");
break;
}
}
}
void start() {
while (1) {
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sockfd = accept(listenfd_, (struct sockaddr *)&client, &len);
if (sockfd < 0) {
printf("accept fail!!!\n");
}
printf("get a new link !!!, sockfd : %d\n", sockfd);
uint16_t client_port = ntohs(client.sin_port);
char client_ip[16];
inet_ntop(AF_INET, &(client.sin_addr), client_ip, sizeof client_ip);
service(sockfd, client_ip, client_port);
close(sockfd);
}
}
~Server() {
close(listenfd_);
}
private:
int listenfd_;
uint16_t server_port_;
std::string server_ip_;
};
TCP 客户端
int main(int argc, char *argv[]) {
if (argc != 3) {
printf("\n\t Usage: %s serverip serverport!\n");
exit(1);
}
uint16_t server_port = atoi(argv[2]);
std::string server_ip = argv[1];
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
printf("sockfd fail !!!\n");
exit(2);
}
printf("sock successful !!!, sockfd : %d\n", sockfd);
struct sockaddr_in server;
memset(&server, 0, sizeof server);
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
inet_pton(AF_INET, server_ip.c_str(), &server.sin_addr);
if (connect(sockfd, (struct sockaddr *)&server, sizeof server) < 0) {
printf("connect fail!!!\n");
}
while (1) {
std::string message;
std::getline(std::cin, message);
write(sockfd, message.c_str(), message.size());
char buffer[1024];
ssize_t s = read(sockfd, buffer, sizeof(buffer) - 1);
if (s > 0) {
buffer[s] = 0;
printf("%s\n", buffer);
} else if (s == 0) {
printf("server close\n");
break;
}
}
close(sockfd);
return 0;
}
这样一个简单的单进程版本的基于 TCP 的 socket 网络编程就成功实现了。但是存在如下问题:由于我们的服务器现在是单进程的,当一个客户端连接到服务器之后,会调用 service 进行服务,而 service 是循环执行,只有当客户端退出的时候,service 才能退出。这就导致我们无法继续监听,另一个客户端访问时没有反应,只有当一个客户端退出之后,这个客户端才得以访问。作为一个服务器,这样的问题是不可以存在的,所以需要改为多进程版本。
多进程版本 TCP 服务器
双 fork
class Server {
public:
Server(uint16_t server_port, std::string server_ip) : server_port_(server_port), server_ip_(server_ip) { }
void init() {
listenfd_ = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd_ < 0) {
printf("listenfd_ create error!!!\n");
exit(1);
}
printf("listenfd_ create successful!!!, listenfd_:%d\n", listenfd_);
struct sockaddr_in server;
memset(&server, 0, sizeof server);
server.sin_family = AF_INET;
server.sin_port = htons(server_port_);
inet_pton(AF_INET, server_ip_.c_str(), &(server.sin_addr));
if (bind(listenfd_, (struct sockaddr *)&server, sizeof(server)) < 0) {
printf("bind fail !!!\n");
exit(2);
}
printf("bind successful!!!\n");
if (listen(listenfd_, 10) < 0) {
printf("listen fail!!!\n");
exit(3);
}
printf("listen successful!!!\n");
}
void service(int sockfd, std::string client_ip, uint16_t client_port) {
while (1) {
char buffer[1024];
ssize_t s = read(sockfd, buffer, sizeof(buffer) - 1);
if (s > 0) {
buffer[s] = 0;
printf("client say:%s\n", buffer);
std::string info = "server say:";
info += buffer;
write(sockfd, info.c_str(), info.size());
} else if (s == 0) {
printf("client quit !!!\n");
break;
} else {
printf("read error !!!\n");
break;
}
}
}
void start() {
while (1) {
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sockfd = accept(listenfd_, (struct sockaddr *)&client, &len);
if (sockfd < 0) {
printf("accept fail!!!\n");
}
printf("get a new link !!!, sockfd : %d\n", sockfd);
uint16_t client_port = ntohs(client.sin_port);
char client_ip[16];
inet_ntop(AF_INET, &(client.sin_addr), client_ip, sizeof client_ip);
pid_t id = fork();
if (id == 0) {
close(listenfd_);
if (fork() > 0) {
exit(0);
}
service(sockfd, client_ip, client_port);
close(sockfd);
exit(0);
}
close(sockfd);
pid_t ret = waitpid(id, nullptr, 0);
}
}
~Server() {
close(listenfd_);
}
private:
int listenfd_;
uint16_t server_port_;
std::string server_ip_;
};
通过这样的方式就可以很好的保证多个客户端都可以享受到服务器的服务。
解释说明:首先我们创建子进程之后,父进程必须关掉 sockfd。这是因为随着客户端的访问人数增多,如果我们不关闭,就会导致文件描述符一直增多,并且我们将任务交给子进程处理之后,父进程就不需要再继续占用这个文件描述符了。而在子进程中,继续创建子进程(孙子进程),然后让子进程直接退出,这样父进程就可以直接获取到退出结果,也就不需要继续等待。对于孙子进程,当它的父进程(也就是子进程)退出之后,孙子进程就会被托孤,交给操作系统进行回收。
简而言之:
1️⃣ 父进程必须关闭 sockfd
否则:
- 文件描述符会不断增加
- 造成资源泄漏
2️⃣ 双 fork 防止僵尸进程
父进程 └── 子进程 └── 孙子进程
当子进程退出:
- 孙子进程变成孤儿进程
- 由 init 接管
- 自动回收
忽略 SIGCHLD 信号
我们还可以不用父进程进行等待,利用信号机制,通过对 SIGCHLD 信号进行忽略,这样子进程在退出之后,也不会出现僵尸进程,同时父进程还可以继续监听。
class Server {
public:
Server(uint16_t server_port, std::string server_ip) : server_port_(server_port), server_ip_(server_ip) { }
void init() {
listenfd_ = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd_ < 0) {
printf("listenfd_ create error!!!\n");
exit(1);
}
printf("listenfd_ create successful!!!, listenfd_:%d\n", listenfd_);
struct sockaddr_in server;
memset(&server, 0, sizeof server);
server.sin_family = AF_INET;
server.sin_port = htons(server_port_);
inet_pton(AF_INET, server_ip_.c_str(), &(server.sin_addr));
if (bind(listenfd_, (struct sockaddr *)&server, sizeof(server)) < 0) {
printf("bind fail !!!\n");
exit(2);
}
printf("bind successful!!!\n");
if (listen(listenfd_, 10) < 0) {
printf("listen fail!!!\n");
exit(3);
}
printf("listen successful!!!\n");
}
void service(int sockfd, std::string client_ip, uint16_t client_port) {
while (1) {
char buffer[1024];
ssize_t s = read(sockfd, buffer, sizeof(buffer) - 1);
if (s > 0) {
buffer[s] = 0;
printf("client say:%s\n", buffer);
std::string info = "server say:";
info += buffer;
write(sockfd, info.c_str(), info.size());
} else if (s == 0) {
printf("client quit !!!\n");
break;
} else {
printf("read error !!!\n");
break;
}
}
}
void start() {
signal(SIGCHLD, SIG_IGN);
while (1) {
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sockfd = accept(listenfd_, (struct sockaddr *)&client, &len);
if (sockfd < 0) {
printf("accept fail!!!\n");
}
printf("get a new link !!!, sockfd : %d\n", sockfd);
uint16_t client_port = ntohs(client.sin_port);
char client_ip[16];
inet_ntop(AF_INET, &(client.sin_addr), client_ip, sizeof client_ip);
pid_t id = fork();
if (id == 0) {
close(listenfd_);
service(sockfd, client_ip, client_port);
close(sockfd);
exit(0);
}
close(sockfd);
}
}
~Server() {
close(listenfd_);
}
private:
int listenfd_;
uint16_t server_port_;
std::string server_ip_;
};
signal(SIGCHLD, SIG_IGN);
让内核自动回收子进程。
多线程版本 TCP 服务器
当客户越来越多时,我们可以通过多线程的方式。注意类成员函数默认隐藏一个 this 指针,而多线程的处理函数中要求函数签名为 void* (*)(void*),因此会造成类型不匹配的问题。为了解决这个问题我们要让它成为一个全局的函数,所以要增加 static。而且全局函数想要调用类内方法是不可以的,所以我们增加了一个 ThreadData 结构体传递 this 指针。
struct ThreadData {
ThreadData(int sockfd, int16_t client_port, std::string client_ip, Server *srv)
: sockfd_(sockfd), client_port_(client_port), client_ip_(client_ip), srv_(srv) { }
int sockfd_;
int16_t client_port_;
std::string client_ip_;
Server *srv_;
};
class Server {
public:
Server(uint16_t server_port, std::string server_ip) : server_port_(server_port), server_ip_(server_ip) { }
void init() {
listenfd_ = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd_ < 0) {
printf("listenfd_ create error!!!\n");
exit(1);
}
printf("listenfd_ create successful!!!, listenfd_:%d\n", listenfd_);
struct sockaddr_in server;
memset(&server, 0, sizeof server);
server.sin_family = AF_INET;
server.sin_port = htons(server_port_);
inet_pton(AF_INET, server_ip_.c_str(), &(server.sin_addr));
if (bind(listenfd_, (struct sockaddr *)&server, sizeof(server)) < 0) {
printf("bind fail !!!\n");
exit(2);
}
printf("bind successful!!!\n");
if (listen(listenfd_, 10) < 0) {
printf("listen fail!!!\n");
exit(3);
}
printf("listen successful!!!\n");
}
void service(int sockfd, std::string client_ip, uint16_t client_port) {
while (1) {
char buffer[1024];
ssize_t s = read(sockfd, buffer, sizeof(buffer) - 1);
if (s > 0) {
buffer[s] = 0;
printf("client say:%s\n", buffer);
std::string info = "server say:";
info += buffer;
write(sockfd, info.c_str(), info.size());
} else if (s == 0) {
printf("client quit !!!\n");
break;
} else {
printf("read error !!!\n");
break;
}
}
}
static void *routine(void *arg) {
pthread_detach(pthread_self());
ThreadData *td = (ThreadData *)arg;
td->srv_->service(td->sockfd_, td->client_ip_, td->client_port_);
close(td->sockfd_);
delete td;
return nullptr;
}
void start() {
while (1) {
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sockfd = accept(listenfd_, (struct sockaddr *)&client, &len);
if (sockfd < 0) {
printf("accept fail!!!\n");
}
printf("get a new link !!!, sockfd : %d\n", sockfd);
uint16_t client_port = ntohs(client.sin_port);
char client_ip[16];
inet_ntop(AF_INET, &(client.sin_addr), client_ip, sizeof client_ip);
pthread_t tid;
ThreadData *td = new ThreadData(sockfd, client_port, client_ip, this);
pthread_create(&tid, nullptr, routine, td);
}
}
~Server() {
close(listenfd_);
}
private:
int listenfd_;
uint16_t server_port_;
std::string server_ip_;
};
三种模型对比总结
| 模型 | 并发能力 | 资源消耗 | 适用场景 |
|---|---|---|---|
| 单进程 | ❌ | 低 | 理解 |
| 多进程 | ✅ | 高 | 中小并发 |
| 多线程 | ✅ | 中 | 常见服务器 |
我们从最简单的单进程版本,一步一步改造成多进程、多线程版本。代码变多了,结构也复杂了,但核心其实很简单:让服务器同时服务多个客户端。单进程能跑起来,多进程能并发,多线程更高效。现在相信我们对 TCP 服务器的整体框架就有了一定清晰的认识了。

