TCP/IP网络编程 学习笔记_13 --基于I/O复用的服务端

前言:前面我们讲了多进程的并发服务端,只要有客服端连接请求就会创建新进程,这虽然也是一种解决方案,但创建进程是需要付出极大代价的,这需要大量运算和内存空间,而且每个进程间具有独立的内存空间,所以相互间的数据交换也相对复杂(管道)。
本章将讨论并发服务器的第二种实现方法——基于I/O复用的服务器端构建。

I/O复用

  • 什么是I/O复用?通俗点讲,其实就是一个事件监听,只是这个监听的事件一般是I/O操作里的读(read)与写(write),只要发生了监听的事件它就会响应。注意与一般服务器的区别,一般服务器是连接请求先进入请求队列里,然后,服务端套接字一个个有序去受理。而I/O复用服务器是事件监听,只要对应监听事件发生就会响应,是属于并发服务器的一种。

  • I/O复用的使用
    1,I/O复用的使用其实就是对select函数的使用,说select函数是I/O复用的全部内容也不为过。但这个函数与一般函数不同,它很难使用,我们先来看看它的调用顺序,分为3步:
    步骤一:

    • 设置文件描述符,即注册要监听的文件描述符,如监听标准输入的文件描述符0 -> FD_SET(0, &reads)
    • 指定监视范围,Linux上创建文件对象生成的对应文件描述符是从0开始递增的,所以最大监视范围为最后创建的文件描述符+1。
    • 设置超时,因为select函数是一个阻塞函数,只有监视的文件描述符发生变化才会返回,设置超时就是为了防止阻塞,如果不想设置超时,则传递NULL。

    步骤二:

    • 调用select函数

    步骤三:

    • 查看调用结果,FD_ISSET(0, &reads)发生变化返回真。

    2,再来讲讲select函数,首先,来看看它的定义:

    int select(int maxfd, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
    maxfd:监视范围
    readset:监视对应文件描述符的接收事件,不监视这个事件则传0
    writeset:监视对应文件描述符的传输事件,不监视则传0
    exceptset:监视异常事件,不监视则传0
    timeout:设置超时时间,不设置则传NULL
    返回值:发生错误返回-1,超时返回0,发生参数2-4事件返回事件发生的对应文件描述符。

    然后,再来看看fd_set数组,它是一个存有0和1的位数组,这个数组中的值0表不监视,1表监视。而每个值对应在数组中的位置与文件描述符一一对应。如Linux上的标准输入的文件描述符是0,则要监听这个文件描述符,就只需把fd_set数组的fd_set[0]设置为1即可。下面是对fd_set数组操作的一些宏:

    FD_ZERO(fd_set *fdset):将fd_set变量的所有位初始化为0
    FD_SET(int fd, fd_set *fdset):设置文件描述符fd为监听状态
    FD_CLR(int fd, fd_set *fdset):取消文件描述符fd的监听状态
    FD_ISSET(int fd, fd_set *fdset):文件描述符fd是否发生相应的监视事件,发生则返回真。

    3,select函数调用示例:

//
// main.cpp
// hello_client
//
// Created by app05 on 15-8-31.
// Copyright (c) 2015年 app05. All rights reserved.
//

#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/select.h>
#define BUF_SIZE 30

int main(int argc, const char * argv[]) {
    fd_set reads, temps;
    int result, str_len;
    char buf[BUF_SIZE];
    struct timeval timeout;
    /*初始化状态设置*/
    FD_ZERO(&reads);   //将位数组reads初始化为0,即不监听任何文件描述符
    FD_SET(0, &reads);  //设置监听,监听文件描述符为0的对象(标准输入)

    while (1)
    {
        //因为select调用后,timeout和初始化状态会发生变化,所以需要每次循环前再初始化一次
        temps = reads;
        timeout.tv_sec = 5;
        timeout.tv_usec = 5000;
        result = select(1, &temps, 0, 0, &timeout);
        if(result == -1)
        {
            puts("select() error");
            break;
        }
        else if (result == 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;
}

TCP/IP网络编程 学习笔记_13 --基于I/O复用的服务端_第1张图片

实现I/O复用的服务端

用I/O复用的方式实现前面的回声程序

  • 服务端代码
//
// main.cpp
// hello_server
//
// Created by app05 on 15-8-31.
// Copyright (c) 2015年 app05. All rights reserved.
//

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

#define BUF_SIZE 100
void error_handling(char *message);

int main(int argc, const 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;
    char buf[BUF_SIZE];
    if (argc != 2) {
        printf("Usage: %s <port> \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_ZERO(&reads);
    FD_SET(serv_sock, &reads);
    fd_max = serv_sock;

    while (1)
    {
        cpy_reads = reads;
        timeout.tv_sec = 5;
        timeout.tv_usec = 5000;

        //监听服务端套接字和与客服端连接的服务端套接字的read事件
        if ((fd_num = select(fd_max + 1, &cpy_reads, 0, 0, &timeout)) == -1)
            break;
        if(fd_num == 0)
            continue;

        if (FD_ISSET(serv_sock, &cpy_reads))//受理客服端连接请求
        {
            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(clnt_sock, buf, BUF_SIZE);
            if (str_len == 0)//客服端发送的退出EOF
            {
                FD_CLR(clnt_sock, &reads);
                close(clnt_sock);
                printf("closed client: %d \n", clnt_sock);
            }
            else
            {
                write(clnt_sock, buf, str_len);
            }
        }
    }

    close(serv_sock);
    return 0;
}


void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}
  • 客服端代码用以前写的,随便copy一个来
//
// main.cpp
// hello_client
//
// Created by app05 on 15-7-6.
// Copyright (c) 2015年 app05. All rights reserved.
//

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 1024
void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

int main(int argc, const char * argv[]) {
    int sock;
    char message[BUF_SIZE];
    int str_len, recv_len, recv_cnt;
    struct sockaddr_in serv_adr;

    if(argc != 3)
    {
        printf("Usage: %s <IP> <port> \n", argv[0]);
        exit(1);
    }

    sock = socket(PF_INET, SOCK_STREAM, 0);
    if(sock == -1)
        error_handling("socket() error");

    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");
    else
        puts("Connected ...............");

    while (1) {
        fputs("Input message(Q to quit): ", stdout);
        fgets(message, BUF_SIZE, stdin);
        if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
            break;

        str_len = write(sock, message, strlen(message));

        /*这里需要循环读取,因为TCP没有数据边界,不循环读取可能出现一个字符串一次发送 但分多次读取而导致输出字符串不完整*/
        recv_len = 0;
        while (recv_len < str_len) {
            recv_cnt = read(sock, &message[recv_len], BUF_SIZE - 1);
            if(recv_cnt == -1)
                error_handling("read() error");
            recv_len += recv_cnt;
        }
        message[recv_len] = 0;
        printf("Message from server: %s", message);
    }

    close(sock);
    return 0;
}

TCP/IP网络编程 学习笔记_13 --基于I/O复用的服务端_第2张图片
TCP/IP网络编程 学习笔记_13 --基于I/O复用的服务端_第3张图片

你可能感兴趣的:(select函数,并发服务器,i-o复用)