[TCP/IP网络编程]实现一个简单的http服务器(普通模型+epoll模型)和测压

知识背景: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

结束。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值