【Java高级工程师蜕变之路】045 Netty高级进阶之基于Netty的Websocket开发网页聊天室
本通过实战演练,学习了如何基于Netty的websocket开发一个网页聊天室。
Netty高级进阶之基于Netty的Websocket开发网页聊天室
Webdocket简介
Websockt是一种在单个TCP连接上进行全双工通信的协议。
Websocket使客户端和服务端的数据交互变得简单,允许服务器主动向客户端推送数据。
在Websocket API中,客户端只需要与服务器完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。
他的应用场景如下:
- 社交订阅
- 协同编辑/编程
- 股票基金报价
- 体育实况更新
- 多媒体聊天
- 在线教育
Websocket和HTTP的区别
HTTP协议是应用层的协议,是基于TCP协议的。
HTTP协议必须经过三次握手才能发送消息。
HTTP连接分为短连接和长链接。短连接是每次都要经过三次握手才能发送消息。就是说每一个request对应一个response。长连接在一定期限内保持TCP连接不断开。
客户端与服务器通信,必须由客户端先发起,然后服务端返回结果。客户端是主动的,服务端是被动的。
客户端想要实时获取服务端的消息,就要不断发送长连接到服务端。
Websocket实现了多路复用,它是全双工通信。在Websocket协议下,服务端和客户端可以同时发送消息。
建立了Websocket连接之后,服务端可以主动给客户端发送消息。信息中不必带有header的部分信息,与HTTP长连接通信对比,这种方式降低了服务器的压力,信息当中也减少了多余的信息。
导入基础环境
-
新建netty-springboot项目
-
导入依赖模块
<dependencies> <!-- 模板引擎 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!-- web模块 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
-
导入静态资源
-
配置yaml
server: port: 8080 resources: static-locations: - classpath:/static/ spring: thymeleaf: cache: false checktemplatelocation: true enabled: true encoding: UTF-8 mode: HTML prefix: classpath:/templates/ suffix: .html
关于Springboot整合thymeleaf的404问题,参考:
/post/404-problem-with-springboot-configuration-thymeleaf.html
代码实现
服务端开发
-
添加Netty相关依赖
<!--引入netty依赖 --> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> </dependency>
-
Netty相关配置
netty: port: 8081 ip: 127.0.0.1 path: /chat
-
Netty配置类
/** * Netty配置类 * * @name: NettyConfig * @author: terwer * @date: 2022-05-01 00:04 **/ @Component @Data @ConfigurationProperties(prefix = "netty") public class NettyConfig { // netty监听端口 private int port; // webdocket访问路径 private String path; }
-
Netty的WebsocketServer开发
/** * Netty的Websocket服务器 * * @name: NettyWebsocketServer * @author: terwer * @date: 2022-05-01 00:11 **/ @Component public class NettyWebsocketServer implements Runnable { @Autowired private NettyConfig nettyConfig; @Autowired private WebsocketChannelInit websocketChannelInit; private NioEventLoopGroup bossGroup = new NioEventLoopGroup(1); private NioEventLoopGroup workerGroup = new NioEventLoopGroup(); @Override public void run() { try { // 1.创建服务端启动助手 ServerBootstrap serverBootstrap = new ServerBootstrap(); // 2.设置线程组 serverBootstrap.group(bossGroup, workerGroup); // 3.设置参数 serverBootstrap.channel(NioServerSocketChannel.class) .handler(new LoggingHandler(LogLevel.DEBUG)) .childHandler(websocketChannelInit); // 4.启动服务端 ChannelFuture channelFuture = serverBootstrap.bind(nettyConfig.getPort()).sync(); System.out.println("------Netty服务端启动成功------"); channelFuture.channel().closeFuture().sync(); } catch (InterruptedException e) { e.printStackTrace(); bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); throw new RuntimeException(e); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } /** * 关闭资源-容器销毁时候关闭 */ @PreDestroy public void close() { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } }
-
通道初始化对象
/** * 通道初始化对象 * * @name: WebsocketChannelInit * @author: terwer * @date: 2022-05-01 23:11 **/ @Component public class WebsocketChannelInit extends ChannelInitializer { @Autowired private NettyConfig nettyConfig; @Autowired private WebsocketHandler websocketHandler; @Override protected void initChannel(Channel channel) throws Exception { ChannelPipeline pipeline = channel.pipeline(); // 对HTTP协议的支持 pipeline.addLast(new HttpServerCodec()); // 对大数据流的支持 pipeline.addLast(new ChunkedWriteHandler()); // post请求分为三个部分:request line/request header/message body // 对POST请求的支持,将多个信息转化成单一的request/response对象 pipeline.addLast(new HttpObjectAggregator(8000)); // 对WebSocket协议的支持 // 将http协议升级为ws协议 pipeline.addLast(new WebSocketServerProtocolHandler(nettyConfig.getPath())); // 自定义处理handler pipeline.addLast(websocketHandler); } }
-
处理对象
/** * 自定义Websocket处理类 * Websocket数据以帧的形式进行处理 * 需要设置通道共享 * * @name: WebsocketHandler * @author: terwer * @date: 2022-05-01 23:21 **/ @Component @ChannelHandler.Sharable public class WebsocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> { public static List<Channel> channelList = new ArrayList<>(); /** * 通道就绪事件 * * @param ctx * @throws Exception */ @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { Channel channel = ctx.channel(); // 有客户端连接时,将通道放入集合 channelList.add(channel); System.out.println("有新的链接"); } /** * 通道未就绪 * * @param ctx * @throws Exception */ @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { // channel下线 Channel channel = ctx.channel(); // 客户端连接端口,移除连接 channelList.remove(channel); System.out.println("连接断开"); } /** * 通道读取事件 * * @param ctx * @param textWebSocketFrame * @throws Exception */ @Override protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame textWebSocketFrame) throws Exception { String msg = textWebSocketFrame.text(); System.out.println("接收到消息:" + msg); // 当前发送消息的通道 Channel channel = ctx.channel(); for (Channel channel1 : channelList) { // 排除自身通道 if (channel != channel1) { channel1.writeAndFlush(new TextWebSocketFrame(msg)); } } } /** * 异常处理事件 * * @param ctx * @param cause * @throws Exception */ @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); Channel channel = ctx.channel(); System.out.println("消息发送异常。"); // 移除 channelList.remove(channel); } }
注意:处理类需要设置成共享的
-
启动类
@SpringBootApplication public class NettySpringbootApplication implements CommandLineRunner { @Autowired private NettyWebsocketServer nettyWebsocketServer; public static void main(String[] args) { SpringApplication.run(NettySpringbootApplication.class, args); } @Override public void run(String... args) throws Exception { new Thread(nettyWebsocketServer).start(); } }
前端js开发
-
建立连接
var ws = new WebSocket("ws://localhost:8081/chat"); ws.onopen = function () { console.log("连接成功") }
-
发送消息
function sendMsg() { var message = $("#my_test").val(); $("#msg_list").append(`<li class="active"}> <div class="main self"> <div class="text">` + message + `</div> </div> </li>`); $("#my_test").val(''); //发送消息 message = username + ":" + message; ws.send(message); // 置底 setBottom(); }
-
接收消息
ws.onmessage = function (evt) { showMessage(evt.data); } function showMessage(message) { // 张三:你好 var str = message.split(":"); $("#msg_list").append(`<li class="active"}> <div class="main"> <img class="avatar" width="30" height="30" src="/img/user.png"> <div> <div class="user_name">${str[0]}</div> <div class="text">${str[1]}</div> </div> </div> </li>`); // 置底 setBottom(); }
-
关闭与错误处理
ws.onclose = function (){ console.log("连接关闭") } ws.onerror = function (){ console.log("连接异常") }
运行效果
![image-20220502001159603](../../../../../../../Library/Application Support/typora-user-images/image-20220502001159603.png)