SpringBoot+Netty开发IM即时通讯系列(二)
上篇 “SpringBoot+Netty开发IM即时通讯系列(一)”介绍了Netty与NIO等基础知识点,感兴趣的可以去看下:
https://blog.csdn.net/qq_26975307/article/details/85004424
本篇使用Netty+WebSocket构建一个最简单的Demo在线IM通信项目(关于接口或类的作用等已在注释中注明)。
前言
实时通信的分类:
(1)Ajax轮训
通过JS以Ajax异步地让浏览器每隔一段时间(10S)发送请求到后端,去询问服务端是否有新消息、新状态等,如果有则取出并通过前端再渲染。但这很容易造成无限循环,也就是前端Ajax会不停地循环后端的数据 (使用场景:浏览器不需要一直刷新,简单的后台管理系统中的数据更新等)
(2)Long Pull
与Ajax轮训类似,也是使用异步请求,只不过它的轮训方式不太友好,阻塞式轮训:当客户端发起请求之后,服务端如果未响应,则Long Pull就不会有响应,直到服务端返回response。过程中不停地建立Http请求,等待服务器端进行处理,被动响应,缺点也是非常明显,也很耗费资源,性能低。
(3)webSokect - 推荐
Http本身就不支持长连接,Http1.1支持长连接,WebSokect就是使用了Http1.1协议来完成一小部分的握手,简单来讲就是,客户端发起请求到服务端,服务端会去找一个副助理,找到之后服务器端会和客户端一直保持连接,为客户端进行服务,并且可以主动推送一些消息给客户端。
关于WebSocket
WebSokect有哪些协议,又有什么优点?
1)首先WebSokect相对于Http这种非持久化来讲,是一种持久化的协议,Http的生命周期可以说是通过一个request来进行判定,有一个request请求到后端,后端也会相应的返回一个response给客户端,或者有多个request对应到多个response,两者之间都是一一对应的,有多少个request请求就会有多少个response相应,不会有偏差。此时response其实也是被动的,它不能由服务器端主动发起相应,必须先有request请求。
2)WebSokect由此诞生,它使得资源不会像以前一样浪费,并且它也是非常的主动,只要链接一旦被建立完毕之后,那么服务端就可以不停的主动推送消息给客户端,客户端不需要主动请求服务端也可以达到一样的效果。 也就是说,只要建立一次Http请求就能达到信息的源源不断的传输。类似于在线Online小游戏,一开始建立连接,就可以一直保持在线了。
WebSocket API(最基础也是最常用的几个)
(1)var socket=new WebSocket("ws://[ip]:[port]");
(2)生命周期:onopen() onmessage() onerror() onclose()
(3)主动方法:Socket.send() Socket.close()
参考API:https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket
导入相关依赖
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.pubing</groupId> <artifactId>helloNetty</artifactId> <version>0.0.1-SNAPSHOT</version> <dependencies> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.25.Final</version> </dependency> </dependencies> </project>
创建服务器启动类 WebSocketServer
package com.phubing.websokect; 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; /** * 用于和客户端进行连接 * * @author phubing * */ public class WebSocketServer { public static void main(String[] args) throws InterruptedException { //定义线程组 EventLoopGroup mainGroup = new NioEventLoopGroup(); EventLoopGroup subGroup = new NioEventLoopGroup(); try { ServerBootstrap server = new ServerBootstrap(); server.group(mainGroup, subGroup) //channel类型 .channel(NioServerSocketChannel.class) //针对subGroup做的子处理器,childHandler针对WebSokect的初始化器 .childHandler(new WebSocketinitializer()); //绑定端口并以同步方式进行使用 ChannelFuture channelFuture = server.bind(10086).sync(); //针对channelFuture,进行相应的监听 channelFuture.channel().closeFuture().sync(); }finally { //针对两个group进行优雅地关闭 mainGroup.shutdownGracefully(); subGroup.shutdownGracefully(); } } }
创建WebSocket初始化器WebSocketinitializer
package com.phubing.websokect; 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 socketChannel) throws Exception { //从Channel中获取对应的pipeline ChannelPipeline channelPipeline = socketChannel.pipeline(); //添加相应的助手类与处理器 /** * WebSokect基于Http,所以要有相应的Http编解码器,HttpServerCodec() */ channelPipeline.addLast(new HttpServerCodec()); //在Http中有一些数据流的传输,那么数据流有大有小,如果说有一些相应的大数据流处理的话,需要在此添加 //ChunkedWriteHandler:为一些大数据流添加支持 channelPipeline.addLast(new ChunkedWriteHandler()); //UdineHttpMessage进行处理,也就是会用到request以及response //HttpObjectAggregator:聚合器,聚合了FullHTTPRequest、FullHTTPResponse。。。,当你不想去管一些HttpMessage的时候,直接把这个handler丢到管道中,让Netty自行处理即可 channelPipeline.addLast(new HttpObjectAggregator(2048*64)); //================华丽的分割线:以上是用于支持Http协议================ //================华丽的分割线:以下是用于支持WebSoket================== // /ws:一开始建立连接的时候会使用到,可自定义 //WebSocketServerProtocolHandler:给客户端指定访问的路由(/ws),是服务器端处理的协议,当前的处理器处理一些繁重的复杂的东西,运行在一个WebSocket服务端 //另外也会管理一些握手的动作:handshaking(close,ping,pong) ping + pong = 心跳,对于WebSocket来讲,是以frames进行传输的,不同的数据类型对应的frames也不同 channelPipeline.addLast(new WebSocketServerProtocolHandler("/ws")); //添加自动handler,读取客户端消息并进行处理,处理完毕之后将相应信息传输给对应客户端 channelPipeline.addLast(new ChatHandler()); } }
添加自定义助手ChatHandler
package com.phubing.websokect; import java.time.LocalDate; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.group.ChannelGroup; import io.netty.channel.group.DefaultChannelGroup; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.util.concurrent.GlobalEventExecutor; //TextWebSocketFrame:处理消息的handler,在Netty中用于处理文本的对象,frames是消息的载体 public class ChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame>{ //用于记录和管理所有客户端的channel,可以把相应的channel保存到一整个组中 //DefaultChannelGroup:用于对应ChannelGroup,进行初始化 private static ChannelGroup channelClient = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); @Override protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception { //text()获取从客户端发送过来的字符串 String content = msg.text(); System.out.println("客户端传输的数据:"+content); //针对channel进行发送,客户端对应的是channel /** * 方式一 */ for (Channel channel : channelClient) { //循环对每一个channel对应输出即可(往缓冲区中写,写完之后再刷到客户端) //注:writeAndFlush不可以使用String,因为传输的载体是一个TextWebSocketFrame,需要把消息通过载体再刷到客户端 channel.writeAndFlush(new TextWebSocketFrame("【服务器于 " + LocalDate.now() + "接收到消息:】 ,消息内容为:" +content)); } /** * 方式二 channelClient.writeAndFlush(new TextWebSocketFrame("【服务器于 " + LocalDate.now() + "接收到消息:】 ,消息内容为:" +content)) */ } //当客户端连接服务端(或者是打开连接之后) @Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { //获取客户端所对应的channel,添加到一个管理的容器中即可 channelClient.add(ctx.channel()); } //客户端断开 @Override public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { //实际上是多余的,只要handler被移除,client会自动的把对应的channel移除掉 channelClient.remove(ctx.channel()); //每一而channel都会有一个长ID与短ID //一开始channel就有了,系统会自动分配一串很长的字符串作为唯一的ID,如果使用asLongText()获取的ID是唯一的,asShortText()会把当前ID进行精简,精简过后可能会有重复 System.out.println("channel的长ID:"+ctx.channel().id().asLongText()); System.out.println("channel的短ID:"+ctx.channel().id().asShortText()); } }
此时,服务端已完成,接下来再新建一个前端页面,用于发送文本与显示服务端推送的数据
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Netty+WebSocket案例</title> </head> <body> <div id="">发送消息:</div><br> <input type="text" name="messageContent" id="messageContent"/> <input type="button" name="" id="" value="发送" onclick="CHAT.chat()"/> <hr> <div id="">接收消息:</div><br> <div id="receiveNsg" style="background-color: gainsboro;"></div> <script type="text/javascript"> window.CHAT = { socket: null, //初始化 init: function(){ //首先判断浏览器是否支持WebSocket if (window.WebSocket){ CHAT.socket = new WebSocket("ws://localhost:10086/ws"); CHAT.socket.onopen = function(){ console.log("客户端与服务端建立连接成功"); }, CHAT.socket.onmessage = function(e){ console.log("接收到消息:"+e.data); var receiveNsg = window.document.getElementById("receiveNsg"); var html = receiveNsg.innerHTML; receiveNsg.innerHTML = html + "<br>" + e.data; }, CHAT.socket.onerror = function(){ console.log("发生错误"); }, CHAT.socket.onclose = function(){ console.log("客户端与服务端关闭连接成功"); } }else{ alert("8102年都过了,升级下浏览器吧"); } }, chat: function(){ var msg = window.document.getElementById("messageContent"); CHAT.socket.send(msg.value); } } CHAT.init(); </script> </body> </html>
注
端口号、IP地址、WebSocket的服务端提供名称一定要与前端相对应,否则会出错
例如:服务端给客户端指定访问的路由为:/ws,IP地址为:192.168.45.96,端口号为:10086
那么前端在建立WebSocket连接时填写为:
CHAT.socket = new WebSocket("ws://localhost:10086/ws"); //ws:// 为固定写法
最后来看看效果图
如有不当之处请指出,虚心接受建议与批评。