前言
今天在慕课网上看到了 Java 的新教程 (Netty 入门之 webSocket 初体验):https://www.imooc.com/learn/941
WebSocket 我是听得很多, 没有真正使用过的技术我之前也去了解过了 WebSocket 究竟是什么东西, 不过一直没有去实践过
我在写监听器博文的时候, 在线人数功能用监听器的是来做, 在评论有说使用 WebSocket 的方式会更加好
那么, 我们就来探究一下 WebSocket 究竟是什么东西, 顺便了解一下 Netty!
WebSocket 介绍
什么是 WebSocket
WebSocket 是一个协议, 归属于 IETF
HTTP 是运行在 TCP 协议传输层上的应用协议, 而 WebSocket 是通过 HTTP 协议协商如何连接, 然后独立运行在 TCP 协议传输层上的应用协议
Websocket 是一个持久化的协议, 相对于 HTTP 这种非持久的协议来说
websocket 约定了一个通信的规范, 通过一个握手的机制, 客户端和服务器之间能建立一个类似 tcp 的连接, 从而方便它们之间的通信
为什么需要 WebSocket
添加 WebSocket 特性, 是为了更好更灵活, 轻量的与服务器通讯因为 WebSocket 提供了简单的消息规范, 可以更快的适应长连接的环境, 其实现在 HTTP 协议自身就可以做, 但是不太轻便
WebSocket 最大的特点就是实现全双工通信: 客户端能够实时推送消息给服务端, 服务端也能够实时推送消息给客户端
WebSocket 可以做聊天室, 股票实时价格显示等应用
纠正 WebSocket 误区
WebSocket 是一种应用协议, 而我们常常看到了 html5 WebSocket 是 API, 不要将其进行混淆
广义上的 HTML5 里面包含的是 WebSocket API, 并不是 WebSocket 简单的说, 可以把 WebSocket 当成 HTTP,WebSocket API 当成 Ajax
Netty 介绍
什么是 Netty
知乎的 @郭无心总结得很好, 我下面就摘抄一下了 (链接在下方):
Netty 是什么?
1) 本质: JBoss 做的一个 Jar 包
2) 目的: 快速开发高性能高可靠性的网络服务器和客户端程序
3) 优点: 提供异步的事件驱动的网络应用程序框架和工具
通俗的说: 一个好使的处理 Socket 的东东
如果没有 Netty?
远古: java.net + java.io
近代: java.nio
其他: Mina,Grizzly
简单来说:
你想写个 tomcat 一样的 Server, 可以用 netty
你想写一个即时通讯的应用, 可以用 netty
你想实现一个高性能 Rpc 框架, 可以用 netty
Netty 优势
Netty 优势: API 简单, 性能高, 入门门槛低, 成熟稳健, 修复了很多原生 NIO 的 bug
回到课程中来
课程是以 Netty 实现 WebSocket 来进行讲解的, 也就上边所说的: 用 Netty 来实现即时通信的应用
源码下载地址: https://img.mukewang.com/down/5a6e804c0001970d00000000.zip
首先创建了一个全局配置类, WebSocket 是全双工通信的, 它是通过通道来进行通信, 因此配置了系统通道组, 管理所有的通道
- /**
- * 存储整个工程的全局配置
- * @author liuyazhuang
- *
- */
- public class NettyConfig {
- /**
- * 存储每一个客户端接入进来时的 channel 对象
- */
- public static ChannelGroup group = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
- }
配置一些通道的信息 (可以理解成 Servlet 时配置 request 对象的 charsetresponse 对象的缓存)
- /**
- * 初始化连接时候的各个组件
- * @author liuyazhuang
- *
- */
- public class MyWebSocketChannelHandler extends ChannelInitializer<SocketChannel> {
- // 配置通道的一些编码格式数据大小处理器 (交由谁处理)
- @Override
- protected void initChannel(SocketChannel e) throws Exception {
- e.pipeline().addLast("http-codec", new HttpServerCodec());
- e.pipeline().addLast("aggregator", new HttpObjectAggregator(65536));
- e.pipeline().addLast("http-chunked", new ChunkedWriteHandler());
- e.pipeline().addLast("handler", new MyWebSocketHandler());
- }
- }
Netty 接收请求, 分别处理 HTTP 请求和 WebSocket 请求, 此部分在视频中单单只是代码编写, 并没有做过多的介绍下面我就整理一下:
该类是用于处理请求的核心业务类
最重要的方法是: messageReceived() 方法, 主要判断是 HTTP 请求还是 WebSocket 请求
是 HTTP 请求时, 就 handHttpRequest() 来进行处理, 该方法判断是否有握手的倾向,
如果不是 WebSocket 握手请求消息, 那么直接返回 HTTP 400 BAD REQUEST 响应给客户端, 应答消息, 并关闭链接
如果是握手请求, 那么就进行握手, 将 WebSocket 相关的编码和解码类动态添加到 ChannelPipeline 中
是 websocket 则群发, 服务端向每个连接上来的客户端群发消息
- package com.imooc.netty;
- 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.HttpResponseStatus;
- import io.netty.handler.codec.http.HttpVersion;
- import io.netty.handler.codec.http.websocketx.*;
- import io.netty.util.CharsetUtil;
- import java.util.Date;
- /**
- * 接收 / 处理 / 响应客户端 websocket 请求的核心业务处理类
- *
- * @author liuyazhuang
- */
- public class MyWebSocketHandler extends SimpleChannelInboundHandler<Object> {
- private WebSocketServerHandshaker handshaker;
- private static final String WEB_SOCKET_URL = "ws://localhost:8888/websocket";
- // 客户端与服务端创建连接的时候调用
- @Override
- public void channelActive(ChannelHandlerContext ctx) throws Exception {
- NettyConfig.group.add(ctx.channel());
- System.out.println("客户端与服务端连接开启...");
- }
- // 客户端与服务端断开连接的时候调用
- @Override
- public void channelInactive(ChannelHandlerContext ctx) throws Exception {
- NettyConfig.group.remove(ctx.channel());
- System.out.println("客户端与服务端连接关闭...");
- }
- // 服务端接收客户端发送过来的数据结束之后调用
- @Override
- public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
- ctx.flush();
- }
- // 工程出现异常的时候调用
- @Override
- public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
- cause.printStackTrace();
- ctx.close();
- }
- // 服务端处理客户端 websocket 请求的核心方法
- @Override
- protected void messageReceived(ChannelHandlerContext context, Object msg) throws Exception {
- // 传统的 HTTP 接入
- // 第一次握手请求消息由 HTTP 协议承载, 所以它是一个 HTTP 消息, 执行 handleHttpRequest 方法来处理 WebSocket 握手请求
- if (msg instanceof FullHttpRequest) {
- handHttpRequest(context, (FullHttpRequest) msg);
- }
- // WebSocket 接入
- // 客户端通过文本框提交请求消息给服务端, WebSocketServerHandler 接收到的是已经解码后的 WebSocketFrame 消息
- else if (msg instanceof WebSocketFrame) {
- handWebsocketFrame(context, (WebSocketFrame) msg);
- }
- }
- /**
- * 处理客户端向服务端发起 http 握手请求的业务
- *
- * @param ctx
- * @param req
- */
- private void handHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) {
- // 如果不是 WebSocket 握手请求消息, 那么就返回 HTTP 400 BAD REQUEST 响应给客户端
- if (!req.getDecoderResult().isSuccess()
- || !("websocket".equals(req.headers().get("Upgrade")))) {
- sendHttpResponse(ctx, req,
- new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST));
- return;
- }
- // 如果是握手请求, 那么就进行握手
- WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
- WEB_SOCKET_URL, null, false);
- handshaker = wsFactory.newHandshaker(req);
- if (handshaker == null) {
- WebSocketServerHandshakerFactory.sendUnsupportedWebSocketVersionResponse(ctx.channel());
- } else {
- // 通过它构造握手响应消息返回给客户端,
- // 同时将 WebSocket 相关的编码和解码类动态添加到 ChannelPipeline 中, 用于 WebSocket 消息的编解码,
- // 添加 WebSocketEncoder 和 WebSocketDecoder 之后, 服务端就可以自动对 WebSocket 消息进行编解码了
- handshaker.handshake(ctx.channel(), req);
- }
- }
- /**
- * 处理客户端与服务端之前的 websocket 业务
- *
- * @param ctx
- * @param frame
- */
- private void handWebsocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {
- // 判断是否是关闭 websocket 的指令
- if (frame instanceof CloseWebSocketFrame) {
- handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());
- }
- // 判断是否是 ping 消息
- if (frame instanceof PingWebSocketFrame) {
- ctx.channel().write(new PongWebSocketFrame(frame.content().retain()));
- return;
- }
- // 判断是否是二进制消息, 如果是二进制消息, 抛出异常
- if (!(frame instanceof TextWebSocketFrame)) {
- System.out.println("目前我们不支持二进制消息");
- throw new RuntimeException(""+ this.getClass().getName() +" 不支持消息 ");
- }
- // 返回应答消息
- // 获取客户端向服务端发送的消息
- String request = ((TextWebSocketFrame) frame).text();
- System.out.println("服务端收到客户端的消息 ====>>>" + request);
- TextWebSocketFrame tws = new TextWebSocketFrame(new Date().toString()
- + ctx.channel().id()
- + "===>>>"
- + request);
- // 群发, 服务端向每个连接上来的客户端群发消息
- NettyConfig.group.writeAndFlush(tws);
- }
- /**
- * 服务端向客户端响应消息
- *
- * @param ctx
- * @param req
- * @param res
- */
- private void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req,
- DefaultFullHttpResponse res) {
- // 返回应答给客户端
- if (res.getStatus().code() != 200) {
- ByteBuf buf = Unpooled.copiedBuffer(res.getStatus().toString(), CharsetUtil.UTF_8);
- res.content().writeBytes(buf);
- buf.release();
- }
- // 如果是非 Keep-Alive, 关闭连接
- ChannelFuture f = ctx.channel().writeAndFlush(res);
- if (res.getStatus().code() != 200) {
- f.addListener(ChannelFutureListener.CLOSE);
- }
- }
- }
最后, 编写入口程序: 启动 WebSocket 服务
- package com.imooc.netty;
- import io.netty.bootstrap.ServerBootstrap;
- import io.netty.channel.Channel;
- import io.netty.channel.EventLoopGroup;
- import io.netty.channel.nio.NioEventLoopGroup;
- import io.netty.channel.socket.nio.NioServerSocketChannel;
- /**
- * 程序的入口, 负责启动应用
- * @author liuyazhuang
- *
- */
- public class Main {
- public static void main(String[] args) {
- EventLoopGroup bossGroup = new NioEventLoopGroup();
- EventLoopGroup workGroup = new NioEventLoopGroup();
- try {
- ServerBootstrap b = new ServerBootstrap();
- b.group(bossGroup, workGroup);
- b.channel(NioServerSocketChannel.class);
- b.childHandler(new MyWebSocketChannelHandler());
- System.out.println("服务端开启等待客户端连接....");
- Channel ch = b.bind(8888).sync().channel();
- ch.closeFuture().sync();
- } catch (Exception e) {
- e.printStackTrace();
- }finally{
- // 优雅的退出程序
- bossGroup.shutdownGracefully();
- workGroup.shutdownGracefully();
- }
- }
- }
客户端代码:
- <html>
- <head>
- <meta http-equiv="Content-Type" content="text/html; charset = utf-8"/>
- <title>WebSocket 客户端 </title>
- <script type="text/javascript">
- var socket;
- if(!window.WebSocket){
- window.WebSocket = window.MozWebSocket;
- }
- if(window.WebSocket){
- socket = new WebSocket("ws://localhost:8888/websocket");
- socket.onmessage = function(event){
- var ta = document.getElementById('responseContent');
- ta.value += event.data + "\r\n";
- };
- socket.onopen = function(event){
- var ta = document.getElementById('responseContent');
- ta.value = "你当前的浏览器支持 WebSocket, 请进行后续操作 \ r\n";
- };
- socket.onclose = function(event){
- var ta = document.getElementById('responseContent');
- ta.value = "";
- ta.value = "WebSocket 连接已经关闭 \ r\n";
- };
- }else{
- alert("您的浏览器不支持 WebSocket");
- }
- function send(message){
- if(!window.WebSocket){
- return;
- }
- if(socket.readyState == WebSocket.OPEN){
- socket.send(message);
- }else{
- alert("WebSocket 连接没有建立成功!!");
- }
- }
- </script>
- </head>
- <body>
- <form onSubmit="return false;">
- <input type = "text" name = "message" value = ""/>
- <br/><br/>
- <input type = "button" value = "发送 WebSocket 请求消息" onClick = "send(this.form.message.value)"/>
- <hr color="red"/>
- <h2 > 客户端接收到服务端返回的应答消息 </h2>
- <textarea id = "responseContent" style = "width:1024px; height:300px"></textarea>
- </form>
- </body>
- </html>
- !](http://upload-images.jianshu.io/upload_images/5291509-66760e9dc063bbd4.png?imageMogr2/auto-orient/strip|imageView2/2/w/1240)
实际使用 WebSocket
上面的例子讲解了 Netty 实现 WebSocket, 一般我们使用 WebSocket 不会自己来实现, 都是用现成的工具包来进行实现
我查到的常用的方式有两种:
Tomcat 实现 WebSocket
整合 Spring 实现 WebSocket
这一部分我就不再赘述了, 等我用到的时候再补教程吧, 先 mark 下相关的博客:
Tomcat 实现
Spring 整合和 Tomcat 实现
基于 Java 实现
总结
WebSocket 最大的特点就是长连接, 能够实时推送数据
来源: http://www.jianshu.com/p/568a1a6ff17e