本章主要学习内容如下:
1、WebSocket入门
2、Netty WebSocket协议开发
第节一:WebSocket入门
WebSocket是HTML5开始提供的一种浏览器与服务器进行全双通信的网络技术,WebSocket通信协议与2011年被IEIF定位标准RFC6455,WebsSocket api被W3c定为标准。
在WebSocket api中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间形成了一条快速通道,两者就可以之间互相传输消息数据了。WebSocket基于TCP双向全双工进行消息传递,同一时刻,既可以发送消息,也可以接受消息,相比HTTP的半双工协议,性能得到很大的提升。
下面总结一下WebSocket的特点
1、单一TCP连接,采用全双工模式通信。
2、对代理、防火墙和路由器透明。
3、无头部信息、Cookie和身份验证
4、无安全开销
5、通过ping/pong 帧保持链路激活状态
6、服务器可以主动传递消息给客户端,不需要客户端轮训。
WebSocket背景
WebSocket设计出来的目的就是取代轮训和Comet技术,使客户端浏览器具备向C/S架构下桌面系统一样的实时通讯能力。浏览器通过javascript 向服务器发出建立WebSocket连接的请求,连接成功后,客户端和服务端可以通过TCP直接交互数据。因为WebSocket连接本质上就是一个TCP连接,所以在数据传输的稳定性和数据传输量的大小方面,和轮训即Comet技术相比,具有很大的优势。WebSocket.org网站对传统的轮训方式和WebSocket 调用方式作了一个详细的测试和比较,将一个简单的web应用分别通过轮训方式和websocket方式来实现,在这里引用一下测试结果,如图所示:
WebSocket 连接建立
客户端和服务端建立连接的示意图如下:
建立WebSocket连接时,需要通过客户端或者浏览器发出握手请求,请求消息如图所示:
为了建立一个WebSocket 连接,客户端浏览器首先向服务器发起一个HTTP请求,这个请求和通常的HTTP请求不同,包含了一些附加头信息,其中附加头信息“Upgrade:Websocket”表明这是一个申请协议升级的HTTP请求。服务端解析这些附加头信息,然后生成应答信息返回客户端,客户端和服务端的WebSoket连接就建立起来了,双方可以通过这个连接通道自由传递信息,并且这个连接会持续存在直到客户端或服务器端的某一方主动关闭连接。
请求消息中的"sec-websocket-key"是随机的,服务器会用这些数据构造出一个SHA-1的消息摘要,把“sec-websocket-key”加上一个魔幻字符串“*************”。使用SHA-1加密,然后进行Base64编码,将结果作为"sec-websocket-accept"头的值,返回给客户端。
WebSocket 生命周期
第三节:Netty WebSocket协议开发
Websocket服务端的功能如下:支持websocket的浏览器通过websocket协议发送请求消息给客户端,服务端对请求消息进行判断,如果是合法的websocket 请求,则获取请求消息文本,并在后面追加字符串“欢迎使用Netty websocket 服务”。
客户端HTML通过内嵌的js 脚本创建websocke 连接,如果握手成功,在文本框中打印“打开websocket 服务正常,浏览器支持websocket ”。客户端界面如图所示:
WebSocketServer.java
package com.viagra.chapter11.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;/*** @Auther: viagra* @Date: 2019/8/2 13:59* @Description:*/
public class WebSocketServer {public void run(int port) throws Exception {EventLoopGroup bossGroup = new NioEventLoopGroup();EventLoopGroup workerGroup = new NioEventLoopGroup();try {ServerBootstrap b = new ServerBootstrap();b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch)throws Exception {ChannelPipeline pipeline = ch.pipeline();pipeline.addLast("http-codec",new HttpServerCodec());pipeline.addLast("aggregator",new HttpObjectAggregator(65536));ch.pipeline().addLast("http-chunked",new ChunkedWriteHandler());pipeline.addLast("handler",new WebSocketServerHandler());}});Channel ch = b.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 + '/');ch.closeFuture().sync();} finally {bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();}}public static void main(String[] args) throws Exception {int port = 17888;new WebSocketServer().run(port);}
}
WebSocketServerHandler.java
package com.viagra.chapter11.websocket;import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
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.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
import io.netty.handler.codec.http.websocketx.PingWebSocketFrame;
import io.netty.handler.codec.http.websocketx.PongWebSocketFrame;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory;
import io.netty.util.CharsetUtil;import java.util.logging.Level;
import java.util.logging.Logger;
/*** @Auther: viagra* @Date: 2019/8/2 14:02* @Description:*/
public class WebSocketServerHandler extends SimpleChannelInboundHandler<Object> {private WebSocketServerHandshaker handshaker;@Overrideprotected void messageReceived(ChannelHandlerContext channelHandlerContext, Object o) throws Exception {// WebSocket接入if (o instanceof WebSocketFrame) {System.out.println("request meesage body:"+o);handleWebSocketFrame(channelHandlerContext, (WebSocketFrame) o);}else if(o instanceof FullHttpRequest){System.out.println("server receiver message is:"+o);handleHttpRequest(channelHandlerContext, (FullHttpRequest) o);}}@Overridepublic void channelReadComplete(ChannelHandlerContext ctx) throws Exception {ctx.flush();}private void handleHttpRequest(ChannelHandlerContext ctx,FullHttpRequest req) throws Exception {// 构造握手响应返回,本机测试WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory("ws://localhost:17888/websocket", null, false);handshaker = wsFactory.newHandshaker(req);if (handshaker == null) {WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());} else {handshaker.handshake(ctx.channel(), req);}}private void handleWebSocketFrame(ChannelHandlerContext ctx,WebSocketFrame frame) {// 判断是否是关闭链路的指令if (frame instanceof CloseWebSocketFrame) {handshaker.close(ctx.channel(),(CloseWebSocketFrame) frame.retain());return;}// 判断是否是Ping消息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();System.out.println("server receiver message is:"+request);ctx.channel().write(new TextWebSocketFrame(request+ " , welocme to:"+ new java.util.Date().toString()));}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {ctx.close();}
}
client.html
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8">Netty WebSocket 时间服务器
</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:17888/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 οnsubmit="return false;"><input type="text" name="message" value="Netty最佳实践"/><br><br><input type="button" value="发送WebSocket请求消息" οnclick="send(this.form.message.value)"/><hr color="blue"/><h3>服务端返回的应答消息</h3><textarea id="responseText" style="width:500px;height:300px;"></textarea>
</form>
</body>
</html>
执行结果服务端:
客户端: