1 客户端框架
客户端就是使用qt编程,例程分析:
https://blog.csdn.net/cainiaofu/article/details/114313526
2 服务器框架
2.1 服务器框架的选择
可以采用的方案:多线程,多进程。多路IO复用
1)、如果使用多进程服务器,会遇到的问题:
用来存储用户登录信息的链表,应该是只有一份的,但是要给所有的进程共享(共享数据链表)。尽管,我们可以使用共享内存的方式来实现,但是,不方便。
2)、而采用多线程服务器,那么,这个链表就可以共享了。而且,一个客户端对应一个线程(有自己的read_buf),各个线程之间独立,问题也就独立。
3)、采用多路IO复用的方式,同样也可以共享数据链表,但是存在的问题是(这个问题,在自己写的简单历程中不会存在,在实际应用中,会存在):
当我们不能一次性的将整个包读出来的时候(eg:网络问题),如果采用阻塞的方式,会卡在read()处,直到有包尾的到达,这会直接导致对其他客户端发来请求的处理。当然,我们也可以采用非阻塞的方式读(如果一次没有读到包尾,直接跳转到epoll_wait继续监听后半部分),但是这样会存在另外一个问题。假设:在后半个包到达之前,又来了一个新的数据包。然后跳转到read(sockfd,read_buf,100);,可是这个read_buf里面已经存放的之前客户端的半个包,这个包的数据就会被覆盖。
我们可以对每个客户端都定义一个read_buf,这样,就可以解决read_buf覆盖问题。但是,又会引入一个新的问题,就是多个read_buf的管理问题。每次,当我们要使用read()函数的时候,都要找到客户端对应的那个read_buf,这就需要遍历多个read_buf。如果如果这个问题得不到优化,那么,多路IO复用的一个优点:io效率不会随着文件按描述符数目的增加而线性的下降将不复存在。
综上,我们的服务器采用多线程的方式的构建。
2.2服务器框架的编写
1)、采用多线程的模式写服务器,这里,我们用到了线程池。
为什么要用线程池呢?
在多线程并发服务器中,都是accept();
有客户端连接之后,再:pthread_create();
但是,这样会存在一个问题:
假设创建一个线程(pthread_create())耗时1ms,那么,当同时有1000个客户端同时连接服务器的时候,服务器就要用1S来创建线程。但是同时监听数是有上限的(eg:128)。这样,如果来不及给每个请求连接的客户端创建线程,那么,机会导致有线程无法连接。
2)、为了解决这个问题,我们使用线程池.
3 线程池
文字分析:
a、线程池创建好以后,我们让线程池中的进程阻塞在某个信号量等待。
刚开始,这个信号量设置为0,这样,所有的线程就会阻塞等待。
b、那什么时候唤醒线程呢?当有一个客户端连接进来以后,就sem_post()。给信号量+1,加1以后,所有的线程就会来争夺这个资源,谁抢到,谁就去处理客户端的请求。
c、这样,我们就剩去了创建线程的时间消耗。因为我们在开始的时候,就已经完成了。
还有另外一种线程池的创建方式:
这种方式,就是不再将accept()放在住线程,而是放在线程池中的线程中去执行。
这种模型,一旦有一个请求过来,就会有一个线程去处理回应的请求。这个线程,就作为这个请求的使用者,用来处理这个请求。这种方式,不需要信号量。而是通过调用多次accept()来实现。
两种方法的对比
这种模型更高效,因为他可以保证一个请求来了,它可以马上accept();而第一种,只是在主线程accept();accept()之后,还要再sem_post()。然后,才会有对应的sem_wait去处理对应的套接字。套接字在传递的过程中,会涉及到同步问题。因此,这种方法,主要是在同步的地方,们有一些开销。
而后者,可以直接使用accept()返回的套接字,不设计涉及套接字从主线程到子线程的传递问题。
注:
早期linux版本,第二种方法在问题:惊群效应
在没有请求到来的时候,所有的线程都处于睡眠状态;现在突然来了一个请求,所有的线程都会被唤醒,都会accept();这样,就会导致CPU在这一时候的使用率迅速升高。后来的linux版本,已经解决了。
可以在accept()之前加一把互斥锁,哪个线程抢到这把锁,就调用accept().等到accept()返回了,再将锁解开!!其他线程又可以争夺互斥锁。