您的位置:首页 > 理论基础 > 计算机网络

【网络模型】一个客户一个线程

2015-01-28 17:07 204 查看
一个客户一个子线程,也是阻塞式网络编程,它的初始化要比一个客户一个进程模型开销要小;但是仍适合于长连接,不适合短连接、并发数不大的情况,尤其不适合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示意图



特点

(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客户端一样,不再赘述;

代码运行

完整目录

|-- Net

| |-- CMakeLists.txt

| |-- SocketsOps.cc

| |-- SocketsOps.h

|-- test2

| |-- CMakeLists.txt

| |-- TcpClient.cc

| |-- TcpClient.h

| |-- TcpServer.cc

| `-- TcpServer.h

(注,其中Net目录下,本程序目前只需要使用SocketsOps.h,SocketsOps.cc)

编译构建环境

线程模型:posix

gcc 版本: 4.9.0 (GCC),注(支持C++11 )

Cmake版本:cmake 2.8.0

完整代码下载链接

https://github.com/sykpour/MyCode/tree/master/Linux/test2

(注,本节关键代码位于test2中)

附录(整体关键代码)

服务器端TcpServer.cc如下:

//Author:sykpour (http://blog.csdn.net/skyuppour)
//Date: 2015.01.28

#include <Net/SocketsOps.h>
#include <test2/TcpServer.h>

#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <pthread.h>
#include <assert.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>

using namespace Net;

namespace
{
char g_errorbuf[512];
}

TcpServer::TcpServer(const struct sockaddr_in& serverAddr):
_listenfd(sockets::createBlockingSocket()),
_serverAddress(serverAddr),
_started(false)
{
assert(_listenfd >= 0);
sockets::setReuseAddr(_listenfd);
}

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)); } } }
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; }

const int SERV_PORT = 6000;

int main(void)
{
struct sockaddr_in serverAddr;
::memset(&serverAddr, 0, sizeof serverAddr);
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = ::htonl(INADDR_ANY);
serverAddr.sin_port = ::htons(SERV_PORT);

TcpServer tcpServer(serverAddr);
tcpServer.start();
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: