操作系统:Ubuntu Server 20.04 LTS 64bit
CPU:2核
内存:4GB
系统盘:60GB SSD云硬盘
2.2项目功能
本项目设计的http服务器是一个轻量级的服务器,使用Reactor模式,即主线程只负责监听文件描述符上是否有事件发生,有的话立即将该事件通知工作线程。除此之外,主线程不做其他实质性的工作。读写数据,接受新的连接,以及处理客户请求均在工作线程中完成。
本项目的基本功能如下:
(1)能接收客户端的GET请求;
(2)能够解析客户端的请求报文,根据客户端要求找到相应的资源;
(2)能够回复http应答报文;
(3)能够读取服务器中存储的文件,并返回给请求客户端,实现对外发布静态资源;
(4)使用I/O复用来提高处理请求的并发度;
(5)服务器端支持错误处理,如要访问的资源不存在时回复404错误等。
2.3技能储备
为了完成本项目,实现本项目的具体功能,需要具有一定的技能储备作为技术支撑。
首先应该掌握Linux操作系统的常用命令,C语言基础,熟练使用vim、gcc编译器、gdb等工具,Linux平台上进行程序的编写、编译以及调试能力,socket网络通信的编程能力,I/O复用理论知识以及编程能力,多线程编程能力,以及一定的HTML语言能力。
三、项目设计
3.1设计概述
本项目是基于Linux操作系统,使用C语言实现的轻量级http服务器。使用socket网络编程技术实现服务器端和客户端之间的通信。同时,为了提高本服务器的并发处理性能,本次http服务器设计使用Reactor模式。通过I/O复用和线程池相结合,实现同时响应多个客户端的请求,保证http服务器的并发性。
3.2 Reactor模式
Reactor模式是指主线程只负责监听文件描述符上是否有事件发生,有的话立即将该事件通知工作线程。除此之外,主线程不做其他实质性的工作。读写数据,接受新的连接,以及处理客户请求均在工作线程中完成。
工作流程如下:
(1)主线程往epoll内核事件表中注册socket上的读就绪事件。
(2)主线程调用epoll_wait等待socket上有数据可读。
(3)当socket上有数据可读时,epoll_wait 通知主线程。主线程则将socket可读事件放入消息队列。
(4)一旦放入消息队列便创建相应的线程即工作线程,在线程函数中处理客户端信息,然后往epoll内核事件表中注册该socket上的写就绪事件。
(5)主线程调用epoll_ wait 等待socket可写。
(6)当socket可写时,epoll _wait 通知主线程。主线程将socket可写事件放入消息队列。
(7)创建工作线程,往socket上写入服务器处理客户请求的结果。
3.3 socket网络编程
本项目通过socket网络编程技术实现http服务器端和客户端实现通信。并且采用的是TCP协议。
TCP 提供的是面向连接的、可靠的、字节流服务。TCP 的服务器端和客户端编程流程如下图:
3.4 http服务器应答报文设计
如果客户端请求响应成功,则想客户端发送成功应答报文。如下表所示:
表3-1 请求成功的应答报文
如果客户端请求响应失败,例如服务器端没有客户端所请求的资源,则回复失败报文。如下表所示:
表3-2 请求失败应答报文
四、代码实现及运行结果
4.1主要功能实现
4.1.1 主函数
主函数中主要调用各个封装好的方法函数,首先调用创建套接字函数,创建套接字,然后创建消息队列。接着创建线程池,子线程同时执行loop_thread线程函数,将在 msgrcv处阻塞,等待获取消息队列中的消息。主线程调用epoll_create方法创建内核事件表,调用epoll_add函数添加描述符和事件。接着使用epoll_wait方法获取就绪描述符,一旦获取到就绪描述符便向消息队列中发送消息,便可以解除子线程中消息队列的阻塞,执行子线程中的程序,连接客户端,实现通信。
int main()
{
signal(SIGPIPE,sig_fun);
sockfd = socket\_init();//调用创建套接字函数
if ( sockfd == -1 )
{
exit(0);
}
msgid = msgget((key\_t)1234,IPC_CREAT|0600);//创建消息队列
if ( msgid == -1 )
{
exit(0);
}
pthread\_t id[4];
for( int i = 0; i < 4; i++ ) //循环创建线程池
{
pthread\_create(&id[i],NULL,loop_thread,NULL);
}
epfd = epoll\_create(MAXFD);//创建内核事件表
if ( epfd == -1 )
{
printf("create epoll err\n");
exit(0);
}
epoll\_add(epfd,sockfd);//调用封装的函数添加描述符和事件
struct epoll\_event evs[MAXFD];
while( 1 )
{
int n = epoll\_wait(epfd,evs,MAXFD,-1);//获取就绪描述符
if( n == -1 )
{
continue;
}
else
{
struct mess m;
m.type = 1;
for(int i = 0; i < n; i++ )
{
m.c = evs[i].data.fd;
if ( evs[i].events & EPOLLIN )
{
msgsnd(msgid,&m,sizeof(int),0); //向消息队列发送消息
}
}
}
}
}
4.1.2创建套接字函数
将初始化创建套接字函数封装。
int socket\_init()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if ( sockfd == -1 )
{
return -1;
}
struct sockaddr\_in saddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(80);
saddr.sin_addr.s_addr = inet\_addr("0.0.0.0");
int res = bind(sockfd,(struct sockaddr\*)&saddr,sizeof(saddr));
if ( res == -1 )
{
printf("bind err\n");
return -1;
}
res = listen(sockfd,5);
if ( res == -1 )
{
return -1;
}
return sockfd;
}
4.1.3线程函数
线程将在 msgrcv处阻塞,等待获取消息队列中的消息,判断描述符类型,进行accept操作或recv操作。收到客户端请求数据后再调用get_filename函数获取客户端请求的资源名称,再判断发送错误或者正确应答报文。
void\* loop\_thread(void\* arg)
{
while( 1 )
{
struct mess m;
msgrcv(msgid,&m,sizeof(int),1,0);//从消息队列中读取消息
int c = m.c;
if ( c == sockfd )
{
struct sockaddr\_in caddr;
int len = sizeof(caddr);
int cli = accept(sockfd,(struct sockaddr\*)&caddr,&len);
if ( cli < 0 )
{
continue;
}
epoll\_add(epfd,cli);
}
else
{
char buff[1024] = {0};
int n = recv(c,buff,1023,0);
if ( n <= 0 )
{
epoll\_del(epfd,c);//调用移除描述符函数
close(c);
printf("close\n");
continue;
}
char\* filename = get\_filename(buff);//调用资源名获取函数
if ( filename == NULL )
{
send\_404status(c);//调用发送错误应答报文函数
epoll\_del(epfd,c);//调用移除描述符函数
close(c);
continue;
}
printf("filename:%s\n",filename);
if ( send\_httpfile(c,filename) == -1 )//调用发送正确应答报文函数
{
printf("主动关闭连接\n");
epoll\_del(epfd,c);
close(c);
continue;
}
}
epoll\_mod(epfd,c);//调用重置函数
}
}
4.1.4封装epoll函数
//添加描述符函数
void epoll\_add(int epfd,int fd)
{
struct epoll\_event ev;
ev.data.fd = fd;
ev.events = EPOLLIN | EPOLLONESHOT;
if ( epoll\_ctl(epfd,EPOLL_CTL_ADD,fd,&ev) == -1)
{
printf("epoll add err\n");
}
}
//移除描述符函数
void epoll\_del(int epfd, int fd )
{
if ( epoll\_ctl(epfd,EPOLL_CTL_DEL,fd,NULL) == -1 )
{
printf("epoll del err\n");
}
}
//重置描述符函数
void epoll\_mod(int epfd, int fd)
{
struct epoll\_event ev;
ev.data.fd = fd;
ev.events = EPOLLIN | EPOLLONESHOT;
if ( epoll\_ctl(epfd,EPOLL_CTL_MOD,fd,&ev) == -1 )
{
printf("epoll mod err\n");
}
}
4.1.5获取资源名函数
通过这个函数来解析客户端请求报文,获取资源名称。
char\* get\_filename(char buff[])
{ char\* ptr = NULL;
char \* s = strtok\_r(buff," ",&ptr);
if ( s == NULL )
{
printf("请求报文错误\n");
return NULL;
}
printf("请求方法:%s\n",s);
s = strtok\_r(NULL," ",&ptr);
if ( s == NULL )
{
printf("请求报文 无资源名字\n");
return NULL;
}
if ( strcmp(s,"/") == 0 )
{
return "/index.html";
}
return s;
}
4.1.6发送正确应答报文函数
如果客户请求资源可以正常访问,则调用该函数发送应答报文。
int send\_httpfile(int c, char\* filename)
{
if ( filename == NULL || c < 0 )
{
send(c,"err",3,0);
return -1 ;
}
char path[128] = {PATH};
strcat(path,filename);// /home/ubuntu/ligong/day12/index.hmtl
int fd = open(path,O_RDONLY);
if ( fd == -1 )
{
//send(c,"404",3,0);
send\_404status(c);
return -1;
}
int size = lseek(fd,0,SEEK\_END);
lseek(fd,0,SEEK\_SET);
char head_buff[512] = {"HTTP/1.1 200 OK\r\n"};
strcat(head_buff,"Server: myhttp\r\n");
sprintf(head_buff+strlen(head_buff),"Content-Length: %d\r\n",size);
strcat(head_buff,"\r\n");//分隔报头和数据 空行
send(c,head_buff,strlen(head_buff),0);
printf("send file:\n%s\n",head_buff);
int num = 0;
char data[1024] = {0};
while( ( num = read(fd,data,1024)) > 0 )
{
send(c,data,num,0);
}
close(fd);
return 0;
}
4.1.7发送错误应答报文函数
如果客户端访问的在服务器端资源不存在,则调用该函数发送应答
报文。
int send\_404status(int c)
{
int fd = open("err404.html",O_RDONLY);
if ( fd == -1 )
{
send(c,"404",3,0);
return 0;
}
int size = lseek(fd,0,SEEK\_END);
lseek(fd,0,SEEK\_SET);
char head_buff[512] = {"HTTP/1.1 404 Not Found\r\n"};
strcat(head_buff,"Server: myhttp\r\n");
sprintf(head_buff+strlen(head_buff),"Content-Length: %d\r\n",size);
strcat(head_buff,"\r\n");//分隔报头和数据 空行
send(c,head_buff,strlen(head_buff),0);
char data[1024] = {0};
int num = 0;
while( ( num = read(fd,data,1024)) > 0 )
{
send(c,data,num,0);
}
close(fd);
return 0;
}
4.1.8 index.htlm
<html>
<head>
<meta charset=utf8>
<title>baixingyu</title>
</head>
<body background="R-C.jpg">
<center>
<h2>bxy</h2>
</center>
<a href="test.html">下一页</a>
</body>
</html>
4.1.9 test.html
<html>
<head>
<meta charset=utf8>
<title>测试</title>
</head>