【网络】Select服务器的实现

来源:互联网 发布:java技术 编辑:程序博客网 时间:2024/06/05 00:10

五种I/O模型

Unix下共有五种I/O模型,分别是

(1)阻塞式I/O;

(2)非阻塞I/O;

(3)I/O复用(select和(e)poll);

(4)信号驱动I/O(SIGIO);

(5)异步I/O(Posix.1的aio_系列函数);

阻塞I/O模型

应用程序调用一个IO函数,导致应用程序阻塞,等待数据准备好。如果数据没有准备好,一直等待。数据准备好了,从内核拷贝到用户空间,表示IO结束,IO函数返回成功指示。

非阻塞I/O模型

我们把一个套接口设置为非阻塞就是告诉内核,当所请求的I/O操作无法完成时,不要将进程睡眠,而是返回一个错误。这样我们的I/O操作函数将不断的测试 数据是否已经准备好,如果没有准备好,继续测试,直到数据准备好为止。在这个不断测试的过程中,会大量的占用CPU的时间。

I/O复用模型

该种模型又被称作是多路转接,I/O复用模型会用到select或者poll函数,这两个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这两个函数可以同时阻塞多个I/O操作。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数。

信号驱动I/O模型

首先我们允许套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。

异步I/O模型

调用aio_read函数,告诉内核描述字,缓冲区指针,缓冲区大小,文件偏移以及通知的方式,然后立即返回。当内核将数据拷贝到缓冲区后,再通知应用程序。也就是它只需要发起这个读写事件,不要等待与数据搬迁,只需要在结束之后得到成果。

举例

(1)张三在钓鱼,当鱼没有上钩时,便一直进行等待;当鱼上钩后,将鱼调出。这便是基本的阻塞式I/O

(2)张三在钓鱼,这次,当鱼没有上钩时,他便翻着看《C语言入门》这本书,也可以玩玩手机;当鱼上钩后,将鱼调出。这是非阻塞I/O

(3)张三在钓鱼,这次他放了一百个鱼竿。依次检查这些鱼竿,当一个鱼竿有鱼上钩后,将鱼调出,然后继续检查所有鱼竿。这是I/O多路复用

(4)张三钓鱼时,将鱼竿上放一个铃铛,当鱼上钩后会响。然后他便可以干自己的事情,当领响时,他知道上钩了,再去收鱼

(5)张三准备钓鱼,这次。。他直接雇了个人给自己钓鱼。这便是异步I/O

相关函数介绍

重定向的dup函数

头文件

#include<unistd.h>

函数原型

int dup(int oldfd);

int dup2(int oldfd, int newfd);

功能

dup 

将oldfd文件描述符进行一份拷贝,返回值为最新拷贝生成的文件描述符(最小的未被使用的文件描述符)

dup2 

使用newfd对oldfd文件描述符做一份拷贝,必要是可以先关闭newfd文件描述符

调用dup2之后,oldfd文件描述符不变,newfd和oldfd相等。

代码测试

#include<stdio.h>#include<sys/types.h>#include<sys/stat.h>#include<unistd.h>#include<fcntl.h>#include<string.h>int main(){umask(0);int fd = open("test.txt",O_CREAT|O_RDWR,0666);const char* msg = "hello world";printf("fd -> %d\n",fd);write(fd,msg,strlen(msg));int new_fd = dup(fd);//将fd进行拷贝,保存到new_fd中printf("new_fd -> %d\n",new_fd);write(new_fd,msg,strlen(msg));int cpfd = dup(1);//拷贝标准输出文件描述符dup2(fd,1);printf("nice\n");dup2(cpfd,1);printf("niec\n");return 0;}

运行结果



select函数

头文件

#include<sys/time.h>

#include<sys/types.h>

#include<unistd.h>

函数原型

int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *expectfds,struct timeval *timeout);

函数功能

同时等待多个文件描述符

参数

nfds

表示的是等待的文件描述符中的最大的那个+1;

fd_set

表示的是是否需要等待某个文件描述符,所以这里的fd_set底层是用位图实现的,所以我们最多可以等待的文件描述符的个数为sizeof(fd_set)*8;

readfds

表示的是需要等待的读事件的文件描述符集;

writefds

表示的是需要等待的写事件的文件描述符集;

exceptfds

表示的是需要等待的异常事件的文件描述符集;

timeout

表示的是每次select的时间为ty_sec秒

返回值

返回集合,该集合表示了收到消息的文件描述符

与fd_set有关的函数

fd_set为一个集合,底层是用位图进行实现的

它的每一位都可以用来表示对应的文件描述符是否收到了事件消息

#include <sys/time.h>  #include <sys/types.h>  #include <unistd.h>    void FD_CLR(int fd, fd_set *set);//将文件描述符中的fd位去掉  int  FD_ISSET(int fd, fd_set *set);//检测文件描述符集set中的fd位是否存在  void FD_SET(int fd, fd_set *set);//为set文件描述符集设置fd为设置  void FD_ZERO(fd_set *set);//将set文件描述符集清空  

Select模型

理解select模型的关键在于理解fd_set

假设fd_set长度为1字节

fd_set中的每⼀一bit

可以对应⼀一个⽂文件描述符fd

则1字节长的fd_set最⼤大可以对应8个fd

步骤:
(1)执⾏行fd_set set; FD_ZERO(&set);则set⽤用位表⽰示是0000,0000。

(2)若fd=5,执⾏行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)

(3)若再加⼊入fd=2,fd=1,则set变为0001,0011

(4)执⾏行select(6,&set,0,0,0)阻塞等待

(5)若fd=1,fd=2上都发⽣生可读事件,则select返回,此时set变为0000,0011。注意:没有事件
发⽣生的fd=5被清空

也就是说,readfds[]数组来表示连接的客户端,该数组大小表示最多可以连接的数量

而rfds是一个集合,底层用位图实现

表示的是对应的文件描述符有没有收到对方发来的消息

若有,则对应的位上被置为1

代码实现

select服务器

#include<stdio.h>#include<stdlib.h>#include<error.h>#include<unistd.h>#include<netinet/in.h>#include<sys/socket.h>#include<sys/types.h>#include<sys/time.h>#include<string.h>int readfds[sizeof(fd_set)*8];int writefds[sizeof(fd_set)*8];static void Usage(){printf("Usage: [ipaddr] [port]\n");}int startUp(char* ip, int port){int sockfd = socket(AF_INET,SOCK_STREAM,0);if(sockfd < 0){perror("socket");exit(2);}struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(port);addr.sin_addr.s_addr = inet_addr(ip);if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr)) < 0){perror("bind");exit(3);}if(listen(sockfd,5)<0){perror("listen");exit(4);}return sockfd;}int main(int argc, char* argv[]){int i = 1;if(argc != 3){Usage();exit(1);}int listen_sock = startUp(argv[1],atoi(argv[2]));int num = sizeof(fd_set)*8;writefds[0] = -1;readfds[0] = listen_sock;for(; i<num; i++){writefds[i] = -1;readfds[i] = -1;}fd_set rfds,wfds;while(1){int maxfd = -1;FD_ZERO(&rfds);FD_ZERO(&wfds);for(i = 0; i<num; ++i){if(readfds[i] != -1){FD_SET(readfds[i],&rfds);}if(writefds[i] != -1){FD_SET(writefds[i],&wfds);}maxfd = readfds[i] > maxfd ? readfds[i] : maxfd;maxfd = writefds[i] > maxfd ? writefds[i] : maxfd;}struct timeval time = {1,0};int n = select(maxfd+1,&rfds,&wfds,NULL,&time);switch(n){case 0:printf("time out...\n");break;case -1:break;default:{for(i = 0; i<num; ++i){if(FD_ISSET(readfds[i],&rfds)){if(i == 0){//监听服务器就绪struct sockaddr_in client;socklen_t len = sizeof(client);int client_sock = accept(listen_sock,(struct sockaddr*)&client,&len);if(client_sock < 0){perror("accpet");exit(5);}else{int tmp = 0;for(; tmp < num; ++tmp){if(readfds[tmp] == -1){readfds[tmp] = client_sock;break;}}if(tmp == num){printf("readfds 满了\n");exit(6);}}}else{//等待的普通文件描述符就绪char buf[1024];ssize_t s = read(readfds[i],buf,sizeof(buf));if(s < 0){perror("read");exit(7);}else if(s == 0){printf("客户端退出..\n");close(readfds[i]);readfds[i] = -1;continue;}else{buf[s] = 0;printf("#client: %s",buf);fflush(stdout);write(readfds[i],buf,strlen(buf));}}}}}}}return 0;}

select客户端

#include<stdio.h>#include<stdlib.h>#include<netinet/in.h>#include<sys/socket.h>#include<sys/types.h>#include<unistd.h>#include<string.h>static void Usage(){printf("Usage: [ipaddr] [port]\n");}int main(int argc,char* argv[]){if(argc != 3){Usage();exit(1);}int sockfd = socket(AF_INET,SOCK_STREAM,0);if(sockfd < 0){perror("socket");exit(2);}struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(atoi(argv[2]));addr.sin_addr.s_addr = inet_addr(argv[1]);if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))<0){perror("connect");exit(3);}printf("连接成功...\n");char buf[1024];while(1){//发数据printf("#client: ");fflush(stdout);ssize_t s = read(0,buf,sizeof(buf)-1);if(s <= 0){perror("read");exit(4);}int fd = dup(1);dup2(sockfd,1);printf("%s",buf);fflush(stdout);dup2(fd,1);//收数据s = read(sockfd,buf,sizeof(buf)-1);if(s == 0){printf("服务器退出...\n");break;}else if(s < 0){perror("read");exit(5);}else{buf[s-1] = '\0';printf("#server: %s\n",buf);}}close(sockfd);return 0;}

select服务器优缺点

优点

1、不需要fork或者pthread_create就可以实现一对多的通信,简化了进程线程的使用

2、同时等待多个文件描述符,效率相对较高

缺点

1、每次调用select,都需要把fd集合从用户态拷贝到内核态,在fd很多的情况下,循环次数多;

2、每次调用select都需要在内核遍历传递进来的所有fd,开销比较大

3、select支持的文件描述符数量过小,默认是1024

原创粉丝点击