從零開始一個http服務器-請求request解析(二)
阿新 • • 發佈:2018-08-12
tor pen ica nice 測試 nec 代碼 acc print
從零開始一個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.1 Host: 127.0.0.1:9734 Connection: keep-alive Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 User-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.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 Accept-Encoding: gzip, deflate, br Accept-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.1 Host: 127.0.0.1:9734 User-Agent: curl/7.54.0 Accept: */* Content-Length: 24 Content-Type: application/x-www-form-urlencoded message=nice to meet you
可以看到頭部信息之後多了一空行和之後的POST的body數據信息。還要註意的是Content-Length頭,代表POST的body數據的大小。
解析 request 的 method url version
先來解析最簡單的第一行: "POST /hell HTTP/1.1", 只需要用空格split出三個字符串就好了。
// request.h
struct 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尾端添加item
1. 若list為空,首尾都指向item
2. 否則,尾端的下一項指向item, 再置尾端為item
3. 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 100
struct 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.1
Host: 127.0.0.1:9734
User-Agent: curl/7.54.0
Accept: */*
Content-Length: 24
Content-Type: application/x-www-form-urlencoded
message=nice to meet you
---------------------------
method is: POST
url is: /hello-everyone
http version is: HTTP/1.1
the 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解析(二)