网络编程学习——客户/服务器程序设计范式(一)

本文介绍了一个TCP并发服务器的实现过程,包括使用子进程处理多个客户端连接的方法,以及如何通过预派生子进程来提高服务器效率。

1 TCP测试客户程序

  下面给出的客户程序用于测试我们的服务器程序的各个变体。

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define MAXLINE 1024
#define MAXN 16384 // max # bytes to request from server

int tcp_connect( const char *host, const char *serv );

int main( int argc, char **argv )
{
  int i, j, fd, nchildren, nloops, nbytes;
  pid_t pid;
  ssize_t n;
  char request[ MAXLINE ], reply[ MAXN ];
  
  // 每次运行本客户程序时,我们指定服务器的主机名或IP地址、服务器的端口、由客户fork的子进程数(以允许并发地
  // 向同一个服务器发起多个连接)、每个子进程发送给服务器的请求数,以及每个请求服务器返回的数据字节数
  if( argc != 6 )
  {
    printf( " usage: client <hostname or IPaddr> <port> <#children> <#loops/child> <#byte/request> " );
    exit( 1 );
  }
  
  nchildren = atoi( argv[ 3 ] );
  nloops = atoi( argc[ 4 ] );
  nbytes = atoi( argv[ 5 ] );
  snprintf( request, sizeof( request ), " %d\n ", nbytes ); // nweline at end
  
  // 父进程调用fork派生指定个数的子进程,每个子进程再与服务器建立指定数目的连接。每个建立连接之后,子进程
  // 就在该连接上向服务器发送一行文本,指出需由服务器返回多少字节的数据,然后在该连接上读入这个数量的数据,
  // 最后关闭该连接。父进程只是调用wait等待所有子进程都终止。需注意的是,这里关闭每个TCP连接的是客户端,
  // 因而TCP的TIME_WAIT状态发生在客户端而不是服务端。这是与通常HTTP连接的差别之一。
  for( i = 0; i < nchildren; i++ )
  {
    if( ( pid = fork() ) == 0 ) // child
    {
      for( j = 0; j < nloops; j++ )
      {
        fd = tcp_connect( argv[ 1 ], argv[ 2 ] );
        write( fd, request, strlen( request ) );
        if( ( n = read( fd, reply, nbytes ) ) != nbytes )
        {
          printf( " server returned %d bytes ", n );
          exit( 1 );
        }
        close( fd ); // TIME_WAIT on client, not server
      }
      printf( " child %d done\n ", i );
      exit( 0 );
    }
    // parent loops around to fork() again
  }
  
  while( wait( NULL ) > 0 ) // now parent waits for all children
    ;
  if( errno != ECHILD )
  {
    printf( " wait error " );
    exit( 1 );
  }
  
  exit( 0 );
}

 

2 TCP并发服务器程序,每个客户一个子进程

  下面我们给出并发服务器程序的main函数。

int main( int argc, int **argv )
{
  int listenfd, connfd;
  pid_t childpid;
  void sig_chld( int ), sig_int( int ), web_child( int );
  socklen_t clilen, addrlen;
  struct sockaddr *cliaddr;
  
  if( argc == 2 )
    listenfd = tcp_listen( NULL, argv[ 1 ], &addrlen );
  else if( argc == 3 )
    listenfd = tcp_listen( argv[ 1 ], argv[ 2 ], &addrlen );
  else
  {
    printf( " usage: serv01 [ <host> ] <port#> " );
    exit( 1 );
  }
  cliaddr = malloc( addrlen );
  
  signal( SIGCHLD, sig_chld );
  signal( SIGINT, sig_int );
  
  for( ; ; )
  {
    clilen = addrlen;
    if( ( connfd = accept( listenfd, cliaddr, &clilen ) ) < 0 )
    {
      if( errno == EINTR )
        continue; // back to for()
      else
      {
        printf( " accpet error " );
        exit( 1 );
      }
    }
    
    if( ( childpid = fork() ) == 0 ) // child process
    {
      close( listenfd ); //close listening socket
      web_child( connfd ); // process request
      exit( 1 );
    }
    close( connfd ); // parent closes connected socket
  }
}

  下面给出SIGINT信号处理函数。

viod sig_int( int signo )
{
  void pr_cpu_time( void );
  
  pr_cpu_time();
  exit( 0 );
}

  下面给出由SIGINT信号处理函数调用的pr_cpu_time函数。

#include <sys/resource.h>

#ifndef HAVE_GETRUSAGE_PROTO
int getusage( int, struct rusage * );
#endif

void pr_cpu_time( void )
{
  double user, sys;
  struct rusage myusage, childusage;
  
  if( getrusage( RUSAGE_SELF, &myusage ) < 0 )
  {
    printf( " getrusage error " );
    exit( 1 );
  }
  if( getrusage( RUSAGE_CHILDREN, &childusage ) < 0 )
  {
    printf( " getrusage error " );
    exit( 1 );
  }
  
  user = ( double ) myusage.ru_utime.tv_sec + myusage.ru_utime.tv_usec / 1000000.0;
  user += ( double ) childusage.ru_utime.tv_sec + childusage.ru_utime.tv_usec / 1000000.0;
  sys = ( double ) myusage.ru_stime.tv_sec + myusage.ru_stime.tv_usec / 1000000.0;
  sys += ( double ) childusage.ru_stime.tv_sec + childusage.ru_stime.tv_usec / 1000000.0;
  
  printf( " \nuser time = %g, sys time = %g\n ", user, sys );
}

  下面是web_child函数。

void web_child( int sockfd )
{
  int ntowrite;
  ssize_t nread;
  char line[ MAXLINE ], result[ MAXN ];
  
  for( ; ; )
  {
    if( ( nread = readline( sockfd, line, MAXLINE ) ) == 0 )
      return; // connection closed by other end
      
    // line from client specifies #bytes to write back
    ntowrite = atol( line );
    if( ( ntowrite <= 0 ) || ( ntowrite > MAXN ) )
    {
      printf( " client request for %d bytes ", ntowrite );
      exit( 1 );
    }
    write( sockfd, result, ntowrite );
  }
}

 

3 TCP预先派生子进程服务器程序,accept无上锁保护

  下面给出预先派生子进程服务器第一个版本的main函数。

static int nchildren;
static pid_t *pids;

int main( int argc, char **argv )
{
  int listenfd, i;
  socklen_t addrlen;
  void sig_int( int );
  pid_t child_make( int, int, int );
  
  // 增设一个命令行参数供用户指定预先派生的子进程数。分配一个存放各个子进程ID的数组,用于在父进程即将终止时
  // 由main函数终止所有进程
  if( argc == 3 )
    listenfd = tcp_listen( NULL, argv[ 1 ], &addrlen );
  else if( argc == 4 )
    listenfd = tcp_listen( argv[ 1 ], argv[ 2 ], &addrlen );
  else
  {
    printf( " usage: serv02 [ <host> ] <port#> <#children> \n" );
    exit( 1 );
  }
  nchildren = atoi( argv[ atgc - 1 ] );
  pids = calloc( nchildren, sizeof( pid_t ) );
  
  // 调用child_make函数创建各个子进程
  for( i = 0; i < nchildren; i++ )
    pids[ i ] = child_make( i, listenfd, addrlen ); // parent returns
    
  signal( SIGINT, sig_int );
  
  for( ; ; )
    pause(); // everything done by children
}

  下面是SIGINT信号处理函数。

void sig_int( int signo )
{
  int i;
  void pr_cpu_time( void );
  
  // 既然getrusage汇报的是已终止子进程的资源利用统计,在调用pr_cpu_time之前就必须终止所有子进程。我们
  // 通过给每个子进程发送SIGTERM信号终止它们,并通过调用wait汇集所有子进程的资源利用统计。
  // terminate all children
  for( i = 0; i < nchildren; i++ )
    kill( pids[ i ], SIGTERM );
    while( wait( NULL ) > 0 ) // wait for all children
      ;
    if( errno != ECHILD )
    {
      printf( " wait error " );
      exit( 1 );
    }
  
  pr_cpu_time();
  exit( 0 );
}

   下面是child_make函数,它由main函数调用以派生各个子进程。

pid_t child_make( int i, int listenfd, int addrlen )
{
  pid_t pid;
  void child_main( int, int, int );
  
  // 调用fork派生子进程后只有父进程返回。子进程调用child_main函数,它是一个无限循环
  if( ( pid = fork() ) > 0 )
    return( pid ); // parent
    
  child_main( i, listenfd, addrlen ); // never returns
}

void child_main( int i, int listenfd, int addrlen )
{
  int connfd;
  void web_child( int );
  socklen_t clilen;
  struct sockaddr *cliaddr;
  
  cliaddr = malloc( addrlen );
  
  printf( " child %ld starting\n ", ( long ) getpid() );
  
  // 每个子进程调用accept返回一个已连接套接字,然后调用web_child处理客户请求,最后关闭连接。子进程一直在
  // 这个循环中反复,知道被父进程终止
  for( ; ; )
  {
    clilen = addrlen;
    connfd = accept( listends, cliaddr, &clilen );
    
    web_child( connfd ); // process the request
    close( connfd );
  }
}

4 TCP预先派生子进程服务器程序,传递描述符

  对于当前预先派生子进程例子,我们必须为每个子进程维护一个信息结构以便管理。下面给出我们的child结构。

typedef struct
{
  pid_t child_pid; // process ID
  int child_pipefd; // parent's stream pipe to/from child
  int child_status; // 0 = ready
  long child_count; // # connections handled
} child;

child *cptr; // array of child structures; calloc'ed

  下面是改进过后的child_make函数。

pid_t child_make( int i, int listenfd, int addrlen )
{
  int sockfd[ 2 ];
  pid_t pid;
  void child_main( int, int, int );
  
  // 创建一个字节流管道,它是一对Unix域字节流套接字
  socketpair( AD_LOCAL, SOCK_STREAM, 0, sockfd );
  
  if( ( pid = fork() ) > 0 )
  {
    // 父进程关闭其中写描述符
    close( sockfd[ 1 ] );
    cptr[ i ].child_pid = pid;
    cptr[ i ].child_pipefd = sockfd[ 0 ];
    cptr[ i ].child_status = 0;
    return( pid ); // parent
  }
  
  // 子进程还把流管道的自身拥有端复制到标准错误输出,这样每个子进程就通过读写标准错误输出和父进程通信
  dup2( sockfd[ 1 ], STDERR_FILENO ); // child's tream pipe to parent
  close( sockfd[ 0 ] );
  close( sockfd[ 1 ] );
  close( listenfd ); // child does not need this open;
  child_main( i, listenfd, addrlen ); // never returns
}

  下面是main函数。相比以前的版本的变动在于:分配描述符集,打开与监听套接字以及到各个子进程的字节流管道对应的位;计算最大描述符值;分配child结构数组的内存空间;主循环由一个select调用驱动。

static int nchildren;

int main( int argc, char **argv )
{
  int listenfd, i, navail, maxfd, nsel, connfd, rc;
  void sig_int( int );
  pid_t child_make( int, int, int );
  ssize_t n;
  fd_set rset, masterset;
  socklen_t addrlen, clilen;
  struct sockaddr *cliaddr;
  
  if( argc == 3 )
    listenfd = tcp_listen( NULL, argv[ 1 ], &addrlen );
  else if( argc == 4 )
    listenfd = tcp_listen( argv[ 1 ], argv[ 2 ], &addrlen );
  else
  {
    printf( " usage: serv05 [ <host> ] <port#> <#children> " );
    exit( 1 );
  }
  
  FD_ZERO( &masterset );
  FD_SET( listenfd, &masterset );
  maxfd = listenfd;
  cliaddr = malloc( addrlen );
  
  nchildren = atoi( argv[ argc - 1 ] );
  navail = nchildren;
  cptr = calloc( nchildren, sizeof( child ) );
  
  // prefork all the children
  for( i = 0; i < nchildren; i++ )
  {
    child_make( i, listenfd, addrlen ); // parent returns
    FD_SET( cptr[ i ].child_pipefd, &masterset );
    maxfd = max( maxfd, cptr[ i ].child_pipefd );
  }
  
  signal( SIGINT, sig_int );
  
  for( ; ; )
  {
    rset = masterset;
    
    // 计数器navail用于跟踪当前可用的子进程数。如果其值为0,那就从select的读描述符集中关掉与监听套接字对应
    // 的位。这么做防止父进程在无可用子进程的情况下accept新连接。内核仍然将这些外来连接排入队列,直到达到
    // listen的backlog数为止,不过我我们在没有得到已准备好处理客户的子进程之前不想accept它们。
    if( navail <= 0 )
      FD_CLR( listenfd, &rset ); // turn off if no available children
    nsel = select( maxfd + 1, &rset, NULL, NULL, NULL );
    
    // check for new connections
    if( FD_ISSET( listenfd, &rset ) )
    {
      clilen = addrlen;
      connfd = accept( listenfd, cliaddr, &clilen );
      
      for( i = 0; i < nchildren; i++ )
        if( cptr[ i ].child_status == 0 )
          break; // available
          
      if( i == nchildren )
      {
        printf( " no  available children " );
        exit( 1 );
      }
      cptr[ i ].child_status = 1; // mark child as busy
      cptr[ i ].child_count++;
      navail--;
      
      n = write_fd( cptr[ i ].child_pipefd, "", 1, connfd );
      close( connfd );
      
      if( --nsel == 0 )
        continue; // all done with select() results
    }
    
    // find any newly-available children
    for( i = 0; i < nchildren; i++ )
    {
      if( FD_ISSET( cptr[ i ].child_pipefd, &rset ) )
      {
        if( ( n = read( cptr[ i ].child_pipefd, &rc, 1 ) ) == 0 )
        {
          printf( " child %d terminated unexpectedly ", i );
          exit( 1 );
        }
        cptr[ i ].child_status = 0;
        navail++;
        if( --nsel == 0 )
          break; // all done with select() results
      }
    }
  }
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

转载于:https://my.oschina.net/u/2537915/blog/668604

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值