I/O多路转接---select服务器

I/O多路复用是通过一种机制,可以监视多个文件描述符,一旦某个文件描述符就绪,就能通知应用进程进行相应的读写操作。select函数作为一种I/O多路复用的机制,程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态变化。

select函数

 #include <sys/select.h>
 #include <sys/time.h>
 #include <sys/types.h>
 #include <unistd.h>

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

参数:

* nfsd是需要监视的最大文件描述符+1。
* readfds,writefds,exceptfds分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集合以及异常文件描述符的集合。是输入输出型参数,将我们关心的文件描述符添加进去,最后将我们关心的文件描述符上的就绪了的事件返回。
* 参数timeout为结构timeval,用来设置select()等待时间

timeout的取值:

* NULL:表示select没有timeout,直到某个文件描述符上发生了事件,属于阻塞式的等。
* 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生,属于非阻塞式的等。
* 特定的时间值:表示如果在指定的时间段内没有事件发生,select将超时返回。

fd_set结构,在头文件select.h中。
这里写图片描述
事实上,fd_set是一个整型数组,更严格的说,是一个“位图”,我们用位图中对应的位来表示要监视的文件描述符。

* 比特位置对应的是文件描述符值;
* 输入时,比特位表示是否关心该文件描述符,为0不关心,为1关心。输出时,表示该文件描述符上的事件是否就绪,为0不关心,为1关心。

测试了一下,我的服务器下fd_set是128个字节,最大的文件描述符为128*8=1024,其他平台可能不相同。

系统提供了一组操作fd_set的接口,使用起来较方便。

void FD_CLR(int fd, fd_set *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的全部位

timeval结构
用于描述一段时间长度,如果在这个时间内,需要监视的文件描述符上没有事件发生,函数就返回,返回值为0.

struct timeval  
 {  
 __time_t tv_sec;        /* Seconds. */  
 __suseconds_t tv_usec;  /* Microseconds. */  
 };

select函数返回值

* 0:表示在描述词状态改变前,已超过timeout时间,没有返回。
* -1:发生错误返回,错误原因存于errno,参数值不可预测。
* 其他:执行成功,返回的是文件描述符状态已经改变的个数。

那么,select的过程是怎么样的呢?
(1)FD_ZERO(&set),则表示的是set(0000,0000);
(2)若fd=1,FD_SET(fd,&set),则set(0000,0001);
(3)加入fd=2,fd=5,则set(0001,0011);
(4)执行select(6,&set,0,0)阻塞等待;
(5)若fd=1,fd=2上发生可读事件,但是5上的没有发生可读事件,则select返回,此时的set为(0000,0011).

socket就绪条件
1.读就绪

* socket内核中,接收缓冲区中的字节数大于等于低水位SO_RCVLOWAT,此时可以无阻塞的读该文件描述符,并且返回值大于0.
* socket TCP通信中,对端关闭连接,此时对该socket读,则返回0.
* 监听的socket上有新的连接请求
* socket上有未处理的错误。

2.写就绪

* socket内核中,发送缓冲区中的可用字节数,大于等于低水位SO_SNDLOWAT,此时可以无阻塞的写,并且返回值大于0.
* socket的写操作被关闭,对于一个写操作被关闭的socket进行写操作,会出发SIGPIPE信号.
* socket使用非阻塞connect连接成功或失败之后。
* socket上有未读取的错误。

3.异常就绪

* socket上收到带外数据。

select的特点

* 我们会借助一个数组,将有效的文件描述符记录保存起来,以致于不会每次都要在fd_set中检查哪个文件描述符是我们所关心的。

* 每次调用select,都要重新将我们关心的文件描述符重新设置在数组中。

* 再select返回后,数组作为源数据和fd_set进行FDISSET判断。

* select一次等待多个文件描述符,性能相对多进程、多线程高。

select缺点

* 每次调用select,都需要重新设置fd_set,是输入输出型参数。
* 每次调用select,都需要将文件描述符从用户态拷贝到内核态,开销大。
* 每次调用,都需要在内核中遍历传递进来的所有fd,开销大。
* select支持的文件描述符数量有限制。

select服务器的实现(检测标准输出和标准输入)

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

#define MAX_FDS sizeof(fd_set)*8

int start_up(int port)
{
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if(sock<0)
    {
        perror("socket");
        exit(2);
    }

    //设置没有TIME_WAIT
    int opt=1;
    setsockopt(sock, SOL_SOCKET,SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_port = htons(port);
    local.sin_addr.s_addr = htonl(INADDR_ANY);

    int ret = bind(sock, (struct sockaddr*)&local, sizeof(local));
    if(ret < 0)
    {
        perror("bind");
        exit(3);
    }

    if(listen(sock, 5)<0)
    {
        perror("listen");
        exit(4);
    }
    return sock;
}

//初始化
void Init(int arr[], int num)
{
    int i=0;
    for(i = 0;i < num; ++i)
    {
        arr[i]= -1;
    }
}

int AddSockToArr(int arr[], int fd, int num)
{
    int i;
    for(i=0; i<num; i++)
    {
        if(arr[i] < 0)
        {
            arr[i] = fd;
            return i;
        }
    }
    return -1;//full
}

int AddArrToFdSet(int arr[], int num, fd_set* rfds)
{
    int max = -1;
    int i;
    for(i=0; i<num; ++i)
    {
        if(arr[i]>= 0)
        {
            FD_SET(arr[i], rfds);
            if(arr[i] > max)
                max = arr[i];
        }
    }           
    return max;
}

void serviceIO(int arr[], int num, fd_set* rfds)
{
    int i;
    for(i=0; i<num; ++i)
    {
        if(arr[i] > -1){
            if(i==0&&FD_ISSET(arr[i],rfds)){
                //listen sock is ready
                struct sockaddr_in client;
                socklen_t len = sizeof(client);
                int sock = accept(arr[i], (struct sockaddr*)&client, &len);
                if(sock < 0)
                {
                    perror("accept");
                    continue;
                }
                printf("get a connet[%s:%d]\n",\
                    inet_ntoa(client.sin_addr),\
                    ntohs(client.sin_port));
                if(AddSockToArr(arr, sock, num) == -1)
                    close(sock);
            }

            else if(i!=0 && FD_ISSET(arr[i],rfds)){   
                //normal sock
                char buf[1024];
                ssize_t ret = read(arr[i], buf, sizeof(buf)-1);
                if(ret > 0){
                    buf[ret]=0;
                    printf("client >:%s\n",buf);
                }
                else if(ret == 0){
                    //读到文件末尾
                    close(arr[i]);
                    arr[i] = -1;
                    printf("client say: goodbye!\n");
                }
                else{
                    perror("read");
                    close(arr[i]);
                    arr[i] = -1;
                }
            }

            else
            {
                //do nothing
            }
        }
    }
}                                                 
//iport
int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        printf("Usag: ./server [port]\n");
        return 1;
    }

    int listen_fd = start_up(atoi(argv[1]));

    fd_set rfds;
    int fd_arr[MAX_FDS];
    Init(fd_arr,MAX_FDS );

    AddSockToArr(fd_arr, listen_fd, MAX_FDS);
    for(;;)
    {
        FD_ZERO(&rfds);
        int max_fd = AddArrToFdSet(fd_arr, MAX_FDS, &rfds);
        switch(select(max_fd+1, &rfds, NULL, NULL, NULL))
        {
            case -1:
                {
                    perror("select");
                    break;        
               }
            case 0:
                {
                    printf("timeout...\n");
                    break;
                }
            default:
                {
                    serviceIO(fd_arr, MAX_FDS, &rfds);
                    break;
                }
        }
    }
}

利用telnet远程登录访问。
这里写图片描述
服务器端读到客户端的标准输入的数据,并显示在标准输出上。
这里写图片描述
我们也可以在浏览器上输入IP地址和端口号访问服务器,但是只能在同一局域网中检测。
这里写图片描述

猜你喜欢

转载自blog.csdn.net/zwe7616175/article/details/80605426