星的天空的博客

种一颗树,最好的时间是十年前,其次是现在。

0%

HTTP协议是怎么实现的?

前言

在上层应用开发中,HTTP协议可以说是最常见,使用最频繁的网络协议了。在网上也有非常多的文章进行解读,但是大部分都是讲解HTTP协议的内容和使用,很少有人讲HTTP协议是怎么实现的。网络协议可以涉及很大的广度和深度,不是一篇文章就能讲清楚的,我这里更多的是提供一个思路供读者来思考。本篇文章会基于iOS平台来进行说明,但是并不代表这篇文章只针对iOS开发,因为协议是跨平台的,其中涉及到的编程思想也是。本文会分四个部分进行讲解:

  1. 第一部分:数据是如何在网络上进行传输的。这部分主要让你对网络模型和各层协议有一个基础的了解,如果您对这部分比较了解,可以直接从第二部分看起。
  2. 第二部分:HTTP协议数据是如何转换为TCP数据收发的。
  3. 第三部分:HTTP协议中RequestResponse的解析和相关逻辑处理。
  4. 第四部分:修改HTTP底层实现,完成自有需求。

数据是如何在网络上进行传输的

数据遵循网络协议进行收发,讲到网络协议,就绕不开OSI模型TCP/IP参考模型,它们有不同的层次划分,OSI模型分为7层,TCP/IP参考模型分为4层。网上有很多将TCP/IP参考模型映射到OSI模型的说法,由于TCP/IP参考模型OSI模型不能精确地匹配,还没有一个完全正确,或者说权威的答案,一般认为的对应关系图示如下:

HTTP协议属于最上层应用层网络模型是比较抽象的,在实际编码时,上层应用的开发者一般只接触到应用层,开发者只需要把一个HTTP Reques丢入网络框架,请求完成后就会返回一个HTTP Response,但是它的底层是怎么实现的类?我们先看下图:

从上图我们可以看到HTTP数据是如何在客户端与服务端之间交互的,网络模型虽然很复杂,但是从某个角度看,可以说是”套娃”,在RFC 1122中描述的沿着不同的层应用数据的封装递减图示如下:

上图中最上层的Data数据代表应用层协议数据,在HTTP协议中,HTTPrequest报文和response报文都包含有headerTCPIP也都有header,它们通过层层”套娃”后发送。

现在我们对数据如何通过网络传输稍微有了一个整体的概念,但是细节是不清楚的。从上述内容中,可以看到,HTTP数据是转换为TCP数据进行传输,对于传输层及以下的内容在这里就不做说明,这里主要讲应用层HTTP数据是如何通过传输层传输的,以及如何解析的。

HTTP协议数据是如何转换为TCP数据收发的

一般来讲,各系统都会给用户提供HTTP网络框架,例如iOSNSURLSession,在系统的HTTP网络框架之上,开发者社区又会开发出各种易用版本的封装,例如AFNetworking。对上层开发者来说,HTTP协议的使用一般就是一个框架封装好的Request对象,甚至只是一个URL,使用框架请求完成后,返回一个Response对象,它的底层实现是隐藏的。

我们都知道,计算机的底层是二进制,数据传输也不例外。要把Request对象从主机传输到服务器,那么必须把它转换为二进制,那么它是怎么转换的?又是怎么传输的?

HTTP协议是怎么转换成二进制的?

网络框架的Request对象为了易用性,经过了层层封装,要传输出去,必须将它转换为二进制数据:Request对象 -> 符合HTTP协议的Request字符串 -> 二进制数据

HTTP协议中的请求报文:

响应报文:

按照图示请求报文格式,我们可以将Request对象转换为符合HTTP协议的字符串并转换为字节流。样例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/// 创建NSURLRequest
NSURL *url = [NSURL URLWithString:@"https://www.baidu.com"];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url];

/// 将NSURLRequest转换为二进制数据(这里只针对POST和GET请求进行说明)
+ (NSData *)httpRequestDataWithRequest:(NSURLRequest *)request {
NSMutableString * requestStrFrmt = [NSMutableString string];
NSURL * url = request.URL;
NSString *requestURI = url.path;
//解析请求行
if ([request.HTTPMethod isEqualToString:@"POST"]) {
if (!url.path || url.path.length == 0) {
requestURI = @"/";
}
}
else if ([request.HTTPMethod isEqualToString:@"GET"]) {
if (url.path.length > 0 && url.query.length > 0) {
requestURI = [NSString stringWithFormat:@"%@?%@", url.path, url.query];
} else if (url.path.length > 0) {
requestURI = url.path;
} else if (url.query.length > 0) {
requestURI = url.query;
} else {
requestURI = @"/";
}
}

[requestStrFrmt appendFormat:@"%@ %@ HTTP/1.1\r\n", request.HTTPMethod, requestURI];
if ([request.allHTTPHeaderFields objectForKey:@"Host"] == nil) {
[requestStrFrmt appendFormat:@"Host: %@\r\n", url.host];
}

//解析请求头
for (NSString * key in request.allHTTPHeaderFields.allKeys) {
[requestStrFrmt appendFormat:@"%@: %@\r\n", key, request.allHTTPHeaderFields[key]];
}

//解析请求数据(body)
if ([request.HTTPMethod isEqualToString:@"POST"] && request.HTTPBody) {
[requestStrFrmt appendFormat:@"Content-Length: %@\r\n", @(request.HTTPBody.length)];
//请求头以两个CRLF结束
[requestStrFrmt appendString:@"\r\n"];
NSData *headerData = [requestStrFrmt dataUsingEncoding:NSUTF8StringEncoding];
NSMutableData * requestData = [NSMutableData dataWithData:headerData];
[requestData appendData:request.HTTPBody];
return requestData;
} else {
//请求头以两个CRLF结束
[requestStrFrmt appendString:@"\r\n"];
return [requestStrFrmt dataUsingEncoding:NSUTF8StringEncoding];
}
}

/// 打印出NSURLRequest报文的文本数据
NSData *data = [NSData httpRequestDataWithRequest:request];
NSString *requestText = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@", requestText);

调用通过上述代码,我们可以得到request报文的文本数据。样例:

1
2
GET / HTTP/1.1
Host: www.baidu.com

response报文可以参考request报文进行分析,为了避免篇幅过长,这里就不再做说明了。

HTTP二进制数据是怎么传输的?

在各操作系统中,通常会为应用程序提供一组应用程序接口,称为套接字接口(socket API),主要作用就是实行进程间通信和网络编程。大白话就是:套接字是用C语言写成的应用程序开发库,它就是一个库。

套接字中的网络套接字,包含有流式套接字(SOCK-STREAM),它使用TCP协议来实现字节流的传输。通过socket框架,将包含HTTP数据的TCP字节流发送给服务端,服务端通过socket框架拿到包含HTTP数据的TCP字节流后,根据HTTP协议进行解析,解析后又被服务端的HTTP网络框架返回,图示如下:

上述图示只包含request报文部分,并不包含response报文部分,由于response报文的数据传输和这个并无太大区别,这里就不再做额外说明了。

HTTP协议中RequestResponse的解析和逻辑处理

看到这里,我们对HTTP实现应该有了较明朗的了解,但是这其中还是有一些细节需要补充。
用过Socket的同学应该都知道,它基于TCP是流式传输,会有半包粘包问题。一般通过对数据添加Header来解决这些问题。我们知道,HTTP数据包含两部分,分别是HeaderBodyHTTP协议定义HeaderBody之间包含两个CRLF,一个CRLF是一个回车加一个换行:\r\n。通过这个标识,我们可以从TCP流中把HTTP数据的Header分离出来。然后再解析出Header中的Content-Length字段,它就是body的长度,读取这个长度的内容,就可以把Body解析出来。

在HTTP/1.1版本,Body的解析还和Transfer-Encoding字段有关,这里就不讨论了。

当然HTTP协议不只是包含数据解析部分,还有很多逻辑控制部分,它的响应头和和请求头中有很多控制字段,例如缓存相关的EtagLast-Modified等,和数据压缩相关的Content-EncodingAccept-Encoding等。系统的网络框架实现了这些控制字段的逻辑,让用户可以开箱即用。

修改HTTP底层实现,完成自有需求。

对HTTP上层的修改是很常见的,例如YTKNewwork就在HTTP协议之上,添加了自定义缓存逻辑,可以通过cacheTimeInSeconds方法来控制缓存时间。但是对HTTP底层的修改却比较少见,我对这部分的了解,是基于一个特殊需求。

我们都知道手机可以通过WiFi或者蜂窝网络通道来收发数据,一般情况下,同时连接WiFi和蜂窝网络时,路由会让流量只走WiFi通道。但是对于一些WiFi连接工具软件来讲,需要在无法上网的WiFi下进行数据获取,以满足WiFi认证上网的需求,这种情况下蜂窝网络是可以访问网络的,那么可以让HTTP请求不走默认的WiFi通道,通过蜂窝网络来请求数据吗?上层的HTTP网络框架是没有这个功能的,但是底层的socket框架却提供这个功能,它可以让数据无视路由,从特定接口收发。我们完全可以在socket之上,自己实现HTTP协议中request,response的解析和逻辑处理,以达成这个功能的支持。当然对HTTP协议的全量支持是无法承受的开发成本,但是满足自我需求的简单实现还是可以的。我把这功能封装成了一个框架:XXSocketReqeust,使用方式如下:

1
2
3
4
5
6
7
8
_manager = [[XXSocketRequestManager alloc] init];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:[NSURL URLWithString:@"https://www.baidu.com"]];
/// 使用XXNetworkInterfaceCellular,这个HTTP请求会无视路由,强制走蜂窝网络通道进行请求。
XXSocketDataTask *task = [_manager dataTaskWithRequest:request viaInterface:XXNetworkInterfaceCellular completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) {
NSLog(@"error is :%@\n response is %@", error, response);
NSLog(@"responseObject: %@", [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding]);
}];
[task start];

感兴趣的同学可以下载看看。

后记

很多时候网络协议是高冷的,通用的网络协议为了通用和满足各种需求,是非常复杂的。但是我们完全可以针对自己的业务自制协议,或者对协议进行魔改,以满足自我的需求,这其中的难度并没有你想象中的那么高。

参考资料