网络协议-HTTP性能优化(Ajax、WebSocket、HTTP2.0)
从 Ajax 到 WebSocket
背景
在建立 HTTP 标准规范的时候,设计者的初衷主要是想把 HTTP 当做传输静态 HTML 文档的协议,但是随着互联网的发展,Web 应用的用途更加多样性,逐渐诞生了电商网站(如淘宝、亚马逊)、社交网络(如Facebook、Twitter)等功能更加复杂的应用,这些网站的功能单纯靠静态 HTML 显然是实现不了的,因此又产生了通过 CGI 将 Web 服务器与后台动态应用连接起来,从而通过后台脚本语言实现的应用驱动网站功能,这些脚本语言包括 PHP、Python、Ruby、Node、JSP、ASP 等,通过这种方式虽然解决了 Web 应用的功能扩展问题,但是 HTTP 协议本身的限制和性能问题却没有得到有效解决。
HTTP 功能和性能上的问题虽然可以通过创建一套新的协议来彻底解决,但是目前基于 HTTP 的服务端和客户端应用遍布全球,完全抛弃不太现实,但问题却要解决,因此,诞生了很多基于 HTTP 协议的新技术和新协议来补足 HTTP 协议本身的缺陷。
Ajax
随着网站功能的复杂,对资源实时性的要求也越来越高,但是 HTTP 本身无法做到实时显示服务器端更新的内容,要获取服务器端的最新内容,就得频繁从客户端发起新的请求(比如刷新页面),如果服务器上没有更新,就会造成通信的浪费,而且从用户体验来说也不够友好。
为了解决这个问题,诞生了 Ajax 技术,其全称是 Asynchronous JavaScript And XML,即异步 Javascript 与 XML 技术,它是一种可以有效利用 JavaScript 与 DOM 操作,实现 Web 页面局部刷新,而不用重新加载页面的异步通信技术。其核心技术是一个名为 XMLHttpRequest 的 API, 通过 JavaScript 的调用就可以实现与服务器的通信,以便在已加载成功的页面发起请求,再通过 DOM 操作实现页面的局部刷新,在早期返回的数据格式是 XML,但是随着更加轻量级的 JSON 出现,现在 Ajax 调用多返回 JSON 格式数据,与返回完整 HTML 文档不同,局部刷新返回的数据体量更小。
Ajax 虽好,但是仍然没有从根本上解决 HTTP 的问题,请求还是得从客户端发起,而且客户端也感知不到服务器上资源的更新,如果想要获取某个部分的实时数据,还是得频繁发起 Ajax 请求,造成通信的浪费,只是这个工作不用用户做,可以交给 JavaScript 定时器去做,而且基于 Ajax 获取资源也不会刷新页面,对用户来说,体验上已经好很多。
为了彻底解决实时显示服务端资源的问题,必须有一种机制能够在服务器资源有更新的时候能够将更新实时推送到客户端,而为了实现这种机制,诞生了 WebSocket 技术。
WebSocket
WebSocket 本来是作为 HTML5 的一部分,而现在却变成了一个独立的协议,它是 Web 客户端与服务器之间实现全双工通信的标准。既然是全双工,就意味着不是之前那种只能从客户端向服务器发起请求的单向通信,服务端在必要的时候也可以推送信息到客户端,而不是被动接收客户端请求再返回响应。
一旦客户端与服务器之间建立起了基于 WebSocket 协议的通信连接,之后所有的通信都依靠这个协议进行,双方可以互相发送 JSON、XML、HTML、图片等任意格式的数据。由于 WebSocket 是基于 HTTP 协议的,所以连接的发起方还是客户端,而一旦建立起 WebSocket 连接,不论是服务器还是客户端,都可以直接向对方发送报文。
为了实现 WebSocket 的通信,在 HTTP 连接建立之后,还需要完成一次「握手」的步骤:
1)请求阶段
WebSocket 复用了 HTTP 的握手通道,要建立 WebSocket 通信,需要在连接发起方的 HTTP 请求报文中通过 Upgrade 字段告知服务器通信协议升级到 Websocket,然后通过 Sec-WebSocket-* 扩展字段提供 WebSocket 的协议、版本、键值等信息:
2)响应阶段
对于上述握手请求,服务器会返回 101 Switching Protocols
响应表示协议升级成功:
响应头中 Sec-WebSocket-Accept
字段的值是根据请求头中 Sec-WebSocket-Key
的字段值生成的,两者结合起来用于防止恶意连接和意外连接。
成功握手确立 WebSocket 连接后,后续通信就会使用 WebSocket 数据帧而不是 HTTP 数据帧。下面是 WebSocket 通信的时序图:
WebSocket 协议对应的 scheme 是 ws,如果是加密的 WebSocket 对应的 scheme 是 wss,域名、端口、路径、参数和 HTTP 协议的 URL 一样。
介绍完 WebSocket 的基本原理,给大家介绍 WebSocket 的客户端和服务器简单实现,客户端部分基于 JavaScript 的 WebSocket API 即可,服务器将基于 Swoole 实现。
WebSocket 客户端和服务端的简单实现
WebSocket 复用了 HTTP 协议来实现握手,通过 Upgrade 字段将 HTTP 协议升级到 WebSocket 协议来建立 WebSocket 连接,一旦 WebSocket 连接建立之后,就可以在这个长连接上通过 WebSocket 数据帧进行双向通信,客户端和服务端可以在任何时候向对方发送报文,而不是 HTTP 协议那种服务端只有在客户端发起请求后才能响应,从而解决了在 Web 页面实时显示最新资源的问题。
在本篇分享中学院君将在服务端基于 Swoole 实现简单的 WebSocket 服务器,然后在客户端基于 JavaScript 实现 WebSocket 客户端,通过这个简单的实现加深大家对 WebSocket 通信过程的理解。
WebSocket 服务器
PHP 异步网络通信引擎 Swoole 内置了对 WebSocket 的支持,通过几行 PHP 代码就可以写出一个异步非阻塞多进程的 WebSocket 服务器:
<?php
// 初始化 WebSocket 服务器,在本地监听 8000 端口
$server = new Swoole\WebSocket\Server("localhost", 8000);
// 建立连接时触发
$server->on('open', function (Swoole\WebSocket\Server $server, $request) {
echo "server: handshake success with fd{$request->fd}\n";
});
// 收到消息时触发推送
$server->on('message', function (Swoole\WebSocket\Server $server, $frame) {
echo "receive from {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}\n";
$server->push($frame->fd, "this is server");
});
// 关闭 WebSocket 连接时触发
$server->on('close', function ($ser, $fd) {
echo "client {$fd} closed\n";
});
// 启动 WebSocket 服务器
$server->start();
将这段 PHP 代码保存到 websocket_server.php
文件。
WebSocket 客户端
在客户端,可以通过 JavaScript 调用浏览器内置的 WebSocket API 实现 WebSocket 客户端,实现代码和服务端差不多,无论服务端还是客户端 WebSocket 都是通过事件驱动的,我们在一个 HTML 文档中引入相应的 JavaScript 代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Chat Client</title>
</head>
<body>
<script>
window.onload = function () {
var nick = prompt("Enter your nickname");
var input = document.getElementById("input");
input.focus();
// 初始化客户端套接字并建立连接
var socket = new WebSocket("ws://localhost:8000");
// 连接建立时触发
socket.onopen = function (event) {
console.log("Connection open ...");
}
// 接收到服务端推送时执行
socket.onmessage = function (event) {
var msg = event.data;
var node = document.createTextNode(msg);
var div = document.createElement("div");
div.appendChild(node);
document.body.insertBefore(div, input);
input.scrollIntoView();
};
// 连接关闭时触发
socket.onclose = function (event) {
console.log("Connection closed ...");
}
input.onchange = function () {
var msg = nick + ": " + input.value;
// 将输入框变更信息通过 send 方法发送到服务器
socket.send(msg);
input.value = "";
};
}
</script>
<input id="input" style="width: 100%;">
</body>
</html>
将这个 HTML 文档命名为 websocket_client.html
。在命令行启动 WebSocket 服务器:
php websocket.php
然后在浏览器中访问 websocket_client.html
,首先会提示我们输入昵称:
输入之后点击确定,JavaScript 代码会继续往下执行,让输入框获取焦点,然后初始化 WebSocket 客户端并连接到服务器,这个时候通过开发者工具可以看到 Console 标签页已经输出了连接已建立日志:
这个时候我们在输入框中输入「你好,WebSocket!」并回车,即可触发客户端发送该数据到服务器,服务器接收到消息后会将其显示出来:
同时将「This is server」消息推送给客户端,客户端通过 onmessage
回调函数将获取到的数据显示出来。在开发者工具的 Network->WS
标签页可以查看 WebSocket 通信细节:
看起来,这个过程还是客户端触发服务器执行推送操作,但实际上,在建立连接并获取到这个客户端的唯一标识后,后续服务端资源有更新的情况下,仍然可以通过这个标识主动将更新推送给客户端,而不需要客户端发起拉取请求。WebSocket 服务器和客户端在实际项目中的实现可能会更加复杂,但是基本原理是一致的。
HTTP/2.0 简介
目前主流的 HTTP 通信都是基于 HTTP/1.1 的,而 HTTP/1.1 自 1999 年发布的 RFC2616 之后再未进行过修订,而随着互联网的蓬勃发展,HTTP/1.1 自身所暴露的问题也越来越多,于是负责互联网技术标准的 IETF 组织创建了专门的工作组来推进下一代 HTTP —— HTTP/2.0 的标准化,其首要目标就是解决 HTTP 的性能瓶颈,缩短 Web 页面的加载时间。
HTTP/1.1 的性能瓶颈主要有以下这些:
- 一条连接同时只能发送一个请求;
- 请求只能从客户端发起;
- 请求/响应首部未经压缩就直接发送,首部信息越多延迟越大;
- 每次请求/响应都会发送冗长的首部信息,造成通信的浪费;
为了解决这些问题,HTTP/2.0 会对 HTTP 首部(或者叫做 HTTP 头)进行一定的压缩,将原来每次通信都要携带的大量头信息(键值对)在两端建立一个索引表,对相同的头只发送索引表中的索引。
另外,HTTP/2.0 协议会将一个 TCP 连接切分成多个流,每个流都有自己的 ID,而且流可以是客户端发往服务端,也可以是服务端发往客户端,为了解决并发请求导致响应慢的问题,还可以为流设置优先级。HTTP/2.0 还将所有的传输信息分割为更小的消息和帧,并对它们采用二进制格式编码,常见的帧有 Header 帧,用于传输 HTTP 头信息,并且会开启一个新的流;再就是 Data 帧,用来传输 HTTP 报文实体,多个 Data 帧属于同一个流。
通过这两种机制,HTTP/2.0 的客户端可以将多个请求分到不同的流中,以实现在一个 TCP 连接上处理所有请求,然后将请求内容拆分成帧,进行二进制传输,这些帧可以打散乱序发送,然后根据每个帧首部的流标识符重新组装,并且可以根据优先级,决定优先处理哪个流的数据。
这样一来,HTTP/2.0 成功消除了 HTTP/1.1 的性能瓶颈和限制,减少了 TCP 连接数对服务器性能的影响,同时可以将页面的多个 css、js、 图片等资源通过一个数据链接进行传输,能够加快页面组件的传输速度。
HTTP/2.0 虽然大大增加了并发性,但还是有问题的,为 HTTP/2.0 还是基于 TCP 协议的,TCP 协议在建立连接时有额外的开销,在处理包时有严格的顺序要求,当其中一个数据包遇到问题,TCP 连接需要等待这个包完成重传之后才能继续进行。要解决这些问题只能通过 UDP 协议来实现才可以最大化提升 Web 的性能,不过这就属于另一个话题了。
HTTP/2.0 标准于 2015 年以 RFC7540 正式发表,具体的标准化工作由 Chrome、Opera、Firefox、IE、Safari、Edge 等浏览器提供支持,目前主流浏览器都已经支持了该协议。