从零开始一个http服务器 (二)
代码地址 : https://github.com/flamedancer/cserver
git checkout step2
解析http request
- 观察收到的http数据
- 解析 request 的 method url version
- 解析 header
- 解析 body
观察收到的http数据
上一节我们完成了一个简单的基于TCP/IP的socket server 程序。而HTTP正式基于TCP/IP的应用层协议,所以只要我们的程序能读懂HTTP数据,并做出符合HTTP协议的响应,那么就能完成HTTP的通信。上一节最后我们用telnet成功连接了我们的服务器,但只是向它传送了一些没有意义的字符。如果是浏览器,会传送什么呢?我们试着在浏览器地址栏输入我们的服务器地址: 127.0.0.1:9734 后访问,发现浏览器说“127.0.0.1 发送的响应无效。”, 那是说我们返回给浏览器的数据浏览器读不懂,因为现代的浏览器默认用http协议请求访问我们的服务器,而我们的返回的数据只是"helloworld"字符串,并不符合http协议的返回格式。虽然如此,但浏览器却是很有诚意的给我们的服务器发标准的http请求,不信我们看下我们的服务器收到的信息:
GET / HTTP/1.1Host: 127.0.0.1:9734Connection: keep-aliveCache-Control: max-age=0Upgrade-Insecure-Requests: 1User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8Accept-Encoding: gzip, deflate, brAccept-Language: zh-CN,zh;q=0.9,en;q=0.8
先观察一会儿,看起来第一行是http请求的类型,第二行开始是一些":"号分割的键值对。的确如此,第一行告诉我们是用的GET请求,请求的url是"/",用的是1.1的HTTP版本。第二行开始是HTTP的请求头部。除了GET请求外,另一种常用的请求是POST。用浏览器发POST请求稍麻烦,我们就借用curl工具来发送个HTTP POST请求给服务器看下数据又会是怎们样的:
curl -d "message=nice to meet you" 127.0.0.1:9734/hello
, 服务器收到的信息:
POST /hell HTTP/1.1Host: 127.0.0.1:9734User-Agent: curl/7.54.0Accept: */*Content-Length: 24Content-Type: application/x-www-form-urlencodedmessage=nice to meet you
可以看到头部信息之后多了一空行和之后的POST的body数据信息。还要注意的是Content-Length头,代表POST的body数据的大小。
解析 request 的 method url version
先来解析最简单的第一行: "POST /hell HTTP/1.1", 只需要用空格split出三个字符串就好了。 ?
// request.hstruct http_request { ???char * method; ???char * url; ???char * version; ???char * body;};void parse_request( ???struct http_request * request, ???char * http_data);
/* request.c*/#include "request.h"void parse_request( ???struct http_request * request, ???char * http_data) { ???char * start = http_data; ???// 解析第一行 ???char * method = start; ???char * url = 0; ???char * version = 0; ???for(;*start && *start != '\n'; start++) { ???????// ?method url version 是由 空格 分割的 ???????if(*start == ' ') { ???????????if(url == 0) { ???????????????url = start + 1; ???????????} else { ???????????????version = start + 1; ???????????} ???????????*start = '\0'; ???????} ???} ???*start = '\0'; ???request->method = method; ???request->url = url; ???request->version = version;}
编写测试用例:
/* test/requestTest.c*/#include <stdio.h>#include "../request.h"int main() { ???struct http_request request; ???char data[] = "POST / HTTP/1.1\n"; ???parse_request(&request, data); ???printf("method is %s; url is %s; version is %s \n", request.method, request.url, request.version);}
在test目录下执行:` gcc ?../request.h ../request.c requestTest.c && ./a.out`,可以看到我们解析的方法正确。
解析 header
header的解析看起来比较复杂,每一行很容易看出是用":"分割的key-value对,所以我们可以用HashMap来表达。如何判断header数据的结束呢,通过前面的观察,可以发现如果是POST会有一个空行和body隔开,是GET的话只能检查客户端的数据是否发完,发完就代表header也结尾了。在正式解析header之前,我们先构造基本数据的数据结构,以方便以后使用。1. 创建链表结构体2. 创建哈希表结构体3. 按行解析header,遇到空行或字符串结尾停止
- 创建链表结构体
首先声明链表结构体- 链表元素结构体,用来存放实际的值,再加一个指向下一个的指针
- 代表链表的结构体,存放链表的关键属性如大小,头尾指针
/* tools/utils.h*/struct ListItem { ???struct ListItem* next; ???char* value;};struct List { ???struct ListItem* start; ???struct ListItem* end; ???int length;};
再声明我们要用到的方法:初始化, 新增元素,打印链表
void initListItem(struct ListItem * listItem);void initList(struct List * listItem);void listAppend(struct List* list, struct ListItem* item);void listPrint(struct List* List);
方法实现
#include <errno.h> ???????/* errno */#include <stdio.h> ??????/* NULL */#include "utils.h"void initListItem(struct ListItem * listItem) { ???listItem->next=NULL; ???listItem->value=NULL;}void initList(struct List * list) { ???list->start=list->end=NULL; ???list->length=0;}/* 在list尾端添加item1. 若list为空,首尾都指向item2. 否则,尾端的下一项指向item, 再置尾端为item3. length + 1*/void listAppend(struct List* list, struct ListItem* item) { ???item->next = NULL; ???if(list->start == NULL) { ???????list->start = list->end = item; ???} else { ???????list->end->next = item; ???????list->end = item; ???} ???list->length++;}void listPrint(struct List* list) { ???struct ListItem* point = list->start; ???printf("["); ???for(int i=0; i<list->length; i++) { ???????if( i>0 ) { ???????????printf(", "); ???????} ???????printf("'%s'", point->value); ???????point = point->next; ???} ???printf("]\n");}
测试
我们尝试增加两个元素,然后打印怎个list
/* test/utilsTest.c ???test cmd : ???gcc ?../tools/utils.h ../tools/utils.c utilsTest.c && ./a.out*/#include <assert.h>#include <stdio.h> ?/* ?printf */#include "../tools/utils.h"void listAppendTest() { ???struct List list_instance; ???struct List* list = &list_instance; ???struct ListItem listItem_instance; ???struct ListItem* listItem = &listItem_instance; ???listItem->value = "hello world"; ???struct ListItem listItem_instance2; ???struct ListItem* listItem2 = &listItem_instance2; ???listItem2->value = "nice to meet you"; ???assert(list->length == 0); ???listAppend(list, listItem); ???assert(list->length == 1); ???listAppend(list, listItem2); ???listPrint(list); ???printf("test listAppend OK\n");}int main() { ???listAppendTest();}
看到输出结果为
['hello world', 'nice to meet you']
完美!
- 创建哈希表结构体
和listItem不一样,我们的mapItem需要两个属性来分别代表key和value,为了方便起见,我们直接改造listItem来兼容map。
将ListItem改为Item:
struct Item { ???struct Item* next; ???char* key; ???char* value;};
然后在构造我们的map结构体。里面最主要的是用数组来表示的哈希表,表里的元素不用纯粹的Item而用List是为了遇到哈希碰撞时可以在相同的index中插入元素。除此之外,我们还需要一个计算字符串哈希值的方法。
/* tools/utils.h*//* .... 省略部分代码*/#define HashTableLen 100struct Map { ???struct List* table[HashTableLen]; ???int table_len; ???int item_cnt;};void initMap(struct Map* map);void releaseMap(struct Map* map);int hashCode(char * str);void mapPush(struct Map* map, struct Item* item);void mapPrint(struct Map* map);void mapGet(char * key);
/* tools/utils.c *//* .... 省略部分代码*/void initMap(struct Map* map){ ???map->table_len = HashTableLen; ???map->item_cnt = 0; ???for(int i=0; i<map->table_len; i++) { ???????map->table[i] = NULL; ???}}void releaseMap(struct Map* map) { ???for(int i=0; i<map->table_len; i++) { ???????if(map->table[i] != NULL) { ???????????free(map->table[i]); ???????????map->table[i] = NULL; ???????} ???}}int hashCode(struct Item* item) { ???char* str = item->key; ???int code; ???int len = 0; ???int maxLen = 100; ???for(code=0; *str != '\0' && len < maxLen; str++) { ???????code = code + 31 * (*str); ???????len++; ???} ???return code % HashTableLen;}void mapPush(struct Map* map, struct Item* item) { ???int index = hashCode(item); ???if(map->table[index] == NULL) { ????????struct List* list = malloc(sizeof(struct List)); ????????initList(list); ????????if(list == NULL) { ????????????perror("Error: out of storeage"); ????????} ????????map->table[index] = list; ???} ???listAppend(map->table[index], item); ???map->item_cnt++;}void mapPrint(struct Map* map) { ???struct List* list; ???struct Item* item; ???int print_item_cnt = 0; ???printf("{"); ???for(int i=0; i<map->table_len; i++) { ????????list = map->table[i]; ????????if(list == NULL) { ???????????continue; ????????} ????????item = list->start; ????????while(item != NULL) { ????????????printf("'%s': '%s'", item->key, item->value); ????????????item = item->next; ????????????print_item_cnt++; ????????????if(print_item_cnt != map->item_cnt) { ????????????????printf(", "); ????????????} ????????} ???} ???printf("}\n");}
测试代码
void mapPushTest() { ???struct Map map_instance; ???initMap(&map_instance); ???struct Map* map = &map_instance; ???struct Item item_instance; ???initItem(&item_instance); ???struct Item* item = &item_instance; ???item->key = "h"; ???item->value = "hello world"; ???mapPush(map, item); ???mapPrint(map); ???struct Item item_instance2; ???initItem(&item_instance2); ???struct Item* item2 = &item_instance2; ???item2->key = "h2"; ???item2->value = "nice to meet you"; ???mapPush(map, item2); ???mapPrint(map); ???releaseMap(map);}看到输出结果为
{‘h‘: ‘hello world‘}
{‘h‘: ‘hello world‘, ‘h2‘: ‘nice to meet you‘}
3. 解析header代码有了map结构体后,解析header就方便多了,只要按行根据":" 拆分成 key和value就行了?``` c/* 第二行开始为 header ?解析hedaer*/start++; ??// 第二行开始initMap(request->headers);char * line = start;char * key;char * value;while( *line != '\r' && *line != '\0') { ???char * key; ???char * value; ???while(*(start++) != ':'); ???*(start - 1) = '\0'; ???key = line; ???value = start; ???// todo 超过 MAXREQUESTLEN 的 判断 ???while(start++, *start!='\0' && *start!='\r'); ???*start++ = '\0'; // \r -> \0 ???start++; ??// skip \n ???printf("key is %s \n", key); ???printf("value is %s \n", value); ???line = start; ???struct Item * item = (struct Item *) malloc(sizeof(struct Item)); ???initItem(item); ???item->key = key; ???item->value = value; ???mapPush(request->headers, item); ???mapPrint(request->headers);}releaseMap(request->headers);
解析body
解析body很简单,如果最后一行不是空格不是空行,说明是有body数据的,空行后面的就是body数据了.
header里面有个关键的key, ‘Content-Length’ 代表了body有多长,我们可以利用这个字段来判断body的结尾。
/* 如果最后一行不是空行 ?说明有body数据 */if(*line == '\r') { ???char * len_str = mapGet(request->headers, "Content-Length"); ???if(len_str != NULL) { ???????int len = atoi(len_str); ???????// 跳过 两个 \n ???????line = line + 2; ???????* (line + len) = '\0'; ???????request->body = line; ???}}printf("the request body is %s \n", request->body);
大功告成 最后打印我们的成果
/* ?打印 request 信息 */printf("---------------------------\n");printf("method is: %s \n", request->method);printf("url is: %s \n", request->url);printf("http version is: %s \n", request->version);printf("the headers are :\n");mapPrint(request->headers);printf("body is %s \n", request->body);printf("---------------------------\n");
执行 gcc request.h request.c main.c tools/utils.c tools/utils.h && ./a.out
然后新开一个终端执行 curl -d "message=nice to meet you" 127.0.0.1:9734/hello-everyone
看到输出结果:
POST /hello-everyone HTTP/1.1Host: 127.0.0.1:9734User-Agent: curl/7.54.0Accept: */*Content-Length: 24Content-Type: application/x-www-form-urlencodedmessage=nice to meet you---------------------------method is: POSTurl is: /hello-everyonehttp version is: HTTP/1.1the headers are :{'User-Agent': ' curl/7.54.0', 'Content-Type': ' application/x-www-form-urlencoded', 'Host': ' 127.0.0.1:9734', 'Accept': ' */*', 'Content-Length': ' 24'}body is message=nice to meet you---------------------------
从零开始一个http服务器-请求request解析(二)
原文地址:https://www.cnblogs.com/chens-smile/p/9462573.html