场景
Netty中实现多客户端连接与通信-以实现聊天室群聊功能为例(附代码下载):
https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/108623306
上面讲了使用使用Socket搭建多客户端的连接与通信。
那么如果在Netty中使用WebSocket进行长连接通信要怎么实现。
WebSocket
现在,很多网站为了实现推送技术,所用的技术都是 Ajax 轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。
HTML5 定义的 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。
WebSocket是一种在单个TCP连接上进行全双工通信的协议。
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。
当你获取 Web Socket 连接后,你可以通过 send() 方法来向服务器发送数据,并通过 onmessage 事件来接收服务器返回的数据。
WebSocket 属性
以下是 WebSocket 对象的属性。假定我们使用了以上代码创建了 Socket 对象:
属性 | 描述 |
---|---|
Socket.readyState |
只读属性 readyState 表示连接状态,可以是以下值:
|
Socket.bufferedAmount |
只读属性 bufferedAmount 已被 send() 放入正在队列中等待传输,但是还没有发出的 UTF-8 文本字节数。 |
WebSocket 事件
以下是 WebSocket 对象的相关事件。假定我们使用了以上代码创建了 Socket 对象:
事件 | 事件处理程序 | 描述 |
---|---|---|
open | Socket.onopen | 连接建立时触发 |
message | Socket.onmessage | 客户端接收服务端数据时触发 |
error | Socket.onerror | 通信发生错误时触发 |
close | Socket.onclose | 连接关闭时触发 |
WebSocket 方法
以下是 WebSocket 对象的相关方法。假定我们使用了以上代码创建了 Socket 对象:
方法 | 描述 |
---|---|
Socket.send() |
使用连接发送数据 |
Socket.close() |
关闭连接 |
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
关注公众号
霸道的程序猿
获取编程相关电子书、教程推送与免费下载。
实现
在IDEA中搭建好Netty的项目并引入环境可以参照如下:
https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/108592418
在此基础上,在src下新建包com.badao.NettyWebSocket
然后新建服务端类WebSocketServer
package com.badao.NettyWebSocket; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; public class WebSocketServer { public static void main(String[] args) throws Exception { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try{ ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class) .handler(new LoggingHandler(LogLevel.INFO)) .childHandler(new WebSocketInitializer()); //绑定端口 ChannelFuture channelFuture = serverBootstrap.bind(70).sync(); channelFuture.channel().closeFuture().sync(); }finally { //关闭事件组 bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } }
服务端的搭建在上面已经讲解,这里又添加了Netty自带的日志处理器LoggingHandler
然后又添加了自定义的初始化器WebSocketInitializer
所以新建类WebSocketInitializer
package com.badao.NettyWebSocket; import io.netty.channel.Channel; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; import io.netty.handler.stream.ChunkedWriteHandler; public class WebSocketInitializer extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new HttpServerCodec()); pipeline.addLast(new ChunkedWriteHandler()); pipeline.addLast(new HttpObjectAggregator(8192)); pipeline.addLast(new WebSocketServerProtocolHandler("/badao")); pipeline.addLast(new WebSocketHandler()); } }
因为Netty也是基于Http的所以这里需要添加HttpServerCodec处理器等。
要想实现WebSocket功能,主要是添加了WebSocketServerProtocolHandler这个WebSocket服务端协议处理器。注意这里的参数需要添加一个WebSocket的路径,这里是/badao
最后添加自定义的处理器WebSocketHandler 用来做具体的处理
所以新建类WebSocketHandler
package com.badao.NettyWebSocket; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import java.time.LocalDateTime; public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> { @Override protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception { System.out.println("收到消息:"+msg.text()); ctx.channel().writeAndFlush(new TextWebSocketFrame("WebSocket服务端在"+ LocalDateTime.now()+"发送消息(公众号:霸道的程序猿)")); } @Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { System.out.println("handlerAdded:"+ctx.channel().id().asLongText()); } @Override public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { System.out.println("handlerRemoved:"+ctx.channel().id().asLongText()); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { System.out.println("异常发生"); ctx.close(); } }
使其继承SimpleChannelInboundHandler注意此时的泛型类型为TextWebSocketFrame
然后重写channelRead0方法用来对收到数据时进行处理
@Override protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception { System.out.println("收到消息:"+msg.text()); ctx.channel().writeAndFlush(new TextWebSocketFrame("WebSocket服务端在"+ LocalDateTime.now()+"发送消息(公众号:霸道的程序猿)")); }
在服务端将收到的消息进行输出并给客户端发送数据。
然后重写handlerAdded方法,在客户端与服务端建立连接时进行操作
@Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { System.out.println("handlerAdded:"+ctx.channel().id().asLongText()); }
这里通过通道的id方法的asLongText方法获取连接的唯一标志。
然后在服务端输出。
同理重写handlerRemoved方法,在断掉连接时将连接的唯一标志进行输出。
@Override public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { System.out.println("handlerRemoved:"+ctx.channel().id().asLongText()); }
最后重写出现异常时的处理方法,在出现异常时将连接关闭。
@Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { System.out.println("异常发生"); ctx.close(); }
至此WebSocket服务端搭建完成,然后客户端通过JS就能实现。
在项目目录下src下新建webapp目录,在此目录下新建badao.html
修改html的代码为
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>公众号:霸道的程序猿</title> </head> <body> <script type="text/javascript"> var socket; if(window.WebSocket) { socket = new WebSocket("ws://localhost:70/badao") socket.onmessage=function (ev) { var ta = document.getElementById("responseText"); ta.value = ta.value+" "+ev.data; } socket.onopen = function (ev) { var ta = document.getElementById("responseText"); ta.value = "连接开启"; } socket.onclose = function (ev) { var ta = document.getElementById("responseText"); ta.value = ta.value+" 连接关闭"; } } else{ alert("当前浏览器不支持WebSocket") } function send(message) { if(!window.WebSocket) { return; }else { if(socket.readyState = WebSocket.OPEN) { socket.send(message); } else { alert("连接尚未开启"); } } } </script> <form> <textarea name="message" style=" 400px;height: 200px"> </textarea> <input type="button" value="发送数据" onclick="send(this.form.message.value)"> <h3>服务端输出:</h3> <textarea id="responseText" style=" 400px;height: 200px"> </textarea> </form> </body> </html>
在js中通过windows.WebSocket判断是否支持WebSocket
如果支持则
socket = new WebSocket("ws://localhost:70/badao")
建立连接,url的写法前面的ws://是固定的类似http://
后面的是跟的ip:端口号/上面配置的WebSocket路径
然后就是以这个WebSocket对象为中心进行连接和数据的显示。
下面的回调方法onopen会在建立连接成功后回调,onclose会在断掉连接后回调,
onmessage会在收到服务端发送的数据时回调并通过ev.data获取数据。
客户端向服务端发送数据时调用的是socket的send方法,通过
if(socket.readyState = WebSocket.OPEN)
判断连接已经成功建立。
实现长连接通信
运行WebSocketServer的main方法,然后在badao.html上右击选择运行
建立连接成功后会在服务端输出连接的id,在客户端会显示连接开启。
此时如果刷新浏览器,服务端会输出一次断开连接和建立连接
再将服务端停掉,客户端会输出连接关闭
再重新启动服务端,在上面的输入框输入内容并点击发送数据
服务端会收到消息并输出,并向客户端发送一个消息。
继续发送也是如此
示例代码下载
https://download.csdn.net/download/BADAO_LIUMANG_QIZHI/12853829