前言
在上层应用开发中,HTTP
协议可以说是最常见,使用最频繁的网络协议了。在网上也有非常多的文章进行解读,但是大部分都是讲解HTTP
协议的内容和使用,很少有人讲HTTP
协议是怎么实现的。网络协议可以涉及很大的广度和深度,不是一篇文章就能讲清楚的,我这里更多的是提供一个思路供读者来思考。本篇文章会基于iOS
平台来进行说明,但是并不代表这篇文章只针对iOS
开发,因为协议是跨平台的,其中涉及到的编程思想也是。本文会分四个部分进行讲解:
- 第一部分:数据是如何在网络上进行传输的。这部分主要让你对
网络模型
和各层协议有一个基础的了解,如果您对这部分比较了解,可以直接从第二部分看起。 - 第二部分:
HTTP
协议数据是如何转换为TCP
数据收发的。 - 第三部分:
HTTP
协议中Request
和Response
的解析和相关逻辑处理。 - 第四部分:修改
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
协议中,HTTP
的request
报文和response
报文都包含有header
,TCP
和IP
也都有header
,它们通过层层”套娃”后发送。
现在我们对数据如何通过网络传输稍微有了一个整体的概念,但是细节是不清楚的。从上述内容中,可以看到,HTTP
数据是转换为TCP
数据进行传输,对于传输层
及以下的内容在这里就不做说明,这里主要讲应用层
的HTTP数据
是如何通过传输层
传输的,以及如何解析的。
HTTP
协议数据是如何转换为TCP
数据收发的
一般来讲,各系统都会给用户提供HTTP
网络框架,例如iOS
的NSURLSession
,在系统的HTTP
网络框架之上,开发者社区又会开发出各种易用版本的封装,例如AFNetworking
。对上层开发者来说,HTTP
协议的使用一般就是一个框架封装好的Request
对象,甚至只是一个URL
,使用框架请求完成后,返回一个Response
对象,它的底层实现是隐藏的。
我们都知道,计算机的底层是二进制,数据传输也不例外。要把Request
对象从主机传输到服务器,那么必须把它转换为二进制,那么它是怎么转换的?又是怎么传输的?
HTTP协议是怎么转换成二进制的?
网络框架的Request
对象为了易用性,经过了层层封装,要传输出去,必须将它转换为二进制数据:Request对象 -> 符合HTTP协议的Request字符串 -> 二进制数据
。
HTTP协议中的请求报文:
响应报文:
按照图示请求报文格式,我们可以将Request
对象转换为符合HTTP
协议的字符串并转换为字节流。样例代码如下:
1 | /// 创建NSURLRequest |
调用通过上述代码,我们可以得到request
报文的文本数据。样例:
1 | GET / HTTP/1.1 |
response
报文可以参考request
报文进行分析,为了避免篇幅过长,这里就不再做说明了。
HTTP二进制数据是怎么传输的?
在各操作系统中,通常会为应用程序提供一组应用程序接口,称为套接字接口(socket API),主要作用就是实行进程间通信和网络编程。大白话就是:套接字是用C语言写成的应用程序开发库,它就是一个库。
套接字中的网络套接字,包含有流式套接字(SOCK-STREAM
),它使用TCP
协议来实现字节流的传输。通过socket
框架,将包含HTTP
数据的TCP
字节流发送给服务端,服务端通过socket
框架拿到包含HTTP
数据的TCP
字节流后,根据HTTP
协议进行解析,解析后又被服务端的HTTP网络框架返回,图示如下:
上述图示只包含request
报文部分,并不包含response
报文部分,由于response
报文的数据传输和这个并无太大区别,这里就不再做额外说明了。
HTTP
协议中Request
和Response
的解析和逻辑处理
看到这里,我们对HTTP实现应该有了较明朗的了解,但是这其中还是有一些细节需要补充。
用过Socket
的同学应该都知道,它基于TCP
是流式传输,会有半包
和粘包
问题。一般通过对数据添加Header
来解决这些问题。我们知道,HTTP
数据包含两部分,分别是Header
和Body
,HTTP
协议定义Header
和Body
之间包含两个CRLF
,一个CRLF
是一个回车加一个换行:\r\n
。通过这个标识,我们可以从TCP流
中把HTTP
数据的Header
分离出来。然后再解析出Header
中的Content-Length
字段,它就是body
的长度,读取这个长度的内容,就可以把Body解析出来。
在HTTP/1.1版本,Body的解析还和Transfer-Encoding字段有关,这里就不讨论了。
当然HTTP协议
不只是包含数据解析部分,还有很多逻辑控制部分,它的响应头和和请求头中有很多控制字段,例如缓存相关的Etag
,Last-Modified
等,和数据压缩相关的Content-Encoding
,Accept-Encoding
等。系统的网络框架实现了这些控制字段的逻辑,让用户可以开箱即用。
修改HTTP
底层实现,完成自有需求。
对HTTP上层的修改是很常见的,例如YTKNewwork就在HTTP协议之上,添加了自定义缓存逻辑,可以通过cacheTimeInSeconds
方法来控制缓存时间。但是对HTTP底层的修改却比较少见,我对这部分的了解,是基于一个特殊需求。
我们都知道手机可以通过WiFi
或者蜂窝网络
通道来收发数据,一般情况下,同时连接WiFi和蜂窝网络时,路由会让流量只走WiFi
通道。但是对于一些WiFi连接工具软件来讲,需要在无法上网的WiFi
下进行数据获取,以满足WiFi认证上网的需求,这种情况下蜂窝网络
是可以访问网络的,那么可以让HTTP请求不走默认的WiFi
通道,通过蜂窝网络
来请求数据吗?上层的HTTP网络框架是没有这个功能的,但是底层的socket
框架却提供这个功能,它可以让数据无视路由,从特定接口收发。我们完全可以在socket之上,自己实现HTTP协议中request
,response
的解析和逻辑处理,以达成这个功能的支持。当然对HTTP协议的全量支持是无法承受的开发成本,但是满足自我需求的简单实现还是可以的。我把这功能封装成了一个框架:XXSocketReqeust,使用方式如下:
1 | _manager = [[XXSocketRequestManager alloc] init]; |
感兴趣的同学可以下载看看。
后记
很多时候网络协议是高冷的,通用的网络协议为了通用和满足各种需求,是非常复杂的。但是我们完全可以针对自己的业务自制协议,或者对协议进行魔改,以满足自我的需求,这其中的难度并没有你想象中的那么高。