《unix网络编程》(14)使用select、shutdown的客户服务器程序

来源:互联网 发布:数控车床螺纹编程g97 编辑:程序博客网 时间:2024/05/17 00:03

      文章《unix网络编程》(11)tcp服务器的几种常见状况分析分析了我们之前的客户服务器程序(《unix网络编程》(10)wait/waitpid处理僵死进程(SIGCHLD信号))存在的问题。

客户端

     如下修改程序中,客户的str_cli函数用select重写,一旦服务器进程终止,客户就能立刻得到通知。之前程序阻塞于fgets,这里阻塞于select调用(或等待标准输入可读、或等待套接字可读)。大大提高了客户端的健壮性。

      shutdown函数的使用允许我们正确批量处理输入。因为在标准输入的EOF并不意味着同时完成了从套接字的读入;可能仍然有请求在去往服务器的路上,或者仍有应答在返回客户的路上。我们需要shutdown提供半连接的TCP。也就是说,我们想给服务器发一个FIN告诉它我们完成了数据发送,但是仍然保持套接字描述符打开以便读取。

//客户端的str_cli函数void str_cli(FILE *fp, int sockfd){  //stdineof是一个初始化为0的标志,只要该标志为0,  //每次在主循环中总是select标准输入的可读性   int maxfdp1, stdineof;   fd_set rset;   char buf[MAXLINE];   int n;   stdineof = 0;   FD_ZERO(&rset);   for ( ; ; ) {      if (stdineof == 0)FD_SET(fileno(fp), &rset);      FD_SET(sockfd, &rset);      maxfdp1 = (fileno(fp) > sockfd ? fileno(fp) : sockfd) + 1;      Select(maxfdp1, &rset, NULL, NULL, NULL);            //当在套接字上读到EOF时,如果我们已经在标准输入上遇到EOF,那就是正常      //终止,函数返回。但是如果还没有在标准输入遇到EOF,那么服务器进程已经      //过早终止。只有服务器进程终止,select就会立刻通知客户端进程。      if (FD_ISSET(sockfd, &rset)) { //socket可读if ((n = Read(sockfd, buf, MAXLINE)) == 0) {  if (stdineof == 1)    return;  else    err_quit("str_cli: server terminated prematurely");}Write(fileno(stdout), buf, n);      }            //当在标准输入遇到EOF,把标志stdineof设为1,      //并把第二个参数设置为SHUT_WR来调用shutdown以发送FIN。      if (FD_ISSET(fileno(fp), &rset)) { //stdout可读if ((n = Read(fileno(fp), buf, MAXLINE)) == 0) {  //从标准输入读到了EOF(Ctrl+C)   stdineof = 1;    Shutdown(sockfd, SHUT_WR);   FD_CLR(fileno(fp), &rset);   continue;}Writen(sockfd, buf, n);  //shutdown后仍可以将缓冲区中剩余的数据全部发送到套接字      }   }}


服务器端

将服务器端程序改为用select来处理任意客户的单进程程序,而不是为每个客户派生一个子进程。它可以避免为每个客户端建立一个进程的开销。但没法阻止拒绝服务攻击,看下一节。

#include "myheader.h"int main(int argc, char **argv){  int i, maxi, maxfd, listenfd, connfd, sockfd;  int nready, client[FD_SETSIZE];  //client数组含有每个客户的已连接套接字描述符,初始化为-1   ssize_t n;  fd_set rset, allset;  char buf[MAXLINE];    char cliip[MAXLINE];  //作为inet_ntop的第三个参数指针,接收点分十进制的ip  socklen_t clilen;  struct sockaddr_in cliaddr, servaddr;    listenfd = Socket(AF_INET, SOCK_STREAM, 0);    memset(&servaddr, 0, sizeof(servaddr));  servaddr.sin_family = AF_INET;  servaddr.sin_addr.s_addr = htonl(INADDR_ANY);  servaddr.sin_port = htons(SERV_PORT);    Bind(listenfd, (const struct sockaddr*)&servaddr, sizeof(servaddr));    Listen(listenfd, LISTENQ);    maxfd = listenfd;  maxi = -1;  for (i = 0; i < FD_SETSIZE; i++)    client[i] = -1;  FD_ZERO(&allset);  FD_SET(listenfd, &allset);    for ( ; ; ) {    //select等待某事件发生:或是新客户连接建立,或是数据、FIN或RST到达    rset = allset;    nready = Select(maxfd + 1, &rset, NULL, NULL, NULL);        //如果某个监听套接字可读,那么建立一个新连接。    //调用accept并相应更新数据结构,使用client数组中    //第一个未用记录这个已连接描述符。就绪描述符数目减1,若其值为0,    //就可以避免进入下一个for循环。这样做让我们可以使用select的返回值    //来避免检查未就绪的描述符    if (FD_ISSET(listenfd, &rset)) {      clilen = sizeof(cliaddr);      connfd = Accept(listenfd, (const struct sockaddr*)&cliaddr, &clilen);      printf("new client: %s, port %d \n",     Inet_ntop(AF_INET, &cliaddr.sin_addr, cliip, sizeof(buf)),             ntohs(cliaddr.sin_port));           for (i = 0; i < FD_SETSIZE; i++)if (client[i] < 0) {   client[i] = connfd;   break;}      if (i == FD_SETSIZE)err_quit("too many clients");            FD_SET(connfd, &allset);      if (connfd > maxfd)maxfd = connfd;      if (i > maxi)maxi = i;            if (--nready <= 0)continue;    }       //对于每个现有的客户连接,要测试其描述符是否在select返回的描述符集中。   //如果是就从该客户读入一行文本并回射给它。如果该客户已经关闭连接,read   //返回0,更新数据结构。    for (i = 0; i <= maxi; i++) {      if ((sockfd = client[i]) < 0)continue;      if (FD_ISSET(sockfd, &rset)) {        if ((n = Read(sockfd, buf, MAXLINE)) == 0) {  Close(sockfd);  FD_CLR(sockfd, &allset);  client[i] = -1;}else  Writen(sockfd, buf, n);if (--nready <= 0)  break;      }    }  }}

拒绝服务攻击

       对于上述程序,如果某个恶意客户连接到服务器后发送一个字节后休眠。服务器会read该字节,然后阻塞于下一个read调用,以等待来自该客户的其余数据。服务器因为一个客户阻塞不能为其它客户服务,直到那个恶意客户发出换行符或终止为止

 解决方法:(1)使用非阻塞I/O;

                       (2)让每个客户由单独控制线程提供服务(为每个客户建立子进程或线程);

                       (3)对I/O操作设置一个超时。



完整源码

http://download.csdn.net/detail/u013074465/8566567

github:服务器端源码   https://github.com/liyangddd/linux_practice/blob/master/network_programming/tcpsrvselect.c

               客户端源码  https://github.com/liyangddd/linux_practice/blob/master/network_programming/tcpcliselect.c

演示结果


如果客户发送RST

            该服务器程序接收到 一个RST后,read返回错误,我们的Read包裹函数时终止服务器。因此,这样的服务器程序太脆弱了,因为一个用户的RST就退出。通常服务器不应该因为这个原因而终止。服务器应该登记错误,关闭出错的套接字,并继续服务其他用户。简单终止服务器是不能接受的。该服务器是单进程的,如果是每个进程或线程服务一个用户,那么线程或进程退出不会有太大影响。

0 0