WebSocket从零开始

##WebSocket从零开始 HTML5在前端的发展已经进入到了高潮阶段了,特别是在移动端已是跨平台开发的首选技术,经过hybird相关技术实现和native无缝的对接(这方面本人不是很了解,只是听到过,貌似某宝的移动端app都是HTML5实现)。HTML5里面除了改进了HTML,CSS和javascript相关前端技术规范,也对前端和后端交互协议提出了新的方式,从而提升前端和后端通讯的效率,它就是WebSocket。 ###What is WebSocket 若是要说什么是Websocket,那么一句话就能够描述它:就是一个浏览器端全双工的TCP长链接。这句话里面两个词可以表达WebSocket特性:全双工,长链接。什么叫全双工?就是客户端和服务器之间创建链接以后能够来回的读写交互操做,服务器能够主动发送消息给客户端,客户端也能够主动发送消息给服务端,注意这些操做都是在一次链接中。那什么叫长连接?就是创建链接后,不回当即断开和服务器的链接(也能够理解是一个有状态的链接,和HTTP无状态链接进行区分)。那么Websocket能够理解是在服务器和客户端之间有了一条高速公路,双方能够自由的来回运输“物资”。<br> ###Why need WebSocket 要回答这个问题,须要有特定的业务场景。当前咱们解决web端实时交互,通常是经过ajax轮询。在设定的时间周期后定时访问后端获取最新的数据。好比:咱们要开发一个web版的客服系统,咱们在前端就要定时的ajax请求服务端获取最新的聊天信息。以上这些都是基于HTTP协议实现的,HTTP协议是基于TCP/IP的短链接,虽然在http头有keep-alive属性标识链接能够存活一段时间,但也有必定的局限性;而且HTTP另外一个很累赘的地方是每发起一次HTTP交互都须要带上HTTP头信息,通常都在十几个字节左右,假如咱们每次只是和服务器同步状态,其实消息就几个字节,可是仍是须要带上十几个字节的消息头,这严重下降了网络的利用率。<br>javascript

上面介绍了HTTP两个主要的缺陷(短链接,较大的消息头)。为了不这两个缺陷,因而便有了Websocket,上面说过他是一个长TCP链接,这就避免了HTTP端每次创建TCP链接消耗的时间;Websocket对客户端和服务端交互的消息格式上面进行了精简,整个消息头缩减到两个字节(16bit)左右,从而提升了整个网络的利用率。因此能够说websocket是为了解决HTTP在实时性比较高的系统上面所遇到的短板而设计出来的。可是也存在必定的缺陷,若是过多的使用WebSocket,因为是长TCP链接,那么对客户端和服务器端一直须要保持链接开启,对资源消耗比较厉害。另外一个是虽然Websocket的消息头虽然很精简,可是都已经不是明文,可能对调试起来带来必定的困难,毕竟HTTP只要打印出来均可以懂。前端

###How WebSocket worked 要知道websocket是如何工做的须要知道websocket协议整个过程当中作了哪些事情? ####Shake hand 这是websocket创建链接的首要完成的事情,由客户端向服务端发起一次握手,因而便创建了和服务器之间的TCP长链接。这个过程基本上是基于HTTP协议来实现的,创建链接以后即是经过websocket协议来通讯。下面看看此次握手客户端和服务器端是怎么交流的。<br>java

客户端发起的握手请求web

GET ws://localhost:8080/ HTTP/1.1<br> Pragma: no-cache<br> Host: localhost:8080<br> Sec-WebSocket-Key: /u/BkxWUx5qWj+HaQemkpg==<br> User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1653.0 Safari/537.36<br> Upgrade: websocket<br> Sec-WebSocket-Extensions: x-webkit-deflate-frame<br> Cache-Control: no-cache<br> Cookie:"c7a8217cbe2e207038395"<br> Connection: Upgrade<br> Sec-WebSocket-Version: 13<br>ajax

上面是一个标准的HTTP头信息,只是添加了一些扩展的消息头。其中Connection: Upgrade表示当前是HTTP协议的升级版本,ws://localhost:8080/ 其中“http”被替换成“ws”表示是websocket协议,而非http协议。Upgrade: websocket 表示当前协议升级为Websocket协议。这些扩展头信息中 Sec-WebSocket-Key: /u/BkxWUx5qWj+HaQemkpg== 这部份内容是最重要的,这是我是过程当中的密钥。服务器会根据这个值来产生一个值反馈给客户端,若是客户端校验成功,则链接创建成功。那么看看服务器响应的消息是什么样的<br> 服务端响应握手请求后端

HTTP/1.1 101 Switching Protocols<br> Date: Fri Dec 12 09:59:23 CST 2014<br> Access-Control-Allow-Credentials: true<br> Sec-WebSocket-Accept: nnTiIdVxrAUgekCw03CBJRjS6DM=<br> Server: bieber websocket server<br> Connection: Upgrade<br> Access-Control-Allow-Headers: content-type<br> Upgrade: WebSocket<br>数组

能够看到服务器端响应也是一个标准的HTTP协议。也是在里面扩展了几个头信息,其中只须要关注Sec-WebSocket-Accept: nnTiIdVxrAUgekCw03CBJRjS6DM= 这个消息,这个值是根据客户端的 Sec-WebSocket-Key 值生成的具体生成规则以下: base64(sha1(Sec-WebSocket-Key+258EAFA5-E914-47DA-95CA-C5AB0DC85B11))浏览器

经过上面一次几乎是标准的HTTP协议的交互则完成的websocket协议的链接创建,后面就能够基于这个链接服务端和客户端进行交互了,而不须要再次简历链接。服务器

####Send message 经过握手成功,那么客户端和服务器端就能够通讯了。这里将给你们介绍一下Websocket如何描述一个消息的。先来看看一个消息格式是怎么样的。websocket

图中基本上就两个字节来描述消息包的内容。下面对这个消息包进行介绍一下:

FIN:1位<br> 表示这是消息的最后一帧(结束帧),一个消息由一个或多个数据帧构成。若消息由一帧构成,起始帧即结束帧。<br> RSV1,RSV2,RSV3:各1位<br> 这几位是预留的扩展,若是没有扩展的时候每一个位都为0,不然为1<br> OPCODE:4位<br> 解释PayloadData,若是接收到未知的opcode,接收端必须关闭链接。<br> 0x0表示附加数据帧<br> 0x1表示文本数据帧<br> 0x2表示二进制数据帧<br> 0x3-7暂时无定义,为之后的非控制帧保留<br> 0x8表示链接关闭<br> 0x9表示ping<br> 0xA表示pong<br> 0xB-F暂时无定义,为之后的控制帧保留<br> MASK:1位<br> 用于标识PayloadData是否通过掩码处理。若是是1,Masking-key域的数据便是掩码密钥,用于解码PayloadData。客户端发出的数据帧须要进行掩码处理,因此此位是1。<br> Payload length:7位,7+16位,7+64位<br> PayloadData的长度(以字节为单位)。<br> 若是其值在0-125,则是payload的真实长度。<br> 若是值是126,则后面2个字节造成的16位无符号整型数的值是payload的真实长度。注意,网络字节序,须要转换。<br> 若是值是127,则后面8个字节造成的64位无符号整型数的值是payload的真实长度。注意,网络字节序,须要转换。<br> 长度表示遵循一个原则,用最少的字节表示长度(我理解是尽可能减小没必要要的传输)。举例说,payload真实长度是124,在0-125之间,必须用前7位表示;不容许长度1是126或127,而后长度2是124,这样违反原则。 Payload长度是ExtensionData长度与ApplicationData长度之和。ExtensionData长度多是0,这种状况下,Payload长度便是ApplicationData长度。<br>

以上是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协议,从而实现服务端,下面贴出实现代码:

<!-- lang:java -->
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来实现通讯。我这里列举出简单的使用:

<!-- lang:javascript -->
var ws = new WebSocket("ws://localhost:8080");
ws.onmessage=function(msg){
	console.log(msg.data);	
};
ws.send("hello world!");

想了解websocket协议更多的内容能够看官方的网站介绍:http://www.websocket.org