阅读 52

Nginx网络epoll多进程系列:应用层协议实现系列(一)——HTTP服务器之仿nginx多进程和多路IO的实现

blog.csdn.net/zhaoxy_thu/…

最近在尝试自己写一个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服务器了。首先是创建套接字的函数:

[cpp]  view plain  copy

  1. //创建socket  
  2. int startup(int port) {  
  3.     struct sockaddr_in servAddr;  
  4.     memset(&servAddr, 0, sizeof(servAddr));  
  5.     //协议域(ip地址和端口)  
  6.     servAddr.sin_family = AF_INET;  
  7.     //绑定默认网卡  
  8.     servAddr.sin_addr.s_addr = htonl(INADDR_ANY);  
  9.     //端口  
  10.     servAddr.sin_port = htons(port);  
  11.     int listenFd;  
  12.     //创建套接字  
  13.     if ((listenFd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {  
  14.         printf("create socket error: %s(errno: %d)\n",strerror(errno),errno);  
  15.         return 0;  
  16.     }  
  17.     unsigned value = 1;  
  18.     setsockopt(listenFd, SOL_SOCKET, SO_REUSEADDR, &value, sizeof(value));  
  19.     //绑定套接字  
  20.     if (bind(listenFd, (struct sockaddr *)&servAddr, sizeof(servAddr))) {  
  21.         printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno);  
  22.         return 0;  
  23.     }  
  24.     //开始监听,设置最大连接请求  
  25.     if (listen(listenFd, 10) == -1) {  
  26.         printf("listen socket error: %s(errno: %d)\n",strerror(errno),errno);  
  27.         return 0;  
  28.     }  
  29.     return listenFd;  
  30. }  


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

[cpp]  view plain  copy

  1. //管理子进程的数组,数组多大就有几个子进程  
  2. static int processArr[PROCESS_NUM];  
  3. //创建若干个子进程,返回当前进程是否父进程  
  4. bool createSubProcess() {  
  5.     for (int i=0; i<GET_ARRAY_LEN(processArr); i++) {  
  6.         int pid = fork();  
  7.         //如果是子进程,返回0  
  8.         if (pid == 0) {  
  9.             return false;  
  10.         }  
  11.         //如果是父进程,继续fork  
  12.         else if (pid >0){  
  13.             processArr[i] = pid;  
  14.             continue;  
  15.         }  
  16.         //如果出错  
  17.         else {  
  18.             fprintf(stderr,"can't fork ,error %d\n",errno);  
  19.             return true;  
  20.         }  
  21.     }  
  22.     return true;  
  23. }  


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

[cpp]  view plain  copy

  1. //信号处理  
  2. void handleTerminate(int signal) {  
  3.     for (int i=0; i<GET_ARRAY_LEN(processArr); i++) {  
  4.         kill(processArr[i], SIGTERM);  
  5.     }  
  6.     exit(0);  
  7. }  


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

[cpp]  view plain  copy

  1. int main(int argc, const char * argv[])  
  2. {  
  3.     int listenFd;  
  4.       
  5.     initMutex();  
  6.     //设置端口号  
  7.     listenFd = startup(8080);  
  8.       
  9.     //创建若干个子进程  
  10.     bool isParent = createSubProcess();  
  11.     //如果是父进程  
  12.     if (isParent) {  
  13.         while (1) {  
  14.             //注册信号处理  
  15.             signal(SIGTERM, handleTerminate);  
  16.             //挂起等待信号  
  17.             pause();  
  18.         }  
  19.     }  
  20.     //如果是子进程  
  21.     else {  
  22.         //套接字集合  
  23.         fd_set rset;  
  24.         //最大套接字  
  25.         int maxFd = listenFd;  
  26.         std::set<int> fdArray;  
  27.         //循环处理事件  
  28.         while (1) {  
  29.             FD_ZERO(&rset);  
  30.             FD_SET(listenFd, &rset);  
  31.             //重新设置每个需要监听的套接字  
  32.             for (std::set<int>::iterator iterator=fdArray.begin();iterator!=fdArray.end();iterator++) {  
  33.                 FD_SET(*iterator, &rset);  
  34.             }  
  35.             //开始监听  
  36.             if (select(maxFd+1, &rset, NULL, NULL, NULL)<0) {  
  37.                 fprintf(stderr, "select error: %s(errno: %d)\n",strerror(errno),errno);  
  38.                 continue;  
  39.             }  
  40.               
  41.             //遍历每个连接套接字  
  42.             for (std::set<int>::iterator iterator=fdArray.begin();iterator!=fdArray.end();) {  
  43.                 int currentFd = *iterator;  
  44.                 if (FD_ISSET(currentFd, &rset)) {  
  45.                     if (!handleRequest(currentFd)) {  
  46.                         close(currentFd);  
  47.                         fdArray.erase(iterator++);  
  48.                         continue;  
  49.                     }  
  50.                 }  
  51.                 ++iterator;  
  52.             }  
  53.             //检查连接监听套接字  
  54.             if (FD_ISSET(listenFd, &rset)) {  
  55.                 if (pthread_mutex_trylock(mutex)==0) {  
  56.                     int newFd = accept(listenFd, (struct sockaddr *)NULL, NULL);  
  57.                     if (newFd<=0) {  
  58.                         fprintf(stderr, "accept socket error: %s(errno: %d)\n",strerror(errno),errno);  
  59.                         continue;  
  60.                     }  
  61.                     //更新最大的套接字  
  62.                     if (newFd>maxFd) {  
  63.                         maxFd = newFd;  
  64.                     }  
  65.                     fdArray.insert(newFd);  
  66.                     pthread_mutex_unlock(mutex);  
  67.                 }  
  68.             }  
  69.         }  
  70.     }  
  71.   
  72.     close(listenFd);  
  73.     return 0;  
  74. }  


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

[cpp]  view plain  copy

  1. //互斥量  
  2. pthread_mutex_t *mutex;  
  3. //创建共享的mutex  
  4. void initMutex()  
  5. {  
  6.     //设置互斥量为进程间共享  
  7.     mutex=(pthread_mutex_t*)mmap(NULL, sizeof(pthread_mutex_t), PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, -1, 0);  
  8.     if( MAP_FAILED==mutex) {  
  9.         perror("mutex mmap failed");  
  10.         exit(1);  
  11.     }  
  12.     //设置attr的属性  
  13.     pthread_mutexattr_t attr;  
  14.     pthread_mutexattr_init(&attr);  
  15.     int ret = pthread_mutexattr_setpshared(&attr,PTHREAD_PROCESS_SHARED);  
  16.     if(ret != 0) {  
  17.         fprintf(stderr, "mutex set shared failed");  
  18.         exit(1);  
  19.     }  
  20.     pthread_mutex_init(mutex, &attr);  
  21. }  


对每个连接的处理如下:

[cpp]  view plain  copy

  1. //处理http请求  
  2. bool handleRequest(int connFd) {  
  3.     if (connFd<=0) return false;  
  4.     //读取缓存  
  5.     char buff[4096];  
  6.     //读取http header  
  7.     int len = (int)recv(connFd, buff, sizeof(buff), 0);  
  8.     if (len<=0) {  
  9.         return false;  
  10.     }  
  11.     buff[len] = '\0';  
  12.     std::cout<<buff<<std::endl;  
  13.       
  14.     return true;  
  15. }  


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

[cpp]  view plain  copy

  1. #include <iostream>  
  2. #include <set>  
  3. #include <signal.h>  
  4. #include <sys/select.h>  
  5. #include <unistd.h>  
  6. #include <sys/socket.h>  
  7. #include <netinet/in.h>  
  8. #include <fcntl.h>  
  9.   
  10. #include <sys/mman.h>  
  11. #include <pthread.h>  
  12.   
  13. #define GET_ARRAY_LEN(array) (sizeof(array) / sizeof(array[0]))  
  14. #define PROCESS_NUM 4  
  15.   
  16. //创建socket  
  17. int startup(int port) {  
  18.     struct sockaddr_in servAddr;  
  19.     memset(&servAddr, 0, sizeof(servAddr));  
  20.     //协议域(ip地址和端口)  
  21.     servAddr.sin_family = AF_INET;  
  22.     //绑定默认网卡  
  23.     servAddr.sin_addr.s_addr = htonl(INADDR_ANY);  
  24.     //端口  
  25.     servAddr.sin_port = htons(port);  
  26.     int listenFd;  
  27.     //创建套接字  
  28.     if ((listenFd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {  
  29.         printf("create socket error: %s(errno: %d)\n",strerror(errno),errno);  
  30.         return 0;  
  31.     }  
  32.     unsigned value = 1;  
  33.     setsockopt(listenFd, SOL_SOCKET, SO_REUSEADDR, &value, sizeof(value));  
  34.     //绑定套接字  
  35.     if (bind(listenFd, (struct sockaddr *)&servAddr, sizeof(servAddr))) {  
  36.         printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno);  
  37.         return 0;  
  38.     }  
  39.     //开始监听,设置最大连接请求  
  40.     if (listen(listenFd, 10) == -1) {  
  41.         printf("listen socket error: %s(errno: %d)\n",strerror(errno),errno);  
  42.         return 0;  
  43.     }  
  44.     return listenFd;  
  45. }  
  46.   
  47. //管理子进程的数组,数组多大就有几个子进程  
  48. static int processArr[PROCESS_NUM];  
  49. //创建若干个子进程,返回当前进程是否父进程  
  50. bool createSubProcess() {  
  51.     for (int i=0; i<GET_ARRAY_LEN(processArr); i++) {  
  52.         int pid = fork();  
  53.         //如果是子进程,返回0  
  54.         if (pid == 0) {  
  55.             return false;  
  56.         }  
  57.         //如果是父进程,继续fork  
  58.         else if (pid >0){  
  59.             processArr[i] = pid;  
  60.             continue;  
  61.         }  
  62.         //如果出错  
  63.         else {  
  64.             fprintf(stderr,"can't fork ,error %d\n",errno);  
  65.             return true;  
  66.         }  
  67.     }  
  68.     return true;  
  69. }  
  70.   
  71. //信号处理  
  72. void handleTerminate(int signal) {  
  73.     for (int i=0; i<GET_ARRAY_LEN(processArr); i++) {  
  74.         kill(processArr[i], SIGTERM);  
  75.     }  
  76.     exit(0);  
  77. }  
  78.   
  79. //处理http请求  
  80. bool handleRequest(int connFd) {  
  81.     if (connFd<=0) return false;  
  82.     //读取缓存  
  83.     char buff[4096];  
  84.     //读取http header  
  85.     int len = (int)recv(connFd, buff, sizeof(buff), 0);  
  86.     if (len<=0) {  
  87.         return false;  
  88.     }  
  89.     buff[len] = '\0';  
  90.     std::cout<<buff<<std::endl;  
  91.       
  92.     return true;  
  93. }  
  94.   
  95. //互斥量  
  96. pthread_mutex_t *mutex;  
  97. //创建共享的mutex  
  98. void initMutex()  
  99. {  
  100.     //设置互斥量为进程间共享  
  101.     mutex=(pthread_mutex_t*)mmap(NULL, sizeof(pthread_mutex_t), PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, -1, 0);  
  102.     if( MAP_FAILED==mutex) {  
  103.         perror("mutex mmap failed");  
  104.         exit(1);  
  105.     }  
  106.     //设置attr的属性  
  107.     pthread_mutexattr_t attr;  
  108.     pthread_mutexattr_init(&attr);  
  109.     int ret = pthread_mutexattr_setpshared(&attr,PTHREAD_PROCESS_SHARED);  
  110.     if(ret != 0) {  
  111.         fprintf(stderr, "mutex set shared failed");  
  112.         exit(1);  
  113.     }  
  114.     pthread_mutex_init(mutex, &attr);  
  115. }  
  116.   
  117. int main(int argc, const char * argv[])  
  118. {  
  119.     int listenFd;  
  120.       
  121.     initMutex();  
  122.     //设置端口号  
  123.     listenFd = startup(8080);  
  124.       
  125.     //创建若干个子进程  
  126.     bool isParent = createSubProcess();  
  127.     //如果是父进程  
  128.     if (isParent) {  
  129.         while (1) {  
  130.             //注册信号处理  
  131.             signal(SIGTERM, handleTerminate);  
  132.             //挂起等待信号  
  133.             pause();  
  134.         }  
  135.     }  
  136.     //如果是子进程  
  137.     else {  
  138.         //套接字集合  
  139.         fd_set rset;  
  140.         //最大套接字  
  141.         int maxFd = listenFd;  
  142.         std::set<int> fdArray;  
  143.         //循环处理事件  
  144.         while (1) {  
  145.             FD_ZERO(&rset);  
  146.             FD_SET(listenFd, &rset);  
  147.             //重新设置每个需要监听的套接字  
  148.             for (std::set<int>::iterator iterator=fdArray.begin();iterator!=fdArray.end();iterator++) {  
  149.                 FD_SET(*iterator, &rset);  
  150.             }  
  151.             //开始监听  
  152.             if (select(maxFd+1, &rset, NULL, NULL, NULL)<0) {  
  153.                 fprintf(stderr, "select error: %s(errno: %d)\n",strerror(errno),errno);  
  154.                 continue;  
  155.             }  
  156.               
  157.             //遍历每个连接套接字  
  158.             for (std::set<int>::iterator iterator=fdArray.begin();iterator!=fdArray.end();) {  
  159.                 int currentFd = *iterator;  
  160.                 if (FD_ISSET(currentFd, &rset)) {  
  161.                     if (!handleRequest(currentFd)) {  
  162.                         close(currentFd);  
  163.                         fdArray.erase(iterator++);  
  164.                         continue;  
  165.                     }  
  166.                 }  
  167.                 ++iterator;  
  168.             }  
  169.             //检查连接监听套接字  
  170.             if (FD_ISSET(listenFd, &rset)) {  
  171.                 if (pthread_mutex_trylock(mutex)==0) {  
  172.                     int newFd = accept(listenFd, (struct sockaddr *)NULL, NULL);  
  173.                     if (newFd<=0) {  
  174.                         fprintf(stderr, "accept socket error: %s(errno: %d)\n",strerror(errno),errno);  
  175.                         continue;  
  176.                     }  
  177.                     //更新最大的套接字  
  178.                     if (newFd>maxFd) {  
  179.                         maxFd = newFd;  
  180.                     }  
  181.                     fdArray.insert(newFd);  
  182.                     pthread_mutex_unlock(mutex);  
  183.                 }  
  184.             }  
  185.         }  
  186.     }  
  187.   
  188.     close(listenFd);  
  189.     return 0;  
  190. }  

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

\

如果大家觉得对自己有帮助的话,还希望能帮顶一下,谢谢:)

个人博客: blog.csdn.net/zhaoxy2850

本文地址: blog.csdn.net/zhaoxy_thu/…

转载请注明出处,谢谢!

\

文章分类
代码人生
文章标签