知识背景:1、对HTTP报文段有所认识。2、掌握基础的epoll模式网络编程。3、基础的HTML知识。
第一部分:实现简单的http服务器
实现的内容:我们能通过 http://127.0.0.1 访问服务器,如果需要找到服务器对应的文件,则在网址后面加上“/想要的内容”。
例如我想要浏览服务器上的一张图片,如下是演示的效果
网站对后的 “/index.html” 就是我想要获取的内容。
开始
第一步
一开始我们先把网络编程基础的东西都写上,例如socket函数、bind函数、listen函数、accept函数,注意http的端口号为80。这些比较基础就不详细说了,先把代码贴上。
#include <sys/socket.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main()
{
int lfd;
lfd = socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in serv_addr;
bzero(&serv_addr,sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(80);
int option = 1;
setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,(void*)&option,sizeof(option));
int ret = bind(lfd,(struct sockaddr*)& serv_addr,sizeof(serv_addr));
if(ret == -1)
{
perror("bind()");
exit(1);
}
ret = listen(lfd,100);
if(ret == -1)
{
perror("listen()");
exit(1);
}
int fd = accept(lfd,NULL,NULL);
printf("有客户端连接\n");
close(fd);
close(lfd);
return 0;
}
这里实现的功能就是检测如果有客户连上,服务器这边就打印“有客户端连接”后立马关闭。让服务器跑起来
现在服务器阻塞在accept函数这里,等待客户的连接,接下来打开浏览器输入 “127.0.0.1”
浏览器那边显示连接失败,但是不慌,因为服务器也没写响应报文发送给客户端,主要看服务器这边,在终端已经能看到“有客户端连接”的字眼,证明其实客户那边已经成功连接到的了。
第二步
接下来我们可以检查客户端连接过来的请求信息了,这里写一个handle_request函数进行分析。功能也十分简单,就是一个read函数把客户端的东西打印在终端。
#include <sys/socket.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
void handle_request(int fd)
{
char buffer[1024*1024];
int nread = read(fd,buffer,sizeof(buffer));
printf("读到的请求是%s\n",buffer);
}
int main()
{
int lfd;
lfd = socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in serv_addr;
bzero(&serv_addr,sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(80);
int option = 1;
setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,(void*)&option,sizeof(option));
int ret = bind(lfd,(struct sockaddr*)& serv_addr,sizeof(serv_addr));
if(ret == -1)
{
perror("bind()");
exit(1);
}
ret = listen(lfd,100);
if(ret == -1)
{
perror("listen()");
exit(1);
}
int fd = accept(lfd,NULL,NULL);
printf("有客户端连接\n");
handle_request(fd);
close(fd);
close(lfd);
return 0;
}
终端打印的信息:
显然,终端把http报文段给我们打印出来了。第一行为请求行,GET 斜杠后面是空的,说明没有请求的文件,如果我们要添加请求的信息,就像在一开始说的那样,在网址后面加上“/想要的内容”。
那我们现在输入“http://127.0.0.1/index.html”,请求index.html文件,则在终端打印这样的信息
大伙发现了没?在GET后面的斜杠后多了一个index.html。
第三步
既然有了请求的信息,那我们需要在http报文段中把需要请求的文件名字提取出来,这里用到sscanf函数
看代码
#include <sys/socket.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
void handle_request(int fd)
{
char buffer[1024*1024];
int nread = read(fd,buffer,sizeof(buffer));
printf("读到的请求是%s\n",buffer);
char filename[10] = {0};
sscanf(buffer,"GET /%s",filename);
printf("解析的文件名是:%s\n",filename);
}
int main()
{
int lfd;
lfd = socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in serv_addr;
bzero(&serv_addr,sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(80);
int option = 1;
setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,(void*)&option,sizeof(option));
int ret = bind(lfd,(struct sockaddr*)& serv_addr,sizeof(serv_addr));
if(ret == -1)
{
perror("bind()");
exit(1);
}
ret = listen(lfd,100);
if(ret == -1)
{
perror("listen()");
exit(1);
}
int fd = accept(lfd,NULL,NULL);
printf("有客户端连接\n");
handle_request(fd);
close(fd);
close(lfd);
return 0;
}
简单的说就是把buffer中要提取的内容格式化,输到filename字符串中,现在filename就保留着请求的文件名了。
继续运行服务器,输入“http://127.0.0.1/index.html”,终端输出如下
看到没有,已经提取出文件名了。
第四步:(这步注意了!!!)
目前为止只是服务器在单向接受请求报文,正常情况下,服务器还应该发送一个响应报文给客户端。于是我们现在需要写一个响应报文。
这里还要先分析请求报文所请求文件的类型,例如上面的类型是.html,知道类型后,服务器写一份响应报文发送给客户端。这里利用strstr()函数获取文件类型
另外,刚才我们请求的是一个“index.html”文件,那服务器这边也必须要有这个文件才行,于是我们可以在服务器代码相同目录下创建一个“index.html”文件
文件内容如下,基础的html语法(原谅我懒得缩进 = =!!)
<html>
<head><h1>My first html</h1></head>
<body>
<p>favorite picture</p>
<img src="picture.jpg"></img>
</body>
</html>
上面就是客户所请求的内容,里面还有一张.jpg,自己随便找一张图片放在同目录下即可。
回到服务器这边的代码,增加完的代码如下
#include <sys/socket.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
void handle_request(int fd)
{
char buffer[1024*1024];
int nread = read(fd,buffer,sizeof(buffer));
printf("读到的请求是%s\n",buffer);
char filename[10] = {0};
sscanf(buffer,"GET /%s",filename);
printf("解析的文件名是:%s\n",filename);
char* mine = NULL; //解析的类型
if(strstr(filename,".HTML"))
mine = "text/html";
if(strstr(filename,"."))
mine = "image/jpg";
//响应报文段
char response[1024*1024];
sprintf(response,"HTTP/1.1 200 OK\r\nContext-Type: %s\r\n\r\n",mine);
int headlen = strlen(response);
//打开客户请求的文件并写进响应报文中
int filefd = open(filename,O_RDONLY);
int filelen = read(filefd,response+headlen,sizeof(response)-headlen);
//把响应报文段发送给客户端
write(fd,response,headlen+filelen);
close(filefd);
}
int main()
{
int lfd;
lfd = socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in serv_addr;
bzero(&serv_addr,sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(80);
int option = 1;
setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,(void*)&option,sizeof(option));
int ret = bind(lfd,(struct sockaddr*)& serv_addr,sizeof(serv_addr));
if(ret == -1)
{
perror("bind()");
exit(1);
}
ret = listen(lfd,100);
if(ret == -1)
{
perror("listen()");
exit(1);
}
int fd = accept(lfd,NULL,NULL);
printf("有客户端连接\n");
handle_request(fd);
close(fd);
close(lfd);
return 0;
}
这次运行服务器,在浏览器中看到如下
图片没打开主要是服务器的main函数中一连接到客户端就关闭,所以我们可以加个while循环不关闭服务器(实际上的服务器也是一直在跑的)。
#include <sys/socket.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
void handle_request(int fd)
{
char buffer[1024*1024];
int nread = read(fd,buffer,sizeof(buffer));
printf("读到的请求是%s\n",buffer);
char filename[10] = {0};
sscanf(buffer,"GET /%s",filename);
printf("解析的文件名是:%s\n",filename);
char* mine = NULL;
if(strstr(filename,".HTML"))
mine = "text/html";
if(strstr(filename,"."))
mine = "image/jpg";
char response[1024*1024];
sprintf(response,"HTTP/1.1 200 OK\r\nContext-Type: %s\r\n\r\n",mine);
int headlen = strlen(response);
int filefd = open(filename,O_RDONLY);
int filelen = read(filefd,response+headlen,sizeof(response)-headlen);
write(fd,response,headlen+filelen);
close(filefd);
}
int main()
{
int lfd;
lfd = socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in serv_addr;
bzero(&serv_addr,sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(80);
int option = 1;
setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,(void*)&option,sizeof(option));
int ret = bind(lfd,(struct sockaddr*)& serv_addr,sizeof(serv_addr));
if(ret == -1)
{
perror("bind()");
exit(1);
}
ret = listen(lfd,100);
if(ret == -1)
{
perror("listen()");
exit(1);
}
while(1)
{
int fd = accept(lfd,NULL,NULL);
printf("有客户端连接\n");
handle_request(fd);
close(fd);
}
close(lfd);
return 0;
}
这次让我们再次打开浏览器就能正常显示图片了,服务器也没关闭
至此,一个简单http服务器已经完成。
有了这个简单版的,我们可以照猫画虎写一个epoll模型的htto服务器
epoll模型的http服务器
#include <sys/socket.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/epoll.h>
void handle_request(int epfd,int fd)
{
char buffer[1024*1024];
int nread = read(fd,buffer,sizeof(buffer));
printf("读到的请求是%s\n",buffer);
char filename[10] = {0};
sscanf(buffer,"GET /%s",filename);
printf("解析的文件名是:%s\n",filename);
char* mime = NULL;
if(strstr(filename,".html"))
mime = "text/html";
else if(strstr(filename,".jpg"))
mime = "image/jpg";
char response[1024*1024];
sprintf(response,"HTTP/1.1 200 OK\r\nContext-Type: %s\r\n\r\n",mime);
int headlen = strlen(response);
int filefd = open(filename,O_RDONLY);
int filelen = read(filefd,response+headlen,sizeof(response)-headlen);
write(fd,response,headlen+filelen);
epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
close(fd);
close(filefd);
}
int main()
{
int lfd;
lfd = socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in serv_addr;
bzero(&serv_addr,sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(80);
int option = 1;
setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,(void*)&option,sizeof(option));
int ret = bind(lfd,(struct sockaddr*)& serv_addr,sizeof(serv_addr));
if(ret == -1)
{
perror("bind()");
exit(1);
}
ret = listen(lfd,10000);
if(ret == -1)
{
perror("listen()");
exit(1);
}
int epfd = epoll_create(1000000);
struct epoll_event event,ready_event[100000];
event.events = EPOLLIN;
event.data.fd = lfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&event);
while(1)
{
int nready = epoll_wait(epfd,ready_event,1000000,-1);
for(int i = 0;i<nready;i++)
{
if(!ready_event[i].events&EPOLLIN)
continue;
if(ready_event[i].data.fd == lfd)
{
int fd = accept(lfd,NULL,NULL);
event.events = EPOLLIN;
event.data.fd = fd;
epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&event);
}
else
{
int sockfd = ready_event[i].data.fd;
handle_request(epfd,sockfd);
}
}
}
close(lfd);
return 0;
}
第二部分:对服务器进行压力测试
这次压力测试我们使用 ab 工具。
使用例子
ab -n 5000 -c 500 -c 1000 http://127.0.0.1/index.html
-n 表示请求数 -c 表示并发数,最后面是测试压力的网站。
接下来我们进行上面两种模型的测压对比。
第一次:请求数100+并发数10
主要看红色部分:
Requsets per second:吞吐率
第一个Time per request:用户平均请求等待时间
第二个Time per request:服务器平均请求处理时间
Transfer rate:平均每秒网络上的流量
可知,请求数比较少的情况下,普通模型和epoll模型没多大区别
第二次:请求数1w+并发数1w
epoll还很轻松应对,普通的模型处理9278个请求就挂了。
第三次:请求数10w+并发数1千
普通模式下,处理完5w多个请求后还是坚持不了了。
第四次:单独测epoll,请求数100w+并发数10
epoll依然游刃有余,汗颜。。。
第五次:单独测epoll,请求数1000w+并发数1
惊讶地发现,一千万的请求epoll依然能承受,虽然服务器跑的时间有点久,大概20min多点,但是还是能承受得了。再一次佩服epoll
结束。