webSocket 是 HTML5 开始提供的一种浏览器于服务器间进行全双工通讯的技术.java
在 WebSocket API 中, 浏览器和服务器只须要作一个握手的动做, 而后, 浏览器和服务器之间就造成了一条快速通道, 二者就能够直接相互传送数据了. WebSocket 基于 TCP 双向全双工进行消息传递, 在同一时刻, 既能够发送消息, 也能够接收消息, 相比 HTTP 的半双工协议, 性能获得很大提高.web
WebSocket 的特色:浏览器
ping/pong
帧保持链路激活;创建 webSocket 链接时, 须要经过客户端或浏览器发出握手请求, 相似下面的 http 报文.安全
这个请求和一般的 HTTP 请求不一样, 包含了一些附加头信息, 其中附加头信息 Upgrade:WebSocket
代表这是一个申请协议升级的 HTTP 请求.服务器
服务器解析这些附加的头信息, 而后生成应答信息返回给客户端, 客户端和服务端的 WebSocket 链接就创建起来了, 双方能够经过这个链接通道自由的传递信息, 而且这个链接会持续存在直到客户端或服务端的某一方主动关闭链接.websocket
服务端返回给客户端的应答消息, 相似以下报文socket
请求消息中的 Sec-WebSocket-Key
是随机的, 服务端会用这些数据来构造出一个 SHA-1 的信息摘要, 把 Sec-WebSocket-Key
加上一个魔幻字符串 258EAFA5-E914-47DA-95CA-C5AB0DC85B11
. 使用 SHA-1 加密, 而后进行 BASE-64 编码, 将结果作为 Sec-WebSocket-Accept
头的值, 返回给客户端.ide
握手成功以后, 服务端和客户端就能够经过 messages
的方式进行通信, 一个消息由一个或多个帧组成.oop
帧都有本身对应的类型, 属于同一个消息的多个帧具备相同类型的数据. 从广义上讲, 数据类型能够是文本数据(UTF-8文字)、二进制数据和控制帧(协议级信令, 例如信号).性能
WebSocket 链接生命周期以下:
public class TimeServer { public void bind(int port) throws Exception { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .option(ChannelOption.SO_BACKLOG, 1024) .handler(new LoggingHandler(LogLevel.DEBUG)) .childHandler(new ChildChannelHandler()); // 绑定端口, 同步等待成功 ChannelFuture f = b.bind(port).sync(); // 等待服务端监听端口关闭 f.channel().closeFuture().sync(); } finally { System.out.println("shutdownGracefully"); bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } private class ChildChannelHandler extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) { ch.pipeline().addLast("http-codec", new HttpServerCodec()); ch.pipeline().addLast("aggregator", new HttpObjectAggregator(65536)); ch.pipeline().addLast("http-chunked", new ChunkedWriteHandler()); ch.pipeline().addLast("handler", new WebSOcketServerHandler()); } } private class WebSOcketServerHandler extends SimpleChannelInboundHandler<Object> { private WebSocketServerHandshaker handshaker; @Override protected void messageReceived(ChannelHandlerContext ctx, Object msg) throws Exception { // 传统的 HTTP 接入 if (msg instanceof FullHttpRequest) { System.out.println("传统的 HTTP 接入"); handleHttpRequest(ctx, (FullHttpRequest) msg); } // WebSocket 接入 else if (msg instanceof WebSocketFrame) { System.out.println("WebSocket 接入"); handleWebSocketFrame(ctx, (WebSocketFrame) msg); } } private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception { // 若是 HTTP 解码失败, 返回HTTP异常 if (!req.getDecoderResult().isSuccess() || (!"websocket".equalsIgnoreCase(req.headers().get("Upgrade")))) { sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST)); return; } // 构造握手响应返回, 本机测试 WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory("ws://localhost:8080/websocket", null, false); handshaker = wsFactory.newHandshaker(req); if (handshaker == null) { WebSocketServerHandshakerFactory.sendUnsupportedWebSocketVersionResponse(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(); ctx.channel().write(new TextWebSocketFrame(request + " , 欢迎使用 Netty WebSocket 服务, 如今时刻: " + new java.util.Date().toString())); } private void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res) { if (res.getStatus().code() != 200) { ByteBuf buf = Unpooled.copiedBuffer(res.getStatus().toString(), CharsetUtil.UTF_8); res.content().writeBytes(buf); buf.release(); setContentLength(res, res.content().readableBytes()); } // 若是是非 Keep-Alive, 关闭链接 ChannelFuture f = ctx.channel().writeAndFlush(res); if (!isKeepAlive(req) || res.getStatus().code() != 200) { f.addListener(ChannelFutureListener.CLOSE); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { ctx.flush(); } } }
HttpServerCodec
: 将请求和应答消息编码或解码为 HTTP 消息.HttpObjectAggregator
: 它的目的是将 HTTP 消息的多个部分组合成一条完整的 HTTP 消息. Netty 能够聚合 HTTP 消息, 使用 FullHttpResponse
和 FullHttpRequest
到 ChannelPipeline
中的下一个 ChannelHandler
, 这就消除了断裂消息, 保证了消息的完整.ChunkedWriteHandler
: 来向客户端发送 HTML5 文件, 主要用于支持浏览器和服务端进行 WebSocket 通讯.
第一次握手请求消息由 HTTP 协议承载, 因此它是一个 HTTP 消息, 执行 handleHttpRequest
方法来处理 WebSocket 握手请求. 经过判断请求消息判断是否包含 Upgrade
字段或它的值不是 websocket, 则返回 HTTP 400 响应.
握手请求校验经过以后, 开始构造握手工厂, 建立握手处理类 WebSocketServerHandshaker
, 经过它构造握手响应消息返回给客户端.
添加 WebSocket Encoder 和 WebSocket Decoder 以后, 服务端就能够自动对 WebSocket 消息进行编解码了, 后面的 handler 能够直接对 WebSocket 对象进行操做.
handleWebSocketFrame
对消息进行判断, 首先判断是不是控制帧, 若是是就关闭链路. 若是是维持链路的 Ping
消息, 则构造 Pong
消息返回. 因为本例程的 WebSocket 通讯双方使用的都是文本消息, 因此对请求新消息的类型进行判断, 而不是文本的抛出异常.
最后, 从 TextWebSocketFrame
中获取请求消息字符串, 对它处理后经过构造新的 TextWebSocketFrame
消息返回给客户端, 因为握手应答时, 动态增长了 TextWebSocketFrame
的编码类, 因此能够直接发送 TextWebSocketFrame
对象.