http协议札记

对http协议的一些重要知识点的梳理,快速掌握对应的知识点

url的构成

URL(Uniform Resource Locator) 中文称为统一资源定位符,如同网络上的门牌,用于描述一个网络上的资源。

基本格式:schema://host[:port#]/path/.../[?query-string][#anchor]

scheme 传输协议: 指定使用的协议(例如:http, https, ftp),这里就是http或者https,大多数网页浏览器不要求用户输入网页中“http(s)://”的部分,因为绝大多数网页内容是超文本传输协议文件。

host 服务器: HTTP服务器的IP地址或者域名,一般人访问网站首页通常只要输入域名部分。

port 端口号: HTTP服务器的默认端口是80,这种情况下端口号可以省略。如果使用了别的端口,必须指明。

path 路径: 访问资源的路径,对应后端路由控制部分代码index.js中定义的路径

query-string 查询字符串: 发送给http服务器的数据,用get请求发送的数据会在这一部分显示,url中?后面的部分。例如:url?name=zzl&age=10。以“?”字符为起点,每个参数以“&”隔开,再以“=”分开参数名称与数据。

anchor 锚: 锚,用来定位到页面中的特定地方。例如:url#title-1。以“#”字符为起点,后面为其值,可以对应页面中某元素的id或name属性。

思考题:写一个函数,方便的获取当前url的各个部分。

http请求的内容

http请求包含“请求(request)”和“响应(response)”两个部分,分别包含头和内容,即:请求头、请求体、响应头、响应体。

request消息的结构

Method | path to resource | http/version number
-----------------------------------------------
Header name 1: value 
Header name 2: value
  ...
-----------------------------------------------

-----------------------------------------------
request body(optional)

可以看到http request分为三个部分:

response消息的结构

http/version number | status code | message
-----------------------------------------------
Header name 1: value
header name 2: value
...
-----------------------------------------------

-----------------------------------------------
response body(optional)

可以看到http response也分为三个部分:

结构上看其实他们是一样的,只不过内容不同而已。请求头和响应头不同,请求发出的表示客户端希望服务端怎么做,而响应头则表示服务端希望客户端如何做。拿Content-type来说,请求头里面可能发出的content-type是aplication/json,而响应头里面返回的可能是application/atom,这是怎么回事呢?这种情况一般是指客户端(比如手机app)希望服务端返回给自己的是json数据,但是服务端这个资源没有对应的json数据,所以就返回了atom数据给客户端,那么客户端在得到这个头信息之后,应该做数据格式转换或其他操作。

TCP/IP

当你在浏览器中输入一个url,到你看到页面的这个过程中,背后都发生了什么?回答这个问题需要你了解TCP/IP网络架构。这个网络通信架构中,网络通信被分为五层模型,分别是:应用层、传输层、网络层、数据链路层、物理层。

应用层

简单的说,应用层是指应用程序怎么处理网络请求和响应的。这里的应用程序包括一整套的相关程序,比如我们这个场景中的浏览器使用http、https进行网络请求,http、ftp、SSH、DNS就属于这一层。

当一个应用程序,比如浏览器,按照应用层的协议(http)处理数据之后,这些数据就会被送往下一层。

传输层

传输层是指当数据经过应用层之后,对数据发向哪里进行处理的阶段。简单的说,就是确定端口。一般而言,一个应用程序运行的时候,会占用一个端口,它发出数据时,会从这个端口发出去,接收数据时(比如数据回来),也从这个端口进来。这里可以举一个例子,就是httpd的80端口,当一个请求过来的时候,请求数据从80端口进来,完成之后又从这里出去。传输层的主要作用就是确定数据由当前系统的哪个端口出去,被送到另外的哪一个端口(可能是本机,也可能是服务器的端口),进来的数据根据端口判断应该丢给哪个程序去处理。

端口号从0到65535,其中0-1023都被系统占用,你新开一个程序的话,会在后面的端口中随机分配一个。但是有些应用程序允许你强制给它安排端口(我们熟悉的服务端程序都是这样)。

TCP和UDP这两种协议就是传输层的协议。当应用层的数据进入传输层之后,被传输层处理,按照TCP协议处理后的数据的单元是“数据段”,而按UDP协议处理后的数据的单元叫“数据报”,也就我们常听说的“报头”里的“报”。

应用层的协议是基于传输层的协议的(实际上,上层的协议都要基于下层协议),比如:

运行在TCP协议上的协议:

  • HTTP(Hypertext Transfer Protocol,超文本传输协议),主要用于普通浏览。
  • HTTPS(Hypertext Transfer Protocol over Secure Socket Layer, or HTTP over SSL,安全超文本传输协议),HTTP协议的安全版本。
  • FTP(File Transfer Protocol,文件传输协议),由名知义,用于文件传输。
  • POP3(Post Office Protocol, version 3,邮局协议),收邮件用。
  • SMTP(Simple Mail Transfer Protocol,简单邮件传输协议),用来发送电子邮件。
  • TELNET(Teletype over the Network,网络电传),通过一个终端(terminal)登陆到网络。
  • SSH(Secure Shell,用于替代安全性差的TELNET),用于加密安全登陆用。

运行在UDP协议上的协议:

  • BOOTP(Boot Protocol,启动协议),应用于无盘设备。
  • NTP(Network Time Protocol,网络时间协议),用于网络同步。
  • DHCP(Dynamic Host Configuration Protocol,动态主机配置协议),动态配置IP地址。

其他:

  • DNS(Domain Name Service,域名服务),用于完成地址查找,邮件转发等工作(运行在TCP和UDP协议上)。
  • ECHO(Echo Protocol,回绕协议),用于查错及测量应答时间(运行在TCP和UDP协议上)。
  • SNMP(Simple Network Management Protocol,简单网络管理协议),用于网络信息的收集和网络管理。
  • ARP(Address Resolution Protocol,地址解析协议),用于动态解析以太网硬件的地址。

这些协议我们还算是比较熟悉。

网络层

网络层简单的理解,就是机器与机器之间的互联。经过传输层的数据能够确定该数据将交给哪个端口来处理,但是并不知道将交给哪一台主机(也可能是自身)来处理。网络传输的概念发生在这一层。

构建起网络层的重要协议就是IP地址。目前广泛采用的IP协议是IPv4,下一代IP协议就是我们经常听到的IPv6. 很明显TCP是基于IP协议的,也就是说TCP是为IP服务的,TCP/IP组合保证了数据传输的可靠性。而UDP则不一定依赖于IP,所以基于IP的网络中UDP的数据并不完全可靠,但是性能上更快。

经由网络层协议处理过的数据的单元结构被称为“数据包”,这些数据包非常清晰自己应该奔向哪一台主机。路由协议和网址解析协议都属于这一层。

数据链路层

前面三层都是在一台电脑上的软件完成的,并没有涉及到硬件。而数据链路层,简单的说,就是要确定网卡(及其驱动程序)在数据传输中的可靠性。当数据经过网络层处理后,就将进入真正的网际传输阶段,但由虚拟的数据转换为网际线路中传输的信号过程中,必须靠网卡进行加工。网卡把来自不同网络层处理好的数据,统一转换为符合数据链路层协议的脉冲信号。

网卡具有唯一的MAC地址,数据能够在不同的实体电脑之间传输,必须依靠这个MAC地址进行识别,经处理后的数据可以通过实体线路找到要去的网卡,因此路由器设备是基于这一层开发的。

数据在这一层的单元结构被称为“数据帧”。

物理层

物理层则是基于实体的传输介质协议层,包括光纤、双绞线、无线电波等。虽然已经是实体了,但是它们之间仍然必须遵循一些网络协议,特别是无线电波(例如WIFI)。数据经过物理层处理之后,就成为真正的点脉冲,在这些介质中传播。

数据在这一层的单元结构被称为“比特”。

TCP三次握手和四次挥手

所谓“三次握手”是指在一个连接的建立阶段,客户端和服务端有三次交互,因为是“建立”连接,所以叫“握手”。所谓“四次挥手”是指在一个连接的释放阶段,客户端和服务端有四次交互,因为是“释放”所以叫“挥手”。

TCP三次握手和四次挥手

一个TCP链接在握手阶段,每次握手都需要传送“包”,这个包包含了发送方的信息,这个信息用于确认。这里面主要指SYN标志位和ACK标志。在第一次握手时,由客户端发送SYN标识,第二次握手,由服务端发送SYN+ACK标识,第三次握手由客户端发送ACK标识。

TCP三次握手

在服务端接收到第三次握手的信息之后,将建立真正的连接,数据传输才会进行。

释放一个连接的时候,既可以是客户端发起,也可以是服务端发起,但任何一方发起,都需要先发送一个FIN标识,另一方回复一个ACK标识,这表示另一方答应断开连接,但是这并不表示连接已经断开,还需要原来回复ACK的那一方再发起一个FIN,对方再回复一个ACK,才表示双方的同意断开连接,这样这个连接才正式断开。

TCP四次挥手

看上去两问两答更简单,但其实更复杂。当一方发送FIN提出“分手”之后,另一方即使返回ACK,也并不代表断开连接,它可能还有自己的数据要发送给对方,所以迟迟不肯发出FIN这个“分手”同意书。这种状态被称为等待方的“FIN_WAIT”。当双方都受到ACK之后,事情还没完,发起释放请求的一方还处于“TIME_WAIT”的状态,需要等一段时间还没有任何对方的消息,才会真正断开。在WAIT状态,很有可能双方会重新连接传输数据。

为什么不能用两次握手进行连接?

三次握手是为了确保双方相互发送数据是可以正常进行的,是发送数据的准备工作。虽然是三次握手,但是实际上完成的是两问两答,只不过服务端把自己的问蕴含在对第一次握手的回答中,只有服务端的问也被客户端答之后,才能确保双方都是可靠的。如果只有两次握手,那么起码服务端无法获得客户端是不是正常的,那谁知道客户端会不会存在问题,无法正常收发数据呢?因此,客户端在收到服务端的SYN之后,也应该回复ACK,才能保证双方通信正常。

但是,正是由于这一点,带来了SYN攻击威胁。SYN实际上就是模拟两次握手,即客户端发送大量SYN(基于一大堆虚假IP),服务端对每个SYN都进行回复ACK,但是服务端发出的SYN这些虚假的IP根本无法回复ACK,也就是第三次握手丢失,导致服务端一直等待这些回复,从而其他的连接请求处于排队状态,造成网络拥堵。

既然如此,为什么释放连接不用三次挥手?

既然三次握手解决了两问两答的问题,为什么释放连接的时候也这样做?释放连接的时候还要考虑数据有没有传完。用http请求打比方,当一个请求发出时,浏览器会把请求头传输到服务器端,当获得服务器的一个状态码时,就可以发出FIN断开连接。但是这是浏览器一厢情愿的做法,服务器收到这个FIN时会予以回复ACK,但服务器还在把数据传输给浏览器,所以自己不会发出FIN,直到响应信息被浏览器接收之后,服务器才会发出自己的FIN,等待浏览器回复ACK予以确认。

上面非常好的解释了为什么不能三次挥手。你可能会问,为什么不等到服务器传输数据完之后再一起把FIN和ACK发回去?因为当浏览器发出FIN之后,如果没有立即收到ACK,可以有两种选择,一种是认为服务器掉线了,所以浏览器应该断开链接,另一种是认为网络出现震荡,所以应该再发送一个FIN。第二种情况相对可取一些,但仍然没有四次挥手来得直接。

KeepAlive

TCP的KeepAlive和HTTP的Keep-Alive有所区别。在三次握手和四次挥手之间,TCP层之上都在发生什么呢?当然是HTTP发送请求和接收数据,也就是request和response,这个可以在浏览器的开发工具中可以看到。

一般来说,建立一次HTTP连接,常常的做法是先建立TCP连接,TCP连接之后,在它之上实现一次request和response之后,就释放掉,断开TCP连接。而如果很多HTTP请求一次性发送,那么就需要不断的建立新的TCP连接,这就很有可能导致后面的连接需要等待。KeepAlive可以让多个HTTP请求共享一个TCP连接。

具体的解释,可以阅读这篇文章。简单总结一下,就是当一个TCP连接建立之后,不要马上释放掉,这样就可以反复利用这个TCP连接来实现HTTP请求。这样就可以节省建立和断开TCP的消耗,提高了http的速度。

而HTTP的Connection: Keep-Alive这个请求头的作用,是通过代码的形式,在HTTP请求头中告知浏览器,当前HTTP请求建立的TCP连接要保持KeepAlive状态,这样HTTP就可以利用这一个TCP连接多次传输数据。HTTP1.1版本之后就默认开启了这个选项,所以不需要在你的代码中特意强调。

Connection: Keep-Alive并不能保证你的TCP连接是一直开启的,TCP连接是否KeepAlive决定于很多因素。因此,在http中使用Connection: Keep-Alive其实没有决定性的意义。

一个http请求背后的过程

http请求的数据将经过上述网络层完成整个数据生命周期。当用户访问一个url时:

应用层:由浏览器根据http协议构建一个基于该url请求的GET请求。
传输层:该请求根据TCP协议将被发送到服务器的80端口(url中没有端口的情况下),解析域名所对应的IP地址。
网络层:发送到服务器,等待服务器响应。
数据链路层和物理层:发送过程中依据网速快慢进行等待。
网络层:本机通过IP协议接收到服务器返回的响应。
传输层:浏览器通过TCP协议获取到响应的信息。
应用层:通过http协议解析数据得到一个响应内容。

http状态码

在上面的过程中,最后一步浏览器“得到响应内容”中,包含了一个http状态码。状态码是http协议中对当前请求状态的规定。它由数字表示:

1xx:请求处理过程中,还没有处理结束,需要等待。
2xx:请求处理成功。
3xx:请求被重定向到另外一个地址进行处理。
4xx:请求处理失败。
5xx:服务器内部错误(程序报错)。

有一些状态码需要稍微留意一下,因为经常用到:200,201(创建成功),202(异步处理时,表示当前还在计算,你需要再次请求以获得结果),301(永久重定向),302(临时性重定向),304(内容没变,返回的消息体中不包含内容,请使用客户端本地数据),401(授权、认证失败)、403(禁止访问)、404(该资源不存在)、406(无法满足请求头的要求,比如客户端请求json数据,但是服务端没有),500(服务器错误,一般是程序代码出错)、502(网关错误)、503(服务器维修或过载)、504(代理超时)。

缓存相关

http协议里面有关缓存的,接触最多了是304和cache-control。它们都要涉及两端。

304/Last-Modified

在浏览器发起请求时,如果在请求头中发送一个If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT,服务端就可以接收到这个请求头,并且根据这个请求头信息与服务器上的资源修改时间进行对比,如果发现在这个时间之后,资源被修改过了,就返回200状态码,并且把资源信息也返回,但是如果发现资源没有被修改过,则返回304状态码,并且不返回资源的内容,浏览器在接收到304状态码之后,就会直接使用本地的缓存,传输的内容小,而且使用本地缓存,所以打开速度就很快。

但是浏览器的请求头中的If-Modified-Since值是从哪里来的呢?是服务器在之前的请求中返回了一个Last-Modified响应头。这时返回的状态码可以是200,也可以是304,但是最主要的是,这个响应头让浏览器知道了这个资源上次更新时间,并且浏览器会把它缓存到本地,并且把Last-Modified的值赋值给本地的If-Modified-Since,下次再请求这个资源时,就会在请求头中加入If-Modified-Since。

Last-Modified只能用于GET或HEADER请求类型中,POST等其他请求用不了。

If-None-Match和Etag也具有等同的功效。只不过Etag返回的是一个标识码,而不是时间,当浏览器发送If-None-Match的值和这个资源的Etag值不同时,就表示资源已经更新了。

Cache-Control/Expires

这两个都是响应头,浏览器第一次访问资源的时候,如果服务器的响应头中有这两个其中之一,就会把它们记录在本地,下次再访问这个资源的时候,会先在本地进行时间上的对比,如果发现本地时间在响应头给出的过期时间之前,就不再发出真实的http请求,直接使用本地的缓存,在chrome的console中会看到200和from cache的字样。

Expires的格式是Expires: Thu, 10 Dec 2015 23:21:37 GMT,Cache-Control的格式是Cache-Control: max-age=3600,所以两个不一样,前者是时间戳(过期时间点),后者是时间长度(秒)。更大的不同是,Cache-Control仅支持HTTP1.1以上的版本,所以一般情况下,会具体指明响应是遵循HTTP1.1版本时才单独使用Cache-Control,否则会考虑把它们俩混用。

Expires: Thu, 10 Dec 2015 23:21:37 GMT
Cache-Control: max-age=3600

但是因为Expires是时间点的形式,它可能和本地时间存在时间差,所以并不可靠,而使用max-age则是规定一个时长,跟时间差没有关系,更靠谱一些。所以能用Cache-Control的情况下尽量用max-age。

对比和混用

上面两种缓存机制都经常被用到,但是他们之间有区别。Cache-Control是强制缓存,也就是说缓存之后不会发出真实的网络请求,速度上更快,而Last-Modified是协商缓存,是会发出真实的网络请求的,只不过在交互后,如果得到304的话,才使用缓存。现代机器性能都很好,一般这两种缓存在时间上不会给人造成太大的差距感。

但是他们都有不足,Cache-Control是时间限制,只要过了这个限制,不管资源是不是真的更新了,都会完全重新再请求一遍资源,这次会真的从服务端获取资源内容,速度就会慢一些,虽然再下一次就会使用新的缓存。而Last-Modified机制会每次都和服务器交互,如果遇到网络问题,就会导致迟迟得不到304。

比较好的办法是将这两种混用,第一次请求发生的时候,既按照Cache-Control的机制强制缓存,也在本地存上一个If-Modified-Since。当Cache-Control机制到期之后,使用If-Modified-Since去请求,得到一个304,使用协商缓存,并同时更新Cache-Control的缓存时长:

<?php

/**
 * $expires number 过期时间,秒
 * $modified string 资源的更新时间,D, d M Y H:i:s GMT时间戳,作为Last-Modified的值
 */
function httpCache($expires = 3600, $modified = false) {
    $currentTime = time();
    $currentGMT = gmdate("D, d M Y H:i:s") . " GMT";

    header("Cache-Control: max-age=$expires");
    header("Expires: " . gmdate("D, d M Y H:i:s", $currentTime + $expire) . " GMT");

    if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
        $lastModified = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
        // 如果没有传入$modified,就拿expires和当前时间进行对比,给10秒作为网络延时,但是这个操作会比较危险,很有可能永远都不会刷新缓存了
        if(!$modified && strtotime($lastModified) + $expires + 10 >= $currentTime) {
            header("Last-Modified: $currentGMT", true, 304);
            exit;
        }
        // 如果浏览器端记录的上次更新时间和服务器端提供的上次更新时间相同
        else if($lastModified == $modified) {
            header("Last-Modified: $modified", true, 304);
            exit;
        }
    }

    header("Last-Modified: " . ($modified ? $modified : $currentGMT));
}

Cookie和Session

Cookie是由浏览器保存起来的小段文件,Session和Cookie类似,但是是保存在服务端。

对于Cookie而言,当用户访问一个网站时,服务器的响应头中如果存在一个Set-Cookie,浏览器就会根据它的值创建cookie。而在浏览器第二次访问这个网站时,就会在请求头中携带一个Cookie字段,并且把对应的cookie内容发送过去。

由于cookie是保存在本地电脑上的文件,所以用户自己可以通过找到浏览器的cookie保存目录,打开cookie查看cookie的内容。其他软件也可以找到这些cookie文件,所以cookie常常被认为是不安全的。

而session是保存在服务端的。当用户访问一个网站时,由服务端的程序生成一个session,这个session文件被放在服务端对应的目录下,网站的管理员虽然可以看到,但一般不会主动去使用,都是让网站的程序去读取并使用。一个用户访问网站,他就会产生一个session,但是怎么确定这个session是这个用户的呢?有两种形式,一种是把这个session对应的一个唯一标识通过cookie的形式保存在浏览器端,浏览器下次访问的时候,会把这个cookie也发过去,服务端会自动去读取这个标识对应的session;另一种是通过url传递这个唯一标识,这样就不用依靠cookie。

session不是http协议的内容,所以无法通过浏览器直接使用session,因此才有了上面那两种形式。Javascript中也可以使用document.cookie实现cookie功能,但是这和http协议层面的cookie不同,而且它们产生的过程也不同,所以不能通过document.cookie获取到由http请求产生的cookie。

了解更加细节的cookie参考这篇文章

https

前面都是再讲http协议,而https是什么呢?https其实不是协议,而是应用,http协议传输数据会存在一些问题:

而为了解决这种安全性问题,在http和TCP之间增加一道SSL(Secure Socket Layer,安全套接层)协议的数据处理。阅读前面你已经知道,http是属于应用层的,数据处理之后会丢给传输层的TCP进行处理。但是为了安全,在这中间增加一道SSL协议的处理,也就对数据进行了加密。当数据回来时,TCP也会将数据丢给SSL协议处理,处理完再交回给http协议处理。因此,我们在访问一个http网站和一个https网站时,虽然使用了相同的url但是其实可以采用不同的应用程序来进行处理,因为http和https是两个应用层的应用,不得不使用不同的端口来收发数据。

TLS(Transport Layer Security,传输层安全协议)和SSL是同一类协议,也是用于加密的,即可以用TLS代替SSL。

https请求握手的大致流程如下:

使用https加密http请求数据,会消耗cpu和系统资源,因此打开速度上会慢一些。

http2.0将基于https,也就是说,只有开启https的情况下才能使用http2.0的特性。

如果你觉得本书对你有帮助,通过下方的二维码向我打赏吧,帮助我写出更多有用的内容。

2017-02-13 |