HTML5在前端的发展已经进入到了高潮阶段了,特别是在移动端已经是跨平台开发的首选技术,通过hybird相关技术实现和native无缝的对接(这方面本人不是很了解,只是听到过,貌似某宝的移动端app都是HTML5实现)。HTML5里面除了改进了HTML,CSS和javascript相关前端技术规范,也对前端和后端交互协议提出了新的方式,从而提高前端和后端通信的效率,它就是WebSocket。
如果要说什么是Websocket,那么一句话就可以描述它:就是一个浏览器端全双工的TCP长连接。这句话里面两个词能够表达WebSocket特性:全双工,长连接。什么叫全双工?就是客户端和服务器之间建立连接之后可以来回的读写交互操作,服务器可以主动发送消息给客户端,客户端也可以主动发送消息给服务端,注意这些操作都是在一次连接中。那什么叫长链接?就是建立连接后,不回立即断开和服务器的连接(也可以理解是一个有状态的连接,和HTTP无状态连接进行区分)。那么Websocket可以理解是在服务器和客户端之间有了一条高速公路,双方可以自由的来回运输“物资”。
要回答这个问题,需要有特定的业务场景。当前我们解决web端实时交互,一般是通过ajax轮询。在设定的时间周期后定时访问后端获取最新的数据。比如:我们要开发一个web版的客服系统,我们在前端就要定时的ajax请求服务端获取最新的聊天信息。以上这些都是基于HTTP协议实现的,HTTP协议是基于TCP/IP的短连接,虽然在http头有keep-alive属性标识连接可以存活一段时间,但也有一定的局限性;并且HTTP另一个很累赘的地方是每发起一次HTTP交互都需要带上HTTP头信息,一般都在十几个字节左右,假如我们每次只是和服务器同步状态,其实消息就几个字节,但是还是需要带上十几个字节的消息头,这严重降低了网络的利用率。
上面介绍了HTTP两个主要的缺陷(短连接,较大的消息头)。为了避免这两个缺陷,于是便有了Websocket,上面说过他是一个长TCP连接,这就避免了HTTP端每次建立TCP连接消耗的时间;Websocket对客户端和服务端交互的消息格式上面进行了精简,整个消息头缩减到两个字节(16bit)左右,从而提高了整个网络的利用率。所以可以说websocket是为了解决HTTP在实时性比较高的系统上面所遇到的短板而设计出来的。但是也存在一定的缺陷,如果过多的使用WebSocket,由于是长TCP连接,那么对客户端和服务器端一直需要保持连接开启,对资源消耗比较厉害。另一个是虽然Websocket的消息头虽然很精简,但是都已经不是明文,可能对调试起来带来一定的困难,毕竟HTTP只要打印出来都可以懂。
要知道websocket是如何工作的需要知道websocket协议整个过程中做了哪些事情?
这是websocket建立连接的首要完成的事情,由客户端向服务端发起一次握手,于是便建立了和服务器之间的TCP长连接。这个过程基本上是基于HTTP协议来实现的,建立连接之后便是通过websocket协议来通信。下面看看这次握手客户端和服务器端是怎么交流的。
客户端发起的握手请求
GET ws://localhost:8080/ HTTP/1.1
Pragma: no-cache
Host: localhost:8080
Sec-WebSocket-Key: /u/BkxWUx5qWj+HaQemkpg==
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1653.0 Safari/537.36
Upgrade: websocket
Sec-WebSocket-Extensions: x-webkit-deflate-frame
Cache-Control: no-cache
Cookie:“c7a8217cbe2e207038395”
Connection: Upgrade
Sec-WebSocket-Version: 13
上面是一个标准的HTTP头信息,只是添加了一些扩展的消息头。其中Connection: Upgrade表示当前是HTTP协议的升级版本,ws://localhost:8080/ 其中“http”被替换成“ws”表示是websocket协议,而非http协议。Upgrade: websocket 表示当前协议升级为Websocket协议。这些扩展头信息中 Sec-WebSocket-Key: /u/BkxWUx5qWj+HaQemkpg== 这部分内容是最重要的,这是我是过程中的密钥。服务器会根据这个值来产生一个值反馈给客户端,如果客户端校验成功,则连接建立成功。那么看看服务器响应的消息是什么样的
服务端响应握手请求
HTTP/1.1 101 Switching Protocols
Date: Fri Dec 12 09:59:23 CST 2014
Access-Control-Allow-Credentials: true
Sec-WebSocket-Accept: nnTiIdVxrAUgekCw03CBJRjS6DM=
Server: bieber websocket server
Connection: Upgrade
Access-Control-Allow-Headers: content-type
Upgrade: WebSocket
可以看到服务器端响应也是一个标准的HTTP协议。也是在里面扩展了几个头信息,其中只需要关注Sec-WebSocket-Accept: nnTiIdVxrAUgekCw03CBJRjS6DM= 这个消息,这个值是根据客户端的 Sec-WebSocket-Key 值生成的具体生成规则如下: base64(sha1(Sec-WebSocket-Key+258EAFA5-E914-47DA-95CA-C5AB0DC85B11))
通过上面一次几乎是标准的HTTP协议的交互则完成的websocket协议的连接建立,后面就可以基于这个连接服务端和客户端进行交互了,而不需要再次简历连接。
通过握手成功,那么客户端和服务器端就可以通信了。这里将给大家介绍一下Websocket如何描述一个消息的。先来看看一个消息格式是怎么样的。
图中基本上就两个字节来描述消息包的内容。下面对这个消息包进行介绍一下:
FIN:1位
表示这是消息的最后一帧(结束帧),一个消息由一个或多个数据帧构成。若消息由一帧构成,起始帧即结束帧。
RSV1,RSV2,RSV3:各1位
这几位是预留的扩展,如果没有扩展的时候每个位都为0,否则为1
OPCODE:4位
解释PayloadData,如果接收到未知的opcode,接收端必须关闭连接。
0x0表示附加数据帧
0x1表示文本数据帧
0x2表示二进制数据帧
0x3-7暂时无定义,为以后的非控制帧保留
0x8表示连接关闭
0x9表示ping
0xA表示pong
0xB-F暂时无定义,为以后的控制帧保留
MASK:1位
用于标识PayloadData是否经过掩码处理。如果是1,Masking-key域的数据即是掩码密钥,用于解码PayloadData。客户端发出的数据帧需要进行掩码处理,所以此位是1。
Payload length:7位,7+16位,7+64位
PayloadData的长度(以字节为单位)。
如果其值在0-125,则是payload的真实长度。
如果值是126,则后面2个字节形成的16位无符号整型数的值是payload的真实长度。注意,网络字节序,需要转换。
如果值是127,则后面8个字节形成的64位无符号整型数的值是payload的真实长度。注意,网络字节序,需要转换。
长度表示遵循一个原则,用最少的字节表示长度(我理解是尽量减少不必要的传输)。举例说,payload真实长度是124,在0-125之间,必须用前7位表示;不允许长度1是126或127,然后长度2是124,这样违反原则。
Payload长度是ExtensionData长度与ApplicationData长度之和。ExtensionData长度可能是0,这种情况下,Payload长度即是ApplicationData长度。
以上是websocket建立连接之后客户端和服务器之间交互的消息格式。这也是官方给出的解释。本人也基于官方给出的解释以及网上收集的资料实现了一个简单的websocket客户端,实现里面没有考虑Payload length超过125以后的情况,只考虑在0-125之内的 情况。
关于实现方面需要注意几点的是:
服务器端接受客户端发送的消息的时候由于浏览器端一般都会带上一个四位的掩码,那么我们接收的消息是通过掩码计算过的,服务端也必须进行相关的解码才能获取真正的消息内容。具体解码方式是:
for(int i=0;i<messagesize;i++){//messagesize是消息的字节数
realyMessage=receiveMessage[i]^maskKey[i%4];
}
这样就可以获取真正的消息内容。
那么服务器向服务器发送消息的时候,也是按照上面的消息格式,但是客户端不接受掩码计算过的,所以消息包中的Maks位置应该是0,而消息体就是消息内容的字节数组。
既然Websocket这么好,那么现在主流的浏览器哪些支持websocket呢?下面给出一个列表:
我这里通过Netty来包装了一下WebSocket协议,从而实现服务端,下面贴出实现代码:
public class WebSocketServerHandler extends ChannelHandlerAdapter {
private static final String key="258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
ByteArrayOutputStream inputStream = new ByteArrayOutputStream();
String secKey=null;
while(byteBuf.isReadable()){
byte c = byteBuf.readByte();
if(c=='\n'){
String content = new String(inputStream.toByteArray());
System.out.println(content);
inputStream.reset();
if(content.startsWith("Sec-WebSocket-Key")){
secKey=content.replaceAll("Sec-WebSocket-Key:","");
secKey=secKey.trim();
}
}else{
inputStream.write(c);
}
}
ReferenceCountUtil.release(msg);
byte[] bytes=null;
if(secKey!=null){//接受的是握手请求
MessageDigest messageDigest = MessageDigest.getInstance("SHA1");
secKey+=key;
messageDigest.update(secKey.getBytes());
secKey=Base64.encodeBase64String(messageDigest.digest());
StringBuffer response = new StringBuffer();
response.append("HTTP/1.1 101 Switching Protocols\r\n");
response.append("Connection:Upgrade\r\n");
response.append("Server:bieber websocket server\r\n");
response.append("Upgrade:WebSocket\r\n");
response.append("Date:").append(new Date()).append("\r\n");
response.append("Access-Control-Allow-Credentials:true\r\n");
response.append("Access-Control-Allow-Headers:content-type\r\n");
response.append("Sec-WebSocket-Accept:").append(secKey).append("\r\n");
response.append("\r\n");
System.out.println(response.toString());
bytes = response.toString().getBytes();
ByteBuf outByte = ctx.alloc().buffer(bytes.length);
outByte.writeBytes(bytes);
ctx.writeAndFlush(outByte);
}else{//接受的是消息
ByteBuf requestBytes = ctx.alloc().buffer(inputStream.size());
requestBytes.writeBytes(inputStream.toByteArray());
requestBytes.readByte();//FIN,RSV1, RSV2, RSV3,Opcode
byte lengthByte=requestBytes.readByte();//Mask 1,Payload length 7 11010100 01111111
int lengthInt = lengthByte;
lengthInt=lengthInt&127;//01111111,可以屏蔽mask位的内容,从而得到纯消息长度位
System.out.println("message size "+lengthInt);
byte[] maskingKeys = new byte[4];
requestBytes.readBytes(maskingKeys,0,4);
byte[] clientByte=new byte[lengthInt];
for(int i=0;i<lengthInt;i++){
clientByte[i]=(byte)(requestBytes.readByte()^maskingKeys[i%4]);//将接受的消息解码
}
String sendContent = new String(clientByte);
System.out.println(sendContent);
byte[] responseBytes = sendContent.getBytes();
int responseContentSize = responseBytes.length;
ByteBuf responseByte = ctx.alloc().buffer(2+responseContentSize);//1一个头,1个length
responseByte.writeByte(128|1);//FIN,RSV1, RSV2, RSV3,Opcode 10000000|00000001
responseByte.writeByte(0|responseContentSize);//Mask 1,Payload length 7 00000000|消息长度(在0-125之间),保持MASK位为0
responseByte.writeBytes(responseBytes);
ctx.writeAndFlush(responseByte);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
<!-- lang:java -->
public class WebSocketServer {
private int port;
public WebSocketServer(int port){
this.port = port;
}
public void run() throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<io.netty.channel.socket.SocketChannel>() {
@Override
protected void initChannel(io.netty.channel.socket.SocketChannel ch) throws Exception {
ch.pipeline().addLast(new WebSocketServerHandler());
}
}).option(ChannelOption.SO_BACKLOG, 128).childOption(ChannelOption.SO_KEEPALIVE, true);
ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
channelFuture.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
int port;
if (args.length > 0) {
port = Integer.parseInt(args[0]);
} else {
port = 8080;
}
new WebSocketServer(port).run();
}
}
如果支持websocket的浏览器会有WebSocket类。通过在javascript里面实例化这个类调用它的相关方法即可实现和服务器端通过websocket来实现通信。我这里列举出简单的使用:
var ws = new WebSocket("ws://localhost:8080");
ws.onmessage=function(msg){
console.log(msg.data);
};
ws.send("hello world!");
想了解websocket协议更多的内容可以看官方的网站介绍:http://www.websocket.org