造轮子系列之http协议

作为一个程序猿,对造轮子这事情可以说是情有独钟,几乎程序猿内心都存在一个梦想是去将开源的技术都实现一遍,所有从本篇开始,我会开一个造轮子系列。



前言

首先,看看这个,想必大家对下面这种简历看得比较多了吧?

  • 精通JAVA,Python,熟练掌握C++
  • 精通Redis,Memcached,Mysql
  • 精通Nginx配置,模块开发
  • 精通Kafka,ActiveMQ 等消息队列
  • 精通常用数据结构和算法
  • 精通网络编程,多线程编程技术,高性能服务器技术
  • 精通tcp/ip协议栈,熟悉内核网络子系统代码
  • 精通nginx代码及模块开发

上面每一条都涉及好多轮子,每一个都是精通,如果真能做到。那这个人可以说是码农中的战斗机。

那我们现在目标就是去做这个战斗机。而这个方法,就是自己去造轮子,造的目的不是为了在项目中使用自己造的轮子,而是为了去了解轮子的构造,然后自己动手去体会造轮子的过程。


后端的轮子们

说起后端的轮子们,大家都可以说出一大串来,我们大致来数一数啊。

  • 抗在最前面的:LVS,F5,HAProxy这类负载均衡
  • 接下来有Nginx,Apache,Lighttpd这类Http服务
  • http服务后则是各种容器,部署着我们的业务逻辑
  • 存储这边有Redis,Memcached这一类KV存储器和缓存系统
  • 如果是多机部署,肯定还有Kafka,ActiveMQ这种负责解耦的消息队列
  • 为了实现集群通信,肯定少不了Thrift这种RPC框架和Protobuf这种序列化技术
  • 再高端点,到了分布式领域了,就是更多的轮子了。。zookeeper、raft等等
  • 还有大数据系列hadoop。spark。。。。。

本文主要讲http协议。

正文分割线


我们都知道http是基于tcp之上的,那我们现在就自己基于tcp来实现一个最小的http服务,功能非常简单:

  • 返回输入参数

先来看请求格式:

造轮子系列之http协议

http的报文大概分为3部分:

  • 请求行
  • 首部
  • 正文部分

此处请求行是格式是固定的,

先写代码来看看的:

造轮子系列之http协议

造轮子系列之http协议

可以看到我们读取的到数据是如上,我们可以看到格式上是符合的。

Ps:上面这个代码有个小问题,因为tcp连接是字节流的,我们通过readAll方法从连接中读取数据的是,只要浏览器上不主动断开,会一直阻塞在readaALL上。。。

上面我们将收到的数据稍微整理下

  • 请求行
  • POST / HPPP/1.1\r\n
  • 格式:方法 space URL space 版本 cr lf
  • 首部
  • Host: 127.0.0.1:8080
  • Content-length: 0
  • 格式:首部字段名 space 字段值 cr lf
  • 正文部分
  • 此处为空

上面首部中Content-length: 0可以说是非常关键,他告诉了我们应该要在两个\r\n后继续读取多少字节。

下面我们开始来写解析代码,先是解析文件头

造轮子系列之http协议

然后我们再解析首部

造轮子系列之http协议

解析完后,我们在写返回值,返回报文的格式放下:

造轮子系列之http协议

下面是返回值的代码:

造轮子系列之http协议

完整的例子可以看GitHub上代码,欢迎star

https://github.com/zhuanxuhit/go-in-practice/tree/master/wheel/http/v1

我们有了第一版http轮子后,我们能和前面介绍的轮子系列:rpc联系起来,在rpc系列中,我们讲了设计通信协议来传递消息,此处http是通过头部的url+method的方法来表示我要调用服务端哪个方法,然后分割符是使用 \r\n

,连续两个\r\n表示后续是消息体,为了高速我们消息体的大小和格式,在header中必须指明content-type和content-length,这些都是在我们在实现http协议的时候遵循的。

那现在写完最初版代码,我们回过头总结下我们之前做的rpc轮子,数据编码采用了protobuf,然后基于tcp自己定义了一套消息协议,其实做的事情跟http/1.1是一样的,我们完全可以在http通信的时候,将content-type设置为protobuf,然后通信双方双方能够编解码即可。

在实现过程中,我们发现如果用http1.1作为通信协议,有什么问题呢?

  1. 每次传输都要完整的http头,浪费带宽
  2. 每次一个http请求,一个request,response都要独占一条tcp连接,不然不知道response对应哪个request,影响实时性和并发性

那上面这两点都是要解决的问题,在http2.0中都有相应的方案

  1. 针对每次都需要传输http头,通信双方建立索引,后续传输只用索引
  2. 针对连接占用,一条连接只能同时有一个请求-响应,http2.0启动了多路复用,即允许一个连接同时发起多个请求

那怎么能做到一个连接同时发起多个请求呢?通信双方就必须对每个请求进行编码,这样不同的响应就能和请求对应上了。

具体可以看两张图:

造轮子系列之http协议

HTTP 2.0 其实是将三个请求变成三个流,将数据分成帧,乱序发送到一个tcp连接中

造轮子系列之http协议

通过stream对不同请求进行区分,然后在将一个消息拆分为多个帧进行发送。

那http2.0后,还能不能更快了呢?于是就有了QUIC协议,这个协议肯定是为了解决http2.0的某些问题的。

  1. 自定义连接机制:tcp连接三次握手慢,由于在移动端,由于网络从wifi到移动网络切换时,必定会导致连接断开重连,再次需要3次握手,那我们就自定义连接机制,原先tcp一条连接是由4元素组成:分别是源 IP、源端口、目的 IP、目的端口,现在以一个64位随机数来作为连接标志,断开了也没事,重新建立连接不需要3次握手了。
  2. 自定义重传机制:tcp是可靠连接,当前面的数据编号没有收到的时候,后面的数据即使收到了,也不会得到确认,这就必须要重传
造轮子系列之http协议

重传有个测不准问题,左边是1.1,我们发现重发100编号的时候,如果后续收到应答101,我们不知道这个是针对第一次100的应答还是第二次重传100的应答,http2.0则定义了每次发送数据,编号都需要增加,然后通过offset来标明数据的前后续关系。

  1. 无阻塞多路复用:因为tcp是面向字节流的可靠连接,所以数据之间是有依赖的,因此为了减少依赖,让不同流之间真的能够独立,可以采用udp
  2. 自定义流量控制:tcp的流量控制是通过滑动窗口协议,udp也是滑动窗口,而且是每个stream都有自己的窗口。

总结

首先本文基于tcp自己实现了http1.1的协议,实现中发现这个通信协议和我们之前轮子系列文章rpc都是消息协议,只是对消息体的编码格式不同而已。

接着我们在自己写的过程中发现了http1.1的种种问题,针对这些问题有了http2.0,继而又有了QUIC。

预告:今天讲完http2.0后,我会接着讲轮子系列:gRpc,这个通信协议使用就是http2.0,欢迎大家关注。

Ps:文章最后关于http2.0和quic的内容主要来自极客时间的趣谈网络协议,写的真的非常好,大家可以去订阅的,当然通过我的邀请码可以有返现的,欢迎加wx: hithangtian

源码地址:https://github.com/zhuanxuhit/go-in-practice/tree/master/wheel/http/v1


分享到:


相關文章: