Linux/C/C++多线程和IO多路转接网络服务端的简单实现

webServer.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <unistd.h>
#include <poll.h>

#include <errno.h>
#include <fcntl.h>
#include <sys/epoll.h>


#define BUFFER_LENGTH   5
#define POLL_SIZE       1024
#define EPOLL_SIZE      1024

/* 设置文件描述符为非阻塞 */
int setNonBlocking(int sockfd)
{
    if (-1 == fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0)|O_NONBLOCK))
    {
        return -1;
    }

    return 0;
}

void *client_route(void *arg) {

    int clientfd = (long)arg;

    char buffer[BUFFER_LENGTH + 1] = {0};
    int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);
    if (ret < 0) {
        if (errno == EAGAIN || errno == EWOULDBLOCK) {
            printf("read all data\n");
        }
    } else if (ret == 0) {
        printf("disconnect \n");
    } else {
        printf("Recv:%s, %d Bytes\n", buffer, ret);
    }

    close(clientfd);

    pthread_exit(0);
}


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

    if (argc < 2) {
        printf("Paramter Error\n");
        return -1;
    }
    int port = atoi(argv[1]);

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

    int opt = 1;
    unsigned int len = sizeof(opt);
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, len);
    setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, &opt, len);

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(struct sockaddr_in));

    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = INADDR_ANY;

    if (bind(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0) {
        perror("bind");
        return 2;
    }

    if (listen(sockfd, 5) < 0) {
        perror("listen");
        return 3;
    }

#if 0
    // 一连接一线程
    while (1) {

        struct sockaddr_in client_addr;
        memset(&client_addr, 0, sizeof(struct sockaddr_in));
        socklen_t client_len = sizeof(client_addr);
        
        int clientfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len);
        if (clientfd <= 0) continue;

        pthread_t thread_id;
        int ret = pthread_create(&thread_id, NULL, client_route, (void*)(long)clientfd);
        pthread_detach(thread_id);
        if (ret < 0) {
            perror("pthread_create");
            exit(1);
        }

    }

#elif 0
    // select方法
    fd_set rfds, rset;

    FD_ZERO(&rfds);
    FD_SET(sockfd, &rfds);

    int max_fd = sockfd;
    int i = 0;

    while (1) {
        rset = rfds;

        int nready = select(max_fd+1, &rset, NULL, NULL, NULL);
        if (nready < 0) {
            printf("select error : %d\n", errno);
            continue;
        }
        if (nready = 0)
        {
            printf("time out...\n");
            continue;
        }

        if (FD_ISSET(sockfd, &rset)) { //accept
            struct sockaddr_in client_addr;
            memset(&client_addr, 0, sizeof(struct sockaddr_in));
            socklen_t client_len = sizeof(client_addr);

            int clientfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len);
            if (clientfd <= 0) continue;

            char str[INET_ADDRSTRLEN] = {0};
            printf("recvived from %s at port %d, sockfd:%d, clientfd:%d\n", inet_ntop(AF_INET, &client_addr.sin_addr, str, sizeof(str)),
                ntohs(client_addr.sin_port), sockfd, clientfd);

            if (max_fd == FD_SETSIZE) {
                printf("clientfd --> out range\n");
                break;
            }
            FD_SET(clientfd, &rfds);

            if (clientfd > max_fd) max_fd = clientfd;

            printf("sockfd:%d, max_fd:%d, clientfd:%d\n", sockfd, max_fd, clientfd);

            if (--nready == 0) continue;
        }

        for (i = sockfd + 1;i <= max_fd;i ++) {
            if (FD_ISSET(i, &rset)) {
                char buffer[BUFFER_LENGTH + 1] = {0};
                int ret = recv(i, buffer, BUFFER_LENGTH, 0);
                if (ret < 0) {
                    if (errno == EAGAIN || errno == EWOULDBLOCK) {
                        printf("read all data");
                    }
                    FD_CLR(i, &rfds);
                    close(i);
                } else if (ret == 0) {
                    printf(" disconnect %d\n", i);
                    FD_CLR(i, &rfds);
                    close(i);
                    break;
                } else {
                    printf("Recv: %s, %d Bytes\n", buffer, ret);
                }
                if (--nready == 0) break;
            }
        }
        
    }

#elif 0
    // poll方法
    struct pollfd fds[POLL_SIZE] = {0};
    fds[0].fd = sockfd;
    fds[0].events = POLLIN;

    int max_fd = 0, i = 0;
    for (i = 1;i < POLL_SIZE;i ++) {
        fds[i].fd = -1;
    }

    while (1) {
        int nready = poll(fds, max_fd+1, 5);
        if (nready <= 0) continue;

        if ((fds[0].revents & POLLIN) == POLLIN) {
            
            struct sockaddr_in client_addr;
            memset(&client_addr, 0, sizeof(struct sockaddr_in));
            socklen_t client_len = sizeof(client_addr);
        
            int clientfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len);
            if (clientfd <= 0) continue;

            char str[INET_ADDRSTRLEN] = {0};
            printf("recvived from %s at port %d, sockfd:%d, clientfd:%d\n", inet_ntop(AF_INET, &client_addr.sin_addr, str, sizeof(str)),
                ntohs(client_addr.sin_port), sockfd, clientfd);

            fds[clientfd].fd = clientfd;
            fds[clientfd].events = POLLIN;

            if (clientfd > max_fd) max_fd = clientfd;

            if (--nready == 0) continue;
        }

        for (i = sockfd + 1;i <= max_fd;i ++) {
            if (fds[i].revents & (POLLIN|POLLERR)) {
                char buffer[BUFFER_LENGTH + 1] = {0};
                int ret = recv(i, buffer, BUFFER_LENGTH, 0);
                if (ret < 0) {
                    if (errno == EAGAIN || errno == EWOULDBLOCK) {
                        printf("read all data");
                    }
                    
                    close(i);
                    fds[i].fd = -1;
                } else if (ret == 0) {
                    printf(" disconnect %d\n", i);
                    
                    close(i);
                    fds[i].fd = -1;
                    break;
                } else {
                    printf("Recv: %s, %d Bytes\n", buffer, ret);
                }
                if (--nready == 0) break;
            }
        }
    }

#else
    // epoll方法, 原理如同小区蜂巢, 监听描述符采用水平触发, 客户端采用边缘触>发非阻塞IO
    int epoll_fd = epoll_create(EPOLL_SIZE);
    struct epoll_event ev, events[EPOLL_SIZE] = {0};

    // 将监听socket加入ev, ev代表当前处理的socket
    ev.events = EPOLLIN;
    ev.data.fd = sockfd;
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sockfd, &ev);

    while (1) {

        int nready = epoll_wait(epoll_fd, events, EPOLL_SIZE, -1);
        if (nready == -1) {
            printf("error:epoll_wait\n");
            break;
        }

        int i = 0;
        for (i = 0;i < nready;i ++) {
            if (events[i].data.fd == sockfd) {

                struct sockaddr_in client_addr;
                memset(&client_addr, 0, sizeof(struct sockaddr_in));
                socklen_t client_len = sizeof(client_addr);

                int clientfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len);
                if (clientfd <= 0) continue;

                char str[INET_ADDRSTRLEN] = {0};
                printf("recvived from %s at port %d, sockfd:%d, clientfd:%d\n", inet_ntop(AF_INET, &client_addr.sin_addr, str, sizeof(str)),
                    ntohs(client_addr.sin_port), sockfd, clientfd);

                ev.events = EPOLLIN | EPOLLET; // 对客户端socket用边缘触发
                ev.data.fd = clientfd;
                setNonBlocking(clientfd);      // 新客户端注册时设置为非阻塞IO
                epoll_ctl(epoll_fd, EPOLL_CTL_ADD, clientfd, &ev);
            } else {
                int clientfd = events[i].data.fd;
                char buffer[BUFFER_LENGTH + 1];

                while (1) // 这个循环必不可少, 边缘触发确保读完所有数据
                {
                    memset(buffer, 0, BUFFER_LENGTH + 1);
                    int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);
                    if (ret < 0) {
                        if (errno == EAGAIN || errno == EWOULDBLOCK) {
                            printf("loop read all data\n");
                            break;
                        }

                        close(clientfd);

                        ev.events = EPOLLIN | EPOLLET;
                        ev.data.fd = clientfd;
                        epoll_ctl(epoll_fd, EPOLL_CTL_DEL, clientfd, &ev);
                        break;
                    } else if (ret == 0) {
                        printf(" disconnect %d\n", clientfd);

                        close(clientfd);

                        ev.events = EPOLLIN | EPOLLET;
                        ev.data.fd = clientfd;
                        epoll_ctl(epoll_fd, EPOLL_CTL_DEL, clientfd, &ev);

                        break;
                    } else {
                        printf("Recv: %s, %d Bytes\n", buffer, ret);
                    }
                }
            }
        }

    }

    close(epoll_fd);
#endif

    close(sockfd);

    return 0;

}

一、一连接一线程

最开始没有多线程多进程没有select、poll、epoll怎么做服务器呢?答案就是信号。信号是可以作为服务器的。信号挂上一个回调函数。主函数直接就是无限睡眠就行了。有消息来了信号触发回调函数,再处理消息就行了。程序实现非常简单,性能就是所有模型里面最低的。直到后来有了多进程和多线程服务器,但是问题仍然是满足不了高并发,技术的迭代就是为了解决实际问题,所以再然后就是IO多路转接,直到最后我们服务器、网络框架(现在多是协程或者异步框架)都用epoll。先铺垫,epoll就是小区的蜂巢,能支撑亿级并发。
多线程服务器就是一请求一处理线程。最原始的一连接一线程,每连接一个客户端就启动一个线程,任务处理完成之后就立即断开,close掉描述符, 这种没多少技术难度,效率不高,所以现在多数网路库不会采用这种模式,实际实现时做好线程间同步就行了,如果业务本身开销很小,时间很短,用线程池可以解决频繁创建和销毁线程所带来的开销。

二、select

select,IO多路转接的先行者。在网上很多介绍select原理的博客文章,在扯内核态用户态,抄来抄去没什么意思,其实大多是抄APUE的,我不介绍详细原理,本文也是参考APUE和UNP完成的,所以建议读者要深入理解详见APUE、UNP。我介绍具体程序的实现流程和细节,作为我自己学习使用。
一、初始化描述符集rfds,监听描述符加入描述符集。此时最大描述符为监听描述符。
二、select循环开始。rset是当前操作的描述符集,是从rfds拷贝来的。这里的重点就是拷贝。因为select函数会改变描述符集,这就不是我们所期望的了,我们希望只有在增加或者删除描述符时才改变我们的描述符集,所以可以看见后续增加描述符时在rfds上增加,而循环开始要拷贝到rset中。
三、执行select,<0执行出错,==0超时。其他为有事件发生。
1、当为监听描述符有事件时,代表有新的客户端链接上来。这时候更改最大fd。再加入到rfds集合中,若只有这一个描述符有事件,那么continue。
2、当为其他描述符有事件时,代表有fd可读。从sockfd+1开始找。FD_ISSET判断fd是否打开,recv接收数据。返回值若小于或者等于0则代表客户端主动断开连接或者执行出错(==0才是主动断连)。其他情况(>0)接收数据。注意细节,当(–nread =0)时代表本次所有事件已经处理完毕!无需真正遍历所有fd!这样可以显著提升select的效率!所以本身并发量不高,不超过1000时,select的效率是很理想的!

三、poll

尴尬的poll。poll和select的实现整体上几乎一致。因为他们的原理也基本一致,poll没有了1024的最大描述符个数的限制,这也是我们认为他相对于select的改进点。数据结构采用pollfd结构。

struct pollfd
{
int fd; /* descriptor to check /
short events; /
events of interest on fd /
short revents; /
events that occurred on fd */
};

poll函数第一个参数就是指向一个结构数组第一个元素的指针。每个元素都是一个pollfd结构。用于测试某个给定描述符fd的条件。
要测试的条件由events成员指定。函数在相应的revents成员中返回结果,从而避免使用值-结果参数。回想select函数中间三个参数都是使用值–结果参数,所以select每次循环开始需要将rfds拷贝一份给rset。这也是和select不同的地方之一。而revents为返回结果。我们就是根据返回结果作为是否可读的依据。
实现方面和select基本一致,依然要记录最大fd。要注意当读完数据或者客户端主动断连时(recv返回值<=0)要将fd置为-1。性能方面没有得到显著提升。虽然突破了最大描述符限制,但再大我们用epoll,连接数并发量小我们用select就好。所以poll显得很尴尬。

四、epoll

一、epoll,就是我们小区的蜂巢。我们想象一下,假设没有蜂巢。快递员寄件要挨家挨户地寄快递。我们取件时又要等快递员准备好了有时间才行。而有了小区蜂巢,不论是寄件还是取件我们直接交给蜂巢就好了,以前一个小区几个快递员都不够,现在一个快递员就够用了,寄还是取就交给蜂巢,蜂巢有快递就会通知住户或快递员来取。性能大大提高而且方便。epoll就是这样的IO管理组件。
没错,epoll本质就是一个IO管理的组件。他只是报告准备好了的fd。而且支持边缘触发,select和poll只有水平触发。本文采用边缘触发。
二、水平触发和边缘触发。
1、每次触发本质其实是TCP协议栈在触发,也就是TCP协议栈有一个回调函数回调到epoll里面的模块,只有这个模块触发,epoll_wait才会解除阻塞!(协议栈本身也有一个主循环,用来把网卡里的数据copy到协议栈里面),当TCP协议栈来了一个信号,水平触发只要有事件没有被处理完我就是一直触发,一直调用那个回调函数,而边缘触发就是来了一个事件只触发一次,不管事件是否被处理是否被处理完我只是触发一次,没收到没收完就是只有下一次再说,报文堆积在一起我也不管。实际业务中基本都要保证报文一次发送完成,所以边缘触发要设置非阻塞IO以保证一次事件完全处理完成。
2、那么既然边缘触发那么麻烦为什么还有存在的必要呢?那就是性能方面边缘触发更高,系统不会充斥大量你不关心的就绪文件描述符。但是对于监听socketfd我们还是采用水平触发保证客户端都能正常连接(边缘触发高并发环境可能导致客户端无法连接)。还有一个问题就是设置非阻塞IO为什么能保证一次事件报文处理完成呢?我们是while(1)的循环来确保数据读完,如果是阻塞IO即使接收完成了数据他还是会卡在recv的地方,直到有数据可读!这个时候新客户端也是无法连接了,所以我们必须将connfd设置为非阻塞IO,再循环读取connfd里面的数据!
三、实现流程
1、epoll_create这个函数参数很有意思,大于0的数随便填,没有影响。参数有当前设置的ev结构体和events结构体数组。开始将监听socket加入epoll_fd。
2、主循环开始,epoll_wait返回值nready为准备好的fd的个数,所以只需要遍历nready就行了,而epoll实现了重排序,意思是有事件的fd自动排到了前面,所以他解决了select和poll的难题,他直接知道具体是哪几个描述符有事件。这是epoll性能高的主要原因。
3、如果是监听描述符有事件那代表有新客户但连接,设置新客户端为边缘触发非阻塞IO,将clientfd加入到epoll_fd。
4、反之就是已经就绪的fd,注意重点,这里的while(1)循环不能省略!保证边缘触发一次读完所有数据!如果是采用默认的水平触发不用这个while循环。大于0代表有数据直接读取。小于0注意,这里如果仅仅是数据已经读完的错误:if (errno == EAGAIN || errno == EWOULDBLOCK)是直接break跳出循环,不要colse掉clientfd。其他不管是断连还是错误我们都要将clientfd对应的ev从epoll_fd删除。并colse掉clientfd。
四、常见的网络框架如何做epoll
1、单线程 --> redis
2、多线程 --> nattysever
3、多核 --> ntyco
4、多进程 --> nginx

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值