昨天我分享了一本学习 socket 的「神书」并介绍了 socket API 的使用 第 11 天:我找到了学习 socket 的正确姿势,今天通过 socket 实现一个 HTTP Server。曾经使用 Node,几行代码便可以实现一个 HTTP Server:
const http = require('http');
// 创建一个 HTTP server
const 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数据:
// 返回 HTML
static 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.直接打卡吧。
推荐阅读: