Level6day6 多路复用作业

1.编写基于tcp模型的IO多路复用(select)程序,在服务器端采用select来实现客户端的多路并发,

当客户端输入为quit时当前客户端退出。
(作业要求:做作业的时候不要再翻看视频上的教程,对函数理解不明白的全部通过man手册去查看,
自己思考框架,使用makefile编译,然后将测试的记录和结果添加到readme.txt文件中提交上来,代码实现完成测试通过后
再提交作业,网络部分学习不写代码不测试看不出问题的,良好的习惯帮助你们快速成长。)

server 代码

#include <cstdio>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>  
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <pthread.h>
#include <errno.h>

#define SERV_PORT 5050
#define SERV_IP_ADDR "192.168.0.158"

int func_recall(int arg);
char botton[64];

int main()
{
    int fd = -1;
    int fdbox[1024], fd_count = 0;;
    int result = 0, i = 0;
    struct sockaddr_in mysockaddr;
    struct sockaddr_in clientscokaddr;
    socklen_t clientaddrlen;

    static pthread_t nptd;

    /* 1. 创建socket fd */
    fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd < 0) {
        perror("socket");
        return -1;
    }

    /*优化4: 允许绑定地址快速重用 */
    int b_reuse = 1;
    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof(int));

    /*2. 绑定 */
    /*2.1 填充struct sockaddr_in结构体变量 */
    bzero((void*)&mysockaddr, sizeof(mysockaddr));
    mysockaddr.sin_family = AF_INET;
    mysockaddr.sin_port = htons(SERV_PORT);//网络字节序的端口号
#if 1    
    //mysockaddr.sin_addr.s_addr = inet_addr(SERV_IP_ADDR);
    mysockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
#else
    result = inet_pton(AF_INET, SERV_IP_ADDR, (void*)&mysockaddr.sin_addr);
    if (result != 1) {
        perror("inet_pton");
        return -1;
    }
#endif
    /*2.2 绑定 */
    result = bind(fd, (struct sockaddr*)&mysockaddr, sizeof(mysockaddr));
    if (result != 0) {
        perror("bind");
        return -1;
    }

    /*3. 调用listen()把主动套接字变成被动套接字 */
    result = listen(fd, 5);
    if (result != 0) {
        perror("listen");
        return -1;
    }

    /*4. 等待客户端连接请求 */

    /* 优化3. 采用多路复用的方式防止阻塞*/
    fd_set rset;
    int fd1;
    fd1 = fd+1;
    fd_count = 1;
    fdbox[0] = fd;

    struct timeval tm;  //非阻塞状态 采用轮询方式
    tm.tv_sec = 0;
    tm.tv_usec = 0;

    int fd_client;
    while(1){
        //清零rset
        FD_ZERO(&rset);
        //置位rset 标识出要监控的文件标识符
        for (i = 0; i < fd_count; i++) {
            FD_SET(fdbox[i], &rset);
        }
        //查询是否有标识符被激活
        result = select(fd1, &rset, NULL, NULL, &tm);

        if (result < 0) {
            perror("select");
            return 0;
        }
        else if (result > 0) {
            //socket fd被激活 接收到新的客户端连接请求
            if (FD_ISSET(fdbox[0], &rset)) {
                bzero((void*)&clientscokaddr, sizeof(clientscokaddr));
                fd_client = accept(fd, (struct sockaddr*)&clientscokaddr, &clientaddrlen);
                if (-1 == result) {
                    perror("accpet");
                    return 0;
                }
                char ipv4_addr[16];
                if (!inet_ntop(AF_INET, (void*)&clientscokaddr.sin_addr, ipv4_addr, clientaddrlen)) {
                    perror("inet_ntop");
                    return -1;
                }

                printf("Clinet(%s:%d) is connected!\n", ipv4_addr, ntohs(clientscokaddr.sin_port));

                //将新建立连接的fd写入数组 以供select调用
                fdbox[fd_count] = fd_client;
                fd_count++;
                fd1++;
            }         
             
            //客户端fd被激活 处理客户端发来信息 
            for (i = 1; i < fd_count; i++) {
                if (FD_ISSET(fdbox[i], &rset)){
                    result = func_recall(fdbox[i]);
                    if (result != 0) {
                        printf("error:func_recall");
                        return -1;
                    }
                }      
            }
            
        }

        sleep(1);
                    
    }

    close(fd);
    
    return 0;
}

int func_recall(int arg) {
    int result = 0;
    int newfd = arg;
    char Quit[] = "quit";
    printf("handler thread: newfd =%d\n", newfd);
    //..和newfd进行数据读写
    char buf[BUFSIZ];
   
    bzero(buf, BUFSIZ);

    do {
        result = read(newfd, buf, BUFSIZ - 1);
    } while (result < 0 && EINTR == errno);

    if (result < 0) {
        perror("read");
        return -1;
    }

    if (!result)
        return 0;

    if (!strcmp(Quit, buf)) {
        printf("client quit...close socket.\n ");
        close(newfd);
    }
    else {
        printf("message is:%s\n", buf);
    }

    return 0;
}

client 代码

#include <cstdio>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>

#define SERV_PORT 5050
#define SERV_IP_ADDR "192.168.0.158"

int main()
{
    int fd = -1;
    int result = 0;
    struct sockaddr_in mysockaddr;
    char MESSAGE[] = "hELLo wOLd...\n";

    fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd < 0) {
        perror("socket");
        return -1;
    }

    bzero((void*)&mysockaddr, sizeof(mysockaddr));
    mysockaddr.sin_family = AF_INET;
    mysockaddr.sin_port = htons(SERV_PORT);
#if 0    
    mysockaddr.sin_addr.s_addr = inet_addr(SERV_IP_ADDR);
#else
    result = inet_pton(AF_INET, SERV_IP_ADDR, (void*)&mysockaddr.sin_addr);
    if (result != 1) {
        perror("inet_pton");
        return -1;
    }
#endif

    result = connect(fd, (struct sockaddr*)&mysockaddr, sizeof(mysockaddr));
    if (result != 0) {
        perror("connect");
        return -1;
    }
    printf("Client staring...OK!\n");

    while (1) {
        do {
            result = write(fd, (void*)MESSAGE, sizeof(MESSAGE));
        } while (result < 0 && EINTR == errno);         
        sleep(10);
    }

    close(fd);
    return 0;
}

效果

2(经典面试题)说下select和epoll的区别;

根据以下链接,整理出epoll的原理和优缺点,写一个测试代码;

优缺点分析

多路复用大体上有三种解决方式:1.select 2.poll 3.epoll。 随着linux内核的发展,poll已经被淘汰基本不在使用了,取而代之的是epoll。
select有很多缺点和不足,为了弥补这些缺点产生出了epoll。
epoll的优点有如下几个方面:

  1. 面对高并发 可以区分出哪些是活跃的,哪些是非活跃的,对活跃的进行高频率的轮询,不活跃的进行低频率的轮询,从而提高效率。
  2. select所用到的FD_SET是有限的 一般为1024 而epoll采用红黑树 双向链表 理论上是无限的。
  3. 内核中实现 select是用轮询方法,即每次检测都会遍历所有FD_SET中的句柄,select要检测的句柄数越多就会越费时

同样,epoll也有缺点
epoll的使用维护需要调用3个函数,分别是 创建 控制 等待,如果连接数很少的情况下,显得十分繁冗。

#include <sys/epoll.h>
int epoll_create ( int size );

#include <sys/epoll.h>
int epoll_ctl ( int epfd, int op, int fd, struct epoll_event *event );

#include <sys/epoll.h>
int epoll_wait ( int epfd, struct epoll_event* events, int maxevents, int timeout );

epoll server代码分析

#include <cstdio>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/epoll.h>

#define SERV_PORT 5050
#define SERV_IP_ADDR "192.168.0.158"

void* func_recall(void* arg);
void handler(int signo) {

    if (signo == SIGCHLD)
    {
        printf("Recall SIGINT\n");
        waitpid(-1, NULL, WNOHANG);
    }
}

int main()
{
    int fd = -1;
    int result = 0;
    struct sockaddr_in mysockaddr;
    struct sockaddr_in clientscokaddr;
    socklen_t clientaddrlen;

    static pthread_t nptd;

    /* 1. 创建socket fd */
    fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd < 0) {
        perror("socket");
        return -1;
    }

    /*优化4: 允许绑定地址快速重用 */
    int b_reuse = 1;
    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof(int));

    /*2. 绑定 */
    /*2.1 填充struct sockaddr_in结构体变量 */
    bzero((void*)&mysockaddr, sizeof(mysockaddr));
    mysockaddr.sin_family = AF_INET;
    mysockaddr.sin_port = htons(SERV_PORT);//网络字节序的端口号
    mysockaddr.sin_addr.s_addr = htonl(INADDR_ANY);

    /*2.2 绑定 */
    result = bind(fd, (struct sockaddr*)&mysockaddr, sizeof(mysockaddr));
    if (result != 0) {
        perror("bind");
        return -1;
    }

    /*3. 调用listen()把主动套接字变成被动套接字 */
    result = listen(fd, 5);
    if (result != 0) {
        perror("listen");
        return -1;
    }

    /*4. 阻塞等待客户端连接请求 */

    /*优化2:通过程序获取刚建立连接的socket的客户端的IP地址和端口号*/
    int fd_client;
    pid_t pid;
    signal(SIGCHLD, handler);

    int epfd, nfds;
    struct epoll_event ev, events[5];                    //ev用于注册事件,数组用于返回要处理的事件
    epfd = epoll_create(1);                                //只需要监听一个描述符——socket fd
    ev.data.fd = fd;
    ev.events = EPOLLIN;                                //监听读状态同时设置LT模式
    epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);    //注册epoll事件

    while (1) {
        nfds = epoll_wait(epfd, events, 5, -1);
        for (int i = 0; i < nfds; i++)
        {
            if (events[i].data.fd == fd)
            {
                printf("welcome to epoll's word!\n");

                bzero((void*)&clientscokaddr, sizeof(clientscokaddr));
                fd_client = accept(fd, (struct sockaddr*)&clientscokaddr, &clientaddrlen);
                if (-1 == result) {
                    perror("accpet");
                    return 0;
                }

                char ipv4_addr[16];
                if (!inet_ntop(AF_INET, (void*)&clientscokaddr.sin_addr, ipv4_addr, clientaddrlen)) {
                    perror("inet_ntop");
                    return -1;
                }

                printf("Clinet(%s:%d) is connected!\n", ipv4_addr, ntohs(clientscokaddr.sin_port));
                pid = fork();
                if (pid < 0) {
                    perror("fork");
                    return -1;
                }
                else if (pid > 0) { //父进程
                    close(fd_client);
                }
                else {  //子进程
                    close(fd);
                    func_recall((void*)&fd_client);
                    return 0;
                }

                ev.data.fd = fd;
                ev.events = EPOLLIN | EPOLLET;                        //设置ET模式
                epoll_ctl(epfd, EPOLL_CTL_MOD, STDIN_FILENO, &ev);    //重置epoll事件(ADD无效)
            }
        }
    }
    

    
    while (1) {
        
    }

    close(fd);
    close(fd_client);
    return 0;
}

void* func_recall(void* arg) {
    int result = 0;
    int newfd = *(int*)arg;

    printf("handler thread: newfd =%d\n", newfd);
    //..和newfd进行数据读写
    char buf[BUFSIZ];


    while (1) {
        bzero(buf, BUFSIZ);

        do {
            result = read(newfd, buf, BUFSIZ - 1);
        } while (result < 0 && EINTR == errno);

        if (result < 0) {
            perror("read");
            break;
        }

        if (!result)
            break;

        printf("message is:%s\n", buf);
    }
    close(newfd);
    return 0;
}

client代码

#include <cstdio>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>

#define SERV_PORT 5050
#define SERV_IP_ADDR "192.168.0.158"

int main()
{
    int fd = -1;
    int result = 0;
    struct sockaddr_in mysockaddr;
    char MESSAGE[] = "hELLo wOLd...\n";

    fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd < 0) {
        perror("socket");
        return -1;
    }

    bzero((void*)&mysockaddr, sizeof(mysockaddr));
    mysockaddr.sin_family = AF_INET;
    mysockaddr.sin_port = htons(SERV_PORT);
#if 0    
    mysockaddr.sin_addr.s_addr = inet_addr(SERV_IP_ADDR);
#else
    result = inet_pton(AF_INET, SERV_IP_ADDR, (void*)&mysockaddr.sin_addr);
    if (result != 1) {
        perror("inet_pton");
        return -1;
    }
#endif

    result = connect(fd, (struct sockaddr*)&mysockaddr, sizeof(mysockaddr));
    if (result != 0) {
        perror("connect");
        return -1;
    }
    printf("Client staring...OK!\n");

    while (1) {
        do {
            result = write(fd, (void*)MESSAGE, sizeof(MESSAGE));
        } while (result < 0 && EINTR == errno);         
        sleep(10);
    }

    close(fd);
    return 0;
}

结果演示

参考文献

参考直播: http://www.makeru.com.cn/live/5413_1937.html
参考博客: https://segmentfault.com/a/1190000003063859
参考播客: http://blog.csdn.net/davidsguo008/article/details/73556811
(注:poll现在用的比较少了,如果感兴趣,可以自己了解下)