TCP/IP网络编程笔记Chapter I -11 I/O复用 & 实现I/O复用服务器端

TCP/IP网络编程笔记Chapter I -11 I/O复用 & 实现I/O复用服务器端

  • 1.I/O复用技术
  • 2.select函数
    • (1)select函数的功能及调用顺序
    • (2)设置文件描述符
    • (3)设置监视范围及超时
    • (4)调用select函数后查看结果
  • 3.实现I/O复用服务器端

我们接下来延伸并发服务器的实现。首先我们需要知道多进程服务器端的缺点,那就是创建进程时需要付出很大的代价,需要大量的运算和内存空间,由于每个进程都具有独立的内存空间,所以相互间的数据交换也需要使用管道这样相对复杂的方法。
那么能否不创建进程也能同时向多个客户端提供服务?当然是可以的!I/O复用技术就是一种。

1.I/O复用技术

I/O复用技术能使程序同时监听多个文件描述符,这对提高程序性能至关重要。Linux下实现I/O复用的系统调用主要有select、poll、epoll。本节的内容是select。
对I/O复用最直观的理解是小时候使用的纸杯电话。
TCP/IP网络编程笔记Chapter I -11 I/O复用 & 实现I/O复用服务器端_第1张图片
TCP/IP网络编程笔记Chapter I -11 I/O复用 & 实现I/O复用服务器端_第2张图片
它的优点是减少了连线长度与纸杯个数。多进程服务器端模型与I/O复用服务器端模型的对比如下图所示,
TCP/IP网络编程笔记Chapter I -11 I/O复用 & 实现I/O复用服务器端_第3张图片
TCP/IP网络编程笔记Chapter I -11 I/O复用 & 实现I/O复用服务器端_第4张图片
可以看出复用技术可以减少进程数,提供服务的进程只有一个。

2.select函数

select函数是最具有代表性的实现复用技术服务器端的方法

(1)select函数的功能及调用顺序

使用select函数时可以将多个文件描述符集中到一起统一监视,监视的项目如下:

  • 是否存在套接字接收数据?
  • 无需阻塞传输数据的套接字有哪些?
  • 哪些套接字发生了异常?

我们把监视项称为事件,发生监视项对应的情况时,称“事件发生”。下图介绍select函数的调用方法和顺序,TCP/IP网络编程笔记Chapter I -11 I/O复用 & 实现I/O复用服务器端_第5张图片
select函数使用和普通函数区别较大,需要一些准备工作,而且在还需要查看调用结果,接下来我们按照步骤逐一讲解。

(2)设置文件描述符

select函数可以同时监视多个文件描述符。当然,监视文件描述符可以视为监视套接字。首先需要将要监视的文件描述符集中到一起,集中时也要按照监视项(接收、传输、异常)进行区分,即按照上述3种监视项分成3类。
使用fd_set位数组执行此操作,最左端的位表示文件描述符0的位置,该位为1表示该文件描述符是监视对象。
在这里插入图片描述
使用如下的宏来对fd_set进行注册和更改

  • FD_ZERO(fd_set *fdset):将fd_set变量的所有位都初始化为0
  • FD_SET(int fd, fd_set *fdset):在参数fdset指向的变量中注册文件描述符fd的信息
  • FD_CLR(int fd, fd_set *fdset):从参数fdset指向的变量中清除文件描述符fd的信息
  • FD_ISSET(int fd, fd_set *fdset):若参数fdset指向的变量中包含文件描述符fd的信息,则返回“真”

eg
TCP/IP网络编程笔记Chapter I -11 I/O复用 & 实现I/O复用服务器端_第6张图片

(3)设置监视范围及超时

我们先介绍下select函数

#include
#include
int select(int maxfd,fd_set * readset,fd_set * writeset,fd_set * exceptset,const struct timeval *timeout);
  • 成功返回大于0的值(该值是发生事件的文件描述符数),失败返回-1
  • maxfd:监视对象的文件描述符
  • readset:将所有关注“是否存在待读取数据”的文件描述符注册到fd_set型变量,并传递其地址值
  • writeset:将所有关注“是否可传输无阻塞数据”的文件描述符注册到fd_set型变量,并传递其地址值
  • exceptset:将所有关注“是否发生异常”的文件描述符注册到fd_set型变量,并传递其地址值
  • timeout:调用select函数后,为防止陷入无限阻塞的状态,传递超时信息

select函数用来验证三种监视项的变化情况,根据监视项声明三个fd_set型变量,分别向其注册文件描述符信息,并把变量的地址值传递到上述函数的第二到第四个参数。但在调用select函数之前,需要决定下面两件事:

  • 文件描述符的监视范围
  • 如何设定select函数的超时时间

文件描述符的监视范围与select函数的第一个参数有关。实际上,select函数要求通过第一个参数传递监视对象文件描述符的数量。因此,需要得到注册在fd_set变量中的文件描述符数。但每次新建文件描述符时,其值都会加一,故只需将最大的文件描述符值加一再传递到select函数即可,加一是因为文件描述符的值从0开始。

select函数的超时时间与select函数的最后一个参数相关,其中timeval结构体定义如下。

struct timeval
{
     
    long tv_sec;     /* seconds */
    long tv_usec;    /* microseconds */
};

select函数在监视的文件描述符发生变化时才返回,如果未发生变化,就会进入阻塞状态,指定超时时间就是为了防止这种情况发生。通过声明上述结构体变量,将秒数填入tv_sec成员,将毫秒数填入tv_usec成员,然后将结构体的地址值传递到select函数的最后一个参数。超时select函数返回0,不设置超时,可以传递NULL。

(4)调用select函数后查看结果

TCP/IP网络编程笔记Chapter I -11 I/O复用 & 实现I/O复用服务器端_第7张图片
select函数调用完成后,向其传递的fd_set变量中将发生变化。原来为1的所有位均变为0,但发生变化的文件描述符对应位除外。因此,可以认为值仍然为1的位置上的文件描述符发生了变化
select函数使用示例伪代码如下


    fd_set reads, temps;
    int result, str_len;
    struct timeval timeout;
    //①设置文件描述符
 	FD_ZERO(&reads);//初始化fd_set变量都为0 
 	//②设置监视范围
    FD_SET(0, &reads);  //监视文件描述符0位置  
    while (1)
    
    {
     	temps = reads;//每次调用select剩余位会被初始化为零,所以需要保存初始值 
        //③设置超时
        timeout.tv_sec = 5;
        timeout.tv_usec = 0;
        //④调用select函数
        result = select(1, &temps, 0, 0, &timeout);
        //⑤查看结果
        if (result == 0)//超时返回0 
        {
     
            //超时
        }
        else if(FD_ISSET(0, &temps)//验证变化的文件描述符是否是标准输入 
        {
     
            //有输入则输出
        }
    }


完整示例如下

#include 
#include 
#include 
#include 
#define BUF_SIZE 30
 
int main(int argc, char *argv[])
{
     
    fd_set reads, temps;
    int result, str_len;
    char buf[BUF_SIZE];
    struct timeval timeout;
 	//初始化fd_set变量都为0 
    FD_ZERO(&reads);
    //监视文件描述符0位置 
    FD_SET(0, &reads); // 0 is standard input(console)
 
    /*
    timeout.tv_sec=5;
    timeout.tv_usec=5000;
    */
 
    while (1)
    {
     
    	//每次调用select剩余位会被初始化为零,所以需要保存初始值 
        temps = reads;
        //初始化超时 
        timeout.tv_sec = 5;
        timeout.tv_usec = 0;
        //select
        result = select(1, &temps, 0, 0, &timeout);
        if (result == -1)
        {
     
            puts("select() error!");
            break;
        }
        else if (result == 0)//超时返回0 
        {
     
            puts("Time-out!");
        }
        else//验证变化的文件描述符是否是标准输入 
        {
     
            if (FD_ISSET(0, &temps))
            {
     
                str_len = read(0, buf, BUF_SIZE);
                buf[str_len] = 0;
                printf("message from console: %s", buf);
            }
        }
    }
    return 0;
}

运行无任何输入每5秒会提示超时,也可以输入字符串,select函数监视后会读取并输出。
TCP/IP网络编程笔记Chapter I -11 I/O复用 & 实现I/O复用服务器端_第8张图片

3.实现I/O复用服务器端

I/O复用服务器端的实现是在之前服务器端的基础上进行更改。关于select的伪代码如下,如果服务端套接字中有变化,将受理连接请求并注册与客户端连接的套接字文件描述符。发生变化的套接字并非服务端套接字时,接收的数据为EOF时需关闭套接字,并从reads中删除相应信息,而接收的数据为字符串时,执行回声服务。

int main(int argc, char *argv[])
{
     

 	//初始化fd_set变量都为0 
    FD_ZERO(&reads);
    //监视文件描述符serv_sock 
    FD_SET(serv_sock, &reads);
    fd_max = serv_sock;
 
    while (1)
    {
     
    	//每次调用select剩余位会被初始化为零,所以需要保存初始值 
        cpy_reads = reads;
        //初始化超时 
        timeout.tv_sec = 5;
        timeout.tv_usec = 5000;
 		//select
        fd_num = select(fd_max + 1, &cpy_reads, 0, 0, &timeout);
 		for //查找状态变化的文件描述符 
        {
     
            if (FD_ISSET(i, &cpy_reads))					//有变化 
            {
     
            	//服务器端套接字有变化 
                if (i == serv_sock) 						// connection request!
                {
     
                	accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);//受理客户端连接请求
                    FD_SET(clnt_sock, &reads);				//注册客户端套接字文件描述符 
                   
                }
                else 										//接受字符串还是断开连接 
                {
     
                    if (str_len == 0) 						// close request!
                    {
     
                        FD_CLR(i, &reads);					//clear                        
                    }
                    else									// read message!
                    {
     
                        // echo!
                    }
                }
            }
        }
    }
    close(serv_sock);
    return 0;
}

完整代码如下:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
 
#define BUF_SIZE 100
void error_handling(char *buf);
 
int main(int argc, char *argv[])
{
     
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_adr, clnt_adr;
    struct timeval timeout;
    fd_set reads, cpy_reads;
 
    socklen_t adr_sz;
    int fd_max, str_len, fd_num, i;
    char buf[BUF_SIZE];
    if (argc != 2) {
     
        printf("Usage : %s \n", argv[0]);
        exit(1);
    }
 
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = htons(atoi(argv[1]));
 
    if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
        error_handling("bind() error");
    if (listen(serv_sock, 5) == -1)
        error_handling("listen() error");
 	
 	//初始化fd_set变量都为0 
    FD_ZERO(&reads);
    //监视文件描述符serv_sock 
    FD_SET(serv_sock, &reads);
    fd_max = serv_sock;
 
    while (1)
    {
     
    	//每次调用select剩余位会被初始化为零,所以需要保存初始值 
        cpy_reads = reads;
        //初始化超时 
        timeout.tv_sec = 5;
        timeout.tv_usec = 5000;
 		
 		//select
        if ((fd_num = select(fd_max + 1, &cpy_reads, 0, 0, &timeout)) == -1)
            break;
        if (fd_num == 0)
            continue;
 
 		//查找状态变化的文件描述符 
        for (i = 0; i < fd_max + 1; i++)
        {
     
            if (FD_ISSET(i, &cpy_reads))//有变化 
            {
     
            	//服务器端套接字有变化 
                if (i == serv_sock) // connection request!
                {
     
                    adr_sz = sizeof(clnt_adr);
                    clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);
                    FD_SET(clnt_sock, &reads);		//注册客户端套接字文件描述符 
                    if (fd_max < clnt_sock)
                        fd_max = clnt_sock;
                    printf("connected client: %d \n", clnt_sock);
                }
                else 								//接受字符串还是断开连接 
                {
     
                    str_len = read(i, buf, BUF_SIZE);
                    if (str_len == 0) 				// close request!
                    {
     
                        FD_CLR(i, &reads);
                        close(i);
                        printf("closed client: %d \n", i);
                    }
                    else							// read message!
                    {
     
                        write(i, buf, str_len); 	// echo!
                    }
                }
            }
        }
    }
    close(serv_sock);
    return 0;
}
 
void error_handling(char *buf)
{
     
    fputs(buf, stderr);
    fputc('\n', stderr);
    exit(1);
}

使用前几节所用的I/O分割的客户端

#include 
#include 
#include 
#include 
#include 
#include 
 
#define BUF_SIZE 30
void error_handling(char *message);
void read_routine(int sock, char *buf);
void write_routine(int sock, char *buf);
 
int main(int argc, char *argv[])
{
     
    int sock;
    pid_t pid;
    char buf[BUF_SIZE];
    struct sockaddr_in serv_adr;
    if (argc != 3) {
     
        printf("Usage : %s  \n", argv[0]);
        exit(1);
    }
 
    sock = socket(PF_INET, SOCK_STREAM, 0);
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_adr.sin_port = htons(atoi(argv[2]));
 
    if (connect(sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
        error_handling("connect() error!");
 
    pid = fork();
    if (pid == 0)
        write_routine(sock, buf);
    else
        read_routine(sock, buf);
 
    close(sock);
    return 0;
}
 
void read_routine(int sock, char *buf)
{
     
    while (1)
    {
     
        int str_len = read(sock, buf, BUF_SIZE);
        if (str_len == 0)
            return;
        buf[str_len] = 0;
        printf("Message from server: %s", buf);
    }
}
void write_routine(int sock, char *buf)
{
     
    while (1)
    {
     
        fgets(buf, BUF_SIZE, stdin);
        if (!strcmp(buf, "q\n") || !strcmp(buf, "Q\n"))
        {
     
            shutdown(sock, SHUT_WR);
            return;
        }
        write(sock, buf, strlen(buf));
    }
}
void error_handling(char *message)
{
     
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

服务器端运行结果如下图,共有文件描述符为4,5,6的客户端连接。
TCP/IP网络编程笔记Chapter I -11 I/O复用 & 实现I/O复用服务器端_第9张图片
客户端发送后也能收到回声消息。
TCP/IP网络编程笔记Chapter I -11 I/O复用 & 实现I/O复用服务器端_第10张图片

你可能感兴趣的:(TCP/IP网络编程,linux,socket,网络)