一、TCP网络通信函数
函数 | 返回值 | 参数 | 备注 |
// 创建socket文件描述符(TCP/UDP 服务器/客户端) int socket(int domain, int type, int protocol); | 成功:返回一个非负整数,即新创建的套接字文件描述符。 失败:返回-1,并设置 |
| 简单来说:
备注:
|
// 绑定端口号(TCP/UDP 服务器) int bind(int socket, const struct sockaddr* address, socklen_t address_len); | 成功:返回0。 失败:返回-1,并设置 |
| 备注:
|
//监听 int listen(int sockfd, int backlog); | 成功:返回0。 失败:返回-1,并设置 |
| listen 函数的作用是将套接字从主动连接模式转换为被动监听模式,使其能够接收客户端的连接请求。在调用 listen 函数之前,服务器端的套接字必须已经创建并绑定到一个本地地址和端口号上。调用 listen 函数后,套接字会进入监听状态,等待客户端的连接请求。当客户端发起连接请求时,服务器会将连接请求放入未完成连接队列中,直到服务器调用 accept 函数来接受连接请求,将其从未完成连接队列中移出,并创建一个新的套接字用于与客户端进行通信。 |
//接受客户端连接请求的函数 int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); | 成功:返回0。 失败:返回-1,并设置 |
| accept 函数的作用是从监听套接字的未完成连接队列中提取一个已完成的连接请求,并创建一个新的套接字用于与客户端进行通信。当客户端发起连接请求时,服务器调用 listen 函数将其放入未完成连接队列中。当 accept 函数被调用时,它会从未完成连接队列中取出一个连接请求,将其转换为已完成连接状态,并创建一个新的套接字。新的套接字描述符可以用于与客户端进行数据传输,而原来的监听套接字仍然保持在监听状态,继续接收其他客户端的连接请求。 |
//用于建立主动连接的函数,通常用于客户端程序,用于向服务器发起连接请求。 int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); | 成功:返回0。 失败:返回-1,并设置 |
| connect 函数的作用是向服务器发起一个连接请求。客户端通过指定服务器的 IP 地址和端口号,向服务器发送一个连接请求。如果服务器接受连接请求,connect 函数会返回成功,并且套接字进入连接状态,客户端可以通过该套接字与服务器进行数据传输。如果服务器拒绝连接请求,或者连接超时,connect 函数会返回失败。 |
二、实现字符串回响的服务-客户端
(一)服务端
namespace y_TcpServer
{
const uint16_t default_port = 8080;
const int backlog = 32;
using func_t = std::function<std::string(std::string)>;
class TcpServer
{
public:
TcpServer(const func_t &func, const uint16_t port = default_port)
: _func(func), _port(port), _isrunning(false)
{
}
~TcpServer() {}
// 初始化服务器
void Init()
{
// 1.创建套接字
_listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (_listen_sock < 0)
{
std::cerr << "create socket error: " << strerror(errno) << std::endl;
exit(ErrorCode::SOCKET_ERR);
}
std::cout << "create socket success:" << _listen_sock << std::endl;
// 2.填充结构体
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 清零
local.sin_family = AF_INET;
local.sin_port = htons(_port);
// #define INADDR_ANY ((in_addr_t) 0x00000000)
local.sin_addr.s_addr = INADDR_ANY; // 绑定任意IP地址
// 3.绑定IP地址和端口号
if (bind(_listen_sock, (struct sockaddr *)&local, sizeof(local)))
{
std::cerr << "bind socket error:" << strerror(errno) << std::endl;
exit(ErrorCode::BIND_ERR);
}
// 4.监听
if (listen(_listen_sock, backlog) == -1)
{
std::cerr << "listen error:" << strerror(errno) << std::endl;
exit(ErrorCode::LISTEN_ERR);
}
std::cout << "listen success:" << std::endl;
}
//启动服务器-v0版本,单线程单进程
void Start()
{
while(!_isrunning)
{
//1.处理连接请求
struct sockaddr_in client;
socklen_t len=sizeof(client);
int sock=accept(_listen_sock,(struct sockaddr*)&client,&len);
if(sock<0)
{
std::cerr<<"accept fail"<<strerror(errno)<<std::endl;
continue;
}
//2.连接成功,获取客户端信息
std::string clientIP=inet_ntoa(client.sin_addr);
uint16_t clientPort=ntohs(client.sin_port);
std::cout<<"server accept "<<clientIP+"-"<<clientPort<<" "
<<sock<<" form "<<_listen_sock<<" success!"<<std::endl;
//根据套接字进行通信业务处理
Service(sock,clientIP,clientPort);
}
}
private:
// 业务处理
void Service(int sock, const std::string &clientIP, const uint16_t &clientPort)
{
char buff[1024];
std::string who = clientIP + "-" + std::to_string(clientPort);
while (true)
{
ssize_t n = read(sock, buff, sizeof(buff) - 1);
if (n > 0)
{
buff[n] = '\0';
std::cout << "server get:" << buff << " form" << who << std::endl;
std::string respond = _func(buff);
write(sock, buff, strlen(buff));
}
else if (n == 0)
{
// 表示当前读取到文件末尾了,结束读取
std::cout << "client " << who << " " << sock << " quit!" << std::endl;
close(sock);
break;
}
else
{
std::cerr << "read fail" << strerror(errno) << std::endl;
close(sock);
break;
}
}
}
private:
int _listen_sock;
uint16_t _port;
bool _isrunning;
func_t _func;
};
}
1.功能概述
这段代码实现了一个基于 TCP 协议的简单服务器,其主要功能包括:
- 监听指定端口,接收客户端的连接请求。
- 与客户端建立连接后,接收客户端发送的数据,通过回调函数处理数据,并将结果返回给客户端。
- 支持单线程单进程的简单通信模式。
2.基本成员变量
_listen_sock
:用于监听客户端连接请求的套接字。_port
:服务器监听的端口号,默认为 8080。_isrunning
:控制服务器是否运行的标志。_func
:一个回调函数,用于处理客户端发送的数据并返回响应。
3.初始化服务器(Init
方法)
创建套接字:
- 使用
socket
函数创建一个 TCP 套接字。 - 如果创建失败,打印错误信息并退出程序。
- 成功创建后,打印套接字编号。
填充地址结构体:
- 创建一个
sockaddr_in
结构体,用于存储服务器的 IP 地址和端口号。 - 将 IP 地址设置为
INADDR_ANY
,表示绑定到本机的所有 IP 地址。 - 将端口号设置为
_port
,并使用htons
函数将其转换为网络字节序。
绑定地址和端口号:
- 使用
bind
函数将套接字绑定到指定的地址和端口号。 - 如果绑定失败,打印错误信息并退出程序。
监听连接请求:
- 使用
listen
函数将套接字设置为监听状态,允许的最大连接队列长度为backlog
(默认为 32)。 - 如果监听失败,打印错误信息并退出程序。
4.启动服务器(Start
方法)
循环接收客户端连接:
- 使用
accept
函数接收客户端的连接请求。 - 如果接收失败,打印错误信息并继续下一次循环。
获取客户端信息
- 获取客户端的 IP 地址和端口号,打印连接成功的日志信息。
处理客户端请求
- 调用
Service
方法处理客户端的通信业务。
5.业务处理(Service
方法)
接收客户端数据
- 使用
read
函数从客户端套接字中读取数据。 - 如果读取成功,将读取到的数据存储到缓冲区
buff
中,并打印接收到的数据。
处理数据并响应
- 将读取到的数据传递给回调函数
_func
,获取处理结果。 - 使用
write
函数将处理结果发送回客户端。
处理异常情况
- 如果客户端关闭连接(
read
返回 0),打印日志并关闭套接字。 - 如果读取失败,打印错误信息并关闭套接字。
(二)客户端
namespace y_TcpClient
{
class TcpClient
{
public:
TcpClient(const std::string& ip,const uint16_t port)
:_server_ip(ip),_server_port(port)
{}
~TcpClient(){}
//初始化服务器
void Init()
{
_sock=socket(AF_INET,SOCK_STREAM,0);
if(_sock<0)
{
std::cerr<<"create socket fail"<<strerror(errno)<<std::endl;
exit(ErrorCode::SOCKET_ERR);
}
std::cout<<"create socket success"<<_sock<<std::endl;
}
//启动服务器
void Start()
{
struct sockaddr_in server;
socklen_t len=sizeof(server);
bzero(&server,len);
server.sin_family=AF_INET;
server.sin_port=htons(_server_port);
inet_aton(_server_ip.c_str(),&server.sin_addr);//与inet_addr功能一样
int n=5;
while(n)
{
int ret=connect(_sock,(struct sockaddr*)&server,len);
//连接成功退出重连模式
if(ret==0) break;
//连接失败重连
std::cerr<<"正在进行重新连接...剩余次数: "<<--n<<std::endl;
sleep(2);
}
//表明连接失败
if(n==0)
{
std::cerr<<"连接失败!"<<strerror(errno)<<std::endl;
close(_sock);
exit(ErrorCode::CONNECT_ERR);
}
//连接成功
std::cout<<"连接成功"<<std::endl;
//进行业务处理
Service();
}
private:
void Service()
{
char buff[1024];
std::string who=_server_ip+"-"+std::to_string(_server_port);
while(true)
{
std::string msg;
std::cout<<"Please Enter Message >>";
std::getline(std::cin,msg);
//发送消息给服务器
write(_sock,msg.c_str(),msg.size());
ssize_t n=read(_sock,buff,sizeof(buff)-1);
if(n>0)
{
buff[n]='\0';
std::cout << "Client get: " << "[ " << buff << " ]" << " from " << who << std::endl;
}
else if(n==0)
{
std::cout<<"Server "<<who<<" quit!"<<std::endl;
close(_sock);
break;
}
else
{
//读取异常
std::cerr<<"Read Fail"<<strerror(errno)<<std::endl;
close(_sock);
break;
}
}
}
private:
int _sock;
uint16_t _server_port;
std::string _server_ip;
};
}
初始化服务器(Init
函数):
-
创建套接字:使用
socket
函数创建一个TCP套接字(AF_INET
表示IPv4协议,SOCK_STREAM
表示TCP套接字)。 -
检查套接字创建是否成功:如果
socket
函数返回值小于0,说明创建失败,输出错误信息并退出程序,错误码为ErrorCode::SOCKET_ERR
。 -
如果创建成功,输出创建成功的套接字描述符。
启动服务器(Start
函数):
初始化服务器地址结构sockaddr_in
:
-
使用
bzero
函数将server
结构清零。 -
设置
server.sin_family
为AF_INET
,表示使用IPv4协议。 -
使用
htons
函数将端口号从主机字节序转换为网络字节序,赋值给server.sin_port
。 -
使用
inet_aton
函数将IP地址从点分十进制字符串转换为网络字节序的二进制形式,赋值给server.sin_addr
。
尝试连接服务器:
-
使用
connect
函数尝试连接服务器,最多重试5次(通过变量n
控制)。 -
如果连接成功(
connect
返回0),退出重连模式。 -
如果连接失败(
connect
返回-1),输出重连信息,等待2秒后继续重试。 -
如果重试次数用完仍未连接成功,输出连接失败信息,关闭套接字并退出程序,错误码为
ErrorCode::CONNECT_ERR
。 -
连接成功后,调用
Service
函数进行业务处理。
业务处理(Service
函数):
定义一个字符数组buff
用于接收服务器发送的数据。
构造一个字符串who
,表示服务器的IP和端口信息。
使用while
循环不断接收用户输入的消息并发送给服务器,同时接收服务器的响应:
- 使用
std::getline
函数从标准输入获取用户输入的消息。 - 使用
write
函数将消息发送给服务器。 - 使用
read
函数接收服务器的响应,将接收到的数据存储到buff
中。 - 如果
read
返回值大于0,说明接收到数据,将数据输出到标准输出。 - 如果
read
返回值为0,说明服务器关闭了连接,输出服务器退出信息,关闭套接字并退出循环。 - 如果
read
返回值小于0,说明读取异常,输出错误信息,关闭套接字并退出循环。
(三)服务端改良版本
1、原始版本
//启动服务器-v0版本,单线程单进程
void Start()
{
while(!_isrunning)
{
//1.处理连接请求
struct sockaddr_in client;
socklen_t len=sizeof(client);
int sock=accept(_listen_sock,(struct sockaddr*)&client,&len);
if(sock<0)
{
std::cerr<<"accept fail"<<strerror(errno)<<std::endl;
continue;
}
//2.连接成功,获取客户端信息
std::string clientIP=inet_ntoa(client.sin_addr);
uint16_t clientPort=ntohs(client.sin_port);
std::cout<<"server accept "<<clientIP+"-"<<clientPort<<" "
<<sock<<" form "<<_listen_sock<<" success!"<<std::endl;
//根据套接字进行通信业务处理
Service(sock,clientIP,clientPort);
}
}
原始版本是一个单进程单线程的版本,他只能允许一个客户端进行连接,不适用于与客户端进行连接交互
2、多进程版本
2.1、多进程版本-阻塞式
// 启动服务器-v1版本-多进程1(阻塞式等待)
void Start()
{
while (!_isrunning)
{
// 1.处理连接请求
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sock = accept(_listen_sock, (struct sockaddr *)&client, &len);
if (sock < 0)
{
std::cerr << "accept fail" << strerror(errno) << std::endl;
continue;
}
// 2.连接成功,获取客户端信息
std::string clientIP = inet_ntoa(client.sin_addr);
uint16_t clientPort = ntohs(client.sin_port);
std::cout << "server accept " << clientIP + "-" << clientPort << " "
<< sock << " form " << _listen_sock << " success!" << std::endl;
// 多线程根据套接字进行通信业务处理
pid_t id=fork();
if(id<0)
{
//创建子进程失败,暂时不与当前客户进行通信连接
close(sock);
std::cerr<<"fork fail"<<std::endl;
}
else if(id==0)
{
//子进程不需要监听,关闭
close(_listen_sock);
//根据套接字进行通信业务处理
Service(sock,clientIP,clientPort);
exit(0);
}
else
{
//父进程等待子进程
pid_t ret=waitpid(id,nullptr,0);//默认阻塞式等待
if(ret==id) std::cout<<"wait "<<id<<"sucsess!";
}
}
}
创建子进程处理客户端请求:
子进程(id == 0
):
- 关闭监听套接字
_listen_sock
,因为子进程不需要继续监听新的连接。 - 调用
Service
函数处理客户端的通信业务。 - 处理完成后,调用
exit(0)
退出子进程。
父进程(id > 0
):
- 调用
waitpid
函数等待子进程结束。waitpid
默认是阻塞式的,会阻塞父进程直到子进程退出。 - 如果
waitpid
成功(返回值等于子进程的ID),输出等待成功的信息。
缺陷:
阻塞式waitpid
导致效率低下:
- 父进程在调用
waitpid
时会阻塞,直到子进程退出。这意味着父进程在等待子进程时无法继续处理新的连接请求。 - 这会导致服务器的并发处理能力受限,尤其是在高并发场景下,服务器无法及时响应新的客户端连接。
子进程退出后父进程可能阻塞:
- 如果子进程在处理客户端请求时花费了较长时间,父进程会一直阻塞在
waitpid
函数上,无法及时响应新的连接请求。这会导致服务器的响应速度变慢,影响用户体验。
2.2、多进程版本-非阻塞式
// 启动服务器-v2版本-多进程2(非阻塞式等待)
void Start()
{
while (!_isrunning)
{
// 1.处理连接请求
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sock = accept(_listen_sock, (struct sockaddr *)&client, &len);
if (sock < 0)
{
std::cerr << "accept fail" << strerror(errno) << std::endl;
continue;
}
// 2.连接成功,获取客户端信息
std::string clientIP = inet_ntoa(client.sin_addr);
uint16_t clientPort = ntohs(client.sin_port);
std::cout << "server accept " << clientIP + "-" << clientPort << " "
<< sock << " form " << _listen_sock << " success!" << std::endl;
// 多线程根据套接字进行通信业务处理
pid_t id=fork();
if(id<0)
{
//创建子进程失败,暂时不与当前客户进行通信连接
close(sock);
std::cerr<<"fork fail"<<std::endl;
}
else if(id==0)
{
//子进程不需要监听,关闭
close(_listen_sock);
//根据套接字进行通信业务处理
Service(sock,clientIP,clientPort);
exit(0);
}
else
{
//父进程等待子进程
pid_t ret=waitpid(id,nullptr,WNOHANG);//非阻塞式等待
if(ret==id) std::cout<<"wait "<<id<<"sucsess!";
}
}
}
创建子进程处理客户端请求:
子进程(id == 0
):
- 关闭监听套接字
_listen_sock
,因为子进程不需要继续监听新的连接。 - 调用
Service
函数处理客户端的通信业务。 - 处理完成后,调用
exit(0)
退出子进程。
父进程(id > 0
):
- 调用
waitpid
函数以非阻塞模式等待子进程结束(通过设置WNOHANG
标志)。 - 如果
waitpid
成功(返回值等于子进程的ID),输出等待成功的信息。
缺陷:
阻塞waitpid
的使用问题:
-
虽然
waitpid
被设置为非阻塞模式(WNOHANG
),但父进程在每次循环中都会调用waitpid
。如果子进程尚未退出,waitpid
会立即返回0
,这会导致父进程频繁调用waitpid
,增加CPU的负担。
accept
的阻塞特性:
accept
函数是阻塞式的,它会等待新的客户端连接请求。当有新的连接请求时,accept
返回客户端的套接字描述符。- 在
accept
阻塞期间,父进程无法执行其他操作,包括调用waitpid
来回收子进程资源。
非阻塞waitpid
的局限性:
waitpid
被设置为非阻塞模式(WNOHANG
),这意味着它会在没有子进程退出的情况下立即返回0
。- 如果子进程在
accept
阻塞期间退出,父进程无法及时调用waitpid
来回收子进程资源,从而导致子进程变成僵尸进程。
僵尸进程的产生:
- 子进程退出后,操作系统会保留其状态信息,直到父进程调用
wait
或waitpid
来回收这些信息。如果父进程没有及时调用这些函数,子进程就会变成僵尸进程,占用系统资源。
2.3、多进程版本-信号忽略(推荐)
// 启动服务器-v3版本-多进程3(忽略SIGCHLD信号)
void Start()
{
while (!_isrunning)
{
//0.忽略SIGCHLD信号-SIG_IGN表示忽略,父进程无需等待
signal(SIGCHLD,SIG_IGN);
// 1.处理连接请求
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sock = accept(_listen_sock, (struct sockaddr *)&client, &len);
if (sock < 0)
{
std::cerr << "accept fail" << strerror(errno) << std::endl;
continue;
}
// 2.连接成功,获取客户端信息
std::string clientIP = inet_ntoa(client.sin_addr);
uint16_t clientPort = ntohs(client.sin_port);
std::cout << "server accept " << clientIP + "-" << clientPort << " "
<< sock << " form " << _listen_sock << " success!" << std::endl;
// 多线程根据套接字进行通信业务处理
pid_t id=fork();
if(id<0)
{
//创建子进程失败,暂时不与当前客户进行通信连接
close(sock);
std::cerr<<"fork fail"<<std::endl;
}
else if(id==0)
{
//子进程不需要监听,关闭
close(_listen_sock);
//根据套接字进行通信业务处理
Service(sock,clientIP,clientPort);
exit(0);
}
}
}
在Unix和类Unix系统中,子进程退出时会向父进程发送
SIGCHLD
信号。父进程可以通过处理这个信号来回收子进程的资源,例如调用wait
或waitpid
来获取子进程的状态信息。当SIGCHLD
信号被忽略时,操作系统会自动回收子进程的资源,清理子进程的状态信息。通过忽略SIGCHLD
信号,父进程不需要显式调用wait
或waitpid
,从而避免了僵尸进程的产生。
2.4、多进程版本-修改SIGCHLD信号默认行为-回收子进程
// 启动服务器-v4版本-多进程4(设置SIGCHLD信号处理动作为子进程回收)
static void handler(int signo)
{
printf("进程 %d 捕捉到了 %d 信号\n",getpid(),signo);
pid_t ret=waitpid(-1,NULL,WNOHANG);
if(ret>0) printf("父进程:%d 已经成功回收了 %d 号进程\n", getpid(), ret);
}
void Start()
{
while (!_isrunning)
{
//0.忽略SIGCHLD信号-SIG_IGN表示忽略,父进程无需等待
signal(SIGCHLD,handler);
// 1.处理连接请求
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sock = accept(_listen_sock, (struct sockaddr *)&client, &len);
if (sock < 0)
{
std::cerr << "accept fail" << strerror(errno) << std::endl;
continue;
}
// 2.连接成功,获取客户端信息
std::string clientIP = inet_ntoa(client.sin_addr);
uint16_t clientPort = ntohs(client.sin_port);
std::cout << "server accept " << clientIP + "-" << clientPort << " "
<< sock << " form " << _listen_sock << " success!" << std::endl;
// 多线程根据套接字进行通信业务处理
pid_t id=fork();
if(id<0)
{
//创建子进程失败,暂时不与当前客户进行通信连接
close(sock);
std::cerr<<"fork fail"<<std::endl;
}
else if(id==0)
{
//子进程不需要监听,关闭
close(_listen_sock);
//根据套接字进行通信业务处理
Service(sock,clientIP,clientPort);
exit(0);
}
}
}
2.5、多进程版本-设置孙子进程(推荐)
启动服务器-v5版本-多进程5(设置孙子进程)
void Start()
{
while (!_isrunning)
{
// 1.处理连接请求
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sock = accept(_listen_sock, (struct sockaddr *)&client, &len);
if (sock < 0)
{
std::cerr << "accept fail" << strerror(errno) << std::endl;
continue;
}
// 2.连接成功,获取客户端信息
std::string clientIP = inet_ntoa(client.sin_addr);
uint16_t clientPort = ntohs(client.sin_port);
std::cout << "server accept " << clientIP + "-" << clientPort << " "
<< sock << " form " << _listen_sock << " success!" << std::endl;
// 多线程根据套接字进行通信业务处理
pid_t id=fork();
if(id<0)
{
//创建子进程失败,暂时不与当前客户进行通信连接
close(sock);
std::cerr<<"fork fail"<<std::endl;
}
else if(id==0)
{
//子进程不需要监听,关闭
close(_listen_sock);
//创建孙子进程
if(fork()>0) exit(0);
//根据套接字进行通信业务处理
Service(sock,clientIP,clientPort);
exit(0);
}
else
{
//父进程等待子进程
pid_t ret=waitpid(id,nullptr,0);//阻塞式等待
if(ret==id) std::cout<<"wait "<<id<<"sucsess!"<<std::endl;
close(sock);//子进程进行网络通信后,父进程不需要使用sock套接字了,就可以将其关闭
}
}
}
通过子进程创建孙子进程,使得子进程可以直接退出,防止了父进程因等待子进程而阻塞,也防止了父进程因为被accept阻塞而无法及时回收子进程,使得子进程成为僵尸进程,把孙子进程交给操作系统去管理。
3、多线程版
3.1多线程版本-使用原生线程库
struct ThreadData
{
public:
ThreadData(int sock, const std::string &ip, const uint16_t &port, TcpServer *current)
: _sock(sock), _clientIP(ip), _clientPort(port), _current(current)
{
}
~ThreadData() {}
int _sock;
std::string _clientIP;
uint16_t _clientPort;
TcpServer *_current;
};
//启动服务器-v6版本,多线程
static void* Routiue(void* args)
{
//线程分离
pthread_detach(pthread_self());
ThreadData* td=static_cast<ThreadData*>(args);
td->_current->Service(td->_sock,td->_clientIP,td->_clientPort);
delete td;
return (void*)0;
}
void Start()
{
while(!_isrunning)
{
//1.处理连接请求
struct sockaddr_in client;
socklen_t len=sizeof(client);
int sock=accept(_listen_sock,(struct sockaddr*)&client,&len);
if(sock<0)
{
std::cerr<<"accept fail"<<strerror(errno)<<std::endl;
continue;
}
//2.连接成功,获取客户端信息
std::string clientIP=inet_ntoa(client.sin_addr);
uint16_t clientPort=ntohs(client.sin_port);
std::cout<<"server accept "<<clientIP+"-"<<clientPort<<" "
<<sock<<" form "<<_listen_sock<<" success!"<<std::endl;
//创建线程进行通信业务处理
ThreadData* td=new ThreadData(sock,clientIP,clientPort,this);
pthread_t p;
pthread_create(&p,nullptr,Routiue,td);
}
}
创建线程处理客户端请求:
- 为每个客户端连接创建一个线程来处理通信业务。
- 创建一个
ThreadData
对象,包含客户端的套接字描述符、IP地址、端口号以及指向当前服务器对象的指针。 - 使用
pthread_create
创建线程,并将ThreadData
对象作为参数传递给线程函数Routiue
。
在线程函数Routiue
中:
- 调用
pthread_detach
将线程设置为分离状态,这样线程退出时资源会自动释放。 - 调用
Service
函数处理客户端的通信业务。 - 删除
ThreadData
对象,释放资源。
缺陷:
线程资源管理问题:
- 每个客户端连接都创建一个线程,这在高并发场景下可能会导致线程数量过多,消耗大量系统资源(如线程栈空间、线程调度开销等)。
- 如果客户端连接频繁,可能会导致线程创建和销毁的开销过大,影响服务器性能。
3.2、多线程版本-引入线程池
//进行业务处理
using cb_t =std::function<void(int,std::string,uint16_t)>;
class Task
{
public:
Task()=default;
Task(int sock,const std::string& ip,const uint16_t& port,const cb_t& cb)
:_sock(sock),_ip(ip),_port(port),_cb(cb)
{}
~Task(){}
void operator()()
{
//直接回调cb
_cb(_sock,_ip,_port);
}
private:
int _sock;
std::string _ip;
uint16_t _port;
cb_t _cb;//回调函数->业务处理
};
// 启动服务器-v7版本,引入线程池
void Start()
{
while (!_isrunning)
{
// 1.处理连接请求
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sock = accept(_listen_sock, (struct sockaddr *)&client, &len);
if (sock < 0)
{
std::cerr << "accept fail" << strerror(errno) << std::endl;
continue;
}
// 2.连接成功,获取客户端信息
std::string clientIP = inet_ntoa(client.sin_addr);
uint16_t clientPort = ntohs(client.sin_port);
std::cout << "server accept " << clientIP + "-" << clientPort << " "
<< sock << " form " << _listen_sock << " success!" << std::endl;
// 3.构建任务对象
Task t(sock, clientIP, clientPort, std::bind(&TcpServer::Service, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
// 4.通过线程池操作将对象push到线程池中处理
MyThreadPool<Task>::getInstance()->PushTask(t);
}
}
缺陷:
- 在当前的多线程服务器实现中,客户端的最大连接数受限于线程池中线程的数量。这是因为每个客户端连接都需要一个线程来处理,而线程资源是有限的。在高并发场景下,线程池的线程数量可能会成为性能瓶颈。
- 为了突破这个限制,可以使用高级I/O多路复用技术,如
epoll
(在Linux上)。这些技术允许单个线程同时管理多个文件描述符(如套接字),从而显著提高服务器的并发处理能力。- 使用
epoll
,单个线程可以同时管理成千上万的连接,而不需要为每个连接创建一个线程。这大大减少了线程创建和销毁的开销,提高了服务器的性能。