前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >I/O复用——单进程服务器(select版)

I/O复用——单进程服务器(select版)

原创
作者头像
jackieluo
修改2019-02-11 13:37:43
2K0
修改2019-02-11 13:37:43
举报
文章被收录于专栏:Jackie技术随笔Jackie技术随笔

多进程服务器

为了可以处理多个客户的请求,我们之前一直使用多进程TCP并发服务器socket()监听一个套接口,accept()多个用户,父进程监听listenfd,子线程们在connfd上进行应答处理。

单进程服务器

通过使用select函数,我们可以在单进程服务器的前提下,处理多客户的请求,而无需为每个客户派生一个子进程。下面描述此模型下的处于不同阶段的服务器状态。

首个客户建立连接前

服务器状态

在还没有客户建立连接时,服务器有单个监听描述字。

第一个客户建立连接前的服务器状态
第一个客户建立连接前的服务器状态

服务器数据结构

读描述字集rset

服务器只维护一个读描述字集。在终端启动服务器,则描述字0、1和2分别为标准输入,标准输出和标准错误输出,因此分给监听套接口的第一个可用描述字是3。

fd 0

fd 1

fd 2

fd 3

0

0

0

1

已连接套接口数组client[]

使用一个整型数组维护客户的已连接描述字,在还没有客户建立连接前,数组中的元素都为初始值-1。

connfd 0

connfd 1

connfd 2

...

connfd FD_SETSIZE-1

-1

-1

-1

...

-1

第一个客户连接后

服务器状态

第一个客户建立连接后的服务器状态
第一个客户建立连接后的服务器状态

服务器数据结构

读描述字集rset

当第一个客户与服务器建立连接时,监听描述字变为可读,服务器调用accept(),分配给已连接套接口的描述字为4。

fd 0

fd 1

fd 2

fd 3

fd 4

0

0

0

1

1

已连接套接口数组client[]

随后在数组中记录客户的已连接套接口描述字,client[0] = 4

connfd 0

connfd 1

connfd 2

...

connfd FD_SETSIZE-1

4

-1

-1

...

-1

第二个客户连接后

服务器状态

第二个客户建立连接后的服务器状态
第二个客户建立连接后的服务器状态

服务器数据结构

读描述字集rset

当第二个客户与服务器建立连接时,监听描述字变为可读,服务器调用accept(),分配给已连接套接口的描述字为5。

fd 0

fd 1

fd 2

fd 3

fd 4

fd 5

0

0

0

1

1

1

已连接套接口数组client[]

随后在数组中记录第二个客户的已连接套接口描述字,client[1] = 5

connfd 0

connfd 1

connfd 2

...

connfd FD_SETSIZE-1

4

5

-1

...

-1

第一个客户终止连接后

服务器数据结构

读描述字集rset

当第一个客户与服务器终止连接时,客户TCP发送一个FIN,服务器侧描述字4变得可读,读此已连接套接口时,readline返回0。接着关闭此已连接套接口并更新数据结构,client0 = -1`

fd 0

fd 1

fd 2

fd 3

fd 4

fd 5

0

0

0

1

0

1

已连接套接口数组client[]

随后在数组中记录第二个客户的已连接套接口描述字,client[1] = 5

connfd 0

connfd 1

connfd 2

...

connfd FD_SETSIZE-1

-1

5

-1

...

-1

修改代码

准备套接口和数据结构

代码语言:txt
复制
#include "unp.h"

int main(int argc, char **argv) {
  int i, maxi, maxfd, listenfd, connfd, sockfd;
  int nready, client[FD_SETSIZE];
  ssize_t n;
  fd_set rset, allset;
  char buf[MAXLINE];
  socklen_t clilen;
  struct sockaddr_in cliaddr, servaddr;

创建监听套接口并初始化select

同样,依次调用socketbindlisten创建监听套接口。一开始select的唯一描述字便是监听套接口描述字。

代码语言:txt
复制
  listenfd = Socket(AF_INET, SOCK_STREAM, 0);

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

  Bind(listenfd, (SA *)&servaddr, sizeof(servaddr));

  Listen(listenfd, LISTENQ);

  maxfd = listenfd; /* initialize */
  maxi = -1;        /* index into client[] array */
  for (i = 0; i < FD_SETSIZE; i++)
    client[i] = -1; /* -1 indicates available entry */
  FD_ZERO(&allset);
  FD_SET(listenfd, &allset);

阻塞于select

代码语言:txt
复制
  for (;;) {
    rset = allset; /* structure assignment */
    nready = Select(maxfd + 1, &rset, NULL, NULL, NULL);

接受新连接

如果监听套接口变为可读,则代表可建立新的连接。记录新的已连接套接口描述字,更新数据结构,直到没有更多的可读描述字。

代码语言:txt
复制
   if (FD_ISSET(listenfd, &rset)) { /* new client connection */
      clilen = sizeof(cliaddr);
      connfd = Accept(listenfd, (SA *)&cliaddr, &clilen);
#ifdef NOTDEF
      printf("new client: %s, port %d\n",
             Inet_ntop(AF_INET, &cliaddr.sin_addr, 4, NULL),
             ntohs(cliaddr.sin_port));
#endif

      for (i = 0; i < FD_SETSIZE; i++)
        if (client[i] < 0) {
          client[i] = connfd; /* save descriptor */
          break;
        }
      if (i == FD_SETSIZE) err_quit("too many clients");

      FD_SET(connfd, &allset);            /* add new descriptor to set */
      if (connfd > maxfd) maxfd = connfd; /* for select */
      if (i > maxi) maxi = i;             /* max index in client[] array */

      if (--nready <= 0) continue; /* no more readable descriptors */
    }

处理现有连接

对所有已连接的客户,测试数据是否准备好被读,若是,则回射一行给客户,若客户终止连接,那么相应地要更新数据结构。

代码语言:txt
复制
    for (i = 0; i <= maxi; i++) { /* check all clients for data */
      if ((sockfd = client[i]) < 0) continue;
      if (FD_ISSET(sockfd, &rset)) {
        if ((n = Read(sockfd, buf, MAXLINE)) == 0) {
          /*4connection closed by client */
          Close(sockfd);
          FD_CLR(sockfd, &allset);
          client[i] = -1;
        } else
          Writen(sockfd, buf, n);

        if (--nready <= 0) break; /* no more readable descriptors */
      }
    }

在腾讯云主机运行select版本的服务器

启动服务器

编译运行服务器程序,

代码语言:txt
复制
[root@VM_0_6_centos tcpcliserv]# make tcpservselect01
gcc -I../lib -g -O2 -D_REENTRANT -Wall   -c -o tcpservselect01.o tcpservselect01.c
gcc -I../lib -g -O2 -D_REENTRANT -Wall -o tcpservselect01 tcpservselect01.o ../libunp.a -lpthread
[root@VM_0_6_centos tcpcliserv]# ./tcpservselect01

查看服务端进程情况

可以看到目前主机上有一个服务器进程

代码语言:txt
复制
[root@VM_0_6_centos ~]# ps -la
F S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 S     0 30506 30352  0  80   0 -  1595 poll_s pts/0    00:00:00 tcpservselect01
0 R     0 30567 30523  0  80   0 - 38300 -      pts/1    00:00:00 ps

启动客户端

在本地终端启动客户端,输入一行文本

代码语言:txt
复制
 jackieluo@JACKIELUO-MB1 ~/Desktop/unpv13e/tcpcliserv ./tcpcli01 150.*.*.*
hello
hello

再次查看服务端进程情况

可以看到此时仍然只有一个进程。

代码语言:txt
复制
[root@VM_0_6_centos ~]# ps -la
F S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 R     0  3941  3908  0  80   0 - 38292 -      pts/2    00:00:00 ps
0 S     0 30506 30352  0  80   0 -  1595 poll_s pts/0    00:00:00 tcpservselect01

这个服务器程序较为复杂,但是它避免了为每个连接的客户创建一个新的进程,是select的一个经典应用。

拒绝服务型攻击

但是,这个服务器程序有一个问题。若有恶意客户连接到服务器上,发送单个字节而非一行之后睡眠。服务器会调用readline,它读完该客户的一个字节,然后就阻塞于下一个read以等待这个客户的其他数据,无法为其他客户提供服务。这种行为被称为拒绝服务型攻击。可能的解决方法是:

a) 使用非阻塞I/O模型

b) 为每个客户开一个单独的线程

c) 对I/O操作设置超时

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 多进程服务器
  • 单进程服务器
    • 首个客户建立连接前
      • 服务器状态
      • 服务器数据结构
    • 第一个客户连接后
      • 服务器状态
      • 服务器数据结构
    • 第二个客户连接后
      • 服务器状态
      • 服务器数据结构
    • 第一个客户终止连接后
      • 服务器数据结构
    • 修改代码
      • 准备套接口和数据结构
      • 创建监听套接口并初始化select
      • 阻塞于select
      • 接受新连接
      • 处理现有连接
    • 在腾讯云主机运行select版本的服务器
      • 启动服务器
      • 查看服务端进程情况
      • 启动客户端
      • 再次查看服务端进程情况
    • 拒绝服务型攻击
    相关产品与服务
    云服务器
    云服务器(Cloud Virtual Machine,CVM)提供安全可靠的弹性计算服务。 您可以实时扩展或缩减计算资源,适应变化的业务需求,并只需按实际使用的资源计费。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档