文章目录
之前我们基于已经Reactor模型实现了一个简单的websocket服务器,在此基础上再实现一个简单的HTTP服务器小框架。实际上,最终我们会实现一个支持websocket的HTTP服务器。具体功能包括:首先要实现GET html页面、图片、pdf文档等;其次是实现POST方法并完成一个简单的表单提交功能。
实现GET方法
关于HTTP报文的消息结构都包含哪些元素可以参考这里。
一个GET请求报文的示例如下:
GET /index.html HTTP/1.1
Host: 192.168.0.103
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
GET方法没有消息体,因此不需要解析其请求数据,只需解析其开头的请求行和请求头部。
对应的响应报文实例如下:
HTTP/1.1 200 OK
Date: Sat, 13 Nov 2021 02:13:47 GMT
Content-Type: text/html;charset=utf-8
Content-Length: 253
<!DOCTYPE html>
<html>
<head>
<title>Title</title>
</head>
<body>
<h1>This is an awesome HTML</h1>
</body>
</html>
也很简单,按格式构造状态行和必要的消息头后,再间隔一个空行并发送页面数据就可以了。这里的数据可能是由代码构造的html文本,也可能是一个文件,比如jpg格式的图片或pdf文档。因此,必须通过Content-Type
指定消息体的格式客户端才能识别,同时指定Content-Length
后客户端才能知道需要接收多少数据。
约定GET时URI的格式
GET方法一般是用于请求某一个具体的页面或文件资源,当然也可以用于获取XML或JSON格式的数据。为了简单起见,我们目前在做URI资源名称的解析时,仅支持GET方法请求服务器上已存在的文件资源,暂时包括.html、jpg、.pdf、ico四种。这些资源都单独放在程序工作目录下的一个public/
目录下,但我们规定GET报文的URI中不需要体现public目录,比如请求index.html
页面,那么开头的请求行只需这么写即可:GET /index.html HTTP/1.1
,由程序自动转换为去获取public/index.html
文件。
状态机与websocket协议兼容
为了兼容之前的websocket协议,我们需要先增加虚拟机中的状态以实现HTTP的请求和接收。
显然websocket协议的握手过程也属于HTTP请求与响应的一部分,二者都是使用GET方法进行请求,我们只需根据请求报文的头部来判断这是不是一个websocket握手请求,如果是则执行websocket握手过程的响应代码,如果不是则执行普通HTTP协议的响应代码。这里先简单地以头部中是否含有``字段来识别是否为websocket的握手请求,大致代码思路如下:
/* 根据方法进行处理并设置下一个状态 */
switch(http->req.method)
{
case HTTP_METHOD_GET:
if(strlen(http->ws.ws_key) > 0) // 获取到了websocket的key,进行 websocket 握手
{
ret = ws_handshake(http);
http->status = WS_DATATRANSFER; // 握手后则进入通信状态
}
else // 否则按普通 http 请求处理
{
ret = http_request_get(http);
http->status = HTTP_RESPONSE; //
}
break;
.......
状态机实现的大致思路如下图所示:
实现几个辅助函数
开始HTTP请求与响应的主体代码编写前,需要先实现一些辅助函数。此处仅列出几个比较关键的辅助函数,主要位于HTTP协议相关的模块代码。
1)解析消息头,获取方法、URI以及需要的头部字段。由于HTTP协议中消息头的结束标志位是一个空行,因此我们用\r\n\r\n
作为消息头的结束字符串。在读取到\r\n\r\n
前,我们不知道头部到底有多长。对此我在代码中作简单处理,为消息头部专门使用一个缓冲区,指定一个最大头部长度,如果头部接收过程中缓冲区已满却还没有收到结束符\r\n\r\n
,则返回错误。
/* 解析消息头,获取方法、URI以及需要的头部字段 */
static int http_resolveReqHeader(http_service_interface *http)
{
if(http == NULL) return -1;
char linebuf[256];
char* value;
int level = 0;
int ret;
/* 1 - 从第1行提取出http方法*/
memset(linebuf, 0, 256);
level = readline(http->req.header, level, linebuf);
if(strstr(linebuf, "GET"))
http->req.method = HTTP_METHOD_GET;
else if(strstr(linebuf, "POST"))
http->req.method = HTTP_METHOD_POST;
else if(strstr(linebuf, "PUT"))
http->req.method = HTTP_METHOD_PUT;
else
http->req.method = HTTP_METHOD_NOTALLOWED; // 从第一行提取不到方法或不支持的方法
/* 2 - 尝试提取请求的资源路径 */
if(value = strchr(linebuf, '/'))
{
memset(http->req.resource, 0, HTTP_MAX_RESOURCE_NAME);
ret = http_getResourceName(http->req.resource, HTTP_MAX_RESOURCE_NAME, value + 1);
if(ret >= 0) printf("# resource requested[%d]: %s\n", ret, http->req.resource);
}
/******* 3 - 提取可能有用的头部字段 *******/ // this piece of code is awful, but I will improve it
/* 都需要提取哪些头部字段目前通过手动加到这里 */
// 3.1 尝试提取 ws 的key字段,后面依次判断是否进行 ws 握手
ret = http_getHeaderValueString(http->req.header, "Sec-WebSocket-Key", http->ws.ws_key);
if(ret >= 0) printf("# WS key[%d]: %s\n\n", ret, http->ws.ws_key);
// 3.2 尝试提取 Content-Length 字段
if(http->req.method != HTTP_METHOD_GET)
{
ret = http_getHeaderValueInt(http->req.header, "Content-Length", &http->req.contentlength);
if(ret >= 0 && http->req.contentlength > 0)
{
// 根据消息体长度申请一块堆内存来单独存放消息体,此处暂不管申请是否成功
http->req.body = (char*)malloc(http->req.contentlength + 1); // 加1若需要的话作为结束符
if(http->req.body)
memset(http->req.body, 0, http->req.contentlength + 1);