前言
本文章为网络编程HTTP篇的学习笔记,文章中的图片,文字部分引用小林coding,阿秀的学习笔记,知识星球如有侵权,请联系删除。
HTTP简介
HTTP就是超文本传输协议,超文本就是指文字,图片,视频等混合体,还包括超链接,所以可以从一个超文本跳转到另一个超文本,最常见的超文本就是指HTML,它本身只是纯文字文件,但内部用了很多的标签定义了图片、视频等的链接,经浏览器解释,就呈现出有文字画面的网页了。
HTTP与HTTPS的区别
1、HTTP 是超文本传输协议,信息是明文传输,存在安全风险的问题。HTTPS 则解决 HTTP 不安全的缺陷,在 TCP 和 HTTP 网络层之间加入了 SSL/TLS 安全协议,使得报文能够加密传输。
2、HTTP 连接建立相对简单, TCP 三次握手之后便可进行 HTTP 的报文传输。而 HTTPS 在 TCP 三次握手之后,还需进行 SSL/TLS 的握手过程,才可进入加密报文传输。
3、两者的默认端口不一样,HTTP 默认端口号是 80,HTTPS 默认端口号是 443。
4、HTTPS 协议需要向 CA(证书权威机构)申请数字证书,来保证服务器的身份是可信的。
HTTPS使用混合加密的方式可以保证信息的机密性,解决了窃听的风险,采用对称加密和非对称加密结合的「混合加密」方式:在通信建立前采用非对称加密的方式交换「会话秘钥」,后续就不再使用非对称加密。在通信过程中全部使用对称加密的「会话秘钥」的方式加密明文数据。
使用摘要算法+数字签名的方式为数据生成独一无二的「指纹」,用于校验数据的完整性,解决了篡改的风险。使用时通过哈希函数(摘要算法)计算出内容的哈希值,然后同内容一起传输给对方。对方收到后,先是对内容也计算出一个「指纹」,然后跟发送方发送的「指纹」做一个比较,如果「指纹」相同,说明内容没有被篡改,否则就可以判断出内容被篡改了。但是并不能保证「内容 + 哈希值」不会被中间人替换,因为这里缺少对客户端收到的消息是否来源于服务端的证明。所以还需要使用数字签名算法对内容的哈希值加密,一般使用非对称加密算法通过两个秘钥,通过「私钥加密,公钥解密」的方式,来确认消息的身份。之后为了防止公钥被伪造还会使用数字证书来对身份进行验证。
SSL/TLS协议基本流程
基本流程可以分为三步,分别是客户端向服务器索要并验证服务器的公钥。双方协商生产「会话秘钥」。双方采用「会话秘钥」进行加密通信。TLS握手有四次通信,使用不同的秘钥交换算法,握手流程也会不一样,常用的秘钥秘钥算法有RSA算法和ECDHE算法。
1、首先客户端向服务器发送信息,包括客户端支持的TLS协议版本,客户端生成的随机数Client Random用于会话秘钥的生成,客户端支持的密码套件如RSA加密算法。
2、服务器发出响应,确认TLS协议版本,如果不支持就关闭加密通信,服务器生成随机数Server Random,用于生产会话秘钥,确认密码套件列表如RSA算法,发送服务器的数字证书。
3、客户端再回应,首先从数字证书中取出服务器公钥,使用它加密报文,向服务器发送随机数pre-master key,这个随机数会被服务器公钥加密,加密通信算法改变通知,表示随后的信息都会用会话秘钥加密通信,还有客户端握手结束通知。
服务器和客户端有了这三个随机数(Client Random、Server Random、pre-master key),接着就用双方协商的加密算法,各自生成本次通信的「会话秘钥」。
4、服务器收到客户端的第三个随机数pre-master key,通过协商加密算法,计算出本次通信的会话秘钥,再向客户端发送加密通信算法改变通知,表示之后都会用会话秘钥进行通信,服务器握手结束通知。
TLS在实现上分为握手协议和记录协议两层,TLS握手协议主要负责协商加密算法和生成对称秘钥,后续用这个秘钥保护HTTP数据,记录协议就是负责消息(HTTP 数据)的压缩,加密及数据的认证。
HTTP/1.1到HTTP/3的演变
1、HTTP/1.1相比HTTP/1.0性能上有了改进,使用长连接的方式改善了HTTP/1.0短连接造成的性能开销,支持管道网络传输,只要请求发出,不用等待其回来,就可以发送第二个请求,减少了整体的响应时间。
HTTP/1.1的性能瓶颈:请求响应头未经压缩就发送了首部信息延迟大,只能压缩body部分,每次发送首部造成的浪费较多,服务器是按请求顺序响应的,若是响应慢客户端会一直得不到数据,会造成阻塞,因为HTTP/1.1没有请求优先级控制,并且服务器只能被动的响应。
2、HTTP/2做出了什么优化:HTTP/2的优化是基于HTTPS的加入了TLS/SSL四次握手,对于数据的安全性有所保障,并且进行了头部压缩,如果发送多个同样的请求,协议会消除重复的部分。
使用二进制格式不再向HTTP/1.1里面使用的是纯文本的报文,而是全面采用了二进制格式,头信息和数据体都是二进制,并且童正维帧,分为头信息帧和数据帧,这对机器很友好,增加了数据传输的效率。
支持并发传输,解决了队头阻塞,引出了Stream概念,多个Stream复用在一条TCP连接上,针对不同的 HTTP 请求用独一无二的 Stream ID 来区分,接收端可以通过 Stream ID 有序组装成 HTTP 消息,不同 Stream 的帧是可以乱序发送的,因此可以并发不同的 Stream ,也就是 HTTP/2 可以并行交错地发送请求和响应。
服务推送,服务端可以主动向客户端发送消息,客户端和服务器双方都可以建立Stream, Stream ID 也是有区别的,客户端建立的 Stream 必须是奇数号,而服务器建立的 Stream 必须是偶数号。
HTTP/2的缺陷:HTTP/2并没有完全解决队头阻塞的问题,HTTP/2 是基于 TCP 协议来传输数据的,TCP 是字节流协议,TCP 层必须保证收到的字节数据是完整且连续的,这样内核才会将缓冲区里的数据返回给 HTTP 应用,那么当「前 1 个字节数据」没有到达时,后收到的字节数据只能存放在内核缓冲区里,只有等到这 1 个字节数据到达时,HTTP/2 应用层才能从内核中拿到数据,这就是 HTTP/2 队头阻塞问题。因为一旦发生了丢包现象,就会触发TCP的重传机制,这样TCP连接中的所有的HTTP请求都必须等待这个丢的包被重传回来。
3、HTTP/3做了什么优化:为了解决队头阻塞的问题,HTTP/3把HTTP下层的TCP协议改成了UDP协议,UDP协议不管顺序,不管丢包,不会出现像HTTP/2队头阻塞的问题,但是因为UDP是不可靠协议,所以在应用层实现了一个QUIC协议,可以实现类似TCP的可靠性传输。
QUIC有几个特点:无队头阻塞:当某个流发生丢包时,只会阻塞这个流,其他流不会受到影响,因此不存在队头阻塞问题,QUIC 连接上的多个 Stream 之间并没有依赖,都是独立的。
可以更快的建立连接:对于 HTTP/1 和 HTTP/2 协议,TCP 和 TLS 是分层的,分别属于内核实现的传输层、openssl 库实现的表示层,因此它们难以合并在一起,需要分批次来握手,先 TCP 握手,再 TLS 握手。HTTP/3虽然需要进行QUIC握手,但是这只是为了确认双方的连接ID,但是 HTTP/3 的 QUIC 协议并不是与 TLS 分层,而是 QUIC 内部包含了 TLS,它在自己的帧会携带 TLS 里的“记录”,再加上 QUIC 使用的是 TLS/1.3,因此仅需 1 个 RTT 就可以「同时」完成建立连接与密钥协商,并且在第二次连接的时候,应用数据包可以和 QUIC 握手信息(连接信息 + TLS 信息)一起发送,达到 0-RTT 的效果。
有连接迁移的特点:基于TCP传输协议的HTTP协议,是通过四元组确定TCP连接,所以当ip地址变化(移动设备网络从4G切换到WIFI)就要断开连接并重新连接。而QUIC是通过连接ID来标记两个端点,这时网络变化导致IP变化时只要上下文信息(ID,TLS秘钥)没变,就可以无缝的复用原连接,没有卡顿实现连接迁移。所以, QUIC 是一个在 UDP 之上的伪 TCP + TLS + HTTP/2 的多路复用的协议。
HTTP与RPC的区别
二者都是在应用层上工作的,RPC(Remote Procedure Call)虽然叫做远程过程调用但本身并不算是一种协议而是一种调用方式。
HTTP 主要用于 B/S (服务器(Server)与浏览器(Browser)建立连接并收发数据)架构,而 RPC 更多用于 C/S(客户端(Client)需要跟服务端(Server)建立连接收发消息) 架构。
两者发现服务的方式不同:在HTTP中知道服务的域名,就可以通过 DNS 服务去解析得到它背后的 IP 地址,默认 80 端口。而RPC会有专门的中间服务去保存服务名和IP信息,比如 Consul 或者 Etcd,要访问某个服务,就去这些中间服务去获得 IP 和端口信息。
两者都是通过建立TCP长连接的方式去进行数据交互,但是RPC还会再建立连接池,有利于提升网络请求性能,所以不少编程语言的网络库里都会给 HTTP 加个连接池。
在传输的数据上HTTP会有很多冗余信息,不需要每次都把一些字段传输过来,而RPC定制化程度更高,不需要考虑各种浏览器的重定向跳转之类的,可以采用体积更小的序列化协议去保存结构体数据(比如Protobuf)。
HTTP与WebSocket的区别
TCP 协议本身是全双工的,但我们最常用的 HTTP/1.1,虽然是基于 TCP 的协议,但它是半双工的,对于大部分需要服务器主动推送数据到客户端的场景,都不太友好,因此我们需要使用支持全双工的 WebSocket 协议。
在 HTTP/1.1 里,只要客户端不问,服务端就不答。基于这样的特点,对于登录页面这样的简单场景,可以使用定时轮询或者长轮询的方式实现服务器推送(comet)的效果。
对于客户端和服务端之间需要频繁交互的复杂场景,比如网页游戏,都可以考虑使用 WebSocket 协议。WebSocket 和 socket 几乎没有任何关系,只是叫法相似。
正因为各个浏览器都支持 HTTP协 议,所以 WebSocket 会先利用HTTP协议加上一些特殊的 header 头进行握手升级操作,升级成功后就跟 HTTP 没有任何关系了,之后就用 WebSocket 的数据格式进行收发数据。
HTTP报文
HTTP的报文分为请求报文和响应报文两种,报文必须按照特定的格式生成,才能被浏览器端识别到,浏览器向服务器发送的是请求报文,服务器处理后返回给浏览器的是响应报文。HTTP的请求报文由请求行、请求头部、空行和请求数据四个部分组成,请求行分为两种分别是GET和POST。
GET和POST的区别:最直观的区别就是GET把参数包含在URL中,POST通过request body传递参数;GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留;GET请求在URL中传送的参数是有长度限制。(大多数)浏览器通常都会限制url长度在2K个字节,而(大多数)服务器最多处理64K大小的url;GET产生一个TCP数据包;POST产生两个TCP数据包。对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);而对于POST,浏览器先发送header,服务器响应100(指示信息—表示请求已接收,继续处理)continue,浏览器再发送data,服务器响应200 ok(返回数据);GET没有消息体,POST会有消息体。
GET报文
GET /562f25980001b1b106000338.jpg HTTP/1.1
Host:img.mukewang.com
User-Agent:Mozilla/5.0 (Windows NT 10.0; WOW64)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36
Accept:image/webp,image/*,*/*;q=0.8
Referer:http://www.imooc.com/
Accept-Encoding:gzip, deflate, sdch
Accept-Language:zh-CN,zh;q=0.8
空行
请求数据为空
POST报文
POST / HTTP1.1
Host:www.wrox.com
User-Agent:Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)
Content-Type:application/x-www-form-urlencoded
Content-Length:40
Connection: Keep-Alive
空行
name=Professional%20Ajax&publisher=Wiley
请求行,用来说明请求类型,要访问的资源以及所使用的HTTP版本。
GET说明请求类型为GET,/562f25980001b1b106000338.jpg(URL)为要访问的资源,该行的最后一部分说明使用的是HTTP1.1版本。
请求头部,紧接着请求行(即第一行)之后的部分,用来说明服务器要使用的附加信息。
HOST,给出请求资源所在服务器的域名。
User-Agent,HTTP客户端程序的信息,该信息由你发出请求使用的浏览器来定义,并且在每个请求中自动发送等。
Accept,说明用户代理可处理的媒体类型。
Accept-Encoding,说明用户代理支持的内容编码。
Accept-Language,说明用户代理能够处理的自然语言集。
Content-Type,说明实现主体的媒体类型。
Content-Length,说明实现主体的大小。
Connection,连接管理,可以是Keep-Alive或close。
空行,请求头部后面的空行是必须的即使第四部分的请求数据为空,也必须有空行。
请求数据也叫主体,可以添加任意的其他数据。
响应报文也由四部分组成分别是状态行、消息报头、空行和响应正文。
HTTP/1.1 200 OK
Date: Fri, 22 May 2009 06:07:21 GMT
Content-Type: text/html; charset=UTF-8
空行
<html>
<head></head>
<body>
<!--body goes here-->
</body>
</html>
状态行,由HTTP协议版本号, 状态码, 状态消息 三部分组成。
第一行为状态行,(HTTP/1.1)表明HTTP版本为1.1版本,状态码为200,状态消息为OK。
消息报头,用来说明客户端要使用的一些附加信息。
第二行和第三行为消息报头,Date:生成响应的日期和时间;Content-Type:指定了MIME类型的HTML(text/html),编码类型是UTF-8。
空行,消息报头后面的空行是必须的。
响应正文,服务器返回给客户端的文本信息。空行后面的html部分为响应正文。
在HTTP报文中每一行的数据都由\r\n作为结束字符,所以可以通过查找\r\n将报文拆解成单独的行进行解析。
HTTP的状态码有5种类型分别为:
1xx:指示信息–表示请求已接收,继续处理。
2xx:成功–表示请求正常处理完毕。
200 OK:客户端请求被正常处理。
206 Partial content:客户端进行了范围请求。
3xx:重定向–要完成请求必须进行更进一步的操作。
301 Moved Permanently:永久重定向,该资源已被永久移动到新位置,将来任何对该资源的访问都要使用本响应返回的若干个URI之一。
302 Found:临时重定向,请求的资源现在临时从不同的URI中获得。
4xx:客户端错误–请求有语法错误,服务器无法处理请求。
400 Bad Request:请求报文存在语法错误。
403 Forbidden:请求被服务器拒绝。
404 Not Found:请求不存在,服务器上找不到请求的资源。
5xx:服务器端错误–服务器处理请求出错。
500 Internal Server Error:服务器在执行请求时出现错误。
下面以社长的web服务器项目为例说明一下一个HTTP请求从浏览器发送到解析再到响应的一般处理过程:
整体流程是浏览器端发出HTTP请求报文,服务器端接收该报文并调用process_read对其进行解析,根据解析结果HTTP_CODE,进入相应的逻辑和模块。其中,服务器子线程完成报文的解析与响应;主线程监测读写事件,调用read_once和http_conn::write完成数据的读取与发送。
一般服务器接收请求的工作流程是这样的浏览器端发出http连接请求,主线程创建http对象接收(通过socket连接和epoll的I/O多路复用技术)请求并将所有数据读入对应buffer,将该对象插入任务队列,工作线程(用了半同步/半反应堆模式建立的线程池)从任务队列中取出一个任务进行处理。
处理数据使用主从状态机,从状态机负责读取报文的一行,主状态机负责对该行数据进行解析,主状态机内部调用从状态机,从状态机驱动主状态机。主状态机有三种状态用来表示解析位置,分别解析请求行,解析请求头,解析消息体(仅用于解析POST请求);而从状态机也有三种状态,标识着解析一行的读取状态,分别为完整读取一行,报文语法有误,读取的行不完整。从状态机就是要读取报文的每一行内容,正常读取完成就将最后的\r\0改为\0\0,这样方便主状态机直接读取字符串进行处理,主状态机从一个初始状态分别读取请求行(获取请求方法、目标URL、HTTP版本号,再转换状态),请求头(判断contene-length是否为0是则为post,进行状态转换到处理post,不是则为get,那么报文解析结束,再分析connection字段决定是keep-alive还是close是长连接还是短连接)。这时如果是GET请求已经结束了因为GET请求没有消息体,而如果是POST请求那么还需要继续解析,因为项目中把连接服务器的用户名和密码都放在了POST请求的消息体中,但是消息体不像请求行和请求头末尾会有\r\n它没有任何字符,所以不能使用从状态机,而使用主状态机的状态作为循环入口的条件,解析完数据就继续更改主状态机的状态。
解析完数据之后就对解析结果进行分析将网站根目录和url文件拼接,然后通过stat判断该文件属性。另外,为了提高访问速度,通过mmap进行映射,将普通文件映射到内存逻辑地址。再通过请求报文中解析出的请求资源去进行不同的操作比如GET跳转到欢迎访问界面,POST跳转到注册界面或是登陆界面或是进行注册校验(若是成功就转到登陆界面,失败就到注册失败界面)或是请求图片资源或是请求视频资源。根据响应的状态调用process_write写入响应报文,记录状态行,响应报文长度(用于判断是否发送完数据),连接状态。完成响应报文后注册epollout事件,调用发送函数将报文发送给浏览器端。这里的具体过程是先初始化byte_to_send记录发送行数,通过writev函数循环发送响应报文数据,根据返回值更新已经发送的信息和用于发送的文件的已经发送完信息的位置,如果单次数据发送成功,判断响应报文是否整体发送成功,全部发送成功就取消mmap映射(将普通的文件映射到内存,通常在需要频繁对文件读写时使用,用内存读写代替了IO读写,可以提高效率),并判断是否是长连接,是长连接就初始化http实例会重新注册为读事件,不会关闭连接,如果是短连接就直接关闭连接。若单次发送不成功就判断是否是写缓冲区满了,如果不是因为缓冲区满了而发送失败就取消mmap()映射,关闭连接。如果因为缓冲区满了就先判断一个数据是否发送完,如果发送完了就准备发送第二个,没发送完就还是发送当前的数据,并注册写事件,等待下一次写事件触发(当前缓冲区从不可写变为可写,触发epollout,重新开始写操作),所以在这个期间无法立刻接收同一用户的下一个请求,但是可以保证连接的完整性。