C++实现基于单线程单客户模型的echo程序

        一个客户一个子线程,也是阻塞式网络编程,它的初始化要比一个客户一个进程模型开销要小;但是仍适合于长连接,不适合短连接、并发数不大的情况,尤其不适合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客户端一样,不再赘述;





  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值