WebSocket协议开发
一,背景
一直以来,网络在很大程度上都是围绕着HTTP的请求/响应模式而构建的。所有HTTP通信仍然是客户端控制的,需要用户进行互动或定期轮询,从服务端加载新数据。
HTTP协议的弊端如下:
(1)HTTP协议为半双工协议。数据在客户端和服务端两个方向上传输,但是不能同时传输。这意味着在同一个时刻,只有一个方向上的数据传送。
(2)HTTP消息冗长而繁琐。HTTP消息包括消息头,消息体,换行符等,通常情况下采用文本方式传输,相比于其他的二进制通信协议,冗长而繁琐。
(3)针对服务器推送的黑客攻击。利用长时间轮询的方式。比较新的一种轮询技术是Comet,使用了Ajax。这种技术虽然可以达到双向通信,但依然需要发出请求,而且在Comet中,普遍采用了长连接,这也会大量消耗服务器带宽和资源。
为了解决这些问题,WebSocket将网络套接字引入到了客户端和服务端,浏览器和服务器之间可以通过套接字建立持久的连接,双方随时都可以互发数据给对方,而不是之前由客户端控制的“请求-应答模式”。
二,WebSocket协议简介
WebSocket是HTML5开始提供的一种浏览器与服务器间进行全双工通信的网络技术,WebSocket通信协议于2011年被IETF定为标准RFC6455,WebSocket API被W3C定为标准。
在WebSocket API中,浏览器和服务器只需要做一个握手的动作,两者就可以直接互相传送数据了。WebSocket基于TCP双向全双工进行消息传递,在同一时刻,既可以发送消息,也可以接收消息,相比于HTTP的半双工协议,性能得到很大的提升。
WebSocket的特点:
1,单一的TCP连接,采用全双工模型通信。
2,对代理,防火墙和路由器透明。
3,无头部信息,Cookie和身份验证。
4,无安全开销。
5,通过"ping/pong"帧保持链路激活。
6,服务器可以主动传递消息给客户端,不再需要客户端轮询。
WebSocket设计出来的目的就是取代轮询和Comet技术,使客户端浏览器具备像C/S架构下桌面系统一样实时通信能力。WebSocket连接本质上就是一个TCP连接,所以在数据传输的稳定性和数据传输量的大小方面,和轮询以及Comet技术相比,具有很大的性能优势。
WebSocket连接建立过程:
1,客户端或者浏览器发出握手请求,请求消息示例如下:
GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGHHUkshfKKJHJKKJ== Origin: http://example.com Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13
这个请求和通常的HTTP请求不同,包含了一些附加头信息,其中附加头信息“Upgrade:WebSocket"表明这是一个申请协议升级的HTTP请求。
2,服务端解析这些附加的头信息,然后生成应答信息返回给客户端,客户端和服务器端的WebSocket连接就建立起来了,双方可以通过这个连接通道自由地传递信息,并且这个连接会持续存在直到客户端或者服务器端的某一方主动关闭连接。
服务端返回给客户端的应答消息如下:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhsfsfskfhsk== Sec-WebSocket-Protocol: chat
WebSocket的生命周期
三次握手成功之后,服务端和客户端就可以通过”messages"的方式进行通信了,一个消息有一个或者多个帧组成,WebSocket的消息并不一定对应一个特定网络层的帧,它可以被分割成多个帧或者被合并。
WebSocket的握手连接关闭消息带有一个状态码和一个可选的关闭原因,它必须按照协议要求发送一个Close控制帧,当对端接收到关闭控制帧指令时,需要主动关闭WebSocket连接。
三,基于Netty的开发实例
Netty基于HTTP协议栈开发了WebSocket协议栈,利用Netty的WebSocket协议栈可以非常方便地开发出WebSocket客户端和服务端。
WebSocket服务端的功能如下:支持WebSocket的浏览器通过WebSocket协议发送请求消息给服务端,服务端对消息进行判断,如果是合法的WebSocket请求,则获取请求消息体,并在后面追加字符串。
客户端HTML通过内嵌JS脚本创建WebSocket连接。
WebSocket服务端代码如下:
package com.huawei.netty.websocket; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.stream.ChunkedWriteHandler; /** * Created by liuzhengqiu on 2017/11/13. */ public class WebSocketServer { public void run(int port) throws Exception { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel socketChannel) throws Exception { ChannelPipeline pipeline = socketChannel.pipeline(); pipeline.addLast("http-codec", new HttpServerCodec()) .addLast("aggregator", new HttpObjectAggregator(65536)) .addLast("http-chunked", new ChunkedWriteHandler()) .addLast("handler", new WebSocketServerHandler()); } }); Channel channel = serverBootstrap.bind(port).sync().channel(); System.out.println("Web socket server started at port"+port+"."); System.out.println("Open your browser and navigate to http://localhost:"+port+"/"); channel.closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } public static void main(String[] args) throws Exception { new WebSocketServer().run(8080); } }
package com.huawei.netty.websocket; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.*; import io.netty.handler.codec.http.websocketx.*; import io.netty.util.CharsetUtil; import static io.netty.handler.codec.http.HttpHeaders.isKeepAlive; import static io.netty.handler.codec.http.HttpHeaders.setContentLength; /** * Created by liuzhengqiu on 2017/11/13. */ public class WebSocketServerHandler extends SimpleChannelInboundHandler<Object> { private WebSocketServerHandshaker handshaker; @Override protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object msg) throws Exception { if (msg instanceof FullHttpRequest) { handleHttpRequest(channelHandlerContext,(FullHttpRequest)msg); } else if (msg instanceof WebSocketFrame) { handleWebSocketFrame(channelHandlerContext,(WebSocketFrame)msg); } } @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { ctx.flush(); } private void handleWebSocketFrame(ChannelHandlerContext ctx,WebSocketFrame frame) { if (frame instanceof CloseWebSocketFrame) { handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain()); return; } if (frame instanceof PingWebSocketFrame) { ctx.channel().write(new PongWebSocketFrame(frame.content().retain())); return ; } if (!(frame instanceof TextWebSocketFrame)) { throw new UnsupportedOperationException(String.format("%s frame types not supported",frame.getClass().getName())); } String request = ((TextWebSocketFrame) frame).text(); ctx.channel().write( new TextWebSocketFrame(request+",欢迎使用Netty WebSocket服务,现在时刻:" + new java.util.Date().toString()) ); } private void handleHttpRequest(ChannelHandlerContext ctx,FullHttpRequest request) throws Exception { if (!request.getDecoderResult().isSuccess() || (!"websocket".equals(request.headers().get("Upgrade")))) { sendHttpResponse(ctx,request,new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST)); return; } WebSocketServerHandshakerFactory webSocketServerHandshakerFactory = new WebSocketServerHandshakerFactory( "ws://localhost:8080/websocket",null,false ); handshaker = webSocketServerHandshakerFactory.newHandshaker(request); if (handshaker == null) { WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel()); } else { handshaker.handshake(ctx.channel(),request); } } private static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest request, FullHttpResponse response) { if (response.getStatus().code() != 200) { ByteBuf buf = Unpooled.copiedBuffer(response.getStatus().toString(), CharsetUtil.UTF_8); response.content().writeBytes(buf); buf.release(); setContentLength(response,response.content().readableBytes()); } ChannelFuture channelFuture = ctx.channel().writeAndFlush(response); if (!isKeepAlive(request) || response.getStatus().code() != 200) { channelFuture.addListener(ChannelFutureListener.CLOSE); } } }
客户端代码如下:
<!DOCTYPE html> <html lang="ch"> <head> <meta charset="UTF-8"> Netty WebSocket时间服务器 <title>Title</title> </head> <br> <body> <br> <script type="text/javascript"> var socket; if(!window.WebSocket) { window.WebSocket = window.MozWebSocket; } if(window.WebSocket) { socket = new WebSocket("ws://localhost:8080/websocket"); socket.onmessage = function(event) { var ta = document.getElementById('responseText'); ta.value=""; ta.value=event.data }; socket.onopen = function(event) { var ta = document.getElementById('responseText'); ta.value="打开WebSocket服务正常,浏览器支持WebSocket!"; }; socket.onclose=function(event) { var ta = document.getElementById('responseText'); ta.value=""; ta.value="WebSocket 关闭!"; }; } else { alert("抱歉,您的浏览器不支持WebSocket协议!"); } function send(message){ if(!window.WebSocket) { return ;} if(socket.readyState == WebSocket.OPEN) { socket.send(message); } else { alert("WebSocket连接没有建立成功!"); } } </script> <form onsubmit="return false;"> <input type="text" id="message" name="message" value="Netty最佳实践"/> <br><br> <input type="button" value="发送WebSocket请求消息" onclick="send(this.form.message.value)"/> <hr color="blue"/> <h3>服务端返回的应答消息</h3> <textarea id="responseText" style="width:500px;height:300px;"></textarea> </form> </body> </html>
四,总结
通过Netty WebSocket开发,可以更好的掌握如何利用Netty提供的WebSocket协议栈进行WebSocket应用程序的开发。
由于WebSocket本身的复杂性,以及可以通过多种形式承载消息,所以它的API和用法也非常多。希望本章的例程能够起到抛砖引玉的作用。