Linux TCP server系列(1)-简单TCP服务器+多进程处理客户请求

来源:互联网 发布:8080端口和80端口 编辑:程序博客网 时间:2024/06/04 17:44

目标:
  完成一个精简TCP服务器,可接收来自多个用户的请求,并返回结果。


思路:
  (1)服务器
      C++ TCP服务器的实现主要由以下几个函数来完成:
        a)socket
     创建服务器监听套接字
  b)bind
     绑定服务器监听信息到套接字上
  c)listen
     开始监听,接收客户端的TCP连接
  d)accept
     从listen所维护的队列中取出一条已连接的TCP,返回该连接的socket描述字
        e)服务器客户端在连接socket描述字上进行消息通信
  f) close
     关闭打开着的套接字
      为了更好的服务多个发起请求的客户端,在e步骤上,我们使用fork以派生子进程来独立处理每个客户端的请求。
      if( (childpid=fork())==0)
     {
      close(listenfd);         //从父进程复制下来的监听socket描述符要关闭。
      //communication(connfd);
      exit(0);
     }
(2)客户端
    客户端的实现主要由以下函数完成:
     a)socket
        创建客户端连接套接字
    b)connect
    向指定服务器发起连接请求
     c)服务器客户端在连接socket描述字上进行消息通信
     d)close
        关闭打开着的套接字


实现:

server.cpp
#include<sys/types.h>
#include<sys/socket.h>
#include<strings.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<signal.h>
#include<sys/wait.h>


#define LISTEN_PORT 84
void str_echo(int sockfd)
{
    ssize_t n;
    char line[512];


    printf("ready to read\n");
    while( (n=read(sockfd,line,512))>0 )
    {
            line[n]='\0';
            printf("Client: %s\n",line);
            bzero(&line,sizeof(line));
    }
    printf("end read\n");
}


int main(int argc, char **argv)
{
    int listenfd, connfd;
    pid_t childpid;
    socklen_t chilen;


    struct sockaddr_in chiaddr,servaddr;


    listenfd=socket(AF_INET,SOCK_STREAM,0);
    if(listenfd==-1)
    {
        printf("socket established error: %s\n",(char*)strerror(errno)); return -1;
    }


    bzero(&servaddr,sizeof(servaddr));
    servaddr.sin_family=AF_INET;
    servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
    servaddr.sin_port=htons(LISTEN_PORT);


    int bindc=bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
    if(bindc==-1)
    {
        printf("bind error: %s\n",strerror(errno)); return -1;
    }


    listen(listenfd,5);
    for(;;)
    {
        chilen=sizeof(chiaddr);


        connfd=accept(listenfd,(struct sockaddr*)&chiaddr,&chilen);
        if(connfd==-1)
        {    printf("accept client error: %s\n",strerror(errno)); return -1; }
        else        
            printf("client connected\n");


        if((childpid=fork())==0)
        {
            close(listenfd);
            printf("client from %s\n",inet_ntoa(chiaddr.sin_addr));
            str_echo(connfd);
            exit(0);    
        }
        else if (childpid<0)
            printf("fork error: %s\n",strerror(errno));
        close(connfd);
    }
}
 
client.cpp
#include<sys/types.h>
#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
#include<sys/socket.h>
#include<strings.h>
#include<string.h>
#include<arpa/inet.h>
#include<errno.h>


#define SERVER_PORT 84
void str_cli(char *data,int sockfd)
{
    char recv[512];


    int wc=write(sockfd,data,strlen(data));


    exit(0);
}
int main(int argc, char **argv)
{
    int sockfd;
    struct sockaddr_in servaddr;


    if(argc!=3)
        return -1;


    sockfd=socket(AF_INET,SOCK_STREAM,0);
    if(sockfd==-1)
    {
        printf("socket established error: %s\n",(char*)strerror(errno)); return -1;
    }


    bzero(&servaddr,sizeof(servaddr));
    servaddr.sin_family=AF_INET;
    servaddr.sin_port=htons(SERVER_PORT);
    inet_pton(AF_INET,argv[1],&servaddr.sin_addr);


    printf("client try to connect\n");
    int conRes=connect(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
    if(conRes==-1)
    {    
        printf("connect error: %s\n",strerror(errno)); return -1;
    }


    str_cli(argv[2],sockfd);


    exit(0);
}

 

分析:
   最简单的服务器仅能完成基本的通信,并没有考虑其他边界或者异常情况,同时,采用子进程处理客户端连接,一旦子进程数量增多,并且子进程需要长时间的运行,那么服务器性能将严重下降。

Linux TCP server系列(2)-简单优化服务器和客户端程序

目标:
 在上个server中考虑更多细节问题,完善server。


思路:
 (1)服务器
父进程使用fork派生子进程后,如果子进程运行结束,那么该进程不会立刻被销毁,而会进入“僵尸状态”,仍然维护着自身的信息,这时候如果服务器父进程不加以处理,那么很快就会消耗完系统的内存空间,所以父进程需要监听子进程SIGCHLD信号,并做出处理以销毁残留信息,这里可以使用wait或者waitpid来实现。
我们在父进程调用listen之后,注册监听信号和信号处理函数signal(SIGCHLD, sig_child);
信号处理函数实现如下:
void sig_child(int signo)         //父进程对子进程结束的信号处理
{
 pid_t pid;
 int   stat;

 while( (pid=waitpid(-1,&stat,WNOHANG))>0)
 printf("child %d terminated\n",pid);

 return;
}
这样一来我们就可以应付子进程退出后的残留信息问题。注意这里使用waitpid,其
中使用WNOHANG参数来防止父进程阻塞在wait上。


(2)客户端
   客户端需要考虑下面几种边界情况,根据自身需求做出调整:
   a)客户端连接成功后,某个时刻向服务器发送信息,接着再调用read从服务器接收信息,如果在发送信息之前,服务器子进程关闭了,那么它会发送FIN给客户端TCP,客户端TCP则以ACK响应,然后客户端向服务器发送消息时,服务器就会以RST响应,如果这时候客户端继续调用read,由于客户端TCP已经从服务器接收FIN,所以read会返回0,这时候客户根本不知道之前所发送的东西没有到达服务器,所以在read返回0时要做恰当提示和处理。
   b)如果客户端不理会read的错误,继续发送信息给服务器,那么当一个进程向接收了RST的套接口进行写时,内核会给它发送一个SIGPIPE信号,该信号会终止进程,所以我们也需要自己捕获该信号并做处理。
   c)在read之前,服务器主机崩溃,它没有回传FIN,这样read会阻塞很长时间才放弃连接,这也是需要考虑的。
   考虑到客户端既需要从用户接收输入,又需要从服务器接收输入,其中一方在使用时会阻塞另一方,影响客户体验,所以这里使用select来做这两个描述符的监听。后文会将select使用到服务器上,提高服务器的性能。
   select的主要用法:
    1 在fd_set中设置被监听的描述符(输入描述符集,输出描述符集,异常输出描述符集)
    2 调用select函数开始监听(可以自行配置超时时间)
    3 如果监听到有数据流动,则使用FD_ISSET判断发生在哪个描述符,并做处理。
   代码如下:
 FD_ZERO(&rset);
 for(;;)
 {
  FD_SET(fileno(fp),&rset);
  FD_SET(sockfd,&rset);
  maxfdp1=std::max( fileno(fp),sockfd) +1;
  select(maxfdp1,&rset,NULL,NULL,NULL);            //使用select使得客户端不需要阻塞在标准IO 或者 TCP read的其中一个上

  if( FD_ISSET(sockfd,&rset))
  {
   if(read(sockfd,recvline,sizeof(recvline))==0) 
   //那边发来需要有换行符表示串尾,不然这边要求接受的字节数还不到,或者还没到尾部,所以没有输出
   { 
    printf("read error\n");
    //handle error;
    return;
   }
  // printf("readed:%d,str:%d",cr,strlen(recvline));
  //      如果tcp缓冲区内有多于预定义取的字节,则会自动调用read再取,直到结尾或者遇到换行符
   fputs(recvline,stdout);       //标准库函数fputs向标准IO上写一行
   bzero(&recvline,strlen(recvline));
  }
  if( FD_ISSET(fileno(fp),&rset))
  {
   if(fgets(sendline,sizeof(sendline),fp)==NULL)    //标准库函数fgets从标准IO上获取一行
   //fgets会加上换行符,如果有
    return;
   write(sockfd,sendline,strlen(sendline)-1);  
   //减一去除换行符
   bzero(&sendline,strlen(sendline));
  }
 }


实现:

server.cpp
#include<sys/types.h>
#include<sys/socket.h>
#include<strings.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<signal.h>
#include<sys/wait.h>
#include<pthread.h>


#define LISTEN_PORT 84


void str_echo(int sockfd);            // 服务器收到客户端的消息后的响应


void sig_child(int signo);             //父进程对子进程结束的信号处理


int main(int argc, char **argv)
{
    int listenfd, connfd;
    pid_t childpid;
    socklen_t chilen;


    struct sockaddr_in chiaddr,servaddr;


    listenfd=socket(AF_INET,SOCK_STREAM,0);
    if(listenfd==-1)
    {
        printf("socket established error: %s\n",(char*)strerror(errno));                 //后面需要采用日志到方式来记录
        //socket创建失败后可以让用户选择重新连接
    }


    bzero(&servaddr,sizeof(servaddr));
    servaddr.sin_family=AF_INET;
    servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
    servaddr.sin_port=htons(LISTEN_PORT);


    int bindc=bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
    if(bindc==-1)
    {
        printf("bind error: %s\n",strerror(errno));
        //绑定失败,错误提示
    }


    listen(listenfd,SOMAXCONN);               //limit是SOMAXCONN


    signal(SIGCHLD,sig_child);                //子进程退出的信号处理
    for(;;)
    {
        chilen=sizeof(chiaddr);
        
        connfd=accept(listenfd,(struct sockaddr*)&chiaddr,&chilen);
        
        if(connfd==-1)
        printf("accept client error: %s\n",strerror(errno));
        else        
        printf("client connected\n");


        if((childpid=fork())==0)
        {
            close(listenfd);                //关闭没用的继承资源
            printf("client from %s\n",inet_ntoa(chiaddr.sin_addr));
            str_echo(connfd);
            exit(0);    
            //子进程结束,会成为僵尸进程,所以注册SIGCHLD来让父亲进程处理僵尸进程的遗留数据
        }
        else if (childpid<0)
        printf("fork error: %s\n",strerror(errno));
        //注意父子进程的执行顺序无法确定。
        close(connfd);                        
    }
}




void str_echo(int sockfd)            // 服务器收到客户端的消息后的响应
{
    ssize_t n;
    char line[512];


    printf("ready to read\n");


    while( (n=read(sockfd,line,512))>0 )
    {
        if(n>0)
        {
            line[n]='\0';
            printf("Client Diary: %s\n",line);


            //写回客户端提示信息
            char msgBack[512];
            snprintf(msgBack,sizeof(msgBack),"recv: %s\n",line);
            write(sockfd,msgBack,strlen(msgBack));
            bzero(&line,sizeof(line));
        }
        else
        { 
            break;
        }
    }


    printf("end read\n");
}


void sig_child(int signo)         //父进程对子进程结束的信号处理
{
    pid_t pid;
    int   stat;


    while( (pid=waitpid(-1,&stat,WNOHANG))>0)
    printf("child %d terminated\n",pid);


    return;
}
client.cpp
#include<sys/types.h>
#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
#include<sys/socket.h>
#include<strings.h>
#include<string.h>
#include<arpa/inet.h>
#include<errno.h>
#include<stdio.h>


#include<algorithm>


#define SERVER_PORT 84


void str_cli(char *data,int sockfd);


int main(int argc, char **argv)
{
    int sockfd;
    struct sockaddr_in servaddr;


    //tcpcli <ipaddress> <data>
    if(argc!=3)
        return -1;


    sockfd=socket(AF_INET,SOCK_STREAM,0);
        if(sockfd==-1)
    {
       printf("socket established error: %s\n",(char*)strerror(errno)); 
    }


    bzero(&servaddr,sizeof(servaddr));
    servaddr.sin_family=AF_INET;
    servaddr.sin_port=htons(SERVER_PORT);
    inet_pton(AF_INET,argv[1],&servaddr.sin_addr);
    
    printf("client try to connect\n");
    int conRes=connect(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
    if(conRes==-1)
    {    
           printf("connect error: %s\n",strerror(errno));
    }


    str_cli(argv[2],sockfd);


    exit(0);
}


void str_cli(char *data,int sockfd)
{
    int n=0;
    char recv[512];


    FILE* fp=stdin;


    int maxfdp1;
    fd_set rset;
    char sendline[512],recvline[512];


    FD_ZERO(&rset);
    for(;;)
    {
        FD_SET(fileno(fp),&rset);
        FD_SET(sockfd,&rset);
        maxfdp1=std::max( fileno(fp),sockfd) +1;
        select(maxfdp1,&rset,NULL,NULL,NULL);            //使用select使得客户端不需要注释在标准IO 或者 TCP read的其中一个上


        if( FD_ISSET(sockfd,&rset))
        {
            if(read(sockfd,recvline,sizeof(recvline))==0) 
            //那边发来需要有换行符表示串尾,不然这边要求接受的字节数还不到,或者还没到尾部,所以没有输出
            { 
                printf("read error\n");
                //handle error;
                return;
            }
        //    printf("readed:%d,str:%d",cr,strlen(recvline));
        //      如果tcp缓冲区内有多于预定义取的字节,则会自动调用read再取,直到结尾或者遇到换行符
            fputs(recvline,stdout);                            //标准库函数fputs向标准IO上写一行
            bzero(&recvline,strlen(recvline));
        }
        if( FD_ISSET(fileno(fp),&rset))
        {
            if(fgets(sendline,sizeof(sendline),fp)==NULL)    //标准库函数fgets从标准IO上获取一行
            //fgets会加上换行符,如果有
                return;
            write(sockfd,sendline,strlen(sendline)-1);        
            //减一去除换行符
            bzero(&sendline,strlen(sendline));
        }
    }
    
}

分析:

  服务器增加了对子进程退出时的信号处理,防止子进程数增多后带来的资源消耗,客户端使用select避免阻塞在标准IO和socket IO上。

Linux TCP server系列(3)-fork注意事项

前两篇文章都是使用fork子进程来处理客户端请求,所以我们需要在这里了解一下fork的具体信息。

    当server从accept返回时,它获得socket连接另一端的client socket 文件描述符和socket地址信息,然后使用fork创建子进程来执行对应client socket上的操作。

    使用fork应该注意以下几点:

    (1)子进程可以关闭不必要的文件描述符或者释放其他资源,因为使用fork后,如果子进程不调用exec以使用新的进程空间的话,子进程会复制父进程的进程空间内容,包括数据段等(代码段是共享的,数据段等采用一种写时复制的策略来提高性能)。所以不必要的资源可以尽快释放。

    (2)由于子进程在退出后会成为僵尸进程,这些信息会遗留在父进程内,所以父进程注册所监听的信号及其处理函数(SIGCHLD是子进程退出时向父进程发送的信号,默认情况下是不采取任何动作,所以我们需要调用wait或者waitpid来彻底清除子进程。)较为常用的信号注册和信号处理可以在fork前调用,使得子进程也可以完成同样的功能。

    (3)最后父进程要关闭已经accept返回的socket文件描述符,因为父进程不处理相应活动,而是交由fork出来的子进程来处理。

    (4)使用fork的时候,要注意它对父子进程有执行顺序是不做任何限制的(vfork的话会先运行子进程),所以必要的时候可以让进程睡眠或者用消息唤醒。

    (5)注意虽然子进程复制了父进程,但是还有下面这些东西是不同的:

       1. 子进程ID

       2. 虽然子进程复制了父进程的file descriptors,但是他们都指向同一个open file description(也就是文件表项),所以类似于文件偏移量等还是共享的。

       3. 子进程的tms_utime, tms_stime,tms_cutime,tms_cstime都被设置为0

       4. 父进程的文件锁不会被继承

       5. the set of signals pending for the child process shall be initialized to the empty set

       6. 父进程中已打开的semaphore,在子进程中仍然继续打开

       7. 最好在单线程的进程内使用fork,不然在多线程的情况下会有很多冗余(例如一些互斥量等可能会带来一些同步性的问题)

    使用fork会有以下问题:

       1 fork是昂贵的,内存映像要从父进程拷贝到子进程,所有描述字要在子进程中复制等等,还有进程的上下文切换开销。

       2 fork子进程后,需要用 进程间通信IPC 在父子进程之间传递信息,特别是fork内的信息。

    这时候可以考虑使用线程!它比较轻,创建速度比进程快10-100倍。但因为线程共享全局内存,所以也就带来了同步的问题。(注意errno自有)

下面是copy过来的一个不完整的例子,主要是处理线程间数据共享的问题,如其中的connfd.



static int sockfd;static FILE *fp;void str_cli( FILE *fp_arg, int sockfd_arg)    {  char recvline[MAXLINE];  pthead_t tid;  sockfd = sockfd_arg;  fp = fp_arg;  pthread_create(&tid,NULL,copyto,NULL);   while( readline(sockfd,recvline,MAXLINE)>0)     fputs(recvline,stdout);}void *copyto( void *arg){   char sendline[MAXLINE];   while( fgets(sendline,MAXLINE,fp)!=NULL)    write(sockfd,sendline,strlen(sendline));  shutdown(sockfd,SHUT_WR);  return NULL;}static void *doit(void *arg) { pthread_detach(pthread_self()); connfd = *((int*)arg);str_echo( connfd ); close(connfd ); return NULL; free(arg); //记得一定要free掉,不然内存泄露}int main( int argc, char **argv){  int listenfd,connfd;  socklen_t addrlen, len;  struct sockaddr *cliaddr;   pthread_t tid;  if( argc==2)    listenfd = tcp_listen(NULL,argv[1],&addrlen);  else if( argc==3)    listenfd = tcp_listen(argv[1] , argv[2], &addrlen);  else    err_quit (“usage: tcpserv01[<host>]<service or port>”);  cliaddr=malloc(addrlen);  for(;;)  {    len=addrlen;    connfd=accept( listenfd, cliaddr, &len);    pthread_create ( &tid, NULL, &doit, (void*)connfd);      //accept之后创建线程来处理connfd上的信息交互   //线程传入的参数是新接的socket   //注意!我们将整型变量connfd强制转换为void指针,虽然通常都是4个字节,但是不能保证在所有系统上工作。   //如果我们传入connfd的地址呢 (void*)&connfd? 这样也不行。因为取地址就是取到实际对象了,而下次accept会覆盖connfd,这样就会造成共享变量的竞争访问!!!   /* //选择下面这种方式会更好    for(;;)    {      len=addrlen;      lptr=malloc(sizeof(int));      *lptr=accept(listenfd,cliaddr,&len);      pthread_create(NULL,NULL,&doit,lptr);    }   */}}

作者:Aga.J 
出处:http://www.cnblogs.com/aga-j 
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
个人学习笔记仅供本人记录知识所用,不属发表性文章。

static int sockfd;static FILE *fp;void str_cli( FILE *fp_arg, int sockfd_arg)    {  char recvline[MAXLINE];  pthead_t tid;  sockfd = sockfd_arg;  fp = fp_arg;  pthread_create(&tid,NULL,copyto,NULL);   while( readline(sockfd,recvline,MAXLINE)>0)     fputs(recvline,stdout);}void *copyto( void *arg){   char sendline[MAXLINE];   while( fgets(sendline,MAXLINE,fp)!=NULL)    write(sockfd,sendline,strlen(sendline));  shutdown(sockfd,SHUT_WR);  return NULL;}static void *doit(void *arg) { pthread_detach(pthread_self()); connfd = *((int*)arg);str_echo( connfd ); close(connfd ); return NULL; free(arg); //记得一定要free掉,不然内存泄露}int main( int argc, char **argv){  int listenfd,connfd;  socklen_t addrlen, len;  struct sockaddr *cliaddr;   pthread_t tid;  if( argc==2)    listenfd = tcp_listen(NULL,argv[1],&addrlen);  else if( argc==3)    listenfd = tcp_listen(argv[1] , argv[2], &addrlen);  else    err_quit (“usage: tcpserv01[<host>]<service or port>”);  cliaddr=malloc(addrlen);  for(;;)  {    len=addrlen;    connfd=accept( listenfd, cliaddr, &len);    pthread_create ( &tid, NULL, &doit, (void*)connfd);      //accept之后创建线程来处理connfd上的信息交互   //线程传入的参数是新接的socket   //注意!我们将整型变量connfd强制转换为void指针,虽然通常都是4个字节,但是不能保证在所有系统上工作。   //如果我们传入connfd的地址呢 (void*)&connfd? 这样也不行。因为取地址就是取到实际对象了,而下次accept会覆盖connfd,这样就会造成共享变量的竞争访问!!!   /* //选择下面这种方式会更好    for(;;)    {      len=addrlen;      lptr=malloc(sizeof(int));      *lptr=accept(listenfd,cliaddr,&len);      pthread_create(NULL,NULL,&doit,lptr);    }   */}}

作者:Aga.J 
出处:http://www.cnblogs.com/aga-j 
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
个人学习笔记仅供本人记录知识所用,不属发表性文章。
0 0
原创粉丝点击