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

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

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

nginx採用的是多路IO复用server模型,即server每监听到一个连接时,会将连接增加到连接数组中,使用epoll多路IO复用函数对每一个连接进行监听,当连接中有数据到来时,epoll会返回对应的连接,依此对各个连接进行处理就可以。epoll的最大连接数量尽管也会受到内存的影响,但因为每一个未激活的连接占用的内存非常小,所以相比于apache能够承载更大的并发。

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

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

在明白了上述原理之后,我们就能够仿照nginx实现一个httpserver了。首先是创建套接字的函数:

//创建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;
}

该函数创建了一个套接字并将其绑定到了一个port上開始监听。因为我们接下来要创建若干个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();
//设置port号
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的高并发server。完整的代码例如以下:

#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 Httpserver的设计与实现(二)——http协议解析》中,将向大家说明怎样对http协议进行解析。

假设大家认为对自己有帮助的话,还希望能帮顶一下,谢谢:)
转载请注明出处,谢谢!

应用层协议实现系列(一)——HTTPserver之仿nginx多进程和多路IO的实现的更多相关文章

  1. 应用层协议实现系列(三)——FTPserver之设计与实现

    在实现了HTTPserver之后.本人打算再实现一个FTPserver. 因为FTP协议与HTTP一样都位于应用层,所以实现原理也类似. 在这里把实现的原理和源代码分享给大家. 首先须要明白的是FTP ...

  2. 应用层协议系列(两)——HTTPserver之http协议分析

    上一篇文章<抄nginx Httpserver设计与实现(一)--多进程和多通道IO现>中实现了一个仿照nginx的支持高并发的server.但仅仅是实现了port监听和数据接收.并没有实 ...

  3. RTSP RTSP(Real Time Streaming Protocol),RFC2326,实时流传输协议,是TCP/IP协议体系中的一个应用层协议

    RTSP 编辑 RTSP(Real Time Streaming Protocol),RFC2326,实时流传输协议,是TCP/IP协议体系中的一个应用层协议,由哥伦比亚大学.网景和RealNetwo ...

  4. http协议学习系列

    深入理解HTTP协议(转)  http://www.blogjava.net/zjusuyong/articles/304788.html http协议学习系列   1. 基础概念篇 1.1 介绍 H ...

  5. http协议学习系列(一个博文链接)

    深入理解HTTP协议(转) http协议学习系列(转自:http://www.blogjava.net/zjusuyong/articles/304788.html) 1. 基础概念篇 1.1 介绍 ...

  6. 应用层协议:HTTPS

    1. HTTPS定义 Hyper Text Transfer Protocol over Secure Socket Layer,安全的超文本传输协议,网景公式设计了SSL(Secure Socket ...

  7. UDS(ISO14229-2006) 汉译(No.7 应用层协议)【未完,待续】

    7.1定义 应用层协议通常作为确认消息的传输,意味着从客户端发送的每一个请求都将有由服务器端产生的与之相对的响应. 唯一的例外在于:例如使用了功能寻址方式,或者该请求/指示没有指定生成响应/确定的少数 ...

  8. SMB2 Protocol – 简介(应用层协议主要用于在计算机间共享文件、打印机、串口等)

    SMB2 Protocol – 简介 SMB协议简介: 服务器信息块(SMB)协议是一个应用层协议主要用于在计算机间共享文件.打印机.串口等. 在介绍SMB协议的时候,一般提到使用的端口为139,44 ...

  9. 常用应用层协议HTTP、RTSP、RTMP比较

    HTTP(超文本传输协议).RTSP(Real Time Streaming Protocol实时流传输协议).RTMP(Routing Table Maintenance Protocol路由选择表 ...

随机推荐

  1. mws文件中的tab文件改为相对路径

    用mapinfo将现有的多个图层(tab)文件保存成一个mws工作空间后,将此mws文件发到另一台电脑上后,打开mws,提示无法打开各个tab文件,文件不存在,显示的路径是当时原电脑添加时的绝对路径. ...

  2. yii下多条件多表组合查询以及自写ajax分页

    多条件组合查询主要用到yii的CDbCriteria,这个类很多oem框架都有,非常好用. 前台表单 前台查询表单效果是这样的,多个条件组,每个组里放多个input,name为数组.当任何一个复选框被 ...

  3. TCP/IP笔记 应用层(2)——FTP

    1. FTP(File Transfer Protocol) 文件传送协议 FTP 只提供文件传送的一些基本的服务,它使用 TCP 可靠的运输服务.FTP 的主要功能是减少或消除在不同操作系统下处理文 ...

  4. File System Shell

    Overview appendToFile cat chgrp chmod chown copyFromLocal copyToLocal count cp du dus expunge get ge ...

  5. VC皮肤库之duilib

    首先是个国产的开源 的,directui 界面库,开放,共享,惠众,共赢,遵循bsd协议,可以免费用于商业项目,目前支持Windows 32 .Window CE.Mobile等平台. Duilib ...

  6. 在IT学习中的“认识论”

    先有“感性认识”,再有“理性认识”.这句经典认识论,在IT领域应用广泛. 一个项目,首先是分析需求,也就是功能层面,这相当于先有“感性认识”:然后才是具体的技术实现,也就是“理性认识”. IT学习分析 ...

  7. ASP.NET 获取来源网站的网址,获取上一网页的网址,获取来源网页的URL,获取上一网页的URL

    ASP.NET 获取来源网站的网址,获取上一网页的网址,获取来源网页的URL, 获取上一网页的URL Uri Url = HttpContext.Current.Request.UrlReferrer ...

  8. ZeroBraneStudio之支持远程调试

    打开ZBS后,如果需要远程调试得先开启调试服务器:Project->Start Debugger Server 打开之后就可以编辑文件进行测试了.示例代码如下: local ZBS = 'D:/ ...

  9. Linux下Java 编译运行说明

    命令行环境下Java编译运行 1. java的运行机制的基本概念: 源文件 也就是我们熟知的.java文件. 类文件  .class文件是编译器由.java文件编译而成.众所周知,Java的跨平台性在 ...

  10. CAVLC

    在H.264标准中,CAVLC(Context-based Adaptive Variable Length Coding)被用于亮度和色度残差数据编码.在标准的码流结构中,CAVLC编码方式描述为c ...