近期在尝试自己写一个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. JavaScript 高阶函数 + generator生成器

    map/reduce map()方法定义在JavaScript的Array中,我们调用Array的map()方法,传入我们自己的函数,就得到了一个新的Array作为结果: function pow(x ...

  2. swift官方文档中的函数闭包是怎么理解的?

    官方文档中的16页: numbers.map({ (number: Int) -> Int in let result = * number return result }) 不知道这个怎么用, ...

  3. css 多出一行或多行后显示...的方法

    一行超出显示... .mui-ellipsis { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } 两行超出的显示. ...

  4. 【转】不同VLAN之间相互通信及VTP、STP、EtherChannel概念

    厘清最后一个概念. 转了网上两个相关帖子: http://www.net130.com/CMS/Pub/Tech/tech_zh/2009_03_12_97386_3.htm http://blog. ...

  5. Android listView scroll 恢复滚动位置

    相信大家尝试过许多方法恢复滚动位置,本人也找了许多方法,唯有这个方法好用,下面把代码贴出来 声明两个变量 private int mPosition; private int lvChildTop; ...

  6. Chrome测试网站加载时间与流量消耗

    加载10M内容,耗时近10秒钟 大概页面图片下载时,同时进行了,有的大图,下载要几秒钟

  7. Linux Shell编程(30)——别名

    Bash别名本质上是一个简称, 缩写, 这可避免键入过长的命令序列. 例如,如果我们添加 alias lm="ls -l | more" 这一行到文件~/.bashrc file里 ...

  8. Linux Shell编程(22)——时间/日期 命令

    date直接调用, date 就会打印日期和时间到 stdout 上. 这个命令有趣的地方在于它的格式化和分析选项上.time输出统计出来的命令执行的时间.touch这是一个用来更新文件被存取或修改的 ...

  9. Web Service和ISAPI的区别与联系 转

    Web Service和ISAPI的区别与联系   1.Web Service 是一种新的web应用程序分支,他们是自包含.自描述.模块化的应用,可以发布.定位.通过web调用.Web Service ...

  10. Linux学习笔记30——套接字

    一 什么是套接字 套接字是一种通信机制,凭借这种机制,客户/服务器系统的开发既可以在本地单机上进行,也可以跨网络进行. 二 套接字属性 套接字的特性由3个属性确定,它们是:域,类型和协议   1 套接 ...