websocket协议-细说WebSocket – Node篇

在上一篇提高到了 web 通信的各种方式,包括 轮询、长连接 以及各种 HTML5 中提到的手段。本文将详细描述 WebSocket协议 在 web通讯 中的实现。

一、WebSocket 协议

1. 概述

websocket协议允许不受信用的客户端代码在可控的网络环境中控制远程主机。该协议包含一个握手和一个基本消息分帧、分层通过TCP。简单点说,通过握手应答之后,建立安全的信息管道,这种方式明显优于前文所说的基于 XMLHttpRequest 的 iframe 数据流和长轮询。该协议包括两个方面,握手链接(handshake)和数据传输(data transfer)。

2. 握手连接

这部分比较简单,就像路上遇到熟人问好。

Client:嘿,大哥,有火没?(烟递了过去)
Server:哈,有啊,来~ (点上)
Client:火柴啊,也行!(烟点上,验证完毕)

握手连接中,client 先主动伸手:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

客户端发了一串 Base64 加密的密钥过去,也就是上面你看到的 Sec-WebSocket-Key。 Server 看到 Client 打招呼之后,悄悄地告诉 Client 他已经知道了,顺便也打个招呼。

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat

Server 返回了 Sec-WebSocket-Accept 这个应答,这个应答内容是通过一定的方式生成的。生成算法是:

mask  = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";  // 这是算法中要用到的固定字符串
accept = base64( sha1( key + mask ) );

key 和 mask 串接之后经过 SHA-1 处理,处理后的数据再经过一次 Base64 加密。分解动作:

1. t = "GhlIHNhbXBsZSBub25jZQ==" + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
   -> "GhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
2. s = sha1(t) 
   -> 0xb3 0x7a 0x4f 0x2c 0xc0 0x62 0x4f 0x16 0x90 0xf6 
      0x46 0x06 0xcf 0x38 0x59 0x45 0xb2 0xbe 0xc4 0xea
3. base64(s) 
   -> "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="

上面 Server 端返回的 HTTP 状态码是 101,如果不是 101 ,那就说明握手一开始就失败了~

下面就来个 demo,跟服务器握个手:

var crypto = require('crypto');

require('net').createServer(function(o){
    var key;
    o.on('data',function(e){
        if(!key){
            // 握手
            // 应答部分,代码先省略
            console.log(e.toString());
        }else{

        };
    });
}).listen(8000);

客户端代码:

var ws=new WebSocket("ws://127.0.0.1:8000");
ws.onerror=function(e){
  console.log(e);
};

上面当然是一串不完整的代码,目的是演示握手过程中,客户端给服务端打招呼。在控制台我们可以看到:

看起来很熟悉吧,其实就是发送了一个 HTTP 请求,这个我们在浏览器的 Network 中也可以看到:

但是 WebSocket协议 并不是 HTTP 协议,刚开始验证的时候借用了 HTTP 的头,连接成功之后的通信就不是 HTTP 了,不信你用 fiddler2 抓包试试,肯定是拿不到的,后面的通信部分是基于 TCP 的连接。

服务器要成功的进行通信,必须有应答,往下看:

//服务器程序
var crypto = require('crypto');
var WS = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
require('net').createServer(function(o){
    var key;
    o.on('data',function(e){
        if(!key){
            //握手
            key = e.toString().match(/Sec-WebSocket-Key: (.+)/)[1];
            key = crypto.createHash('sha1').update(key + WS).digest('base64');
            o.write('HTTP/1.1 101 Switching Protocols\r\n');
            o.write('Upgrade: websocket\r\n');
            o.write('Connection: Upgrade\r\n');
            o.write('Sec-WebSocket-Accept: ' + key + '\r\n');
            o.write('\r\n');
        }else{
            console.log(e);
        };
    });
}).listen(8000);

关于crypto模块,可以看看官方文档,上面的代码应该是很好理解的,服务器应答之后,Client 拿到 Sec-WebSocket-Accept ,然后本地做一次验证,如果验证通过了,就会触发 onopen 函数。

//客户端程序
var ws=new WebSocket("ws://127.0.0.1:8000/");
ws.onopen=function(e){
    console.log("握手成功");
};

可以看到

3. 数据帧格式

官方文档提供了一个结构图

 0                   1                   2                   3
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-------+-+-------------+-------------------------------+
 |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
 |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
 |N|V|V|V|       |S|             |   (if payload len==126/127)   |
 | |1|2|3|       |K|             |                               |
 +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
 |     Extended payload length continued, if payload len == 127  |
 + - - - - - - - - - - - - - - - +-------------------------------+
 |                               |Masking-key, if MASK set to 1  |
 +-------------------------------+-------------------------------+
 | Masking-key (continued)       |          Payload Data         |
 +-------------------------------- - - - - - - - - - - - - - - - +
 :                     Payload Data continued ...                :
 + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
 |                     Payload Data continued ...                |
 +---------------------------------------------------------------+

第一眼瞟到这张图恐怕是要吐血,如果大学修改计算机网络这门课应该不会对这东西陌生,数据传输协议嘛,是需要定义字节长度及相关含义的。

FIN      1bit 表示信息的最后一帧,flag,也就是标记符
RSV 1-3  1bit each 以后备用的 默认都为 0
Opcode   4bit 帧类型,稍后细说
Mask     1bit 掩码,是否加密数据,默认必须置为1 (这里很蛋疼)
Payload  7bit 数据的长度
Masking-key      0 or 4 bit 掩码
Payload data     (x + y) bytes 数据
Extension data   x bytes  扩展数据
Application data y bytes  程序数据
https://tools.ietf.org/html/rfc6455#section-5.2

 Masking-key:  0 or 4 bytes

      All frames sent from the client to the server are masked by a
      32-bit value that is contained within the frame.  This field is
      present if the mask bit is set to 1 and is absent if the mask bit
      is set to 0.  See Section 5.3 for further information on client-
      to-server masking.

每一帧的传输都是遵从这个协议规则的,知道了这个协议,那么解析就不会太难了,下面我就直接拿了次碳酸钴同学的代码。

4. 数据帧的解析和编码

数据帧的解析代码:

function decodeDataFrame(e){
  var i=0,j,s,frame={
    //解析前两个字节的基本数据
    FIN:e[i]>>7,Opcode:e[i++]&15,Mask:e[i]>>7,
    PayloadLength:e[i++]&0x7F
  };
  //处理特殊长度126和127
  if(frame.PayloadLength==126)
    frame.length=(e[i++]<<8)+e[i++];
  if(frame.PayloadLength==127)
    i+=4, //长度一般用四字节的整型,前四个字节通常为长整形留空的
    frame.length=(e[i++]<<24)+(e[i++]<<16)+(e[i++]<<8)+e[i++];
  //判断是否使用掩码
  if(frame.Mask){
    //获取掩码实体
    frame.MaskingKey=[e[i++],e[i++],e[i++],e[i++]];
    //对数据和掩码做异或运算
    for(j=0,s=[];j<frame.PayloadLength;j++)
      s.push(e[i+j]^frame.MaskingKey[j%4]);
  }else s=e.slice(i,frame.PayloadLength); //否则直接使用数据
  //数组转换成缓冲区来使用
  s=new Buffer(s);
  //如果有必要则把缓冲区转换成字符串来使用
  if(frame.Opcode==1)s=s.toString();
  //设置上数据部分
  frame.PayloadData=s;
  //返回数据帧
  return frame;
}

数据帧的编码:

//NodeJS
function encodeDataFrame(e){
  var s=[],o=new Buffer(e.PayloadData),l=o.length;
  //输入第一个字节
  s.push((e.FIN<<7)+e.Opcode);
  //输入第二个字节,判断它的长度并放入相应的后续长度消息
  //永远不使用掩码
  if(l<126)s.push(l);
  else if(l<0x10000)s.push(126,(l&0xFF00)>>2,l&0xFF);
  else s.push(
    127, 0,0,0,0, //8字节数据,前4字节一般没用留空
    (l&0xFF000000)>>6,(l&0xFF0000)>>4,(l&0xFF00)>>2,l&0xFF
  );
  //返回头部分和数据部分的合并缓冲区
  return Buffer.concat([new Buffer(s),o]);
}

有些童鞋可能没有明白,应该解析哪些数据。这的解析任务主要是服务端处理,客户端送过去的数据是二进制流形式的,比如:

var ws = new WebSocket("ws://127.0.0.1:8000/"); 
ws.onopen = function(){ 
  ws.send("握手成功"); 
};

Server 收到的信息是这样的:

一个放在Buffer格式的二进制流。而当我们输出的时候解析这个二进制流:

//服务器程序
var crypto = require('crypto');
var WS = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
require('net').createServer(function(o){
    var key;
    o.on('data',function(e){
        if(!key){
            //握手
            key = e.toString().match(/Sec-WebSocket-Key: (.+)/)[1];
            key = crypto.createHash('sha1').update(key + WS).digest('base64');
            o.write('HTTP/1.1 101 Switching Protocols\r\n');
            o.write('Upgrade: websocket\r\n');
            o.write('Connection: Upgrade\r\n');
            o.write('Sec-WebSocket-Accept: ' + key + '\r\n');
            o.write('\r\n');
        }else{
            // 输出之前解析帧
            console.log(decodeDataFrame(e));
        };
    });
}).listen(8000);

那输出的就是一个帧信息十分清晰的对象了:

5. 连接的控制

上面我买了个关子,提到的Opcode,没有详细说明,官方文档也给了一张表:

 |Opcode  | Meaning                             | Reference |
-+--------+-------------------------------------+-----------|
 | 0      | Continuation Frame                  | RFC 6455  |
-+--------+-------------------------------------+-----------|
 | 1      | Text Frame                          | RFC 6455  |
-+--------+-------------------------------------+-----------|
 | 2      | Binary Frame                        | RFC 6455  |
-+--------+-------------------------------------+-----------|
 | 8      | Connection Close Frame              | RFC 6455  |
-+--------+-------------------------------------+-----------|
 | 9      | Ping Frame                          | RFC 6455  |
-+--------+-------------------------------------+-----------|
 | 10     | Pong Frame                          | RFC 6455  |
-+--------+-------------------------------------+-----------|

decodeDataFrame 解析数据,得到的数据格式是:

{
    FIN: 1,
    Opcode: 1,
    Mask: 1,
    PayloadLength: 4,
    MaskingKey: [ 159, 18, 207, 93 ],
    PayLoadData: '握手成功'
}

那么可以对应上面查看,此帧的作用就是发送文本,为文本帧。因为连接是基于 TCP 的,直接关闭 TCP 连接,这个通道就关闭了,不过 WebSocket 设计的还比较人性化,关闭之前还跟你打一声招呼,在服务器端,可以判断frame的Opcode:

var frame=decodeDataFrame(e);
console.log(frame);
if(frame.Opcode==8){
    o.end(); //断开连接
}

客户端和服务端交互的数据(帧)格式都是一样的,只要客户端发送 ws.close(), 服务器就会执行上面的操作。相反,如果服务器给客户端也发送同样的关闭帧(close frame):

o.write(encodeDataFrame({
    FIN:1,
    Opcode:8,
    PayloadData:buf
}));

客户端就会相应 onclose 函数,这样的交互还算是有规有矩,不容易出错。

二、注意事项

1. WebSocket URIs

很多人可能只知道 ws://text.com:8888,但事实上 websocket 协议地址是可以加 path 和 query 的。

ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ]
wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ]

如果使用的是 wss 协议,那么 URI 将会以安全方式连接。 这里的 wss 大小写不敏感。

2. 协议中”多余”的部分(吐槽)

握手请求中包含Sec-WebSocket-Key字段,明眼人一下就能看出来是websocket连接,而且这个字段的加密方式在服务器也是固定的,如果别人想黑你,不会太难。

再就是那个 MaskingKey 掩码,既然强制加密了(Mask为1表示加密,加密方式就是 MaskingKey 与 PayLoadData 进行异或处理),还有必要让开发者处理这个东西么?直接封装到内部不就行了?

3. 与 TCP 和 HTTP 之间的关系

WebSocket协议是一个基于TCP的协议,就是握手链接的时候跟HTTP相关(发了一个HTTP请求),这个请求被Server切换到(Upgrade)websocket协议了。websocket把 80 端口作为默认websocket连接端口,而websocket的运行使用的是443端口。

三、参考资料

四、特别感谢

再次感谢 次碳酸钴 跟我交流了几个小时 : ),本文部分 node 代码参考自他的博客。

下次将以php作为后台,讲解websocket的相关知识。

细说WebSocket-php篇


文章部分内容纠正:

1、t = “GhlIHNhbXBsZSBub25jZQ==”,串最前面少了一个d
2、console.log(crypto.createHash(‘sha1’).update(‘dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11’).digest(‘sha1’)); //输出:<SlowBuffer b3 7a 4f 2c c0 62 4f 16 90 f6 46 06 cf 38 59 45 b2 be c4 ea>,不像你的,前面带0x,你是怎么弄的?

3、解码部分:if(frame.PayloadLength==126) frame.length=(e[2]<<8) + e[3], i=4; //其中的frame.length,好像应该是frame.Payloadlength。下一行中也是。看了一下,次碳酸钴,那边已经改了。

4、编码部分:s.push(126,(l&0xFF00)>>2,l&0xFF); 其中的>>2,好像应该是>>8。下一行类似。

websocket协议-websocket简史

WebSocket 简介及应用实例

HTML5 的出现,标志着后 Flash 时代各种现代浏览器的集体爆发,也是谨防 Adobe 一家独大的各家厂商们,历经多年各自为战,想换个活法儿并终于达成一定共识后,所积kao累bei的技术的一次集中释放 — 正所谓 “H5 是个筐,什么都可以往里装”。

其中引人瞩目并被广泛支持的一项,就是此次要谈论的 WebSocket 了。本文将尝试说明它被用来解决什么问题,以及与久经沙场的“传统” Socket 又有什么异同等基础问题。

I. 定义及由来

望文而生义,面对 WebSocket 这个名称,web 无需做太多解释,傻傻分不清楚的 socket 看着也是相当的面熟;甭管有没有联系,先来了解一下也无妨:

(1.1) 传统的 Socket API

Socket 往往指的是 TCP/IP 网络环境中的两个连接端,以及为方便此类开发所设计的一组编程 API

如图,英文单词 “socket” 的字面原义是 “孔” 或 “插座”。

作为一个技术用语时,socket 通常取后一种意思,像一个多孔插座。用于描述一个通信链路两端的 IP 地址和端口等,可以用来实现不同设备之间的通信。SocketTCP Socket都是通用的叫法,中文一般习惯性的译作**“套接字”、“TCP套接字”** 等。

…至于为嘛把“插座儿”翻译成“套接字”,好奇的程序猿并不在少数,科考文章在文章底部参考链接中可以找到。

可以将服务端主机想象成一个布满各种插座的房间,每个插座有一个编号,有的插座提供 220 伏交流电,有的提供固定电话信号,有的则提供有线电视节目。客户端软件将插头接入不同编号的插座,就可以得到不同的服务

Socket API 所处的楼层

OSI 模型作为一种概念模型,由国际标准化组织(ISO)提出,一个试图使各种计算机在世界范围内互连为网络的标准框架。我们熟悉的 HTTP、FTP 等协议都工作在最顶端的应用层(Application Layer)。

而 **TCP/IP 协议族(Protocol Suite)**将软件通信过程抽象化为四个抽象层,常被视为是简化的七层OSI模型。当多个层次的协议共同工作时,类似数据结构中的堆栈,因此又被称为 TCP/IP 协议栈(Protocol Stack)

单说 TCP 的话,指的是面向连接的一种传输控制协议。TCP 连接之后,客户端和服务器可以互相发送和接收消息,在客户端或者服务器没有主动断开之前,连接一直存在,故称为长连接。

Socket 其实并不是一个标准的协议,而是应用层与 TCP/IP 协议族通信的中间软件抽象层,它是一组接口,工作位置基本在 OSI 模型会话层(第5层),是为了方便大家直接使用更底层协议(一般是 TCP 或 UDP )而存在的一个抽象层。

在设计模式中,Socket其实就是一个门面(facade)模式,它把复杂的 TCP/IP 协议族隐藏在 Socket API 后面,对用户来说,一组简单的接口就是全部,让 Socket 去组织数据,以符合指定的协议

最早的一套 Socket API 是采用 C 语言实现的,也就成为了 Socket 的事实标准。

常见的 Socket API 实现

一些语言的实现

传统的后端编程语言基本都有 Socket API 的封装;而在 HTML5 出现之前,要想用纯前端技术实现 TCP Socket 的客户端,也基本只有 Java Applet (java.net.Socketjava.net.DatagramSocketjava.net.MulticastSocket) 、Flash (flash.net.Socketflash.net.XMLSocket) 或 Silverlight(System.Net.Sockets) 等可以选择。

下面以 PHP 的 服务器/客户端 实现为例,演示一个最基础的例子:

<?php
//server.php

set_time_limit(0);
$ip = '127.0.0.1';
$port = 1999;
// 创建一个Socket
$sock = socket_create(AF_INET,SOCK_STREAM,SOL_TCP);
// 绑定Socket地址和端口
$ret = socket_bind($sock,$ip,$port);
// 开始监听链接
$ret = socket_listen($sock,4);

$count = 0; //最多接受几次请求后就退出
do {
	// 另一个Socket来处理通信
    if (($msgsock = socket_accept($sock)) >= 0) {        
        // 发到客户端
        $msg ="server: HELLO CLIENTS!\n";
        if (socket_write($msgsock, $msg, strlen($msg))) {
        	echo "发送成功!\n";
        }
        // 获得客户端的输入
        $buf = socket_read($msgsock,8192);
        
        $talkback = "接受成功!内容为:$buf\n";
        echo $talkback;
        
        if(++$count >= 5){
            break;
        };    
    }
    // 关闭sockets
    socket_close($msgsock);
} while (true);
socket_close($sock);
echo "TCP 连接关闭OK\n";
?>
<?php
//client.php

error_reporting(E_ALL);
set_time_limit(0);
$port = 1999;
$ip = "127.0.0.1";

// 创建Socket
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
// 绑定Socket地址和端口
$result = socket_connect($socket, $ip, $port);
if ($result >= 0) echo "TCP 连接OK\n";

$in = "client: HELLO SERVER!\r\n";
if(socket_write($socket, $in, strlen($in))) {
    echo "发送成功!\n";
}
$out = '';
while($out = socket_read($socket, 8192)) {
    echo "接受成功!内容为:",$out;
}

socket_close($socket);
echo "TCP 连接关闭OK\n";
?>

(1.2) HTML5 带来的 WebSocket 协议

WebSockets 为 C/S 两端提供了实时交互通信的能力,允许服务器主动发送信息给客户端,是一种区别于 HTTP 的全新双向数据流协议

简单的说,传统的 TCP Socket 是一套相对标准化的 API,而出现时间不久的 WebSocket 是一种网络协议 — 两码事。

WebSocket 底层是基于 TCP 协议的,所以早期草案中叫做 TCPConnection,最后之所以改名,其实是借用了传统 Socket 沟通 TCP 网络两端的意思而已。

要解决的问题

*HTTP 的工作方式*
在基于 请求/响应 模式的 HTTP/HTTPS 下,如果是对实时性要求较高的场景,客户端就需要不停的询问服务端有无可用的数据,这在各方面都是笨拙而不划算的。

*WebSocket 的工作方式*
而在 WebSocket 的全双工(允许数据在两个方向上同时传输)方式下,客户端只要静静地听招呼即可,有可用数据时服务端会自动通知它。

WebSocket 的用武之地

大部分传统的方式既浪费带宽(HTTP HEAD 是比较大的),又消耗服务器 CPU 占用(没有信息也要接受请求);而 WebSocket 则会大幅降低上述的消耗,更适用于以下场景:

  • 实时性要求高的应用
  • 聊天室
  • IoT (物联网 – internet of things)
  • 在线多人游戏

兼容性也令人满意,非要说何时不适用的话,大概就是少数必须兼容老旧浏览器,或者对实时要求明显不高的情况下了。

HTTP 的扩展

WebSocket 连接的 URL 使用 ws://wss:// 等开头,其加密、cookie 等策略和对应的 HTTP/HTTPS 基本相同。

HTTP、WebSocket 等应用层协议,都是基于 TCP 协议来传输数据的,可以把这些高级协议理解成对 TCP 的封装。


Websocket协议是为了解决web即时应用中服务器与客户端浏览器全双工通信的问题而设计的,是完全意义上的Web应用端的双向通信技术,可以取代之前使用半双工HTTP协议而模拟全双工通信,同时克服了带宽和访问速度等的诸多问题。协议定义为ws和wss协议,分别为普通请求和基于SSL的安全传输,占用端口与http协议系统,ws为80端口,wss为443端口,这样可以支持HTTP代理。

协议包含两个部分,第一个是“握手”,第二个是数据传输。

一、Websocket URI

定义的两个协议框架ws和wss与http类似,而且各自部分的要求也是在HTTP协议中使用的一样,各自的URI如下:

ws-URI = “ws:” “//” host [ “:” port ] path [ “?” query ]
wss-URI = “wss:” “//” host [ “:” port ] path [ “?” query ]

其中port是可选项,query前接“?”。

二、握手(Opening & Closing Handshake)打开连接

当建立一个Websocket连接时,为了保持基于HTTP协议的服务器软件和中间件进行兼容工作,客户端打开一个连接时使用与HTTP连接的同一个端口到服务器进行连接,这样被设计为一个升级的HTTP请求。

1、发送握手请求

此时的连接状态是CONNECTING,客户端需要提供host、port、resource-name和一个是否是安全连接的标记,也就是一个WebSocket URI。

客户端发送的一个到服务器端握手请求如下:

 

        GET /chat HTTP/1.1
        Host: server.example.com
        Upgrade: websocket
        Connection: Upgrade
        Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
        Origin: http://example.com
        Sec-WebSocket-Protocol: chat, superchat
        Sec-WebSocket-Version: 13

这个升级的HTTP请求头中的字段顺序是可以随便的。与普通HTTP请求相比多了一些字段。

    • Connection必须设置Upgrade,表示客户端希望连接升级。
    • Upgrade字段必须设置Websocket,表示希望升级到Websocket协议。
    • Sec-WebSocket-Key是随机的字符串,服务器端会用这些数据来构造出一个SHA-1的信息摘要。把“Sec-WebSocket-Key”加上一个特殊字符串“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后计算SHA-1摘要,之后进行BASE-64编码,将结果做为“Sec-WebSocket-Accept”头的值,返回给客户端。如此操作,可以尽量避免普通HTTP请求被误认为Websocket协议。
    • Sec-WebSocket-Version 表示支持的Websocket版本。RFC6455要求使用的版本是13,之前草案的版本均应当弃用。
    • Origin字段是可选的,通常用来表示在浏览器中发起此Websocket连接所在的页面,类似于Referer。但是,与Referer不同的是,Origin只包含了协议和主机名称。
    • 其他一些定义在HTTP协议中的字段,如Cookie等,也可以在Websocket中使用。
    • Sec-WebSocket-Protocol:字段表示客户端可以接受的子协议类型,也就是在Websocket协议上的应用层协议类型。上面可以看到客户端支持chat和superchat两个应用层协议,当服务器接受到这个字段后要从中选出一个协议返回给客户端。
发送请求的要求:
  • 请求的WebSocket URI必须要是定义的有效的URI。
  • 如果客户端已经有一个WebSocket连接到远程服务器端,不论是否是同一个服务器,客户端必须要等待上一个连接关闭后才能发送新的连接请求,也就是同一客户端一次只能存在一个WebSocket连接。如果想同一个服务器有多个连接,客户端必须要串行化进行。如果客户端检测到多个到不同服务器的连接,应该限制一个最大连接数,在web浏览器中应该设定最多可以打开的标签页的数目。这样可以防止到远程服务器的DDOS攻击,但这是对到多个服务器的连接,如果是到同一个服务器连接,并没有数目限制。
  • 如果使用了代理服务器,那么客户端建立连接的时候需要告知代理服务器向目标服务器打开TCP连接。
  • 如果连接没有打开,一定是某一方出现错误,此时客户端必须要关闭再次连接的尝试。
  • 连接建立后,握手必须要是一个有效的HTTP请求
  • 请求的方式必须是GET,HTTP协议的版本至少是1.1
  • Upgrade字段必须包含而且必须是”websocket”,Connection字段必须内容必须是“Upgrade”
  • Sec-Websocket-Version必须,而且必须是13 (固定版本号)

2、返回握手应答

服务器返回正确的相应头后,客户端验证后将建立连接,此时状态为OPEN。服务器响应头如下:
        HTTP/1.1 101 Switching Protocols
        Upgrade: websocket
        Connection: Upgrade
        Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
        Sec-WebSocket-Protocol: chat

响应头握手过程中是服务器返回的是否同意握手的依据。

  • 首行返回的是HTTP/1.1协议版本和状态码101,表示变换协议(Switching Protocol)
  • Upgrade 和 Connection:这两个字段是服务器返回的告知客户端同意使用升级并使用websocket协议,用来完善HTTP升级响应
  • Sec-WebSocket-Accept:服务器端将加密处理后的握手Key通过这个字段返回给客户端表示服务器同意握手建立连接。
  • Sec-Websocket-Procotol:服务器选择的一个应用层协议。

上述响应头字段被客户端浏览器解析,如果验证到Sec-WebSocket-Accept字段的信息符合要求就会建立连接,同时就可以发送WebSocket的数据帧了。如果该字段不符合要求或者为空或者HTTP状态码不为101,就不会建立连接。

服务器端响应步骤:
  • 解析握手请求头:获取握手依据Key并进行处理,检测HTTP的GET请求和版本是否准确,Host字段是否有权限,Upgrade字段中websocket是一个与大小写无关的ASCII字符串,Connection字段是一个大小写无关的”Upgrade”ASCII字符串,Websocket协议版本必须为13,其他的关于Origin、Protocol和Extensions可选。
  • 发送握手响应头:检测是否是wss协议连接,如果是就是用TLS握手连接,否则就是普通连接。服务器可以添加额外的验证信息到客户端进行验证。当进行一系列验证之后,服务器必须返回一个有效的HTTP响应头。响应头中每一行一个字段,结束必须为“\r\n”,使用的ABNF语法。
除了上述必要头字段之外,其他的HTTP协议定义的字段都可以使用,如Set-Cookie等。
websocket 采用 帧格式传输数据,详见  有关 websocket 基础 的博客。

 浏览器中的实现

在浏览器中可以直接调用 WebSocket 对象,其定义如下:

enum BinaryType { "blob", "arraybuffer" };
[Constructor(USVString url, optional (DOMString or sequence<DOMString>) protocols = []), Exposed=(Window,Worker)]
interface WebSocket : EventTarget {
  readonly attribute USVString url;

  // ready state
  const unsigned short CONNECTING = 0;
  const unsigned short OPEN = 1;
  const unsigned short CLOSING = 2;
  const unsigned short CLOSED = 3;
  readonly attribute unsigned short readyState;
  readonly attribute unsigned long long bufferedAmount;

  // networking
  attribute EventHandler onopen;
  attribute EventHandler onerror;
  attribute EventHandler onclose;
  readonly attribute DOMString extensions;
  readonly attribute DOMString protocol;
  void close([Clamp] optional unsigned short code, optional USVString reason);

  // messaging
  attribute EventHandler onmessage;
  attribute BinaryType binaryType;
  void send(USVString data);
  void send(Blob data);
  void send(ArrayBuffer data);
  void send(ArrayBufferView data);
};

使用起来大概是这样的:

var ws = new WebSocket('ws://www.xxx.com/some.php');
ws.send('xxx'); //每次只能发送字符串
ws.onmessage = function(event) {
	var data = event.data;
};
ws.onerror = function() {
	ws.close();
};

II. 一个多用户交互的 WebSocket 实例

这里随便设想一个用户场景,比如我们要做一个在线纸牌游戏,肯定就是一个多人进入同一个房间的形式,并且每个人的动作能广播给其他人。

下面用 WebSocket 做一个最基础的验证原型,让每个玩家知道其他人的进入、离开、出牌、悔牌,甚至是耍赖换牌等:

(2.1) 服务器端的实现

我们用 nodejs+expressjs 搭建基础服务器,并用 https://github.com/websockets/ws 封装的库实现 WebSocket 协议的服务器端逻辑:

// server.js

var express = require('express')
var ws = require('./ws')

var app = express()

app.get('/', function (req, res) {
    res.sendFile(__dirname + '/ws.html');
})

app.listen(3000, function () {
  console.log('Example app listening on port 3000!')
})
// ws.js

const { Server, OPEN } = require('ws');

const clients = []; //array of websocket clients
const cardsArr = []; //array of {cardId, count, title, ...}

let _lock = false;

const wss = new Server({port: 40510})
wss.on('connection', function (ws) {

	const _cid = clients.push(ws) - 1;

	ws.on('message', function (json) {

		const {
			act,
			cid,
			data
		} = JSON.parse(json);

		switch (act) {
			case 'client:join':
				_onCustomerJoin(ws, _cid);
				break;
			case 'client:leave':
				_onCustomerLeave(cid);
				break;
			case 'client:add': //增加牌
				_onAddCard(cid, data);
				break;
			case 'client:update': //修改牌
				_onUpdateCard(cid, data);
				break;
			case 'client:remove': //删除牌
				_onRemoveCard(cid, data);
				break;
			case 'client:win': //下单
				_onWin(cid);
				break;
			default:
				console.log('received: %s', act, cid)
				break;
		}

	});
});

function _ensureLock(func) {
	return function() {
		if (_lock) return;
		_lock = true;
		const rtn = func.apply(null, arguments);
		_lock = false;
		return rtn;
	};
}

function _findCard(cardId) {
	const cidx = cardsArr.map(Card=>Card.cardId).indexOf(cardId);
	return cidx;
}

const _broadcast = (excludeId, msg, data=null)=>{
	clients.forEach( (client, cidx)=>{
		if (cidx === excludeId) return;
		if (client && client.readyState === OPEN) {
			client.send(JSON.stringify({
				act: 'server:broadcast',
				msg: msg,
				data: data
			}));
		}
	} );
};

const _onCustomerJoin = (ws, cid)=>{
	ws.send(JSON.stringify({
		act: 'server:regist',
		data: {
			cid: cid
		}
	}));
	_broadcast(cid, '玩家加入:', {cid: cid});
};
const _onCustomerLeave = (cid)=>{
	clients[cid].terminate();
	clients.splice(cid, 1);
	_broadcast(cid, '玩家退出:', {cid: cid});
};
const _onAddCard = _ensureLock( (cid, data)=>{
	const d = _findCard(data.cardId);
	if (d !== -1) {
		cardsArr.splice(d, 1);
	}
	cardsArr.push(data);
	_broadcast(-1, '玩家添加了牌', {
		cid: cid,
		Card: data,
		cardsArr: cardsArr
	});
} );
const _onUpdateCard = _ensureLock( (cid, data)=>{
	const d = _findCard(data.cardId);
	if (d === -1) return;
	cardsArr[d] = data;
	_broadcast(-1, '玩家更改了牌', {
		cid: cid,
		Card: data,
		cardsArr: cardsArr
	});
} );
const _onRemoveCard = _ensureLock( (cid, data)=>{
	const d = _findCard(data.cardId);
	if (d === -1) return;
	cardsArr.splice(d, 1);
	_broadcast(-1, '玩家删除了牌', {
		cid: cid,
		Card: data,
		cardsArr: cardsArr
	});
} );
const _onWin = _ensureLock( (cid)=>{
	//do sth. here
	_broadcast(cid, '玩家胡牌了');
} );

(2.2) 客户端的实现

<h1></h1>
<div></div>

<button onclick="_add()">出牌</button>
<button onclick="_update()">换牌</button>
<button onclick="_remove()">悔牌</button>
<button onclick="_win()">胡牌</button>
<button onclick="_leave()">离开</button>

<script>
    let cid = null; 

    const ws = new WebSocket('ws://localhost:40510');

    ws.onopen = function () {
        console.log('websocket is connected ...');

        _send({
            act: 'client:join'
        });
    };

    ws.onmessage = function (ev) {
        
        const {
            act,
            msg,
            data
        } = JSON.parse(ev.data);

        switch(act) {
            case 'server:regist':
                cid = data.cid;
                console.log(`regist: cid is ${cid}`);
                document.querySelector('h1').innerHTML = 'I AM: ' + cid;
                break;
            case 'server:broadcast':
                console.log('从服务器端接收的广播:', msg, data);
                if (data && data.cardsArr) {
                    document.querySelector('div').innerHTML = JSON.stringify(
                        data.cardsArr, null, 4
                    );
                }
                break;
            default:
                console.log(ev);
                break;
        }
    }

    function _send(json) {
        ws.send(JSON.stringify(json));
    }

    function _add() {
        _send({
            act: 'client:add',
            cid: cid,
            data: {
                cardId: 111,
                count: 1,
                title: '红桃A'
            }
        })
    }
    function _update() {
        _send({
            act: 'client:update',
            cid: cid,
            data: {
                cardId: 111,
                count: 2,
                title: '黑桃9'
            }
        })
    }
    function _remove() {
        _send({
            act: 'client:remove',
            cid: cid,
            data: {
                cardId: 111
            }
        })
    }
    function _win() {
        _send({
            act: 'client:win',
            cid: cid
        })
    }
    function _leave() {
        _send({
            act: 'client:leave',
            cid: cid
        })
    }
</script>

(2.3) 运行效果

玩家 0 加入:

玩家 1 加入:

玩家 1 出牌:

玩家 1 胡牌并退出:

与 WebSocket 类似的技术

实际上,每当谈到实时双向通信的问题时,我们自然会想起历年来一些基于 HTTP 技术的尝试;也正是基于这些之前工作中的实践和困扰,WebSocket 才应运而生。让我们大概回顾一下相关的方案及其缺陷:

轮询 (Polling)

借助于 setInterval() 等方式,客户端不断的发送请求并得到响应。这种做法比较简单,可以在一定程度上解决问题。不过对于轮询的时间间隔需要进行仔细考虑。轮询的间隔过长,会导致用户不能及时接收到更新的数据;轮询的间隔过短,会导致查询请求过多,增加服务器端的负担。

长轮询 (Long Polling)

这是对轮询的一种改进。客户端发出请求后,服务器端用 while(true) 等方式阻塞住请求,直到有可用数据才发送响应数据,而客户端收到响应后再发送下一个请求。

这种方式又被成为 “Hanging GET”、“反向 Ajax” 或 “Comet” 等,虽然看上去很像服务器推送,但仍然是基于 HTTP 的一种慢响应;且在数据更新频繁的情况下,其效率并不优于一般的轮询。

HTTP 流 (Streaming)

使用 HTTP 1.1 且响应头中包含 Transfer-Encoding: chunked 的情况下,服务器发送给客户端的数据可以分成多个部分,保持打开(while-true, sleep等),并周期性 flush() 分块传输。

客户端只发送一个HTTP连接,在 xhr.readyState==3 状态下,用 xhr.responseText.substring 获取每次的数据。

但是数据响应可能会因 代理服务器 或 防火墙 等中间人造成延迟,所以可能还要额外探测这种情况以切换到长轮询方式。

SSE (Server-Sent Events)

SSE 规范也是 HTML 5 规范的一个组成部分。服务器端响应的内容类型是text/event-stream,在浏览器端使用 EventSource 对象处理返回的数据。

比之于 WebSocket,SSE 的缺点在于:

  • 不支持 CORS
  • 单向通道,只能服务器向浏览器端发送
  • 浏览器兼容性稍差

III. 总结

  • 传统的 TCP Socket 往往指的是 TCP/IP 网络环境中的两个连接端,以及为方便此类开发所设计的一组编程 API
  • WebSockets 为 C/S 两端提供了实时交互通信的能力,允许服务器主动发送信息给客户端
  • WebSockets 是 HTML 5 规范的一个组成部分,是一种区别于 HTTP 的全新双向数据流协议
  • 全双工通信的 WebSockets 有效改善了之前 长轮询 等方式的弊端
  • WebSockets 适用于实时性要求高的应用、聊天室、多人游戏等

参考:

https://juejin.im/post/5ae3eb9b51882567382f5767

https://www.cnblogs.com/oshyn/p/3574497.html


http://www.cnblogs.com/hustskyking/p/websocket-with-node.html

http://www.cnblogs.com/hustskyking/p/websocket-with-php.html

https://www.cnblogs.com/zxtceq/p/6963964.html

web通信方式-概述和总结

web通信,一个特别大的topic,涉及面也是很广的。因最近学习了 javascript 中一些 web 通信知识,在这里总结下。文中应该会有理解错误或者表述不清晰的地方,还望斧正!

一、前言


1. comet技术

浏览器作为 Web 应用的前台,自身的处理功能比较有限。浏览器的发展需要客户端升级软件,同时由于客户端浏览器软件的多样性,在某种意义上,也影响了浏览器新技术的推广。在 Web 应用中,浏览器的主要工作是发送请求、解析服务器返回的信息以不同的风格显示。AJAX 是浏览器技术发展的成果,通过在浏览器端发送异步请求,提高了单用户操作的响应性。但 Web 本质上是一个多用户的系统,对任何用户来说,可以认为服务器是另外一个用户。现有 AJAX 技术的发展并不能解决在一个多用户的 Web 应用中,将更新的信息实时传送给客户端,从而用户可能在“过时”的信息下进行操作。而 AJAX 的应用又使后台数据更新更加频繁成为可能。

随着互联网的发展,web 应用层出不穷,也不乏各种网站监控、即时报价、即时通讯系统,为了让用户得到更好的体验,服务器需要频繁的向客户端推送信息。开发者一般会采用基于 AJAX 的长轮询方式或者基于 iframe 及 htmlfile 的流方式处理。当然有些程序需要在客户端安装各种插件( Java applet 或者 Flash )来支持性能比较良好的“推”信息。

2. HTTP协议中的长、短连接

短连接的操作步骤是:建立连接——数据传输——关闭连接…建立连接——数据传输——关闭连接
长连接的操作步骤是:建立连接——数据传输…(保持连接)…数据传输——关闭连接

长连接与短连接的不同主要在于client和server采取的关闭策略不同。短连接在建立连接以后只进行一次数据传输就关闭连接,而长连接在建立连接以后会进行多次数据数据传输直至关闭连接(长连接中关闭连接通过Connection:closed头部字段)。

二、web 通信


首先要搞清楚,xhr 的 readystate 各种状态。

属性 描述
onreadystatechange 存储函数(或函数名),每当 readyState 属性改变时,就会调用该函数。
readyState 存有 XMLHttpRequest 的状态。从 0 到 4 发生变化。

0: 请求未初始化
1: 服务器连接已建立
2: 请求已接收
3: 请求处理中
4: 请求已完成,且响应已就绪

status 200: “OK”
404: 未找到页面

1.轮询

轮询是一种“拉”取信息的工作模式。设置一个定时器,定时询问服务器是否有信息,每次建立连接传输数据之后,链接会关闭。

前端实现:

var polling = function(url, type, data){
    var xhr = new XMLHttpRequest(), 
        type = type || "GET",
        data = data || null;

    xhr.onreadystatechange = function(){
        if(xhr.readyState == 4) {
            receive(xhr.responseText);
            xhr.onreadystatechange = null;
        }
    };

    xhr.open(type, url, true);
    //IE的ActiveXObject("Microsoft.XMLHTTP")支持GET方法发送数据,
    //其它浏览器不支持,已测试验证
    xhr.send(type == "GET" ? null : data);
};

var timer = setInterval(function(){
    polling();
}, 1000);

在轮询的过程中,如果因为网络原因,导致上一个 xhr 对象还没传输完毕,定时器已经开始了下一个询问,上一次的传输是否还会在队列中,这个问题我没去研究。如果感兴趣可以自己写一个ajax的请求管理队列。(研究结果:后一个会把前一个覆盖掉 即前一个就算返回 也不会认了)

2.长轮询(long-polling)

长轮询其实也没啥特殊的地方,就是在xhr对象关闭连接的时候马上又给他接上~ 看码:

var longPoll = function(type, url){
    var xhr = new XMLHttpRequest();

    xhr.onreadystatechange = function(){
        // 状态为 4,数据传输完毕,重新连接
        if(xhr.readyState == 4) {
            receive(xhr.responseText);
            xhr.onreadystatechange = null;

            longPoll(type, url);
        }
    };

    xhr.open(type, url, true);
    xhr.send();
}

只要服务器断开连接,客户端马上连接,不让他有一刻的休息时间,这就是长轮询。

3.数据流

数据流方式,在建立的连接断开之前,也就是 readystate 状态为 3 的时候接受数据,但是麻烦的事情也在这里,因为数据正在传输,你拿到的 xhr.response 可能就是半截数据,所以呢,最好定义一个数据传输的协议,比如前2个字节表示字符串的长度,然后你只获取这个长度的内容,接着改变游标的位置。

假如数据格式为: data splitChar   data为数据内容,splitChar为数据结束标志(长度为1)。 那么传输的数据内容为 data splitChar data splitChar data splitChar…

var dataStream = function(type, url){
    var xhr = new XMLHttpRequest();

    xhr.onreadystatechange = function(){

        // 状态为 3,数据接收中
        if(xhr.readyState == 3) {
            var i, l, s;

            s = xhr.response; //读取数据
            l = s.length;     //获取数据长度

            //从游标位置开始获取数据,并用分割数据
            s = s.slice(p, l - 1).split(splitChar);

            //循环并操作数据
            for(i in s) if(s[i])  deal(s[i]);

            p = l;  //更新游标位置

        }

        // 状态为 4,数据传输完毕,重新连接
        if(xhr.readyState == 4) {
            xhr.onreadystatechange = null;

            dataStream(type, url);
        }
    };

    xhr.open(type, url, true);
    xhr.send();
};

这个代码写的是存在问题的,当readystate为3的时候可以获取数据,但是这时获取的数据可能只是整体数据的一部分,那后半截就拿不到了。readystate在数据传输完毕之前是不会改变的,也就是说他并不会继续接受剩下的数据。我们可以定时去监听readystate,这个下面的例子中可以看到。

这样的处理不算复杂,但是存在问题。上面的轮询和长轮询是所有浏览器都支持的,所以我就没有写兼容IE的代码,但是这里,低版本IE不允许在readystate为3的时候读取数据,所以我们必须采用其他的方式来实现。

在ajax还没有进入web专题之前,我们已经拥有了一个法宝,那就是iframe,利用iframe照样可以异步获取数据,对于低版本IE可以使用iframe来接受数据流。

if(isIE){
    var dataStream = function(url){
        var ifr = document.createElement("iframe"), doc, timer;

        ifr.src = url;
        document.body.appendChild(ifr);

        doc = ifr.contentWindow.document;

        timer = setInterval(function(){

            if(ifr.readyState == "interactive"){
                // 处理数据,同上
            }

            // 重新建立链接
            if(ifr.readyState == "complete"){
                clearInterval(timer);

                dataStream(url);
            }
        }, 16);
    };
};

定时去监听iframe的readystate的变化,从而获取数据流,不过,上面的处理方式还是存在问题。数据流实现“服务器推”数据的原理是什么呢,简单点说,就是文档(数据)还没有加载完,这个时候浏览器的工作就是去服务器拿数据完成文档(数据)加载,我们就是利用这点,给浏览器塞点东西过去~ 所以上述利用iframe的方式获取数据,会使浏览器一直处于加载状态,title上的那个圈圈一直在转动,鼠标的状态也是loading,这看着是相当不爽的。幸好,IE提供了HTMLFile对象,这个对象就相当于一个内存中的Document对象,它会解析文档。所以我们创建一个HTMLFile对象,在里面放置一个IFRAME来连接服务器。这样,各种浏览器就都支持了。

if(isIE){
    var dataStream = function(url){
        var doc = new ActiveXObject("HTMLFile"), 
            ifr = doc.createElement("iframe"), 
            timer, d;

        doc.write("<body/>");

        ifr.src = url;
        doc.body.appendChild(ifr);

        d = ifr.contentWindow.document;

        timer = setInterval(function(){

            if(d.readyState == "interactive"){
                // 处理数据,同上
            }

            // 重新建立链接
            if(d.readyState == "complete"){
                clearInterval(timer);

                dataStream(url);
            }
        }, 16);
    };
};

4.websocket

websocket是前端一个神器,ajax用了这么久了,相关技术也是很成熟,不过要实现个数据的拉取确实十分不易,从上面的代码中也看到了,各种兼容性问题,各种细节处理问题,自从有了websocket,哈哈,一口气上五楼…

var ws = new WebSocket("ws://www.example.com:8888");

ws.onopen = function(evt){};
ws.onmessage = function(evt){
    deal(evt.data);
};
ws.onclose  = function(evt){};

//ws.close();

新建一个WebSocket实例,一切就OK了,ws:// 是websocket的连接协议,8888为端口号码。onmessage中提供了data这个属性,相当方便。

(WebSocket一种在单个 TCP 连接上进行全双工通讯的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并被RFC7936所补充规范,WebSocketAPI被W3C定为标准。)

5.EventSource

HTML5中提供的EventSource这玩意儿,这是无比简洁的服务器推送信息的接受函数。

new EventSource("test.php").onmessage=function(evt){
    console.log(evt.data);
};

简洁程度和websocket是一样的啦,只是这里有一个需要注意的地方,test.php输出的数据流应该是特殊的MIME类型,要求是”text/event-stream”,如果不设置的话,你试试~ (直接抛出异常)

6.ActionScript

情非得已就别考虑这第六种方式了,虽说兼容性最好,要是不懂as,出了点bug你也不会调试。

具体实现方法:在 HTML 页面中内嵌入一个使用了 XMLSocket 类的 Flash 程序。JavaScript 通过调用此 Flash 程序提供的套接口接口与服务器端的套接口进行通信。JavaScript 在收到服务器端以 XML 格式传送的信息后可以很容易地控制 HTML 页面的内容显示。flash现在差不多也快淘汰了)

7.Java Applet套接口

这玩意儿原理和Flash类似,不过我不懂,就不细说了。(Java Applet现在差不多也快淘汰了)

三、后端处理方式


本文主要是总结Javascript的各种通讯方式,后端配合node来处理,应该是挺给力的。

var conns = new Array();

var ws = require("websocket-server");
var server = ws.createServer();

server.addListener("connection", function(connection){
  console.log("Connection request on Websocket-Server");
  conns.push(connection);
  connection.addListener('message',function(msg){
        console.log(msg);
        for(var i=0; i<conns.length; i++){
            if(conns[i]!=connection){
                conns[i].send(msg);
            }
        }
    });
});
server.listen(8888);

下面是一个php的测试demo。

header('Content-Type:text/html; charset=utf-8');
while(1){
    echo date('Y-m-d H:i:s');
    flush();
    sleep(1);
};

四、web 通信方式利弊分析

  • 轮询,这种方式应该是最没技术含量的,操作起来最方便,不过是及时性不强,把定时器的间隔时间设置的短一些可以稍微得到缓和。
  • 长轮询,算是比较不错的一个web通讯方式,不过每次断开连接,比较耗服务器资源,客户端到无所谓。
  • 数据流,他和长轮询不同之处是接受数据的时间不一样,数据流是readystate为3的时候接受,低版本IE不太兼容,处理起来略麻烦,而且还要自己设计数据传输协议。不过他对资源的消耗比上面几种都可观。
  • websocket和EventSource,两个利器,不过,没几个浏览器支持,这是比较让人伤心~(现在2018年,主流浏览器都支持了,毕竟原博客2013年发的)
  • ActionScript和Java Applet,两者都是需要在客户端安装插件的,一个是Flash插件,一个是Java插件,而且搞前端的人一般对这东西不太熟悉,如果没有封装比较好的库可以使用,那建议还是别用了。

五、参考资料


来自:http://www.cnblogs.com/hustskyking/p/web-communication.html

http协议-headers-各字段详解

Cache-Control:must-revalidate, no-cache, private。

这个值告诉客户端,服务端不希望客户端缓存资源,在下次请求资源时,必须要从新请求服务器,不能从缓存副本中获取资源。
Cache-Control是响应头中很重要的信息,当客户端请求头中包含Cache-Control:max-age=0请求,明确表示不会缓存服务器资源时,Cache-Control作为作为回应信息,通常会返回no-cache,意思就是说,“不缓存就不缓存呗”;当客户端在请求头中没有包含Cache-Control时,服务端往往会定,不同的资源不同的缓存策略,比如说oschina在缓存图片资源的策略就是Cache-Control:max-age=86400,这个意思是,从当前时间开始,在86400秒的时间内,客户端可以直接从缓存副本中读取资源,而不需要向服务器请求。

Connection:keep-alive

这个字段作为回应客户端的Connection:keep-alive,告诉客户端服务器的tcp连接也是一个长连接,客户端可以继续使用这个tcp连接发送http请求。关于长连接的更多知识,后面我再详细讲。

Content-Encoding:gzip

告诉客户端,服务端发送的资源是采用gzip编码的,客户端看到这个信息后,应该采用gzip对资源进行解码。

Content-Type:text/html;charset=UTF-8

告诉客户端,资源文件的类型,还有字符编码,客户端通过utf-8对资源进行解码,然后对资源进行html解析。通常我们会看到有些网站是乱码的,往往就是服务器端没有返回正确的编码。

Content-type: text/html; charset=xxx的优先级要高于
<META http-equiv=”content-type” content=”text/html; charset=xxx”>
因为一个在headers中,一个在网页中。浏览器响应时,有时候是json文件,pdf文件等,这些文件里面没有设置编码,不像网页文件内部可以设置编码。所以headers中的>Content-type 优先级更高。

服务器可以设置响应文件的编码,其实功能相当于在 headers中设置 Content-type。如果headers已经有了Content-type,那就按服务器设置的方案,修改编码方式。

对于网页编码,headers中Content-type: text/html; charset=xxx;可以不用指定charset=xxx;因为网页内部也可以设置编码方案。

当时做了一个简易的web服务器,关键细节总结。

返回消息时,http 请求头和请求体 之间 需要 加 一个  【回车+换行】两个步骤。当然,java里面就一句  xxx.println();

当时,输出响应头时:

xxx.println(“Content-Type: text/css”); 这个css的类型,没有单独写。只是用if else 单独判断输出了   text/html   text/javascript   text/png    image/jpeg   image/gif  ,其他的 一律输出  application/octet-stream  类型。所以一开始,请求 css 文件 时,响应头是  application/octet-stream  类型。然后,浏览器加载网页时,居然没有加载 css 文件。因为它认为 下载 的css文件,类型不对。所以网页渲染时,页面布局 混乱了。后来改成 输出 Content-Type: text/css 时,页面渲染成功了。

ETAG:字符串(出现在http1.1协议中,可选字段)

ETag在HTTP头字段中的使用是可选的(不像HTTP/1.1的其他头字段是强制性的)。HTTP规范从未指定生成ETag的方法。

ETag又叫实体标签(entity tag)是HTTP协议提供的若干机制中的一种Web缓存验证机制,并且允许客户端进行缓存协商。这就使得缓存变得更加高效,而且节省带宽。如果资源的内容没有发生改变,Web服务器就不需要发送一个完整的响应。ETag也可用于乐观并发控制,作为一种防止资源同步更新而相互覆盖的方法。

ETag是一个不透明的标识符,由Web服务器根据URL上的资源的特定版本而指定。如果那个URL上的资源内容改变,一个新的不一样的ETag就会被分配。用这种方法使用ETag即类似于指纹,并且他们能够被快速地被比较,以确定两个版本的资源是否相同。ETag的比较只对同一个URL有意义——不同URL上的资源的ETag值可能相同也可能不同,从他们的ETag的比较中无从推断。

Date:Sun, 21 Sep 2014 06:18:21 GMT

这个是服务端发送资源时的服务器时间,刚开始我不知道GMT是格林尼治所在地的标准时间,以为是服务器的时间错了,还去服务器上查看过时间。http协议中发送的时间都是GMT的,这主要是解决在互联网上,不同时区在相互请求资源的时候,时间混乱问题。

Expires:Sun, 1 Jan 2000 01:00:00 GMT

这个响应头也是跟缓存有关的,告诉客户端在这个时间前,可以直接访问缓存副本,很显然这个值会存在问题,因为客户端和服务器的时间不一定会都是相同的,如果时间不同就会导致问题。所以这个响应头是没有Cache-Control:max-age=***这个响应头准确的,因为max-age=date中的date是个相对时间,不仅更好理解,也更准确。

Pragma:no-cache

这个含义与Cache-Control等同。

Server:Tengine/1.4.6

这个是服务器和相对应的版本,只是告诉客户端服务器信息,没有更多的意思。

Transfer-Encoding:chunked

这个响应头告诉客户端,服务器发送的资源的方式是分块发送的。一般分块发送的资源都是服务器动态生成的,在发送时还不知道发送资源的大小,所以采用分块发送,每一块都是独立的,独立的块都能标示自己的长度,最后一块是0长度的,当客户端读到这个0长度的块时,就可以确定资源已经传输完了。


HTTP Header 之Content-Type介绍

Content-Type 实体头部用于指示资源的MIME类型 media type 。

在响应中,Content-Type标头告诉客户端实际返回的内容的内容类型。浏览器会在某些情况下进行MIME查找,并不一定遵循此标题的值; 为了防止这种行为,可以将标题 X-Content-Type-Options 设置为 nosniff

在请求中 (如POST 或 PUT),客户端告诉服务器实际发送的数据类型。

Header type Entity header
Forbidden header name no
CORS-safelisted response-header yes
句法
Content-Type: text/html; charset=utf-8
Content-Type: multipart/form-data; boundary=something
指令
media-type
资源或数据的 MIME type 。
charset
字符编码标准。
boundary
对于多部分实体,boundary 是必需的,其包括来自一组字符的1到70个字符,已知通过电子邮件网关是非常健壮的,而不是以空白结尾。它用于封装消息的多个部分的边界。
例子

Content-Type 在HTML表单中

在通过HTML form提交生成的POST请求中,请求头的Content-Type由<form>元素上的enctype属性指定

<form action="/" method="post" enctype="multipart/form-data">
  <input type="text" name="description" value="some text">
  <input type="file" name="myFile">
  <button type="submit">Submit</button>
</form>

请求头看起来像这样(在这里省略了一些 headers):

POST /foo HTTP/1.1
Content-Length: 68137
Content-Type: multipart/form-data; boundary=---------------------------974767299852498929531610575

---------------------------974767299852498929531610575
Content-Disposition: form-data; name="description" 

some text
---------------------------974767299852498929531610575
Content-Disposition: form-data; name="myFile"; filename="foo.txt" 
Content-Type: text/plain 

(content of the uploaded file foo.txt)
---------------------------974767299852498929531610575
规范
Specification Title
RFC 7233, section 4.1: Content-Type in multipart Hypertext Transfer Protocol (HTTP/1.1): Range Requests
RFC 7231, section 3.1.1.5: Content-Type Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content

 

参考:

https://my.oschina.net/manmao/blog/549123

http协议-浏览器和服务器协商http协议版本的过程探讨

How does a HTTP 1.0 only server respond to a HTTP 1.1 request?

Fairly simple question (I think). If a client sends a message like GET /whatever HTTP/1.1 to a server that only supports HTTP 1.0, how does the server react? What are the rules for header fields added in HTTP 1.1 that a HTTP 1.0 server doesn’t recognise? Does the server simply ignore 1.1 requests, ignore headers it doesn’t understand, return an error, or something else?

answer:

HTTP/1.0 200 OK

All headers of HTTP/1.1 (that a HTTP 1.0 server doesn’t recognise) will be ignored.

总之:http 目前支持协议 协商的只有 http1.1 ,浏览器通过 https 协商过程 或者直接用 upgrade 请求头  与服务器 进行协议协商。其他情况,客户端 服务端支持的最高http协议如果相同,则以最高http协议通信。服务端比客户端支持的最高http协议低,服务器会直接 忽略不能识别的请求头。

目前的浏览器,都是采用 http1.1 协议  率先进行 通信,然后 看看服务器 能不能支持 协议 升级,通过http2.0 进行通信。

http协议-headers-http1.1中新增的内容协商功能

HTTP协议的内容协商

一、起因

我们在日常的抓包过程中经常可以看到以Accept开头的请求首部,比如:Accept-Language 有一个q值,肯定有人好奇在HTTP规范中为什么要定义这个q值;还有在响应首部有一个名为Vary的首部,这个首部又有什么意义?如图所示:

本文记录我对 Vary 的一些研究,其中就包含这些问题的答案。

二、内容协商(两种方式)

一种是服务端把文档可用版本列表发给客户端让用户选,这可以使用 300 Multiple Choices 状态码来实现。这种方案有不少问题,首先多一次网络往返;其次服务端同一文档的某些版本可能是为拥有某些技术特征的客户端准备的,而普通用户不一定了解这些细节。举个例子,服务端通常可以将静态资源输出为压缩和未压缩两个版本,压缩版显然是为支持压缩的客户端而准备的,但如果让普通用户选,很可能选择错误的版本。

所以 HTTP 的内容协商通常使用另外一种方案:服务端根据客户端发送的请求头中某些字段自动发送最合适的版本。可以用于这个机制的请求头字段又分两种:内容协商专用字段(Accept 字段)、其他字段。

首先来看 Accept 字段,详见下表:

请求头字段 说明 响应头字段
Accept 告知服务器发送何种媒体类型 Content-Type
Accept-Language 告知服务器发送何种语言 Content-Language
Accept-Charset 告知服务器发送何种字符集 Content-Type
Accept-Encoding 告知服务器采用何种压缩方式 Content-Encoding

例如客户端发送以下请求头:

Accept:*/*
Accept-Encoding:gzip,deflate,sdch
Accept-Language:zh-CN,en-US;q=0.8,en;q=0.6

表示它可以接受任何 MIME 类型的资源;支持采用 gzip、deflate 或 sdch 压缩过的资源;可以接受 zh-CN、en-US 和 en 三种语言,并且 zh-CN 的权重最高(q 取值 0 – 1,最高为 1,最低为 0,默认为 1),服务端应该优先返回语言等于 zh-CN 的版本。q值的范围从0.0~1.0(1.0优先级最高)

浏览器的响应头可能是这样的:

Content-Type: text/javascript
Content-Encoding: gzip

表示这个文档确切的 MIME 类型是 text/javascript;文档内容进行了 gzip 压缩;响应头没有 Content-Language 字段,通常说明返回版本的语言正好是请求头 Accept-Language 中权重最高的那个。

三、vary的由来

有时候,上面四个 Accept 字段并不够用,例如要针对特定浏览器如 IE6 输出不一样的内容,就需要用到请求头中的 User-Agent 字段。类似的,请求头中的 Cookie 也可能被服务端用做输出差异化内容的依据。

由于客户端和服务端之间可能存在一个或多个中间实体(如缓存服务器),而缓存服务最基本的要求是给用户返回正确的文档。如果服务端根据不同 User-Agent 返回不同内容,而缓存服务器把 IE6 用户的响应缓存下来,并返回给使用其他浏览器的用户,肯定会出问题 。

所以 HTTP 协议规定,如果服务端提供的内容取决于 User-Agent 这样「常规 Accept 协商字段之外」的请求头字段,那么响应头中必须包含 Vary 字段,且 Vary 的内容必须包含 User-Agent。同理,如果服务端同时使用请求头中 User-Agent 和 Cookie 这两个字段来生成内容,那么响应中的 Vary 字段看上去应该是这样的:

Vary: User-Agent, Cookie

也就是说 Vary 字段用于列出一个响应字段列表,告诉缓存服务器遇到同一个 URL 对应着不同版本文档的情况时,如何缓存和筛选合适的版本。

 

不过查看:RFC 的vary头的例子就是 Vary: accept-encoding, accept-language ,可见accept-*也可以放在vary中。cache的唯一性只取决于request的vary里指定的headers。也就是从RFC 的文本来看,http设计上vary/cache机制和内容协商机制是正交的。

大白话:就是说 缓存机制 看的就是 vary,vary 里面不仅有User-Agent, Cookie字段,也可以有accept-* 字段,用来标记区分缓存。

关于vary 里面的 accept-encoding,比较特殊。第一,(按道理)压缩并不改变真正的内容,所以稍微聪明点的proxy就可以自行处理而不影响cache。第二,新版spec好像要求所有client必须支持gzip。所以accept-encoding有没有就没什么关系了。但是其他accept头就不一样。如果要作为区分缓存的标记,必须在vary中写明。

举例来说:
web服务器添加响应首部Vary: Accept-Encoding 告知代理服务器根据客户端的请求首部Accept-Encoding缓存不同的版本,这样下次客户端请求同一资源时,根据Accept-Encoding选择相应的缓存版本响应。
其实我们还可以禁用中间实体的缓存功能解决该问题:
web服务器设置响应首部: Cache-Control: private
通常添加Vary首部的解决方案比较通用,因为我们还是希望充分利用中间实体的缓存功能的。

四、Nginx 和 SPDY

通常,上面说的这些工作,Web Server 都可以帮我们搞定。对于 Nginx 来说,下面这个配置可以自动给启用了 gzip 的响应加上 Vary: Accept-Encoding:

gzip_vary on;

用 curl 验证我博客的 js 文件,响应头如下:

jerry@www:~$ curl --head https://imququ.com/.../xx.js

HTTP/1.1 200 OK
Server: nginx
Date: Tue, 31 Dec 2013 16:34:48 GMT
Content-Type: application/x-javascript
Content-Length: 66748
Last-Modified: Tue, 31 Dec 2013 14:30:52 GMT
Connection: keep-alive
Vary: Accept-Encoding
ETag: "52c2d51c-104bc"
Expires: Fri, 29 Dec 2023 16:34:48 GMT
Cache-Control: max-age=315360000
Strict-Transport-Security: max-age=31536000
Accept-Ranges: bytes

可以看到,服务端正确输出了「Vary: Accept-Encoding」,一切正常。
但是用 Chrome 自带抓包工具看下,这个响应头却是这样:

HTTP/1.1 200 OK
cache-control: max-age=315360000
content-encoding: gzip
content-type: application/x-javascript
date: Tue, 31 Dec 2013 16:35:27 GMT
expires: Fri, 29 Dec 2023 16:35:27 GMT
last-modified: Tue, 31 Dec 2013 14:30:52 GMT
server: nginx
status: 200
strict-transport-security: max-age=31536000
version: HTTP/1.1

我的博客支持 SPDY/2 协议,用 Chrome 访问我博客会走 SPDY,所以上面的响应头看上有点不同寻常,例如字段名都变成了小写;多了 status、version 等字段,这些变化下次专门介绍(注:见「SPDY 3.1 中的请求 / 响应头」)。神奇的是尽管服务端没任何变化,但响应中的 Vary: Accept-Encoding 却不见了。

SPDY 规定客户端必须支持压缩,这意味着 SPDY 服务器可以直接启用压缩而不用关心请求头中的 Accept-Encoding 字段。下面这段来自 Nginx 支持的 SPDY/2 协议:

User-agents are expected to support gzip and deflate compression. Regardless of the Accept-Encoding sent by the user-agent, the server may select gzip or deflate encoding at any time. [via]

于是,对于支持 SPDY 的客户端来说,Vary: Accept-Encoding 没有用途,Nginx 选择直接去掉它,可以节省一点流量。curl 或其他不支持 SPDY 协议的客户端还是走 HTTP 协议,所以看到的响应头是常规的。

Nginx 的这个做法是否合适一直有争论,实际上并不是所有支持 SPDY 的 Web Server 都会这么做。例如即使通过 SPDY 协议访问 Google 首页的 js 文件,依然可以看到 vary: Accept-Encoding:

HTTP/1.1 200 OK
status: 200 OK
version: HTTP/1.1
age: 25762
alternate-protocol: 443:quic
cache-control: public, max-age=31536000
content-encoding: gzip
content-length: 154614
content-type: text/javascript; charset=UTF-8
date: Tue, 31 Dec 2013 23:23:51 GMT
expires: Wed, 31 Dec 2014 23:23:51 GMT
last-modified: Mon, 16 Dec 2013 21:54:35 GMT
server: sffe
vary: Accept-Encoding
x-content-type-options: nosniff
x-xss-protection: 1; mode=block

PS:Vary 在 IE 下有很多坑,使用时要格外小心。网上这部分文章比较多,例如 hax 早年写的 IE 与 Vary 头,可以点过去了解下。[但是看评价有很多踩得,不知道可信度怎么样,就当个参考思路吧]


参考
书籍:《HTTP权威指南》
屈屈的博文:https://www.imququ.com/post/vary-header-in-http.html

http协议- HTTP/0.9, HTTP/1.0, HTTP/1.1, Keep-Alive, Upgrade, and HTTPS的发展简史

原文:https://medium.com/platform-engineer/evolution-of-http-69cfe6531ba0
译文:

了解http是如何在真实世界工作的

Disclaimer: This article focuses on explaining some underlying implementation details of HTTP, which will be helpful for readers to better understand my blog article — “Web API Design with HTTP and Websockets


在1989 – 1991年由 Tim Berners-Lee 在 CERN(欧洲核子研究组织) 发明的HTTP(超文本传输​​协议)是万维网的底层通信协议。HTTP在客户端 – 服务器计算模型中用作请求 – 响应协议。HTTP标准由互联网工程任务组(IETF)和万维网联盟(W3C)开发,最终发布了一系列征求意见稿(RFC)。HTTP有四个版本 – HTTP / 0.9,HTTP / 1.0,HTTP / 1.1和HTTP / 2.0。今天常用的版本是HTTP / 1.1,未来将是HTTP / 2.0。

HTTP / 0.9 – 单行协议

  • HTTP的初始版本 – 一种简单的客户端服务器,请求响应和远程协议
  • 请求性质:单行(方法+请求文档的路径)
  • 支持的方法:GET仅限
  • 响应类型:仅限超文本
  • 连接性质:响应后立即终止
  • 没有HTTP标头(不能传输其他内容类型文件),没有状态/错误代码,没有URL,没有版本
$> telnet ashenlive.com 80
(Connection 1 Establishment - TCP Three-Way Handshake)
Connected to xxx.xxx.xxx.xxx
(Request)
GET /my-page.html
(Response in hypertext)
<HTML>
A very simple HTML page
</HTML>
(Connection 1 Closed - TCP Teardown)

流行的Web服务器(Apache,Nginx)仍支持HTTP / 0/9。尝试打开Telnet会话并访问google.com

HTTP / 1.0 – 拥有扩展性

  • 浏览器友好的协议
  • 提供的头字段包括有关请求和响应的丰富元数据(HTTP版本号,状态代码,内容类型)
  • 回应:不限于超文本(Content-Type头文件提供传输纯HTML文件以外的文件的能力 – 例如脚本,样式表,媒体)
  • 支持的方法:GET ,HEAD ,POST
  • 连接性质:响应后立即终止
(Connection 1 Establishment - TCP Three-Way Handshake)
Connected to xxx.xxx.xxx.xxx
(Request)
GET /my-page.html HTTP/1.0 
User-Agent: NCSA_Mosaic/2.0 (Windows 3.1)
(Response)
HTTP/1.0 200 OK 
Content-Type: text/html 
Content-Length: 137582
Expires: Thu, 01 Dec 1997 16:00:00 GMT
Last-Modified: Wed, 1 May 1996 12:45:26 GMT
Server: Apache 0.84

<HTML> 
A page with an image
  <IMG SRC="/myimage.gif">
</HTML>
(Connection 1 Closed - TCP Teardown)
------------------------------------------
(Connection 2 Establishment - TCP Three-Way Handshake)
Connected to xxx.xxx.xxx.xxx
(Request)
GET /myimage.gif HTTP/1.0
User-Agent: NCSA_Mosaic/2.0 (Windows 3.1)

(Response)
HTTP/1.0 200 OK 
Content-Type: text/gif 
Content-Length: 137582
Expires: Thu, 01 Dec 1997 16:00:00 GMT
Last-Modified: Wed, 1 May 1996 12:45:26 GMT
Server: Apache 0.84
[image content]
(Connection 2 Closed - TCP Teardown)

HTTP / 0.9和HTTP / 1.0中的主要问题-为每个请求建立新连接

HTTP / 0.9和HTTP / 1.0都需要为每个请求打开一个新连接(并在发送响应后立即关闭它)。每次建立新的连接时,TCP三方握手也应该发生。为了获得更好的性能,减少客户端和服务器之间的这些往返行为至关重要。HTTP / 1.1通过持久连接解决了这个问题。


一个典型的TCP三次握手(查看TCP状态机如何改变其状态)来自  lwn.net

HTTP / 1.1 – 标准化协议

  • 这是目前常用的HTTP版本。
  • 引入了关键性能优化和功能增强 – 持久和流水线连接,分块传输,压缩/解压缩,内容协商,虚拟主机(具有多个域的单个IP地址的服务器),通过添加缓存支持更快的响应和更大的带宽节省。
  • 支持的方法:GET ,HEAD ,POST ,PUT ,DELETE ,TRACE ,OPTIONS
  • 连接性:保持连接
(Connection 1 Establishment - TCP Three-Way Handshake)
Connected to xxx.xxx.xxx.xxx
(Request 1)
GET /en-US/docs/Glossary/Simple_header HTTP/1.1
Host: developer.mozilla.org
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:50.0) Gecko/20100101 Firefox/50.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: https://developer.mozilla.org/en-US/docs/Glossary/Simple_header

(Response 1)
HTTP/1.1 200 OK
Connection: Keep-Alive
Content-Encoding: gzip
Content-Type: text/html; charset=utf-8
Date: Wed, 20 Jul 2016 10:55:30 GMT
Etag: "547fa7e369ef56031dd3bff2ace9fc0832eb251a"
Keep-Alive: timeout=5, max=1000
Last-Modified: Tue, 19 Jul 2016 00:59:33 GMT
Server: Apache
Transfer-Encoding: chunked
Vary: Cookie, Accept-Encoding

[content]

(Request 2)
GET /static/img/header-background.png HTTP/1.1
Host: developer.cdn.mozilla.net
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:50.0) Gecko/20100101 Firefox/50.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: https://developer.mozilla.org/en-US/docs/Glossary/Simple_header

(Response 2)
HTTP/1.1 200 OK
Age: 9578461
Cache-Control: public, max-age=315360000
Connection: keep-alive
Content-Length: 3077
Content-Type: image/png
Date: Thu, 31 Mar 2016 13:34:46 GMT
Last-Modified: Wed, 21 Oct 2015 18:27:50 GMT
Server: Apache

[image content of 3077 bytes]
(Connection 1 Closed - TCP Teardown)

在建立任何连接之前,会发生TCP三次握手。最后,在将所有数据发送到客户端之后,服务器发送一条消息,表示没有更多数据要发送。然后客户端关闭连接(TCP拆卸)。HTTP / 1.0中的问题是,对于每个请求 – 响应周期,都需要打开和关闭连接。而使用HTTP / 1.1的优点是,我们可以重复使用相同的开放连接来处理多个请求 – 响应周期。(图片来自informit.com

Keep-Alive和Upgrade 头字段

Keep-Alive 报头

  • Keep-Alive报头在HTTP/ 1.1之前就已经使用(需要显示指明),到了HTTP / 1.1时Keep-Alive成为默认行为。Keep-Alive头可用于定义主机之间长期通信的策略(即允许连接保持活动状态直到发生事件)。为现代Web通信协议中的持久性,可重用连接,流水线和更多增强功能奠定了基础。
  • 客户端,服务器或任何中介都可以为Keep-Alive 添加额外信息。此外,主机可以添加timeoutmax以设置一个超时或每个连接限制最大请求数参数。
HTTP/1.1 200 OK
Connection: Keep-Alive
Content-Encoding: gzip
Content-Type: text/html; charset=utf-8
Date: Thu, 11 Aug 2016 15:23:13 GMT
Keep-Alive: timeout=5, max=1000
Last-Modified: Mon, 25 Jul 2016 04:32:39 GMT
Server: Apache

[body]

来自ietf.org的例子展示了如何使用Keep-Alive头文件。所有连接都是独立协商的。客户端指示600秒(10分钟)的超时,但代理仅准备保持连接至少120秒(2分钟)。在代理和服务器之间的链接上,代理请求超时1200秒,服务器将其减少到300秒。如本例所示,代理维护的超时策略对于每个连接都不相同。每个连接跳是独立的

  • 凭借Keep-Alive行为,HTTP流水线,多连接复用以及更多的改进才能实现。

HTTP流水线和多个并行连接(图片来自informit.com

Upgrade 头字段

  • 使用UpgradeHTTP / 1.1中引入的头文件,可以使用常用协议(如HTTP / 1.1)启动连接,然后请求连接切换到增强协议类型(如HTTP / 2.0或WebSockets)。
  • 在升级的协议连接中,max不存在参数(最大请求计数)。升级后的协议可以为timeout参数提供新的策略(如果没有明确定义,它使用底层协议中的默认超时值)。
这个来自ietf.org的例子显示了从HTTP / 1.1到WebSocket [RFC6455] 的升级中包含的头文件。通过websocket升级,每跳中的连接不能在中介的任何一边都有独立的生命周期。升级后,超时策略不能独立于每个连接。代理调整超时值以反映客户端和代理策略设置的值中较低的值,以便服务器知道连接特征; 同样,来自服务器的值被提供给客户端。这个叫逐跳首部 。

第6章 HTTP头部
  • End-to-end 端到端头部
    此类头部字段会转发给 请求/响应 的最终接收目标。
    必须保存在由缓存生成的响应头部。
    必须被转发。
  • Hop-by-hop 逐跳首部
    此类头部字段只对单次转发有效。会因为转发给缓存/代理服务器而失效。
    HTTP 1.1 版本之后,如果要使用Hop-by-hop头部字段则需要提供Connection字段。
    除了一下8个字段为逐跳字段,其余均为端到端字段。

    • Connection
    • Keep-Alive
    • Proxy-Authenticate
    • Proxy-Authenrization
    • Trailer
    • TE
    • Tranfer-Encoding
    • Upgrade

来自 https://blog.csdn.net/u010369338/article/details/69397307

HTTPS

  • 超文本传输​​协议安全(HTTPS)是HTTP的安全版本。它使用SSL / TLS进行安全加密通信。
  • SSL(安全套接字层)最初由Netscape在20世纪90年代中期开发,是对HTTP的加密协议增强,它定义了客户端和服务器应如何安全地相互通信。TLS(传输层安全)是SSL的后继者。
  • 通过为客户端和服务器之间的通信提供双向加密,HTTPS连接可以保护数据传输免受中间人攻击和常见安全威胁的侵害。

来自msdn.microsoft.com上 SSL握手的图解表示 - TCP连接> SSL / TLS客户端Hello> SSL / TLS服务器Hello> SSL / TLS证书> SSL / TLS客户端密钥交换> SSL / TLS新会话票证> HTTPS加密数据交换

 HTTPS中的主要问题 – SSL / TLS握手

  • 虽然HTTPS的设计是安全的,但SSL / TLS握手过程在建立HTTPS连接之前会消耗大量时间。它通常需要1-2秒,并且会大大降低网站的启动性能。

HTTP / 2.0和未来

如今,主要的Web服务器和浏览器都在使用上述所有功能。但是像HTTP / 2.0,服务器端事件(SSE)和Websockets等现代增强功能改变了传统HTTP的工作方式。在我的下一篇关于使用HTTP和Websockets的Web API设计的文章中,我们将讨论如何在实际项目中选择它们。

References:

  1. Evolution of HTTP — MDN Web Docs
  2. Hypertext Transfer Protocol (HTTP) Keep-Alive Header — ietf.org
  3. Protocol upgrade mechanism — MDN Web Docs
  4. Brief History of HTTP — High Performance Browser Networking (hpbn.co)

http协议-http2.0原理详细分析

HTTP 2.0是在SPDY(An experimental protocol for a faster web, The Chromium Projects)基础上形成的下一代互联网通信协议。HTTP/2 的目的是通过支持请求与响应的多路复用来较少延迟,通过压缩HTTPS首部字段将协议开销降低,同时增加请求优先级和服务器端推送的支持。
本文目的是学习HTTP 2.0的原理并研究其通信的详细细节。大部分知识点源于《Web性能权威指南》。

1. 二进制分帧层

二进制分帧层,是HTTP 2.0性能增强的核心。
HTTP 1.x在应用层以纯文本的形式进行通信,而HTTP 2.0将所有的传输信息分割为更小的消息和帧,并对它们采用二进制格式编码。这样,客户端和服务端都需要引入新的二进制编码和解码的机制。
如下图所示,HTTP 2.0并没有改变HTTP 1.x的语义,只是在应用层使用二进制分帧方式传输。

因此,也引入了新的通信单位:

1.1 帧(frame)

HTTP 2.0通信的最小单位,包括帧首部、流标识符、优先值和帧净荷等。

其中,帧类型又可以分为:

  • DATA:用于传输HTTP消息体;
  • HEADERS:用于传输首部字段;
  • SETTINGS:用于约定客户端和服务端的配置数据。比如设置初识的双向流量控制窗口大小;
  • WINDOW_UPDATE:用于调整个别流或个别连接的流量
  • PRIORITY: 用于指定或重新指定引用资源的优先级。
  • RST_STREAM: 用于通知流的非正常终止。
  • PUSH_ PROMISE: 服务端推送许可。
  • PING: 用于计算往返时间,执行“ 活性” 检活。
  • GOAWAY: 用于通知对端停止在当前连接中创建流。

标志位用于不同的帧类型定义特定的消息标志。比如DATA帧就可以使用End Stream: true表示该条消息通信完毕。流标识位表示帧所属的流ID。优先值用于HEADERS帧,表示请求优先级。R表示保留位。
下面是Wireshark抓包的一个DATA帧:

1.2 消息(message)

消息是指逻辑上的HTTP消息(请求/响应)。一系列数据帧组成了一个完整的消息。比如一系列DATA帧和一个HEADERS帧组成了请求消息。

1.3 流(stream)

流是连接中的一个虚拟信道,可以承载双向消息传输。每个流有唯一整数标识符。为了防止两端流ID冲突,客户端发起的流具有奇数ID,服务器端发起的流具有偶数ID。
所有HTTP 2. 0 通信都在一个TCP连接上完成, 这个连接可以承载任意数量的双向数据流Stream。 相应地, 每个数据流以 消息的形式发送, 而消息由一 或多个帧组成, 这些帧可以乱序发送, 然后根据每个帧首部的流标识符重新组装。

二进制分帧层保留了HTTP的语义不受影响,包括首部、方法等,在应用层来看,和HTTP 1.x没有差别。同时,所有同主机的通信能够在一个TCP连接上完成。

2. 多路复用共享连接

基于二进制分帧层,HTTP 2.0可以在共享TCP连接的基础上,同时发送请求和响应。HTTP消息被分解为独立的帧,而不破坏消息本身的语义,交错发送出去,最后在另一端根据流ID和首部将它们重新组合起来。
我们来对比下HTTP 1.x和HTTP 2.0,假设不考虑1.x的pipeline机制,双方四层都是一个TCP连接。客户端向服务度发起三个图片请求/image1.jpg,/image2.jpg,/image3.jpg。
HTTP 1.x发起请求是串行的,image1返回后才能再发起image2,image2返回后才能再发起image3。

HTTP 2.0建立一条TCP连接后,并行传输着3个数据流,客户端向服务端乱序发送stream1~3的一系列的DATA帧,与此同时,服务端已经在返回stream 1的DATA帧

性能对比,高下立见。HTTP 2.0成功解决了HTTP 1.x的队首阻塞问题(TCP层的阻塞仍无法解决),同时,也不需要通过pipeline机制多条TCP连接来实现并行请求与响应。减少了TCP连接数对服务器性能也有很大的提升。

3. 请求优先级

流可以带有一个31bit的优先级:

  • 0:表示最高优先级
  • 231-1:表示最低优先级

客户端明确指定优先级,服务端可以根据这个优先级作为依据交互数据,比如客户端优先级设置为.css>.js>.jpg(具体可参见《高性能网站建设指南》), 服务端按优先级返回结果有利于高效利用底层连接,提高用户体验。
然而,也不能过分迷信请求优先级,仍然要注意以下问题:

  • 服务端是否支持请求优先级
  • 会否引起队首阻塞问题,比如高优先级的慢响应请求会阻塞其他资源的交互。

4. 服务端推送

HTTP 2.0增加了服务端推送功能,服务端可以根据客户端的请求,提前返回多个响应,推送额外的资源给客户端。如下图所示,客户端请求stream 1,/page.html。服务端在返回stream 1消息的同时推送了stream 2(/script.js)和stream 4(/style.css)。

PUSH_PROMISE帧是服务端向客户端有意推送资源的信号。

  • 如果客户端不需要服务端Push,可在SETTINGS帧中设定服务端流的值为0,禁用此功能
  • PUSH_PROMISE帧中只包含预推送资源的首部。如果客户端对PUSH_PROMISE帧没有意见,服务端在PUSH_PROMISE帧后发送响应的DATA帧开始推送资源。如果客户端已经缓存该资源,不需要再推送,可以选择拒绝PUSH_PROMISE帧。
  • PUSH_PROMISE必须遵循请求-响应原则,只能借着对请求的响应推送资源。
    目前,Apache的mod_http2能够开启 H2Push on服务端推送Push。Nginx的ngx_http_v2_module还不支持服务端Push。
Apache mod_headers example
<Location /index.html>
    Header add Link "</css/site.css>;rel=preload"
    Header add Link "</images/logo.jpg>;rel=preload"
</Location>

5. 首部压缩

HTTP 1.x每一次通信(请求/响应)都会携带首部信息用于描述资源属性。HTTP 2.0在客户端和服务端之间使用“首部表”来跟踪和存储之前发送的键-值对。首部表在连接过程中始终存在,新增的键-值对会更新到表尾,因此,不需要每次通信都需要再携带首部。

另外,HTTP 2.0使用了首部压缩技术,压缩算法使用HPACK。可让报头更紧凑,更快速传输,有利于移动网络环境。
需要注意的是,HTTP 2.0关注的是首部压缩,而我们常用的gzip等是报文内容(body)的压缩。二者不仅不冲突,且能够一起达到更好的压缩效果。

6. 一个完整的HTTP 2.0通信过程

考虑一个问题,客户端如何知道服务端是否支持HTTP 2.0?是否支持对二进制分帧层的编码和解码?所以,在两端使用HTTP 2.0通信之前,必然存在协议协商的过程。

6.1 基于ALPN的协商过程

支持HTTP 2.0的浏览器可以在TLS会话层自发完成和服务端的协议协商以确定是否使用HTTP 2.0通信。其原理是TLS 1.2中引入了扩展字段,以允许协议的扩展,其中ALPN协议(Application Layer Protocol Negotiation, 应用层协议协商, 前身是NPN)用于客户端和服务端的协议协商过程。
服务端使用ALPN,监听443端口默认提供HTTP 1.1,并允许协商其他协议,比如SPDY和HTTP 2.0。
比如,客户端在TLS握手Client Hello阶段表明自身支持HTTP 2.0

服务端收到后,响应Server Hello,表示自己也支持HTTP 2.0。双方开始HTTP 2.0通信。

6.2 基于HTTP的协商过程

然而,HTTP 2.0一定是HTTPS(TLS 1.2)的特权吗?
当然不是,客户端使用HTTP也可以开启HTTP 2.0通信。只不过因为HTTP 1. 0和HTTP 2. 0都使用同一个 端口(80), 又没有服务器是否支持HTTP 2. 0的其他任何 信息,此时 客户端只能使用HTTP Upgrade机制(OkHttp, nghttp2等组件均可实现,也可以自己编码完成)通过协调确定适当的协议:

HTTP Upgrade request
GET / HTTP/1.1
host: nghttp2.org
connection: Upgrade, HTTP2-Settings
upgrade: h2c        /*发起带有HTTP2.0 Upgrade头部的请求*/       
http2-settings: AAMAAABkAAQAAP__   /*客户端SETTINGS净荷*/
user-agent: nghttp2/1.9.0-DEV

HTTP Upgrade response    
HTTP/1.1 101 Switching Protocols   /*服务端同意升级到HTTP 2.0*/
Connection: Upgrade
Upgrade: h2c

HTTP Upgrade success               /*协商完成*/

6.3 完整通信过程

TCP连接建立:

TLS握手和HTTP 2.0通信过程:

另外,在chrome中通过chrome://net-internals/#http2命令也能捕获HTTP 2.0通信过程:

42072: HTTP2_SESSION
textlink.simba.taobao.com:443 (PROXY 10.19.110.55:8080)
Start Time: 2017-04-05 11:39:11.459

t=370225 [st=    0] +HTTP2_SESSION  [dt=32475+]
                     --> host = "textlink.simba.taobao.com:443"
                     --> proxy = "PROXY 10.19.110.55:8080"
t=370225 [st=    0]    HTTP2_SESSION_INITIALIZED
                       --> protocol = "h2"
                       --> source_dependency = 42027 (PROXY_CLIENT_SOCKET_WRAPPER)
t=370225 [st=    0]    HTTP2_SESSION_SEND_SETTINGS
                       --> settings = ["[id:3 flags:0 value:1000]","[id:4 flags:0 value:6291456]","[id:1 flags:0 value:65536]"]
t=370225 [st=    0]    HTTP2_STREAM_UPDATE_RECV_WINDOW
                       --> delta = 15663105
                       --> window_size = 15728640
t=370225 [st=    0]    HTTP2_SESSION_SENT_WINDOW_UPDATE_FRAME
                       --> delta = 15663105
                       --> stream_id = 0
t=370225 [st=    0]    HTTP2_SESSION_SEND_HEADERS
                       --> exclusive = true
                       --> fin = true
                       --> has_priority = true
                       --> :method: GET
                           :authority: textlink.simba.taobao.com
                           :scheme: https
                           :path: /?name=tbhs&cna=IAj9EOy3fngCAXBQ5kJ9yusH&nn=&count=13&pid=430266_1006&_ksTS=1491363551394_94&callback=jsonp95
                           user-agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36
                           accept: */*
                           referer: https://www.taobao.com/
                           accept-encoding: gzip, deflate, sdch, br
                           accept-language: zh-CN,zh;q=0.8
                           cookie: [382 bytes were stripped]
                       --> parent_stream_id = 0
                       --> stream_id = 1
                       --> weight = 147
t=370256 [st=   31]    HTTP2_SESSION_RECV_SETTINGS
                       --> host = "textlink.simba.taobao.com:443"
t=370256 [st=   31]    HTTP2_SESSION_RECV_SETTING
                       --> flags = 0
                       --> id = 3
                       --> value = 128
t=370256 [st=   31]    HTTP2_SESSION_UPDATE_STREAMS_SEND_WINDOW_SIZE
                       --> delta_window_size = 2147418112
t=370256 [st=   31]    HTTP2_SESSION_RECV_SETTING
                       --> flags = 0
                       --> id = 4
                       --> value = 2147483647
t=370256 [st=   31]    HTTP2_SESSION_RECV_SETTING
                       --> flags = 0
                       --> id = 5
                       --> value = 16777215
t=370256 [st=   31]    HTTP2_SESSION_RECEIVED_WINDOW_UPDATE_FRAME
                       --> delta = 2147418112
                       --> stream_id = 0
t=370256 [st=   31]    HTTP2_SESSION_UPDATE_SEND_WINDOW
                       --> delta = 2147418112
                       --> window_size = 2147483647
t=370261 [st=   36]    HTTP2_SESSION_RECV_HEADERS
                       --> fin = false
                       --> :status: 200
                           date: Wed, 05 Apr 2017 03:39:11 GMT
                           content-type: text/html; charset=ISO-8859-1
                           vary: Accept-Encoding
                           server: Tengine
                           expires: Wed, 05 Apr 2017 03:39:11 GMT
                           cache-control: max-age=0
                           strict-transport-security: max-age=0
                           timing-allow-origin: *
                           content-encoding: gzip
                       --> stream_id = 1
t=370261 [st=   36]    HTTP2_SESSION_RECV_DATA
                       --> fin = false
                       --> size = 58
                       --> stream_id = 1
t=370261 [st=   36]    HTTP2_SESSION_UPDATE_RECV_WINDOW
                       --> delta = -58
                       --> window_size = 15728582
t=370261 [st=   36]    HTTP2_SESSION_RECV_DATA
                       --> fin = true
                       --> size = 0
                       --> stream_id = 1
t=370295 [st=   70]    HTTP2_STREAM_UPDATE_RECV_WINDOW
                       --> delta = 58
                       --> window_size = 15728640
t=402700 [st=32475]

7. HTTP 2.0性能瓶颈

是不是启用HTTP 2.0后性能必然提升了?任何事情都不是绝对的,虽然总体而言性能肯定是能提升的。
我想HTTP 2.0会带来新的性能瓶颈。因为现在所有的压力集中在底层一个TCP连接之上,TCP很可能就是下一个性能瓶颈,比如TCP分组的队首阻塞问题,单个TCP packet丢失导致整个连接阻塞,无法逃避,此时所有消息都会受到影响。未来,服务器端针对HTTP 2.0下的TCP配置优化至关重要,有机会我们再跟进详述。

参考文献

《Web性能权威指南》
《使用 nghttp2 调试 HTTP/2 流量》 https://imququ.com/post/intro-to-nghttp2.html

来自:https://blog.csdn.net/zhuyiquan/article/details/69257126

http协议-http1.1协议分析

学习Web开发不好好学习HTTP报文,将会“打拳不练功,到老一场空”,你花在犯迷糊上的时间比你沉下心来学习HTTP的时间肯定会多很多。

HTTP请求报文解剖

HTTP请求报文由3部分组成(请求行+请求头+请求体):

 

下面是一个实际的请求报文:

①是请求方法,GET和POST是最常见的HTTP方法,除此以外还包括DELETE、HEAD、OPTIONS、PUT、TRACE。不过,当前的大多数浏览器只支持GET和POST,Spring 3.0提供了一个HiddenHttpMethodFilter,允许你通过“_method”的表单参数指定这些特殊的HTTP方法(实际上还是通过POST提交表单)。服务端配置了HiddenHttpMethodFilter后,Spring会根据_method参数指定的值模拟出相应的HTTP方法,这样,就可以使用这些HTTP方法对处理方法进行映射了。

②为请求对应的URL地址,它和报文头的Host属性组成完整的请求URL,③是协议名称及版本号。

④是HTTP的报文头,报文头包含若干个属性,格式为“属性名:属性值”,服务端据此获取客户端的信息。

⑤是报文体,它将一个页面表单中的组件值通过param1=value1&param2=value2的键值对形式编码成一个格式化串,它承载多个请求参数的数据。不但报文体可以传递请求参数,请求URL也可以通过类似于“/chapter15/user.html? param1=value1&param2=value2”的方式传递请求参数。

对照上面的请求报文,我们把它进一步分解,你可以看到一幅更详细的结构图:

引用
HttpWatch是强大的网页数据分析工具,安装后将集成到Internet Explorer工具栏中。它不用代理服务器或一些复杂的网络监控工具,就能抓取请求及响应的完整信息,包括Cookies、消息头、查询参数、响应报文等,是Web应用开发人员的必备工具。

HTTP请求报文头属性

报文头属性是什么东西呢?我们不妨以一个小故事来说明吧。

引用
快到中午了,张三丰不想去食堂吃饭,于是打电话叫外卖:老板,我要一份[鱼香肉丝],要12:30之前给我送过来哦,我在江湖湖公司研发部,叫张三丰。

这里,你要[鱼香肉丝]相当于HTTP报文体,而“12:30之前送过来”,你叫“张三丰”等信息就相当于HTTP的报文头。它们是一些附属信息,帮忙你和饭店老板顺利完成这次交易。

请求HTTP报文和响应HTTP报文都拥有若干个报文关属性,它们是为协助客户端及服务端交易的一些附属信息。

常见的HTTP请求报文头属性

Accept

请求报文可通过一个“Accept”报文头属性告诉服务端 客户端接受什么类型的响应。

如下报文头相当于告诉服务端,俺客户端能够接受的响应类型仅为纯文本数据啊,你丫别发其它什么图片啊,视频啊过来,那样我会歇菜的~~~:

Accept:text/plain  

Accept属性的值可以为一个或多个MIME类型的值,关于MIME类型,大家请参考:http://en.wikipedia.org/wiki/MIME_type

Cookie

客户端的Cookie就是通过这个报文头属性传给服务端的哦!如下所示:

Cookie: $Version=1; Skin=new;jsessionid=5F4771183629C9834F8382E23BE13C4C

服务端是怎么知道客户端的多个请求是隶属于一个Session呢?注意到后台的那个jsessionid=5F4771183629C9834F8382E23BE13C4C木有?原来就是通过HTTP请求报文头的Cookie属性的jsessionid的值关联起来的!(当然也可以通过重写URL的方式将会话ID附带在每个URL的后面哦)。

Referer

表示这个请求是从哪个URL过来的,假如你通过google搜索出一个商家的广告页面,你对这个广告页面感兴趣,鼠标一点发送一个请求报文到商家的网站,这个请求报文的Referer报文头属性值就是http://www.google.com。

引用
唐僧到了西天.
如来问:侬是不是从东土大唐来啊?
唐僧:厉害!你咋知道的!
如来:呵呵,我偷看了你的Referer…

很多貌似神奇的网页监控软件(如著名的 我要啦),只要在你的网页上放上一段JavaScript,就可以帮你监控流量,全国访问客户的分布情况等报表和图表,其原理就是通过这个Referer及其它一些HTTP报文头工作的。

Cache-Control

对缓存进行控制,如一个请求希望响应返回的内容在客户端要被缓存一年,或不希望被缓存就可以通过这个报文头达到目的。

如以下设置,相当于让服务端将对应请求返回的响应内容不要在客户端缓存:

Cache-Control: no-cache

其它请求报文头属性

参见:http://en.wikipedia.org/wiki/List_of_HTTP_header_fields

如何访问请求报文头

由于请求报文头是客户端发过来的,服务端当然只能读取了,以下是HttpServletRequest一些用于读取请求报文头的API:

//获取请求报文中的属性名称  
java.util.Enumeration<java.lang.String>   getHeaderNames();  
  
//获取指定名称的报文头属性的值  
java.lang.String getHeader(java.lang.String name)

由于一些请求报文头属性“太著名”了,因此HttpServletRequest为它们提供了VIP的API:

//获取报文头中的Cookie(读取Cookie的报文头属性)  
 Cookie[]   getCookies() ;  
  
//获取客户端本地化信息(读取 Accept-Language 的报文头属性)  
java.util.Locale    getLocale()   
  
//获取请求报文体的长度(读取Content-Length的报文头属性)  
int getContentLength();

HttpServletRequest可以通过

HttpSession getSession()

获取请求所关联的HttpSession,其内部的机理是通过读取请求报文头中Cookie属性的JSESSIONID的值,在服务端的一个会话Map中,根据这个JSESSIONID获取对应的HttpSession的对象。(这样,你就不会觉得HttpSession很神秘了吧,你自己也可以做一个类似的会话管理  )

HTTP响应报文解剖

响应报文结构

HTTP的响应报文也由三部分组成(响应行+响应头+响应体):

以下是一个实际的HTTP响应报文:

①报文协议及版本;
②状态码及状态描述;
③响应报文头,也是由多个属性组成;
④响应报文体,即我们真正要的“干货”。

响应状态码

和请求报文相比,响应报文多了一个“响应状态码”,它以“清晰明确”的语言告诉客户端本次请求的处理结果。

HTTP的响应状态码由5段组成:

  • 1xx 消息,一般是告诉客户端,请求已经收到了,正在处理,别急…
  • 2xx 处理成功,一般表示:请求收悉、我明白你要的、请求已受理、已经处理完成等信息.
  • 3xx 重定向到其它地方。它让客户端再发起一个请求以完成整个处理。
  • 4xx 处理发生错误,责任在客户端,如客户端的请求一个不存在的资源,客户端未被授权,禁止访问等。
  • 5xx 处理发生错误,责任在服务端,如服务端抛出异常,路由出错,HTTP版本不支持等。

以下是几个常见的状态码:

200 OK

你最希望看到的,即处理成功!

303 See Other

我把你redirect到其它的页面,目标的URL通过响应报文头的Location告诉你。

引用
悟空:师傅给个桃吧,走了一天了
唐僧:我哪有桃啊!去王母娘娘那找吧

304 Not Modified

告诉客户端,你请求的这个资源至你上次取得后,并没有更改,你直接用你本地的缓存吧,我很忙哦,你能不能少来烦我啊!

404 Not Found

你最不希望看到的,即找不到页面。如你在google上找到一个页面,点击这个链接返回404,表示这个页面已经被网站删除了,google那边的记录只是美好的回忆。

500 Internal Server Error

看到这个错误,你就应该查查服务端的日志了,肯定抛出了一堆异常,别睡了,起来改BUG去吧!

其它的状态码参见:http://en.wikipedia.org/wiki/List_of_HTTP_status_codes

有些响应码,Web应用服务器会自动给生成。你可以通过HttpServletResponse的API设置状态码:

//设置状态码,状态码在HttpServletResponse中通过一系列的常量预定义了,如SC_ACCEPTED,SC_OK  
void    setStatus(int sc)

常见的HTTP响应报文头属性

Cache-Control

响应输出到客户端后,服务端通过该报文头属告诉客户端如何控制响应内容的缓存。

下面,的设置让客户端对响应内容缓存3600秒,也即在3600秒内,如果客户再次访问该资源,直接从客户端的缓存中返回内容给客户,不要再从服务端获取(当然,这个功能是靠客户端实现的,服务端只是通过这个属性提示客户端“应该这么做”,做不做,还是决定于客户端,如果是自己宣称支持HTTP的客户端,则就应该这样实现)。

Cache-Control: max-age=3600

ETag

一个代表响应服务端资源(如页面)版本的报文头属性,如果某个服务端资源发生变化了,这个ETag就会相应发生变化。它是Cache-Control的有益补充,可以让客户端“更智能”地处理什么时候要从服务端取资源,什么时候可以直接从缓存中返回响应。

关于ETag的说明,你可以参见:http://en.wikipedia.org/wiki/HTTP_ETag
Spring 3.0还专门为此提供了一个org.springframework.web.filter.ShallowEtagHeaderFilter(实现原理很简单,对JSP输出的内容MD5,这样内容有变化ETag就相应变化了),用于生成响应的ETag,因为这东东确实可以帮助减少请求和响应的交互。

下面是一个ETag:

ETag: "737060cd8c284d8af7ad3082f209582d"

Location

我们在JSP中让页面Redirect到一个某个A页面中,其实是让客户端再发一个请求到A页面,这个需要Redirect到的A页面的URL,其实就是通过响应报文头的Location属性告知客户端的,如下的报文头属性,将使客户端redirect到iteye的首页中:

Location: http://www.iteye.com

Set-Cookie

服务端可以设置客户端的Cookie,其原理就是通过这个响应报文头属性实现的:

Set-Cookie: UserID=JohnDoe; Max-Age=3600; Version=1

其它HTTP响应报文头属性

更多其它的HTTP响应头报文,参见:http://en.wikipedia.org/wiki/List_of_HTTP_header_fields

如何写HTTP请求报文头

在服务端可以通过HttpServletResponse的API写响应报文头的属性:

//添加一个响应报文头属性  
void    setHeader(String name, String value)

象Cookie,Location这些响应都是有福之人,HttpServletResponse为它们都提供了VIP版的API:

//添加Cookie报文头属性  
void addCookie(Cookie cookie)   
  
//不但会设置Location的响应报文头,还会生成303的状态码呢,两者天仙配呢  
void    sendRedirect(String location)

来自:https://blog.csdn.net/u010256388/article/details/68491509

web概述-http发展简史简明版

HTTP协议是如今互联网与服务端技术的基石,HTTP协议的演进也从侧面反应了互联网技术的快速发展。这两天在准备一次关于HTTP1.1协议特性的技术分享过程中,顺便了解了下各版本HTTP协议的特点,在这里做个简单的总结。

维基百科:提到了http协议  各版本的 请求方法

HTTP defines methods (sometimes referred to as verbs) to indicate the desired action to be performed on the identified resource. What this resource represents, whether pre-existing data or data that is generated dynamically, depends on the implementation of the server. Often, the resource corresponds to a file or the output of an executable residing on the server. The HTTP/1.0 specification[13] defined the GET, POST and HEAD methods and the HTTP/1.1 specification[14]added 5 new methods: OPTIONS, PUT, DELETE, TRACE and CONNECT. By being specified in these documents their semantics are well known and can be depended on. Any client can use any method and the server can be configured to support any combination of methods. If a method is unknown to an intermediate, it will be treated as an unsafe and non-idempotent method. There is no limit to the number of methods that can be defined and this allows for future methods to be specified without breaking existing infrastructure. For example, WebDAV defined 7 new methods and RFC 5789specified the PATCH method.

HTTP协议到现在为止总共经历了3个版本的演化,第一个HTTP协议诞生于1989年3月。

1、HTTP 0.9

 

HTTP 0.9是第一个版本的HTTP协议,已过时。它的组成极其简单,只允许客户端发送GET这一种请求,且不支持请求头。由于没有协议头,造成了HTTP 0.9协议只支持一种内容,即纯文本。不过网页仍然支持用HTML语言格式化,同时无法插入图片。

HTTP 0.9具有典型的无状态性,每个事务独立进行处理,事务结束时就释放这个连接。由此可见,HTTP协议的无状态特点在其第一个版本0.9中已经成型。一次HTTP 0.9的传输首先要建立一个由客户端到Web服务器的TCP连接,由客户端发起一个请求,然后由Web服务器返回页面内容,然后连接会关闭。如果请求的页面不存在,也不会返回任何错误码。

HTTP 0.9协议文档:
http://www.w3.org/Protocols/HTTP/AsImplemented.html

 

2、HTTP 1.0

 

HTTP协议的第二个版本,第一个在通讯中指定版本号的HTTP协议版本,至今仍被广泛采用。相对于HTTP 0.9 增加了如下主要特性:

  • 请求与响应支持头域
  • 响应对象以一个响应状态行开始
  • 响应对象不只限于超文本
  • 开始支持客户端通过POST方法向Web服务器提交数据,支持GET、HEAD、POST方法
  • 支持长连接(但默认还是使用短连接),缓存机制,以及身份认证

3、HTTP 1.1

 

HTTP协议的第三个版本是HTTP 1.1,是目前使用最广泛的协议版本 。HTTP 1.1是目前主流的HTTP协议版本,因此这里就多花一些笔墨介绍一下HTTP 1.1的特性。

HTTP 1.1引入了许多关键性能优化:keepalive连接,chunked编码传输,字节范围请求,请求流水线等

  • Persistent Connection(keepalive连接)
    允许HTTP设备在事务处理结束之后将TCP连接保持在打开的状态,一遍未来的HTTP请求重用现在的连接,直到客户端或服务器端决定将其关闭为止。
    在HTTP1.0中使用长连接需要添加请求头 Connection: Keep-Alive,而在HTTP 1.1 所有的连接默认都是长连接,除非特殊声明不支持( HTTP请求报文首部加上Connection: close )

  • chunked编码传输
    该编码将实体分块传送并逐块标明长度,直到长度为0块表示传输结束, 这在实体长度未知时特别有用(比如由数据库动态产生的数据)
  • 字节范围请求
    HTTP1.1支持传送内容的一部分。比方说,当客户端已经有内容的一部分,为了节省带宽,可以只向服务器请求一部分。该功能通过在请求消息中引入了range头域来实现,它允许只请求资源的某个部分。在响应消息中Content-Range头域声明了返回的这部分对象的偏移值和长度。如果服务器相应地返回了对象所请求范围的内容,则响应码206(Partial Content)
  • Pipelining(请求流水线)
    A client that supports persistent connections MAY “pipeline” its requests (i.e., send multiple requests without waiting for each response). A server MUST send its responses to those requests in the same order that the requests were received.(摘自http://www.ietf.org/rfc/rfc2616.txt)

另外,HTTP 1.1还新增了如下特性:

  • 请求消息和响应消息都应支持Host头域
    在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。因此,Host头的引入就很有必要了。
  • 新增了一批Request method
    HTTP1.1增加了OPTIONS,PUT, DELETE, TRACE, CONNECT方法
  • 缓存处理
    HTTP/1.1在1.0的基础上加入了一些cache的新特性,引入了实体标签,一般被称为e-tags,新增更为强大的Cache-Control头。

【上面提到了标准的请求方法总共就8种,外加后面提到的 patch方法  RFC5789

4、HTTP 2.0

自从 RFC2616 即http1.1 协议正式发布
[http 1.1 协议正式发布  http://www.ietf.org/rfc/rfc2616.txt 发布时间:1999年3月

Network Working Group R. Fielding
Request for Comments: 2616 UC Irvine
Obsoletes: 2068 J. Gettys
Category: Standards Track Compaq/W3C
J. Mogul
Compaq
H. Frystyk
W3C/MIT
L. Masinter
Xerox
P. Leach
Microsoft
T. Berners-Lee
W3C/MIT
June 1999

Hypertext Transfer Protocol — HTTP/1.1

。。。。。。。

]
发布以来,一直是互联网发展的基石。HTTP协议也成为了可以在任何领域使用的核心协议,基于这个协议人们设计和部署了越来越多的应用。HTTP的简单本质是其快速发展的关键,但随着越来越多的应用被部署到WEB上,HTTP的问题慢慢凸显出来。今天,用户和开发者都迫切需要通过THHP1.1达到一种几近实时的响应速度和协议性能,而要满足这个需求,只在原有协议上进行修补是不够的。为了应对这些挑战,HTTP必须继续发展。HTTP工作组已经在2012年宣布要设计和开发HTTP2.0。HTTP2.0的主要目标是改进传输性能,实现低延迟和高吞吐量。

在HTTP2.0真正诞生之前,谷歌开发了一个实验性质的协议-SPDY,它定位于解决HTTP1.1中的一些性能限制,来减少网页的延时。自从2009年SPDY发布之后,这个协议得到了众多浏览器厂商和大型网站的支持,实践证明它确实可以很大幅度的提升性能,已经具备成为一个标准的条件。于是,HTTP-WG于2012年初提出要重在SPDY的一些实践基础上新设计和开发HTTP2.0,以期使数据传输具有更好的性能和更少的延时。SPDY是HTTP2.0的先驱,但二者并不能初略的划为等号,SPDY V2草案是HTTP2.0标准制定的起点,从此之后SPDY标准并没有停滞,而是在不断进化,它成为了HTTP2.0新功能及新建议的实验场,为HTTP2.0标准收纳的每一项建议,提供事前的测试和评估手段,总体来说SPDY比HTTP2.0更为激进。HTTP2.0协议版本发布历程如下:

  • 2012年3月,HTTP2.0征集建议;
  • 2012年11月,HTTP2.0第一稿;
  • 2014年8月,HTTP2.0 draft-17和HPACK draft-12发布;
  • 2014年8月,工作组最后征集HTTP2.0建议;
  • 2015年2月,IESG批准HTTP2.0和HPACK草稿;
  • 2015年5月,RFC 7540 (HTTP2.0) 和 RFC 7541 (HPACK) 发布;

在新的协议中,将会从根本上解决以往HTTP1.1版本中所做的“特殊优化”,将在这些解决方案内置在传输层中,使数据传输更加便捷和高效,如HTTP1.1及以前的版本中影响性能的很大一个问题,就是队首阻塞问题,在HTTP2.0中会将会通过新的组帧机制来解决这个问题,使连接可以多路复用,再通过压缩HTTP首部字段将协议开销降到最低。HTTP2.0不会改动HTTP语义,很好的继承以往版本的HTTP方法、状态码、URI及首部字段等核心概念,下面将对这些内容进行细致的描述。(本文最初发布于公司内网,外网原文地址:腾云阁HTTP 2.0 简明笔记)

HTTP 2.0是下一代HTTP协议,目前应用还非常少。主要特点有:

  • 多路复用(二进制分帧)
    HTTP 2.0最大的特点: 不会改动HTTP 的语义,HTTP 方法、状态码、URI 及首部字段,等等这些核心概念上一如往常,却能致力于突破上一代标准的性能限制,改进传输性能,实现低延迟和高吞吐量。而之所以叫2.0,是在于新增的二进制分帧层。在二进制分帧层上, HTTP 2.0 会将所有传输的信息分割为更小的消息和帧,并对它们采用二进制格式的编码 ,其中HTTP1.x的首部信息会被封装到Headers帧,而我们的request body则封装到Data帧里面。

    HTTP 2.0 通信都在一个连接上完成,这个连接可以承载任意数量的双向数据流。相应地,每个数据流以消息的形式发送,而消息由一或多个帧组成,这些帧可以乱序发送,然后再根据每个帧首部的流标识符重新组装。
  • 头部压缩
    当一个客户端向相同服务器请求许多资源时,像来自同一个网页的图像,将会有大量的请求看上去几乎同样的,这就需要压缩技术对付这种几乎相同的信息。
  • 随时复位
    HTTP1.1一个缺点是当HTTP信息有一定长度大小数据传输时,你不能方便地随时停止它,中断TCP连接的代价是昂贵的。使用HTTP2的RST_STREAM将能方便停止一个信息传输,启动新的信息,在不中断连接的情况下提高带宽利用效率。
  • 服务器端推流: Server Push
    客户端请求一个资源X,服务器端判断也许客户端还需要资源Z,在无需事先询问客户端情况下将资源Z推送到客户端,客户端接受到后,可以缓存起来以备后用。
  • 优先权和依赖
    每个流都有自己的优先级别,会表明哪个流是最重要的,客户端会指定哪个流是最重要的,有一些依赖参数,这样一个流可以依赖另外一个流。优先级别可以在运行时动态改变,当用户滚动页面时,可以告诉浏览器哪个图像是最重要的,你也可以在一组流中进行优先筛选,能够突然抓住重点流。
来自微信公众号:测试那点事儿

参考资料

Http 0.9:http://www.w3.org/Protocols/HTTP/AsImplemented.html
Http 1.0:http://www.ietf.org/rfc/rfc1945.txt
Http 1.1:http://www.ietf.org/rfc/rfc2616.txt
Http 2.0:https://tools.ietf.org/html/rfc5741


Http Keep-Alive seems to be massively misunderstood. Here’s a short description of how it works, under both 1.0 and 1.1

HTTP/1.0

Under HTTP 1.0, there is no official specification for how keepalive operates. It was, in essence, tacked on to an existing protocol. If the browser supports keep-alive, it adds an additional header to the request:

ConnectionKeep-Alive

Then, when the server receives this request and generates a response, it also adds a header to the response:

ConnectionKeep-Alive

Following this, the connection is NOT dropped, but is instead kept open. When the client sends another request, it uses the same connection. This will continue until either the client or the server decides that the conversation is over, and one of them drops the connection.

HTTP/1.1

Under HTTP 1.1, the official keepalive method is different. All connections are kept alive, unless stated otherwise with the following header:

Connection: close

The ConnectionKeep-Alive header no longer has any meaning because of this.

Additionally, an optional Keep-Alive: header is described, but is so underspecified as to be meaningless. Avoid it.

Not reliable

HTTP is a stateless protocol – this means that every request is independent of every other. Keep alive doesn’t change that. Additionally, there is no guarantee that the client or the server will keep the connection open. Even in 1.1, all that is promised is that you will probably get a notice that theconnection is being closed. So keepalive is something you should not write your application to rely upon.

KeepAlive and POST

The HTTP 1.1 spec states that following the body of a POST, there are to be no additional characters. It also states that “certain” browsers may not follow this spec, putting a CRLF after the body of the POST. Mmm-hmm. As near as I can tell, most browsers follow a POSTed body with a CRLF. There are two ways of dealing with this: Disallow keepalive in the context of a POST request, or ignore CRLF on a line by itself. Most servers deal with this in the latter way, but there’s no way to know how a server will handle it without testing.


https://tools.ietf.org/html/rfc7230#appendix-A.1.2

A.1.2. Keep-Alive Connections

   In HTTP/1.0, each connection is established by the client prior to
   the request and closed by the server after sending the response.
   However, some implementations implement the explicitly negotiated
   ("Keep-Alive") version of persistent connections described in Section
   19.7.1 of [RFC2068].

   Some clients and servers might wish to be compatible with these
   previous approaches to persistent connections, by explicitly
   negotiating for them with a "Connection: keep-alive" request header
   field.  However, some experimental implementations of HTTP/1.0
   persistent connections are faulty; for example, if an HTTP/1.0 proxy
   server doesn't understand Connection, it will erroneously forward
   that header field to the next inbound server, which would result in a
   hung connection.

   One attempted solution was the introduction of a Proxy-Connection
   header field, targeted specifically at proxies.  In practice, this
   was also unworkable, because proxies are often deployed in multiple
   layers, bringing about the same problem discussed above.

   As a result, clients are encouraged not to send the Proxy-Connection
   header field in any requests.

   Clients are also encouraged to consider the use of Connection:
   keep-alive in requests carefully; while they can enable persistent
   connections with HTTP/1.0 servers, clients using them will need to
   monitor the connection for "hung" requests (which indicate that the
   client ought stop sending the header field), and this mechanism ought
   not be used by clients at all when a proxy is being used.