一个客户一个子线程,也是阻塞式网络编程,它的初始化要比一个客户一个进程模型开销要小;但是仍适合于长连接,不适合短连接、并发数不大的情况,尤其不适合pthread_create()的开销大于本身服务的情况;
编程模型
(1)并发服务器1,类似于一个客户一个进程的并发服务器1,它通常阻塞在accept,阻塞返回后派生一个子线程来处理每个客户端,每一个客户一个线程,创建线程的开销比fork()要低,进程的地址空间在线程内共享;(注:fork子进程是拷贝父进程的地址空间,但是写内容时才会申请对应的内存,也就是写时复制的思想,简称COW,而主进程创建的子线程仍与主进程位于同一个地址空间)
(3)并发服务器2,类似于一个客户一个进程的并发服务器2,只不过预先派生一定数量为N的子线程,子线程也同时也监听,当各个客户连接到达时,这些子线程就能够立即为它们服务,无需创建的开销;但是如果连接数等于N时(注,父进程不参与服务),此时子进程将会被使用完,新到的连接需等到一个子线程可用,如果连接数还未到达listen调用的backlog数,三次握手已经完成,但是客户端无法被服务,需要等到子线程执行到accept返回才可被服务,客户端将会明显察觉到服务器在响应时间上的恶化,虽然客户端的connect会立即返回,但是第一个请求在一段时间之后才会被服务器处理;
(4)并发服务器3,它与并发服务器2类似,只不过在accpet加上互斥量,使得accept这段代码成为临界区;由原先的accept争用变成了锁争用,最终只有一个进程阻塞在accept上,也就是临界区只有一个线程阻塞在accept上;
(5)并发服务器4,它使用分发机制;子线程不进行accept调用,统一由父进程accept然后将连接描述符传递(共享)给对应的子线程,然后由子线程为客户端服务,父线程可使用普通的轮转法来选择子线程服务;
(1)TCP是一个全双工的协议,同时支持read()和write();而阻塞式网络编程中,服务器主进程通常阻塞在accept上,而由子线程具体负责与具体的客户端通信,客户端通常阻塞在read系统调用上,等待客户端发来的命令;这样就需要服务端和客户端的编程需要相互配合起来;假设客户端进程由于错误的程序逻辑阻塞在read上,服务器端也阻塞在read上,那么双方出现了通信死锁的情况;
(2)某些客户端继续阻塞的读连接数据,又需要读键盘输入,如果阻塞的读连接数据,那么是无法从键盘读输入的;服务器为每一个连接准备一个线程,一个连接将会独占一个线程,服务器的开销较大;如果客户端不主动退出,将会耗费服务器端的资源;
(3)适合计算响应的工作量大于本身创建开销的服务;
(1)下面是针对并发服务器1的具体实现;
(2)实现的内容是一个echo服务器,由客户端从键盘输入相关内容,发送给服务器,然后由服务器收到后转发至客户端,客户端打印至终端;
(3)服务器不主动断开连接,而由客户端从键盘获得EOF或客户端退出后,服务器也将会退出;
TcpServer服务端实现
TcpServer接口
class TcpServer final { public: TcpServer(const TcpServer&) = delete; TcpServer& operator=(const TcpServer&) = delete; explicit TcpServer(const struct sockaddr_in& serverAddr); void start(); private: static void* _service(void* conn); const int _listenfd; const struct sockaddr_in _serverAddress; bool _started; };说明几点:
(1)与一个客户一个进程接口类似;不同点:只不过少了子进程终止的信号处理程序,以及缺少析构函数,因为只有一个主进程,没有孩子线程产生;这里也并没有处理终止线程的pthread_join方法;可以使用一个列表将所有的pthread搜集起来,最后TcpServer析构的时候来使用pthread_join终止每一个子线程;
(2)_serviceCount将表示TcpServer服务的次数,而不是fork后的子进程服务次数;(注:fork子进程拷贝父进程的地址空间,但是写时才会申请对应的内存,也就是写时复制的思想,简称COW,而主进程创建的子线程仍与主进程位于同一个地址空间)
服务器启动
void TcpServer::start() { assert(!_started); sockets::bind(_listenfd, _serverAddress); sockets::listen(_listenfd); printf("TcpServer start...\n"); _started = true; while (_started) { int connfd = sockets::accept(_listenfd, NULL); //so far, we will not concern client address if (connfd >= 0) { pthread_t tid; ::pthread_create(&tid, NULL, &TcpServer::_service, reinterpret_cast<void *>(connfd)); } else { printf("In TcpServer::_service, open error : %s\n", strerror_r(errno, g_errorbuf, sizeof g_errorbuf)); } } }说明几点:
(1) ::pthread_create创建线程,并将连接描述符,通过参数传入;
(2)与一个客户一个进程不同的是,并不需要在创建线程后关闭connd,因为线程与主进程共享,并不像fork()将增加connd的引用计数;
服务实现
void* TcpServer::_service(void* arg) { ::pthread_detach(::pthread_self()); int connfd = reinterpret_cast<int>(arg); char buf[20]; int n; while ((n = sockets::read(connfd, buf, sizeof buf)) > 0) { sockets::writen(connfd, buf, n); } sockets::close(connfd); return NULL; }
说明几点:
(1)服务内容,主要就是将从客户端读取的内容直接转发至对应的连接,由于read和write属于阻塞操作,可以保证接收到的字节全部转发至客户端;
(3)最后当read到0,说明客户端已经断开连接;服务器执行::exit(0)将会使内核发送Fin报文,服务器端将会从CLOSE_WAIT变为LAST_ACK状态;
TcpClient客户端实现
与一个客户一个进程的TcpClient客户端一样,不再赘述;
版权声明:本文为博主原创文章,未经博主允许不得转载。
原文地址:http://blog.csdn.net/linuxcprimerapue/article/details/47363599