前言
最近跟着完成了一个简单的B/S模型服务器的搭建,一路写下来,虽然最终是完成了预定的效果,但是写的过程中老想着参考参考,于是编着编着有些地方就开始变成“打字”训练了。所以为了让自己的印象能够深刻一点,在这里把搭建的流程简单梳理一下,包括记录一些代码。
涉及知识
1. 边沿触发下的epoll模型
2. http基础协议
3. html超文本标记语言
逻辑流程图
1. epoll模型流程
不管多线程/进程并发服务器,还是多路I/O转接服务器,我们平时写练习所完成的基本都是C/S模型。而搭建Web服务器,需要我们完成一个B/S模型。但既然它是服务器,那么它就与我们搭建C/S模型一样,都需要基础的服务器模型,所以这里我们给出使用较多的epoll模型的搭建流程。而与C/S模型不一样的是数据交流的部分,所以我们将重点放在do_read()函数中。
在C/S模型中,我们需要完成两份代码的编写,即客户端和服务器端。所以两者的信息交流部分,是可以由我们根据需求自定义编写的。而在B/S模型中,我们只需要完成服务器端代码的编写。同时由于我们需要借助浏览器,那么在数据交流部分,我们就必须要遵循浏览器和Web服务器之间的通信协议,即http协议。根据协议内容,进行请求的接收和响应的回发。
2. 基于http协议的数据交流流程
当发生读事件时,也就是建立好连接后的客户端向我们的服务器发送了数据,即http请求消息,此时do_read()函数被调用。
在do_read()函数中,我们主要完成两件事:
1) 处理http请求消息。根据http协议内容,我们可以知道,发送过来的请求消息中,最重要的内容为请求方式以及请求的文件对象 (本次Web实验,只处理GET请求) 。所以我们需要在请求消息中,提取出这两个信息,并且对它们加以判断,根据不同的判断结果进行不同的处理:当用户请求了一个普通文件时,我们就单纯的将这个文件的内容回发。当请求了一个目录文件时,我们需要将目录的内容呈现在浏览器中。
2) 回复http响应消息。根据http协议内容,我们需要按照 响应格式 发送响应消息,其中重要的内容包括:文件类型、文件大小、连接关闭以及响应正文(数据)。 对于普通文件,我们直接发送源文件数据即可。为此需要先判断出它的文件类型,如.txt / .jpg / .mp3等,然后回复规定的传输内容;对于目录文件,我们需要将目录内容呈现在浏览器中。也就是说,我们需要回发的文件类型为.html,回发的响应正文就是我们所编写的html语言。由于浏览器本身就是编译器,所以它能将这些代码标签以网页的形式展现出来。
最终效果 (部分结果图)
1. 请求目录
(因为未做汉字字符对应的编码和译码部分的代码,所以这里的文件名采用的都是拼音的方式)
2. 请求未提供的文件
部分源码
1. get_line() 函数 -- 读取一行数据
int get_line(int cfd,char *buf,int size)
{
int n,i=0;
char c = '\0';
while( i<size-1 && c!='\n'){
n = recv(cfd,&c,1,0); //读取一个字符
if(n > 0){
if(c == '\r'){ //判断是否到结尾 '以/r/n结尾'
n = recv(cfd,&c,1,MSG_PEEK); //尝试能否读取到/n
if(n > 0 && c == '\n'){
recv(cfd,&c,1,0); //真正去读取
} else {
c = '\n'; //未读到则补上
}
}
buf[i] = c;
i++;
} else {
c = '\n'; //仅起到退出循环的作用
}
buf[i] = '\0';
if(-1 == n){ //出错
i = n;
}
}
return i; //返回读取到的字符数
}
2. sscanf() 函数 / scandir() 函数 的用法
/* sscanf() -- 拆分请求数据 */
sscanf(buf,"%[^ ] %[^ ] %[^ ]",method,path,protocol); //通过正则表达式拆分信息
/* scandir() -- 遍历目录项 */
struct dirent** ptr; //目录项结构体二级指针
int num = scandir(dir_name,&ptr,NULL,alphasort); //按字母顺序获取目录项
for(i=0; i<num ; i++){ //循环遍历
char *name = ptr[i]->d_name; //从ptr指针数组中获取文件名
/* 向浏览器回发目录中各文件名 */
...
}
3. do_read() 函数
void do_read(int cfd,int epfd) //数据交流函数
{
char buf[1024] = { 0 };
char method[16],path[256],protocol[16];
int len = get_line(cfd,buf,sizeof(buf)); //获取一行数据
if(len == 0){
printf("客户端断开连接......\n");
disconnected(cfd,epfd); //断开连接
} else {
sscanf(buf,"%[^ ] %[^ ] %[^ ]",method,path,protocol); //拆分
while(1){ //由于是非阻塞ET,所以需要我们主动读取掉多余的信息
char bin[1024] = { 0 };
len = get_line(cfd,bin,sizeof(bin));
if(len == -1){
break;
}
}
if( strncasecmp(method,"GET",3) == 0 ){ //GET请求方法
char *file_name = path+1; //获取请求文件名称
if(strcmp(path,"/") == 0){ //若请求了工作根目录
file_name = "./";
}
http_request(cfd,file_name); //根据文件名,回复http响应
}
}
}