在上一篇博客中,我们基于 UDP 实现了一个简单的群聊模型。
今天,我们正式进入 TCP 网络编程,实现一个最经典的功能 ——
🧾 服务器回显(Echo Server)
就是我们发送的消息,服务器不做处理,直接给我们返回即可。
一、TCP 服务器整体流程
一个最基础的 TCP 服务器,需要经历以下步骤:
socket() bind() listen() accept() read()/write() close()
流程图可以理解为:
创建套接字 → 绑定端口 → 开始监听 → 等待客户端连接 → 收发数据 → 关闭连接
我们都知道TCP是连接的,可靠的传输层协议,所以每一个客户端在访问服务器的时候都会建立连接(也就是我们课本上说的三次握手),在客户端没有申请建立连接的时候,服务器要始终保持这监听状态(调用系统调用接口listen)(因为用户可是一天24小时内任意时间都有可能对服务器进行访问,所以服务器必须始终保持这监听状态,这就好比我们半夜不睡觉,就是刷抖音短视频,我们可从来没有打不开抖音的时候,这就是因为服务器保持着监听状态,即使你半夜进行访问,也可以与你进行连接,不会妨碍你半夜刷抖音),一旦客户端进行申请连接(调用系统调用接口connect),服务器就得与客户端进行连接(调用系统调用接口accept),然后与客户端进行通信,通信结束后进行关闭,这就是一个TCP服务器整体的简单流程。
为什么有 listenfd 还要 accept 生成新的 sockfd?
现在我们先来回答一个关于TCP网络编程中的一个常见的问题就是,为什么我们通过socket已经创建了一个listenfd_,为什么还要系统调用accept再创建一个sockfd呢?
我们用生活中一个简单的例子进行举例,相信大家都和自己的好朋友去过万达商场吧,万达商场的最上面两层都是美食,等你们玩累了进行品尝,而我们经常会看到有一些门店会派一些人拿着店里的传单或者食物,吸引顾客来它们餐厅进行吃饭,现在我们假设这个人叫做张三,这个张三负责的就是进行拉客的工作,现在张三凭借着自己的三寸不烂之舌吸引了一位顾客,将这个顾客带到了它们店里,然后赶紧招呼店里的员工,比如李四,来了两位客人,你来接待以下,然后李四就带着这两位顾客找了一个座位,然后给这两位顾客倒水点菜等等服务,这个时候张三则继续出去外面进行拉客的活,吸引到新的顾客之后,再将其带到店里,再叫王五出来接待贵客,然后王五就像李四一样给顾客提供服务,然后张三又继续出去接客,以此类推。
这就是为什么我们通过socket已经创建了一个listenfd_,为什么还要系统调用accept再创建一个sockfd,这就是因为listenfd_就是上面的张三,而sockfd就是上面的李四,王五等等服务员,我们的TCP是基于连接的协议,所以我们就需要像张三一样的人一直进行监听,一旦有人进行访问的时候,我们就需要再安排一个人进行服务,然后张三继续监听,等到又有新人来的时候,再安排一个人进行服务,这样我们的服务器就可以对每一个客户端保持连接,同时让所有的客户端都可以享受到服务器的服务。
这就是 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_;
};
int main()
{
Server srv = Server(8080, "0.0.0.0");
srv.init();
srv.start();
return 0;
}
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服务端,必须保证一个客户端访问的同时,另一个客户端也可以进行访问。
多进程版本 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);
//单进程版本
// service(sockfd, client_ip, client_port);
// close(sockfd);
//多进程版本
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(也就是李四对应的文件描述符),这是因为随着客户端的访问人数增多,如果我们不关闭,就会导致文件描述符一直增多,并且我们将我们的任务交给子进程处理之后,父进程就不需要再继续占用这个文件描述符了,全权交给子进程就可以了,这样就可以缓解父进程文件描述符一直增多的问题。
而在我们的子进程中,我为什么要继续创建子进程呢?这是因为我们创建完子进程之后,父进程需要对子进程的退出状态进行等待回收,这就会导致我们的父进程被阻塞,影响我们与下一个客户端的连接,因此我们可以直接在子进程中再创建子进程(孙子进程),然后让子进程直接退出,这样父进程就可以直接获取到退出结果,也就不需要继续等待,而对于孙子进程,当它的父进程(也就是子进程)退出之后,孙子进程就会被托孤,交给我们的操作系统进行回收,所以我们不必担心孙子进程会成为僵尸进程。这样就实现了一个多进程版本的TCP服务端。
简而言之就是如下:
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()
{
while (1)
{
signal(SIGCHLD, SIG_IGN);
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);
// 多进程版本
// 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);
// 信号
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 服务器
但是当客户越来越多时,我们通过创建多线程的方式实在很消耗资源,所以我们还可以通过多线程的方式。
class Server;
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)
{
signal(SIGCHLD, SIG_IGN);
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);
// 多进程版本
// 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);
// 信号
// pid_t id = fork();
// if (id == 0)
// {
// close(listenfd_);
// service(sockfd, client_ip, client_port);
// close(sockfd);
// exit(0);
// }
// close(sockfd);
// 多线程版本
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服务器,我们要注意类成员函数默认隐藏一个 this 指针:
void routine(Server* this, void*)
而我们的多线程的处理函数中要求函数签名为:
void* (*)(void*)
因此就会造成类型不匹配的问题,为了解决这个问题我们要让它成为一个全局的函数,所以要增加static:
static void* routine(void*)
而且全局函数想要调用类内方法是不可以的,所以我们增加了一个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_;
};
pthread_t tid;
ThreadData *td = new ThreadData(sockfd, client_port, client_ip, this);
pthread_create(&tid, nullptr, routine, td);
三种模型对比总结
| 单进程 | ❌ | 低 | 理解 |
| 多进程 | ✅ | 高 | 中小并发 |
| 多线程 | ✅ | 中 | 常见服务器 |
到这里,我们从最简单的单进程版本,一步一步改造成多进程、多线程版本。
代码变多了,结构也复杂了,但核心其实很简单:
让服务器同时服务多个客户端。
单进程能跑起来,多进程能并发,多线程更高效。
现在相信我们对 TCP 服务器的整体框架就有了一定清晰的认识了。
网硕互联帮助中心






评论前必须登录!
注册