Netty WebSocket 入门 home 编辑时间 2024/04/25 ![](/api/file/getImage?fileId=6629ddd0da7405001403f5e0) <br><br> ## 前言 头都要学秃了,真的难,建议非必要别学netty 官网:[https://netty.io/](https://netty.io/) <br> 本文主要是观看以下BiliBili视频中P1 - P11学习笔记 [【Netty项目】这可能是B站唯一完整的Netty实战项目,2小时手把手带你用Netty开发IM在线聊天项目](https://www.bilibili.com/video/BV1Ua4y1r7vP) <br><br> ## 折腾 #### ===> 基本款(能跑起来) IDEA新建一个Maven项目 Archetype选择queststart 进入项目界面后需要稍作调整 在`File` - `Project Structure`中,确保每个地方的JDK都是17 在`Settings`中的Maven按自己实际情况优化一下,最好是用本地Maven `pom.xml` 加入netty-all和fastjson2依赖 ```xml <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.zzzmh</groupId> <artifactId>netty-demo</artifactId> <version>1.0.0</version> <packaging>jar</packaging> <name>netty-demo</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.109.Final</version> </dependency> <dependency> <groupId>com.alibaba.fastjson2</groupId> <artifactId>fastjson2</artifactId> <version>2.0.49</version> </dependency> </dependencies> </project> ``` <br><br> **后端核心代码** `WebSocketServer.java` ```java import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; 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 WebSocketServer { public static void main(String[] args) throws InterruptedException { EventLoopGroup boss = new NioEventLoopGroup(); EventLoopGroup worker = new NioEventLoopGroup(); ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(boss, worker) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel socketChannel) throws Exception { ChannelPipeline pipeline = socketChannel.pipeline(); pipeline.addLast(new HttpServerCodec()) .addLast(new ChunkedWriteHandler()) .addLast(new HttpObjectAggregator(1024 * 64)) .addLast(new WebSocketServerProtocolHandler("/")) .addLast(new WebSocketHandler()); } }); bootstrap.bind(7001).sync(); } } ``` `WebSocketHandler.java` ```java import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> { @Override protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame frame) throws Exception { System.out.println(frame.text()); } } ``` 之后在IDEA中启动WebSocketServer的main方法即可启动Socket服务器端 <br><br> 客户端随便用html手搓个毛坯房版 `index.html` ```html <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>Netty WebSocket Client Demo</title> </head> <body> <input id="text" type="text"/> <button onclick="send()">Send</button> <button onclick="closeWebSocket()">Close</button> <div id="message"></div> <script> if (!'WebSocket' in window) { alert('当前浏览器不支持WebSocket 请更换浏览器或设备!') } const websocket = new WebSocket("ws://localhost:7001"); //连接发生错误的回调方法 websocket.onerror = function () { setMessageInnerHTML("连接错误"); }; //连接成功建立的回调方法 websocket.onopen = function (event) { setMessageInnerHTML("已连接"); } //接收到消息的回调方法 websocket.onmessage = function (event) { setMessageInnerHTML("已收到消息:" + event.data); } //连接关闭的回调方法 websocket.onclose = function () { setMessageInnerHTML("已断开"); } //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。 window.onbeforeunload = function () { websocket.close(); } //将消息显示在网页上 function setMessageInnerHTML(innerHTML) { document.getElementById('message').innerHTML += '<br>' + innerHTML; } //关闭连接 function closeWebSocket() { websocket.close(); } //发送消息 function send() { const message = document.getElementById('text').value; setMessageInnerHTML("已发送消息:" + message) websocket.send(message); } </script> </body> </html> ``` <br><br> 启动后效果如图 ![](/api/file/getImage?fileId=6629e248da7405001403f625) ![](/api/file/getImage?fileId=6629e247da7405001403f624) #### ===> 进阶款(能聊天) 过程略 直接上代码 后端一共4个类 `WebSocketServer.java` ```java package com.zzzmh.websocket.server; import com.zzzmh.websocket.server.handler.BaseWebSocketHandler; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.*; import io.netty.channel.group.ChannelGroup; import io.netty.channel.group.DefaultChannelGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; 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; import io.netty.util.concurrent.GlobalEventExecutor; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class WebSocketServer { public static final Map<String, Channel> CHANNELS = new ConcurrentHashMap<>(); public static final ChannelGroup GROUP = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); public static void main(String[] args) throws InterruptedException { EventLoopGroup boss = new NioEventLoopGroup(); EventLoopGroup worker = new NioEventLoopGroup(); ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(boss, worker) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel socketChannel) throws Exception { ChannelPipeline pipeline = socketChannel.pipeline(); pipeline.addLast(new HttpServerCodec()) .addLast(new ChunkedWriteHandler()) .addLast(new HttpObjectAggregator(1024 * 64)) .addLast(new WebSocketServerProtocolHandler("/")) .addLast(new BaseWebSocketHandler()); } }); bootstrap.bind(7001).sync(); } } ``` `ResultFrame.java` ```java package com.zzzmh.websocket.server.utils; import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import lombok.Data; @Data public class ResultFrame extends JSONObject { public static TextWebSocketFrame ok(Integer code, Object data) { return new TextWebSocketFrame(JSONObject.of( "code", code, "msg", "success", "data", data ).toJSONString()); } public static TextWebSocketFrame ok(Integer code, JSONObject data) { return new TextWebSocketFrame(JSONObject.of( "code", code, "msg", "success", "data", data ).toJSONString()); } public static TextWebSocketFrame ok(Integer code, JSONArray data) { return new TextWebSocketFrame(JSONObject.of( "code", code, "msg", "success", "data", data ).toJSONString()); } public static TextWebSocketFrame error() { return error(500, "网络异常!"); } public static TextWebSocketFrame error(Integer code, String message) { return new TextWebSocketFrame(JSONObject.of( "code", code, "msg", message, "data", null ).toJSONString()); } } ``` `BaseWebSocketHandler.java` ```java package com.zzzmh.websocket.server.handler; import com.alibaba.fastjson2.JSONObject; import com.zzzmh.websocket.server.WebSocketServer; import com.zzzmh.websocket.server.utils.ResultFrame; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import java.util.Iterator; import java.util.Map; public class BaseWebSocketHandler extends ChannelInboundHandlerAdapter { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { super.channelActive(ctx); WebSocketServer.GROUP.add(ctx.channel()); System.out.println(ctx.channel().id().asShortText() + " 匿名用户连接"); } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { // 用户断开连接事件 super.channelInactive(ctx); WebSocketServer.GROUP.remove(ctx.channel()); Iterator<Map.Entry<String, Channel>> iterator = WebSocketServer.CHANNELS.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<String, Channel> entry = iterator.next(); if(ctx.channel().equals(entry.getValue())){ System.out.println(entry.getKey() + " 断开连接"); iterator.remove(); WebSocketServer.GROUP.writeAndFlush(ResultFrame.ok(201, WebSocketServer.CHANNELS.keySet() )); } } } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { try { TextWebSocketFrame frame = (TextWebSocketFrame) msg; JSONObject req = JSONObject.parseObject(frame.text()); switch (req.getIntValue("code")) { // 客户端连接事件 case 10000 -> ConnectionHandler.execute(ctx, req); case 10001 -> ChatHandler.execute(ctx, req); // code 不在枚举范围中 default -> ctx.channel().writeAndFlush(ResultFrame.error(501, "无效请求!")); } } catch (Exception e) { ctx.channel().writeAndFlush(ResultFrame.error()); } } } ``` `ChatHandler.java` ```java package com.zzzmh.websocket.server.handler; import com.alibaba.fastjson2.JSONObject; import com.zzzmh.websocket.server.WebSocketServer; import com.zzzmh.websocket.server.utils.ResultFrame; import io.netty.channel.ChannelHandlerContext; import io.netty.util.internal.StringUtil; public class ChatHandler { public static void execute(ChannelHandlerContext ctx, JSONObject req) { String nickname = req.getString("nickname"); String message = req.getString("message"); if (StringUtil.isNullOrEmpty(nickname)) { ctx.channel().writeAndFlush(ResultFrame.error(401, "昵称不能为空")); return; } if (StringUtil.isNullOrEmpty(message)) { ctx.channel().writeAndFlush(ResultFrame.error(401, "消息不能为空")); return; } // 群发消息 这里用code区分方法 WebSocketServer.GROUP.writeAndFlush(ResultFrame.ok(202, JSONObject.of( "nickname", nickname, "message", message ))); } } ``` `ConnectionHandler.java` ```java package com.zzzmh.websocket.server.handler; import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; import com.zzzmh.websocket.server.WebSocketServer; import com.zzzmh.websocket.server.utils.ResultFrame; import io.netty.channel.ChannelHandlerContext; import io.netty.util.internal.StringUtil; public class ConnectionHandler { public static void execute(ChannelHandlerContext ctx, JSONObject req) { // 初次连接 需要保存 user chanel String nickname = req.getString("nickname"); if (StringUtil.isNullOrEmpty(nickname)) { ctx.channel().writeAndFlush(ResultFrame.error(401, "昵称不能为空")); return; } WebSocketServer.CHANNELS.put(nickname, ctx.channel()); // 群发消息 这里用code区分方法 WebSocketServer.GROUP.writeAndFlush(ResultFrame.ok(201, WebSocketServer.CHANNELS.keySet() )); } } ``` 前端也就100行代码 `index.html` ```html <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>Netty WebSocket Client Demo</title> </head> <body style="display: flex;flex-direction: column;align-items: center;padding: 16px"> <div style="display:flex;"> <textarea id="console" style="width: 500px;height: 800px" placeholder="控制台"></textarea> <textarea id="messages" style="width: 500px;height: 800px" placeholder="消息列表"></textarea> <textarea id="users" style="width: 500px;height: 800px" placeholder="人员列表"></textarea> </div> <div style="display: flex;flex-direction: row;justify-content: start;width: 1518px;padding: 16px 0;"> <input id="nickname" type="text" placeholder="昵称"/> <input id="message" type="text" placeholder="消息"/> <button onclick="sendMessage()">发送消息</button> <button onclick="openConn()">加入聊天</button> <button onclick="closeConn()">中断连接</button> </div> <script> if (!'WebSocket' in window) { alert('当前浏览器不支持WebSocket 无法继续进行游戏,请更换浏览器或设备!') } const websocket = new WebSocket("ws://localhost:7001"); //连接发生错误的回调方法 websocket.onerror = function () { console("连接错误"); }; //连接成功建立的回调方法 websocket.onopen = function (event) { console("已连接"); } //连接关闭的回调方法 websocket.onclose = function () { console("已断开"); } // 监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。 window.onbeforeunload = function () { websocket.close(); } // 接收到消息的回调方法 websocket.onmessage = function (event) { console("已收到消息:" + event.data); const resp = JSON.parse(event.data); switch (resp.code) { case 201 : users(resp.data); break; case 202 : messages(resp.data.nickname, resp.data.message); break; } } // 关闭连接 function closeConn() { websocket.close(); } // 初始化数据 function openConn() { const nickname = document.querySelector('#nickname').value; if (!nickname) { alert('昵称不能为空'); return; } websocket.send(JSON.stringify({ 'code': 10000, 'nickname': nickname })); } // 发消息 function sendMessage() { const nickname = document.querySelector('#nickname').value; const message = document.querySelector('#message').value; websocket.send(JSON.stringify({ 'code': 10001, 'nickname': nickname, 'message': message, })); console("已发送消息:" + message) } // 控制台 function console(string) { const area = document.querySelector('#console'); area.value += getCurrentTime() + ' ' + string + '\n'; area.scrollTop = area.scrollHeight; } function messages(nickname, string) { const area = document.querySelector('#messages'); area.value += getCurrentTime() + ' ' + nickname + ': ' + string + '\n'; area.scrollTop = area.scrollHeight; } function users(nicknames) { const area = document.querySelector('#users'); area.value = ''; nicknames.forEach(nickname => { area.value += nickname + '\n'; }) area.scrollTop = area.scrollHeight; } // 时间戳 function getCurrentTime() { const now = new Date(); return `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`; } </script> </body> </html> ``` 大致思路是,前端打开页面自动匿名连接socket,匿名可以收到聊天,但不能发消息。填入nickname就可以加入聊天,右侧textarea显示在线人员名单,加入聊天后再填写message点发送消息就可以正常聊天,所有人都是同步收到。 截图 ![](/api/file/getImage?fileId=662b0abfda74050014040059) ![](/api/file/getImage?fileId=662b0abfda7405001404005a) ## END 先折腾到这里,还有客户端的下次在回来更新 累了 ![](/api/file/getImage?fileId=64c9c860da74050014005b69) 送人玫瑰,手留余香 赞赏 Wechat Pay Alipay 2024 HTTPS 免费SSL证书 acme.sh DNS 全自动申请 Let's Encrypt ZeroSSL Unix、Linux、Android、鸿蒙 操作系统简史