上一次尝试了一个最简单的TCP通信,但是一般来说,服务器可以和多个主机同时进行通信,所以这次我们要实现的是多客户端和服务器进行通信。
客户端和上次一样,不清楚的可以看看我之前写的客户端和服务端。服务端的变化主要是在接受连接之后,下面就依次来介绍有哪些地方是需要变动的。
目录
一、包装"读写套接字"的功能
因为现在有多个进程或者线程,为了方便了解整体是如何运作的,个人建议把读取套接字、向套接字写内容等功能放到一个函数里。这里我起名为ServiceIO,内容和之前完全一样,只是全都放到ServiceIO这个函数里了。
void ServiceIO(int sock)
{
while (1)
{
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
int s = read(sock, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = 0;
std::cout << "client["<<getpid()<<"]# " << buffer << std::endl;
std::string msg = "服务端收到了客户端的消息: ";
msg.append(buffer);
write(sock, msg.c_str(), msg.size());
}
else if (s == 0)
{
std::cout << "客户端已退出..." << std::endl;
}
else
{
std::cerr << "读取出错..." << std::endl;
break;
}
}
}
二、多进程模式
1、创建子进程
创建子进程使用的是fork函数,返回值为子进程的pid,如果pid>0,代表当前进程是父进程;如果pid = 0,代表当前进程是子进程(子进程自己不会创建新的进程,所以pid = 0)
父进程:把提供服务的任务全部交给子进程,自己呢,就去继续接受下一个连接。由于父进程不需要提供服务,用于提供服务的套接字 new_sock 也就用不上了。
子进程:子进程继承父进程的文件描述符,也就是说子进程会有 监听套接字、用于提供服务的套接字。但是子进程无需去接收新的连接,所以一开始就可以把监听套接字 server 关闭,然后提供服务,提供完服务以后,把 用于提供服务的套接字 new_sock也关闭。
//这里列举出之写的部分内容
for (;;)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int new_sock = accept(server, (struct sockaddr *)&peer, &len);
if (new_sock < 0)
{
continue;
}
//从这里开始是新的内容
int pid = fork();
if (pid == 0)
{
close(server); //server是监听套接字
//子进程
ServiceIO(new_sock);
close(new_sock); //new_sock是用于提供服务的套接字
}
//父进程
close(new_sock); //父进程无需提供服务,关闭new_sock,回到accpet函数处,继续接收下一个连接
}
2、回收子进程
说到子进程,那就需要回收子进程,否则会出现僵尸进程,从而造成内存泄漏。回收子进程的方式有两种,一种是通过wait函数,一种是通过信号。
解决僵尸进程的两种方式(重温waitpid函数、了解17号信号SIGCHLD)_abs(ln(1+NaN))的博客-优快云博客https://blog.youkuaiyun.com/challenglistic/article/details/124627448当子进程退出的时候,会向父进程发送17号信号,我们对17号信号的处理设置为忽视,这样就无需主动回收子进程。
//无视17号信号,无需回收子进程
signal(17,SIG_IGN);
for(;;){
//accept接收客户端连接
//创建子进程,让子进程提供服务
}
3、打印连接到服务端的客户端IP和端口号(非必要)
为了方便看效果,这里打印出连接的客户端IP和端口号。
for(;;){
//accept接收客户端连接
//打印客户端的IP和端口号
const char *client_ip = inet_ntoa(peer.sin_addr);
uint16_t client_port = ntohs(peer.sin_port);
std::cout << "客户端:[" << client_ip << ":" << client_port << "]已连接...." << std::endl;
//创建子进程,让子进程提供服务
}
4、测试结果
测试结果如下
=========================服务端=========================
=========================客户端=========================
三、多线程模式
进程承担的是系统资源,创建的子进程太多,会给系统造成较重的负担。我们可以采用轻量级进程 —— 线程来实现多个客户端和一个服务端的TCP通信。
1、创建新线程
每当我们接收到一个连接,我们就创建一个新线程来提供服务
//这里列举出之写的部分内容
for (;;)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int new_sock = accept(server, (struct sockaddr *)&peer, &len);
if (new_sock < 0)
{
continue;
}
const char *client_ip = inet_ntoa(peer.sin_addr);
uint16_t client_port = ntohs(peer.sin_port);
std::cout << "客户端:[" << client_ip << ":" << client_port << "]已连接...." << std::endl;
//从这里开始是新的内容
pthread_t tid;
pthread_create(&tid,nullptr,ServiceRoutine,(void*)&new_sock);
}
2、线程执行函数
如果你不想写pthread_join来回收子线程,可以采用线程分离的方式。
void* ServiceRoutine(void* args){
pthread_detach(pthread_self()); //分离子线程
int sock = *(int*)args;
ServiceIO(sock);
close(sock);
}
3、测试
测试结果和多进程的测试结果差不太多。