TCP Echo Server 深度解析:从单进程到线程池的演进之路(中)-CSDN博客
1. 四大版本演进
1.0 Service() ------ 核心服务逻辑
void Service(int sockfd, InetAddr &peer)
{
char buffer[1024];
while (true)
{
ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = 0;
LOG(LogLevel::DEBUG) << peer.StringAddr() << " say#" << buffer;
std::string echo_string = "echo# " + buffer;
write(sockfd, echo_string.c_str(), echo_string.size());
}
else if (n == 0) // 对端关闭连接
{
LOG(LogLevel::DEBUG) << peer.StringAddr() << "退出了...";
close(sockfd);
break;
}
else // 读取错误
{
LOG(LogLevel::DEBUG) << peer.StringAddr() << "异常了...";
close(sockfd);
break;
}
}
}
read 返回值含义:
n > 0: 读取到 n 字节数据
n == 0: 对端关闭连接(读到文件结尾,类似 pipe)
n < 0: 读取错误
1.1 Version 0 ------ 单进程(测试版本)
// 直接在 Run() 中调用 Service
Service(sockfd, addr);

同一时刻只能服务一个客户端,后续连接必须等待。
如果
Service内部是一个while循环,那么后续连接永远得不到处理。单进程只适合教学演示,毫无实用价值。
1.2 Version 1 ------ 多进程版本
pid_t id = fork();
if (id < 0) {
// fork 失败
} else if (id == 0) {
// 子进程
close(_listensockfd); // 子进程不需要监听套接字
if (fork() > 0) // 再次 fork,子进程退出
exit(OK); // 父进程(第一代)等待这个子进程
// 孙子进程成为孤儿进程,由 init 进程收养
Service(sockfd, addr);
exit(OK);
} else {
// 父进程
close(sockfd); // 父进程不需要通信套接字
waitpid(id, nullptr, 0); // 非阻塞等待(子进程立即退出)
}
为什么要 fork 两次?

为什么要两次 fork?
第一次 fork 创建子进程,**之后子进程立刻再 fork 一个孙子进程,**然后子进程退出。
效果 :孙子进程的父进程变成 init(PID=1),由 init 自动回收。
好处 :父进程(原始服务器)中的
waitpid只会等待子进程(很短的生命周期),不会阻塞。孙子进程成为孤儿,独立提供服务,服务器父进程无需关心它的回收问题,从而可以继续accept新连接。同时注意:
子进程关闭
_listensockfd:防止文件描述符泄漏,且不需要。父进程关闭
sockfd:每个连接的文件描述符由服务进程持有,父进程只负责监听。多进程缺点
每个连接一个进程,进程切换开销大。
大并发会导致系统创建大量进程,资源紧张。
1.3 Version 2 ------ 多线程版本
class ThreadData {
public:
int sockfd;
InetAddr addr;
TcpServer *tsvr;
ThreadData(int fd, InetAddr &ar, TcpServer *s)
: sockfd(fd), addr(ar), tsvr(s) {}
};
static void *Routine(void *args) {
pthread_detach(pthread_self()); // 分离线程,自动回收
ThreadData *td = static_cast<ThreadData *>(args);
td->tsvr->Service(td->sockfd, td->addr);
delete td;
return nullptr;
}
// 在 Run() 中:
ThreadData *td = new ThreadData(sockfd, addr, this);
pthread_t tid;
pthread_create(&tid, nullptr, Routine, td);
线程 vs 进程 :
| 特性 | 多进程 | 多线程 |
|---|---|---|
| 资源开销 | 大(独立地址空间) | 小(共享地址空间) |
| 切换开销 | 大(需切换页表) | 小(共享页表) |
| 通信方式 | IPC(管道、共享内存等) | 直接共享变量 |
| 稳定性 | 一个进程崩溃不影响其他 | 一个线程崩溃可能拖垮整个进程 |
| 创建速度 | 慢(fork 需拷贝页表) | 快(只需分配栈) |
-
线程比进程轻量,共享地址空间,传递数据更方便。
-
需注意线程安全(例如共享的日志模块用了互斥锁)。
静态成员函数 Routine 的作用
pthread_create 要求线程入口函数具有 C 语言风格 void*(*)(void*),不能直接是成员函数(因为有隐含 this)。
所以我们定义一个静态成员函数,将 this 通过 ThreadData 传入,在内部再调用真正的 Service。
static void* Routine(void *args) {
pthread_detach(pthread_self()); // 分离线程,自动回收
ThreadData *td = static_cast<ThreadData*>(args);
td->tsvr->Service(td->sockfd, td->addr);
delete td;
return nullptr;
}
另外调用了 pthread_detach(pthread_self()),使线程结束后自动释放资源,避免需要 pthread_join。
1.4 Version 3 ------ 线程池版本
// 线程池适合处理"短服务"(快速响应的任务)
ThreadPool<task_t>::GetInstance()->Enqueue([this, &sockfd, &addr](){
this->Service(sockfd, addr);
});
线程池的优势 :


-
预先创建一批线程,从任务队列取任务执行。
-
任务用 lambda 表达式封装,捕获
sockfd和addr(注意按值捕获,因为变量会出作用域)。 -
**适合处理短服务,**如 HTTP 请求;对于长连接(比如聊天),线程池中的线程仍会被长期占用,此时多线程模型更合适。
1.5 客户端需要 bind 吗?

1.6 客户端不需要 listen/accept

总结细节:

2. 相关代码
2.1 TcpServer.cc
#include "Tcpserver.hpp"
#include "Log.hpp"
using namespace LogModule;
void Usage(std::string proc)
{
std::cerr << "Usage: " << proc << " port" << std::endl;
}
// ./tcoserver port
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = std::stoi(argv[1]); //端口号
Enable_Console_Log_Stratege();
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port);
tsvr->Init();
tsvr->Run();
return 0;
}
2.2 TcpServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <memory> //可能会用到智能指针
#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"
#include <sys/wait.h>
#include <pthread.h>
#include "ThreadPool.hpp"
#include <signal.h>
using namespace LogModule;
using namespace ThreadPoolModule;
using task_t = std::function<void()>;
using func_t = std::function<std::string(const std::string&)>;
const static int defaultsockfd = -1;
const static int backlog = 8;
// 服务器往往是禁止拷贝的
class TcpServer : public NoCopy
{
public:
TcpServer(uint16_t port, func_t func)
: _port(port),
_listensockfd(defaultsockfd),
_isrunning(false),
_func(func)
{
}
void Init()
{
// signal(SIGCHLD,SIG_IGN); //忽略
// 1.创建套接字
_listensockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_listensockfd < 0)
{
LOG(LogLevel::DEBUG) << "socket error";
exit(SOCKET_ERR);
}
LOG(LogLevel::INFO) << "socket success: " << _listensockfd; // 3
// 2.bind总所周知的端口号
InetAddr local(_port);
int n = bind(_listensockfd, local.NetAddrPtr(), local.NetAddrLen());
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind error";
exit(BIND_ERR);
}
LOG(LogLevel::INFO) << "bind success: " << _listensockfd; // 3
// 3.设置socket状态为listen
n = listen(_listensockfd, backlog);
if (n < 0)
{
LOG(LogLevel::FATAL) << "listen error";
exit(LISTEN_ERR);
}
LOG(LogLevel::INFO) << "listen success: " << _listensockfd;
}
class ThreadData
{
//InetAddr无需构造
public:
ThreadData(int fd, InetAddr &ar, TcpServer *s) : sockfd(fd), addr(ar), tsvr(s)
{
}
public:
int sockfd;
InetAddr addr;
TcpServer *tsvr;
};
//长服务 -- 你不退出,它不退:多进程多线程合适
void Service(int sockfd, InetAddr &peer)
{
char buffer[1024];
while (true)
{
// 1.先读取数据
// a.n > 0 读取成功
// b.n < 0 读取失败
// c.n ==0 对端把链接关闭,读到了文件结尾 -- pipe
ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = 0; // 设置为C风格的字符串 n<=sizeof(buffer)-1
LOG(LogLevel::DEBUG) << peer.StringAddr() << " say#" << buffer;
// 2.写回数据
std::string echo_string = "echo# ";
echo_string += buffer;
write(sockfd, echo_string.c_str(), echo_string.size());
}
else if (n == 0)
{
LOG(LogLevel ::DEBUG) << peer.StringAddr() << "退出了...";
close(sockfd);
break;
}
else
{
LOG(LogLevel ::DEBUG) << peer.StringAddr() << "异常了...";
close(sockfd);
break;
}
}
}
static void *Routine(void *args)
{
pthread_detach(pthread_self());
ThreadData *td = static_cast<ThreadData *>(args);
td->tsvr->Service(td->sockfd, td->addr);
delete td;
return nullptr;
}
void Run()
{
_isrunning = true;
while (_isrunning)
{
// a.服务器必须先获取链接
struct sockaddr_in peer;
socklen_t len = sizeof(sockaddr_in);
// 如果没有连接,accept就会阻塞
int sockfd = accept(_listensockfd, CONV(peer), &len);
if (sockfd < 0)
{
LOG(LogLevel::WARNING) << "accept error";
continue;
}
InetAddr addr(peer);
LOG(LogLevel::INFO) << "accept success, peer addr: " << addr.StringAddr();
// version0 -- test version -- 单进程程序 -- 不会存在的
// Service(sockfd, addr);
// version1 -- 多进程版本
// pid_t id = fork(); //父进程
// if (id < 0)
// {
// LOG(LogLevel::FATAL) << "fork error";
// exit(FORK_ERR);
// }
// else if (id == 0)
// {
// // 子进程,子进程除了看到 sockfd,能看到listensocket?
// //我们不想让子进程访问listensocket
// close(_listensockfd);
// if(fork() > 0) //再次fork,子进程退出
// exit(OK);
// Service(sockfd,addr); //孙子进程,孤儿进程,1,系统回收
// exit(OK); //正确处理完任务
// }
// else
// {
// // 父进程
// close(sockfd);
// //父进程是不是要等待子进程?否则会僵尸
// pid_t rid = waitpid(id,nullptr,0); //阻塞?不会,因为子进程立马退出了
// (void)rid;
// }
// version2:多线程版本
// ThreadData *td = new ThreadData(sockfd, addr, this);
// pthread_t tid;
// pthread_create(&tid, nullptr, Routine, td);
// version3: 线程池版本,线程池一般适合处理短服务
//将新链接和客户端构建一个新任务,push到线程池中
ThreadPool<task_t>::GetInstance()->Enqueue([this,&sockfd,&addr](){
this->Service(sockfd,addr);
});
}
_isrunning = false;
}
~TcpServer() {}
private:
uint16_t _port;
int _listensockfd; // 监听套接字
bool _isrunning;
func_t _func; // 设置回调处理
};
2.3 TcpClient.cc
#include <iostream>
#include "Common.hpp"
#include "InetAddr.hpp"
void Usage(std::string proc)
{
std::cerr << "Usage: " << proc << " server_ip server_prot" << std::endl;
}
// ./tcpclient serveer_ip server_port
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
// 1.创建套接字socket
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
std::cerr << "socket error" << std::endl;
exit(SOCKET_ERR);
}
// 2.需要bind吗? --> 需要!
// 是否需要显式绑定? --> 不需要,采用随即方式选择端口号
// 需要listen?accpet? --> 不需要
// 直接向目标服务器发起建立连接的请求
InetAddr serveraddr(serverip,serverport);
int n = connect(sockfd,serveraddr.NetAddrPtr(),serveraddr.NetAddrLen());
if( n < 0)
{
std::cerr << "connect error" << std::endl;
exit(CONNECT_ERR);
}
//3.echo client
while(true)
{
std::string line;
std::cout << "Please Enter@ ";
std::getline(std::cin,line);
write(sockfd,line.c_str(),line.size());
char buffer[1024];
ssize_t size = read(sockfd,buffer,sizeof(buffer));
if(size > 0)
{
buffer[size] = 0;
std::cout << "server echo# " << buffer << std::endl;
}
}
close(sockfd);
return 0;
}
2.4 Makefile
.PHONY:all
all:tcpclient tcpserver
tcpclient:TcpClient.cc
g++ -o $@ $^ -std=c++17
tcpserver:TcpServer.cc
g++ -o $@ $^ -std=c++17
.PHONY:clean
clean:
rm -f tcpclient tcpserver