应用层协议实现系列——HTTP服务器之仿nginx多进程和多路IO的实现

Posted 心砚thu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了应用层协议实现系列——HTTP服务器之仿nginx多进程和多路IO的实现相关的知识,希望对你有一定的参考价值。

最近在尝试自己写一个Http服务器,在粗略研究了nginx的代码之后,决定仿照nginx中的部分设计自己实现一个高并发的HTTP服务器,在这里分享给大家。

目前使用的较多的Http服务器就是apache和nginx,apache的主要特点就是稳定,而nginx的主要特点是承载的并发量高。在这里从实现原理上做一个分析:

apache采用的是多进程服务器模型,即服务器每监听到一个连接时,会创建一个新的进程去处理连接,进程与进程之间是独立的,因此就算进程在处理连接的过程中崩溃了,也不会影响其他进程的运行。但由于服务器能创建的进程数目与内存有关,因此服务器的最大并发数会受到机器内存的影响,同时如果有人发起大量恶意长链接攻击,就会导致服务器超载。

nginx采用的是多路IO复用服务器模型,即服务器每监听到一个连接时,会将连接加入到连接数组中,使用epoll多路IO复用函数对每个连接进行监听,当连接中有数据到来时,epoll会返回相应的连接,依此对各个连接进行处理即可。epoll的最大连接数量虽然也会受到内存的影响,但由于每个未激活的连接占用的内存很小,所以相比于apache可以承载更大的并发。

但由于多路IO复用是在一个进程中进行的,所以如果在处理连接的过程中崩溃了,其他连接也会受到影响。为了解决这个问题,nginx中也引入了多进程,即nginx服务器由一个master进程和多个worker进程组成。master进程作为父进程在启动的时候会创建socket套接字以及若干个worker进程,每个worker进程会使用epoll对master创建的套接字进行监听。当有新连接到来时,若干个worker进程的epoll函数都会返回,但只有一个worker进程可以accept成功。该进程accept成功之后将该连接加入到epoll的监听数组中,该连接之后的数据都将由该worker进程处理。如果其中一个worker进程在处理连接的过程中崩溃了,父进程会收到信号并重启该进程以保证服务器的稳定性。

另外,每次新连接到来都会唤醒若干个worker进程同时进行accept,但只有一个worker能accept成功,为了避免这个问题,nginx引入了互斥信号量,即每个worker进程在accept之前都需要先获取锁,如果获取不到则放弃accept。

在明确了上述原理之后,我们就可以仿照nginx实现一个http服务器了。首先是创建套接字的函数:

//创建socket
int startup(int port) 
    struct sockaddr_in servAddr;
    memset(&servAddr, 0, sizeof(servAddr));
    //协议域(ip地址和端口)
    servAddr.sin_family = AF_INET;
    //绑定默认网卡
    servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    //端口
    servAddr.sin_port = htons(port);
    int listenFd;
    //创建套接字
    if ((listenFd = socket(AF_INET, SOCK_STREAM, 0)) == -1) 
        printf("create socket error: %s(errno: %d)\\n",strerror(errno),errno);
        return 0;
    
    unsigned value = 1;
    setsockopt(listenFd, SOL_SOCKET, SO_REUSEADDR, &value, sizeof(value));
    //绑定套接字
    if (bind(listenFd, (struct sockaddr *)&servAddr, sizeof(servAddr))) 
        printf("bind socket error: %s(errno: %d)\\n",strerror(errno),errno);
        return 0;
    
    //开始监听,设置最大连接请求
    if (listen(listenFd, 10) == -1) 
        printf("listen socket error: %s(errno: %d)\\n",strerror(errno),errno);
        return 0;
    
    return listenFd;

该函数创建了一个套接字并将其绑定到了一个端口上开始监听。由于我们接下来要创建若干个worker进程,可以通过fork函数实现:

//管理子进程的数组,数组多大就有几个子进程
static int processArr[PROCESS_NUM];
//创建若干个子进程,返回当前进程是否父进程
bool createSubProcess() 
    for (int i=0; i<GET_ARRAY_LEN(processArr); i++) 
        int pid = fork();
        //如果是子进程,返回0
        if (pid == 0) 
            return false;
        
        //如果是父进程,继续fork
        else if (pid >0)
            processArr[i] = pid;
            continue;
        
        //如果出错
        else 
            fprintf(stderr,"can't fork ,error %d\\n",errno);
            return true;
        
    
    return true;

在以上代码中,创建的进程数目由数组大小决定,建议将该进程数目设置为CPU的核数,以充分利用多核CPU。为了避免在父进程退出后,子进程仍然存在产生僵尸进程,我们还需要实现一个信号处理函数:

//信号处理
void handleTerminate(int signal) 
    for (int i=0; i<GET_ARRAY_LEN(processArr); i++) 
        kill(processArr[i], SIGTERM);
    
    exit(0);

该函数实现了当父进程收到退出信号时,向每个子进程也发送退出信号。下面来看看main函数的实现,由于本人是在mac os下进行开发,mac下不支持epoll函数,于是改为类似的select函数:

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

    int listenFd;
    
    initMutex();
    //设置端口号
    listenFd = startup(8080);
    
    //创建若干个子进程
    bool isParent = createSubProcess();
    //如果是父进程
    if (isParent) 
        while (1) 
            //注册信号处理
            signal(SIGTERM, handleTerminate);
            //挂起等待信号
            pause();
        
    
    //如果是子进程
    else 
        //套接字集合
        fd_set rset;
        //最大套接字
        int maxFd = listenFd;
        std::set<int> fdArray;
        //循环处理事件
        while (1) 
            FD_ZERO(&rset);
            FD_SET(listenFd, &rset);
            //重新设置每个需要监听的套接字
            for (std::set<int>::iterator iterator=fdArray.begin();iterator!=fdArray.end();iterator++) 
                FD_SET(*iterator, &rset);
            
            //开始监听
            if (select(maxFd+1, &rset, NULL, NULL, NULL)<0) 
                fprintf(stderr, "select error: %s(errno: %d)\\n",strerror(errno),errno);
                continue;
            
            
            //遍历每个连接套接字
            for (std::set<int>::iterator iterator=fdArray.begin();iterator!=fdArray.end();) 
                int currentFd = *iterator;
                if (FD_ISSET(currentFd, &rset)) 
                    if (!handleRequest(currentFd)) 
                        close(currentFd);
                        fdArray.erase(iterator++);
                        continue;
                    
                
                ++iterator;
            
            //检查连接监听套接字
            if (FD_ISSET(listenFd, &rset)) 
                if (pthread_mutex_trylock(mutex)==0) 
                    int newFd = accept(listenFd, (struct sockaddr *)NULL, NULL);
                    if (newFd<=0) 
                        fprintf(stderr, "accept socket error: %s(errno: %d)\\n",strerror(errno),errno);
                        continue;
                    
                    //更新最大的套接字
                    if (newFd>maxFd) 
                        maxFd = newFd;
                    
                    fdArray.insert(newFd);
                    pthread_mutex_unlock(mutex);
                
            
        
    

    close(listenFd);
    return 0;

在以上代码中,还涉及了进程间互斥信号量的定义,代码如下:

//互斥量
pthread_mutex_t *mutex;
//创建共享的mutex
void initMutex()

    //设置互斥量为进程间共享
    mutex=(pthread_mutex_t*)mmap(NULL, sizeof(pthread_mutex_t), PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, -1, 0);
    if( MAP_FAILED==mutex) 
        perror("mutex mmap failed");
        exit(1);
    
    //设置attr的属性
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    int ret = pthread_mutexattr_setpshared(&attr,PTHREAD_PROCESS_SHARED);
    if(ret != 0) 
        fprintf(stderr, "mutex set shared failed");
        exit(1);
    
    pthread_mutex_init(mutex, &attr);

对每个连接的处理如下:

//处理http请求
bool handleRequest(int connFd) 
    if (connFd<=0) return false;
    //读取缓存
    char buff[4096];
    //读取http header
    int len = (int)recv(connFd, buff, sizeof(buff), 0);
    if (len<=0) 
        return false;
    
    buff[len] = '\\0';
    std::cout<<buff<<std::endl;
    
    return true;

这样就实现了一个仿nginx的高并发服务器。完整的代码如下:

#include <iostream>
#include <set>
#include <signal.h>
#include <sys/select.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>

#include <sys/mman.h>
#include <pthread.h>

#define GET_ARRAY_LEN(array) (sizeof(array) / sizeof(array[0]))
#define PROCESS_NUM 4

//创建socket
int startup(int port) 
    struct sockaddr_in servAddr;
    memset(&servAddr, 0, sizeof(servAddr));
    //协议域(ip地址和端口)
    servAddr.sin_family = AF_INET;
    //绑定默认网卡
    servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    //端口
    servAddr.sin_port = htons(port);
    int listenFd;
    //创建套接字
    if ((listenFd = socket(AF_INET, SOCK_STREAM, 0)) == -1) 
        printf("create socket error: %s(errno: %d)\\n",strerror(errno),errno);
        return 0;
    
    unsigned value = 1;
    setsockopt(listenFd, SOL_SOCKET, SO_REUSEADDR, &value, sizeof(value));
    //绑定套接字
    if (bind(listenFd, (struct sockaddr *)&servAddr, sizeof(servAddr))) 
        printf("bind socket error: %s(errno: %d)\\n",strerror(errno),errno);
        return 0;
    
    //开始监听,设置最大连接请求
    if (listen(listenFd, 10) == -1) 
        printf("listen socket error: %s(errno: %d)\\n",strerror(errno),errno);
        return 0;
    
    return listenFd;


//管理子进程的数组,数组多大就有几个子进程
static int processArr[PROCESS_NUM];
//创建若干个子进程,返回当前进程是否父进程
bool createSubProcess() 
    for (int i=0; i<GET_ARRAY_LEN(processArr); i++) 
        int pid = fork();
        //如果是子进程,返回0
        if (pid == 0) 
            return false;
        
        //如果是父进程,继续fork
        else if (pid >0)
            processArr[i] = pid;
            continue;
        
        //如果出错
        else 
            fprintf(stderr,"can't fork ,error %d\\n",errno);
            return true;
        
    
    return true;


//信号处理
void handleTerminate(int signal) 
    for (int i=0; i<GET_ARRAY_LEN(processArr); i++) 
        kill(processArr[i], SIGTERM);
    
    exit(0);


//处理http请求
bool handleRequest(int connFd) 
    if (connFd<=0) return false;
    //读取缓存
    char buff[4096];
    //读取http header
    int len = (int)recv(connFd, buff, sizeof(buff), 0);
    if (len<=0) 
        return false;
    
    buff[len] = '\\0';
    std::cout<<buff<<std::endl;
    
    return true;


//互斥量
pthread_mutex_t *mutex;
//创建共享的mutex
void initMutex()

    //设置互斥量为进程间共享
    mutex=(pthread_mutex_t*)mmap(NULL, sizeof(pthread_mutex_t), PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, -1, 0);
    if( MAP_FAILED==mutex) 
        perror("mutex mmap failed");
        exit(1);
    
    //设置attr的属性
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    int ret = pthread_mutexattr_setpshared(&attr,PTHREAD_PROCESS_SHARED);
    if(ret != 0) 
        fprintf(stderr, "mutex set shared failed");
        exit(1);
    
    pthread_mutex_init(mutex, &attr);


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

    int listenFd;
    
    initMutex();
    //设置端口号
    listenFd = startup(8080);
    
    //创建若干个子进程
    bool isParent = createSubProcess();
    //如果是父进程
    if (isParent) 
        while (1) 
            //注册信号处理
            signal(SIGTERM, handleTerminate);
            //挂起等待信号
            pause();
        
    
    //如果是子进程
    else 
        //套接字集合
        fd_set rset;
        //最大套接字
        int maxFd = listenFd;
        std::set<int> fdArray;
        //循环处理事件
        while (1) 
            FD_ZERO(&rset);
            FD_SET(listenFd, &rset);
            //重新设置每个需要监听的套接字
            for (std::set<int>::iterator iterator=fdArray.begin();iterator!=fdArray.end();iterator++) 
                FD_SET(*iterator, &rset);
            
            //开始监听
            if (select(maxFd+1, &rset, NULL, NULL, NULL)<0) 
                fprintf(stderr, "select error: %s(errno: %d)\\n",strerror(errno),errno);
                continue;
            
            
            //遍历每个连接套接字
            for (std::set<int>::iterator iterator=fdArray.begin();iterator!=fdArray.end();) 
                int currentFd = *iterator;
                if (FD_ISSET(currentFd, &rset)) 
                    if (!handleRequest(currentFd)) 
                        close(currentFd);
                        fdArray.erase(iterator++);
                        continue;
                    
                
                ++iterator;
            
            //检查连接监听套接字
            if (FD_ISSET(listenFd, &rset)) 
                if (pthread_mutex_trylock(mutex)==0) 
                    int newFd = accept(listenFd, (struct sockaddr *)NULL, NULL);
                    if (newFd<=0) 
                        fprintf(stderr, "accept socket error: %s(errno: %d)\\n",strerror(errno),errno);
                        continue;
                    
                    //更新最大的套接字
                    if (newFd>maxFd) 
                        maxFd = newFd;
                    
                    fdArray.insert(newFd);
                    pthread_mutex_unlock(mutex);
                
            
        
    

    close(listenFd);
    return 0;

下一篇文章仿nginx Http服务器的设计与实现(二)——http协议解析中,将向大家说明如何对http协议进行解析。


如果大家觉得对自己有帮助的话,还希望能帮顶一下,谢谢:) 个人博客: http://blog.csdn.net/zhaoxy2850 本文地址: http://blog.csdn.net/zhaoxy_thu/article/details/24624729 转载请注明出处,谢谢!

以上是关于应用层协议实现系列——HTTP服务器之仿nginx多进程和多路IO的实现的主要内容,如果未能解决你的问题,请参考以下文章

Nginx系列--04HTTP常用指令及常用模块

应用层协议实现系列——FTP服务器之设计与实现

应用层协议实现系列——DNS服务器之设计与实现

Nginx Web 服务

浅谈HTTP协议

Nginx系列了解介绍