第 12 天:从 0 徒手实现一个 HTTP Server

昨天我分享了一本学习 socket 的「神书」并介绍了 socket API 的使用 第 11 天:我找到了学习 socket 的正确姿势,今天通过 socket 实现一个 HTTP Server。曾经使用 Node,几行代码便可以实现一个 HTTP Server:

const http = require('http');// 创建一个 HTTP serverconst server = new http.Server();
/** * request 事件,当客户端发起请求后会响应这个事件 * req:请求对象 * res:响应对象 * */ server.on('request', function(req, res) {    let path = req.url;     res.writeHead(200, {          "Content-type" : "application/json"      });      let data = {          title: "前端小课",          des: "内容由素燕公众号发布"      };      // 最终数据需要转换成 json 字符串      res.write(JSON.stringify(data)); res.end();});
// 监听 8888 端口server.listen(8888, function() { console.log('Server run in: http://127.0.0.1:8888');});

那问题来了,Node 是如何实现的呢?我们可不可以自己动手写一个呢?抱着这种刨根问底的精神,我决定试一试。

HTTP 属于「应用层」的协议,它通过 TCP 传输数据。也就是说 HTTP 是卖家,负责把商品打包好交给快递员(TCP),快递员只负责把货送到目的地即可,他不关心是什么包裹。

操作系统都会提供一个 Socket 接口,通过它可以与其它应用程序进行通信,无论是本机的还是网络中的计算机。Socket 接口提供了 TCP 和 UDP 协议的实现,也就是说其实我们做的事情很简单,通过 Socket 创建一个 TCP 服务端和客户端,发送的数据内容是 HTTP 报文内容,如果你不熟悉 HTTP 报文,可以看 第 8 天:弄懂 HTTP 请求报文 和 第 9 天:HTTP 响应报文与状态码 的内容。

Socket 接口使用 C 语言实现,由于我以前是做 iOS 的,我直接通过 Xcode(开发iOS、Mac 软件的 IDE)来实现一个 TCP 服务端和客户端。

TCP 服务端

TCP 服务端直接创建一个 mac 终端应用程序充当 server 端,起名 HTTPServer。它可以可以返回 Json 和 HTML,支持在浏览器中访问。

1.首先需要构建 response:

static inline NSString *ResponseForBody(NSObject *obj) {    NSString *resJson;    NSString *contentType = @"application/json";    if ([obj isKindOfClass:[NSString class]]) {        resJson = (NSString *)obj;        contentType = @"text/html; charset=utf-8";    } else {        NSData *jsonData = [NSJSONSerialization dataWithJSONObject:obj options:NSJSONWritingPrettyPrinted error: nil];        resJson = [[NSString alloc]initWithData:jsonData encoding:NSUTF8StringEncoding];    }        NSString *res = resJson;
NSData *resData = [res dataUsingEncoding:NSUTF8StringEncoding]; NSString *statusLine = @"HTTP/1.1 200 OK"; NSMutableDictionary *headerDict = [NSMutableDictionary dictionary]; [headerDict setObject:contentType forKey:@"Content-Type"]; [headerDict setObject:@(resData.length).stringValue forKey:@"Content-Length"]; NSMutableString *response = [NSMutableString string]; [response appendString:statusLine]; [response appendString:@"\n"]; [headerDict enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSString * _Nonnull obj, BOOL * _Nonnull stop) { NSString *headerRaw = [NSString stringWithFormat:@"%@: %@\n",key, obj]; [response appendString:headerRaw]; }]; [response appendString:@"\n"]; [response appendString:res]; return response;}

2.构建JSON 数据:

// 返回是 JSON 数据static inline NSString *JSONRes() {    NSDictionary *resDict = @{        @"code": @"201",        @"msg": @"response",        @"data": @[@"wsy", @"lefex"]    };    return ResponseForBody(resDict);}

3.构建HTML数据:

// 返回 HTMLstatic inline NSString *HTMLRes() {    NSString *html =  @"<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\"><title>Socket</title></head><body><h1>I am a socket王素燕</h1></body></html>";    return ResponseForBody(html);}

4.构建 TCP 服务,需要先导入头文件:

#include <sys/socket.h>#include <netinet/in.h>// 端口号#define PORT 8888
int server() {    const char *response = [HTMLRes() UTF8String];    // creating socket    int socket_id = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);    if (socket_id < 0) {        printf("create socket error \n");        exit(EXIT_FAILURE);    }    // create address    struct sockaddr_in address;    int addrlen = sizeof(address);    address.sin_family = AF_INET;    address.sin_addr.s_addr = INADDR_ANY;    address.sin_port = htons( PORT );    // alloc memory    memset(address.sin_zero, '\0', sizeof address.sin_zero);    // bind address    if (bind(socket_id, (struct sockaddr *)&address, sizeof(address)) < 0){        printf("bind error \n");        exit(EXIT_FAILURE);    }    // listen    if (listen(socket_id, 10) < 0) {        printf("linten error \n");        exit(EXIT_FAILURE);    }    while(1) {        printf("waiting for new connection \n");        // wait socket to connect        int new_socket = accept(socket_id, (struct sockaddr *)&address, (socklen_t*)&addrlen);        if (new_socket < 0) {            perror("accept error \n");            exit(EXIT_FAILURE);        }        // read data from new socket        char buffer[30000] = {0};        read(new_socket , buffer, 30000);        printf("receive client(%d) message -------------------\n", new_socket);        printf("%s\n",buffer );        // write data to new socket        write(new_socket , response , strlen(response));        // close socket        close(new_socket);    }    return 0;}

代码写完后,直接运行。

TCP客户端

TCP客户端用来连接 server 端,发送请求报文。请求报文格式如下:

直接新建一个 iOS App 作为请求客户端,起名 HTTPClient。

1.首先构建请求报文:

static inline NSString *RequestString() {    NSString *requestLine = @"POST /bauhinia/v1/class/purchase/info HTTP/1.1";    NSDictionary *headerDict = @{        @"Host": @"entree.get.com",        @"Accept": @"json",        @"Accept-Encoding": @"gzip, deflate, br",        @"Content-Length": @"45",        @"User-Agent": @"LuoJiFMIOS/7.5.0 (iPhone; iOS 13.2.2; Scale/2.00)",        @"Cookie": @"acw_tc=276aede015; aliyungf_tc=AQAFUr/nbWX",        @"Accept-Language": @"zh-Hans-CN;q=1, en-CN;q=0.9",        @"Connection": @"keep-alive",        @"Content-Type": @"application/x-www-form-urlencoded"    };    NSDictionary *bodyDict = @{        @"userid": @"123",        @"class_id": @"lefex"    };        NSMutableString *request = [NSMutableString string];    [request appendFormat:@"%@\n", requestLine];    [headerDict enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL * _Nonnull stop) {        NSString *raw = [NSString stringWithFormat:@"%@: %@\n", key, obj];        [request appendString:raw];    }];        NSMutableArray *bodies = [NSMutableArray array];    [bodyDict enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL * _Nonnull stop) {        NSString *raw = [NSString stringWithFormat:@"%@=%@", key, obj];        [bodies addObject:raw];    }];        if([bodies count] > 0) {        [request appendString:@"\n"];        [request appendString:[bodies componentsJoinedByString:@"&"]];    }        return request;}

2.创建客户端:

static inline void createRequestSocket() {    const char *request = [RequestString() UTF8String];    // create socket    int socket_id = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);    if (socket_id < 0) {        printf("create error \n");        return;    }    struct sockaddr_in serv_addr;    memset(&serv_addr, 0, sizeof(serv_addr));    serv_addr.sin_family = AF_INET;    serv_addr.sin_port = htons(PORT);    // connect to server    if (connect(socket_id, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {        printf("connect error \n");        return;    }    // send request to server    send(socket_id, request, strlen(request), 0);    char buffer[3000] = {0};    // read response from server    read(socket_id, buffer, 3000);    printf("receive response:\n %s\n\n", buffer);}

运行一下哦

TCP 服务端和客户端的代码已经写完了,运行 HTTPServer,效果如下,运行成功后会打印 waiting for new connection (如果报 bind error 错误,换个端口试试):

运行 TCP 客户端 HTTPClient,运行起来后,轻点模拟器。客户端打印,接收到响应的请求:

服务端接收到的请求:

然后在浏览器中输入:http://127.0.0.1:8888/,发现浏览器可以正常解析 response,并把 HTML 渲染展示出来(需要把响应对象修改为 HTML)。

当在浏览器中发起 HTTP 请求时,在服务端可以看到请求报文,它是由浏览器自己构建的请求报文:

其实我们的请求和响应是写死的,如果想根据具体的请求对象返回不同的响应对象,就需要解析请求对象,根据请求对象返回客户端需要的数据。

HTTP 解析

解析 HTTP 可以使用 http_parser 这个库,node 使用的也是这个库对 HTTP 请求和响应对象进行解析的。它通过 C 语言实现,使用起来也比较简单,我们解析一下请求对象。添加 http_parser.h 和 http_parser.c,添加解析代码:

http_parser_settings setting;setting.on_message_begin = my_parser_begin;setting.on_url = my_url_callback;setting.on_header_field = my_hader_field_cb;setting.on_header_value = my_hader_value_cb;setting.on_status = my_status_cb;setting.on_headers_complete = my_header_complete;setting.on_body = my_body_cb;setting.on_message_complete = my_message_complete;
http_parser *parser = malloc(sizeof(http_parser));http_parser_init(parser, HTTP_REQUEST);http_parser_execute(parser, &setting, buffer, sizeof(buffer));

解析回调函数:

// HTTP 解析回调int my_parser_begin(http_parser* parser) {    printf("????begin parser \n");    return 0;}
int my_url_callback(http_parser* parser, const char *at, size_t length) { NSString *req = [NSString stringWithUTF8String:at]; printf("????url: %s\n", [[req substringToIndex:length] UTF8String]); return 0;}
int my_hader_field_cb(http_parser* parser, const char *at, size_t length) { NSString *req = [NSString stringWithUTF8String:at]; printf("????header: parser %s\n", [[req substringToIndex:length] UTF8String]); return 0;}
int my_hader_value_cb(http_parser* parser, const char *at, size_t length) { NSString *req = [NSString stringWithUTF8String:at]; printf("????header value: %s\n", [[req substringToIndex:length] UTF8String]); return 0;}
int my_status_cb(http_parser* parser, const char *at, size_t length) { NSString *req = [NSString stringWithUTF8String:at]; printf("????status: %s\n", [[req substringToIndex:length] UTF8String]); return 0;}
int my_body_cb(http_parser* parser, const char *at, size_t length) { printf("????body: %s\n", at); return 0;}
int my_header_complete(http_parser* parser) { printf("????header complete \n"); return 0;}
int my_message_complete(http_parser* parser) { printf("????message complete \n"); return 0;}

解析结果:

????begin parser ????url: /bauhinia/v1/class/purchase/info????header: parser Accept????header value: json????header: parser Accept-Encoding????header value: gzip, deflate, br????header: parser Cookie????header value: acw_tc=276aede015; aliyungf_tc=AQAFUr/nbWX????header: parser Connection????header value: keep-alive????header: parser Content-Type????header value: application/x-www-form-urlencoded????header: parser Host????header value: entree.get.com????header: parser Content-Length????header value: 45????header: parser User-Agent????header value: LuoJiFMIOS/7.5.0 (iPhone; iOS 13.2.2; Scale/2.00)????header: parser Accept-Language????header value: zh-Hans-CN;q=1, en-CN;q=0.9????header complete ????body: userid=123&class_id=lefex????message complete

总结

本文介绍了从0搭建一个HTTP 客户端和服务端,HTTP 属于应用层协议,通过 TCP 进行数据传输,传输的内容是请求和响应报文,通过 http_parser 来解析报文内容。当然本文只是搭建了一个简单的 HTTP 服务,像 node.js 的 HTTP 模块考虑了非常多的情况,有兴趣的可以读一读它的源码。大家加油!!!

可以在 https://github.com/lefex/FE 这里找到本次内容的源码。

今天的打卡指令:

1.通过本文你对 HTTP 的感受是什么?

2.直接打卡吧。


推荐阅读:

第3天:HTTP 之客户端与服务端

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值