目录
前言
在上一篇文章中使用TCP协议实现了一个简单的服务器,可以用来服务端和客户端通信,但是之前的服务器存在一个问题,就是当有多个客户端连接服务器的时候,服务器只能和一个客户端通信,其它的客户端是无法通信的,这是为什么呢?有该如何解决呢?将在这篇文章中为大家介绍
1.存在的问题
如图所示:
为什么会存在这样的问题呢?
如上图所示,之前我们实现的服务器是单进程版的,所以当第一次和客户端建立连接完成之后,就死循环处理读取信息的逻辑了,所以就无法再和新的客户端建立连接了 。找到问题之后,很明显解决方式就是将建立连接和通信分开执行,此时我们就可以使用多进程和多线程来解决了,下面我们就具体来实现以下如何使用多进程和多线程。
2.多进程版
实现思路:与客户端建立连接完成,fork创建子进程,让父进程继续建立连接,让子进程实现后续的数据通信。
这样实现存在的问题:当父进程创建完子进程之后,需要使用waitpid回收子进程的资源,否则子进程就会变为僵尸进程,导致资源泄漏,但是使用waitpid回收子进程资源,程序变为串行化执行了,就无法实现之前的需求,让父进程负责建立连接,子进程负责数据通信了。
解决方式有两种:
1.fork创建子进程,在子进程内部再fork创建子进程,然后让父进程直接退出,此时之前的子进程作为父进程退出,新创建的子进程就变为孤儿进程被操作系统领养,并不会造成资源泄漏,并且让该子进程负责通信
2.因为子进程退出之后操作系统会发送一个SIGCHLD信号,所以可以使用signal函数捕捉SIGCHLD信号,将默认处理动作设置为SIG_IGN,在这个默认动作里会回收子进程的资源,并不会造成资源泄漏,并且让该子进程负责通信
思路1代码:
cpp
pid_t id = fork();
if(id == 0)//child
{
close(_sock);
if(fork() > 0)
exit(0);
serviceIO(sock);
close(sock);
exit(0);
}
close(sock);
waitpid(id,nullptr,0);
思路2代码:
cpp
signal(SIGCHLD,SIG_IGN);
if(id == 0)//child
{
// 子进程会继承父进程的文件描述符表,当子进程不需要时进行关闭,
// 防止子进程文件描述符资源泄露
close(_sock);
serviceIO(sock);
close(sock);
exit(0);
}
运行截图:
[myl@VM-8-12-centos tcp]$ ./tcpServer 8080
create socket success
bind socket success
listen socket success
accept a new link success
sock: 4
accept a new link success
sock: 4
recvice message: 你好,我是客户端1
recvice message: 你好,我是客户端2
此时就实现了一个客户端可以被多个服务端连接并且实现通信。
3.多线程版
说明:相比于多进程,多线程的创建和销毁对操作系统是更轻量的,消耗的资源也是更少的,所以实现数据通信可以采用多线程的方式,让主线程负责建立,让从线程负责数据通信
代码实现:
cpp
class TcpServerData
{
public:
TcpServerData(TcpServer* self,int sock)
:_self(self),_sock(sock) {}
public:
TcpServer* _self;
int _sock;
};
cout << "我是主线程" << endl;
pthread_t tid;
TcpServerData* tsd = new TcpServerData(this,sock);
pthread_create(&tid,nullptr,start_routine,tsd);
//因为是类内成员函数,必须包含this指针,但是start_routine作为参数是没有this指针的
//所以start_routine函数必须加上static,静态成员方法是不能访问类内成员的,所以参数传递this
//调用serviceIO,但是serviceIO函数需要传递参数sock,所以可以封装一个结构体,在结构体中包含成员
//属性sock和this
static void* start_routine(void* args) {
//设置与主线程分离,此时主线程不需要等待从线程退出了,而是继续建立连接
cout << "我是从线程" << endl;
pthread_detach(pthread_self());
TcpServerData* t = static_cast<TcpServerData*>(args);
t->_self->serviceIO(t->_sock);
close(t->_sock);
delete t;
return nullptr;
运行截图:
[myl@VM-8-12-centos tcp]$ ./tcpServer 8080
create socket success
bind socket success
listen socket success
accept a new link success
sock: 4
我是主线程
我是从线程
recvice message: 你好,我是客户端1
accept a new link success
sock: 5
我是主线程
我是从线程
recvice message: 你好,我是客户端2
4.线程池版
说明:线程池版的实现思路是基于多线程,虽然多线程创建和销毁的消耗比多进程的低,但是为了更进一步提升效率,可以预先创建好一批线程,主线程负责建立连接获取任务,然后将任务加入到队列中,让预先创建好的线程从队列中获取任务,然后处理获取到的任务。
代码实现:
cpp
void start()
{
//线程池初始化:预先创建好一批线程:
ThreadPool<Task>::getInstance()->run();
for (;;)
{
// 建立连接:
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(_sock, (struct sockaddr *)&peer, &len);
if (sock < 0)
{
logMessage(ERROR, "accept error, next");
continue;
}
logMessage(NORMAL, "accept a new link success");
std::cout << "sock: " << sock << std::endl;
//未来通信全部用sock,面向字节流的,后续全部都是文件操作:
ThreadPool<Task>::getInstance()->push(Task(sock,serviceIO));
}
}
运行截图:
[myl@VM-8-12-centos tcp]$ ./tcpServer 8080
create socket success
bind socket success
listen socket success
thread-1 start ...
thread-2 start ...
thread-3 start ...
thread-4 start ...
thread-5 start ...
thread-6 start ...
thread-7 start ...
thread-8 start ...
thread-9 start ...
thread-10 start ...
accept a new link success
sock: 4
accept a new link success
sock: 5
recv message: 你好,我是客户端1
recv message: 你好,我是客户端2
注:关于线程池详细的设计与实现可以观看线程池这篇文章,里面有相信的代码实现
总结
以上就是关于TCP服务器实现多进程版,多线程版,线程池版的详细介绍,可以通过这篇文章发现之前在系统中学习的知识在网络中全部结合起来了,今天的介绍就到这里了,感谢大家的阅读,我们下次再见!