Netty Websocket多人多房间聊天室Demo
Netty Websocket多人多房间聊天室Demo
描述:可任意输入自己的昵称和要加入的聊天室名,即可加入聊天室进行聊天,在同一个聊天室内的所有人的发言每个人都会立即收到
项目使用springboot,源码https://download.csdn.net/download/HumorChen99/15843430
https://gitee.com/HumorChen/netty_websocket_chatroom_demo
主要是这么些步骤:
- 建立两个事件轮询组NioEventLoopGroup,一个用来处理连接的建立,一个用来处理消息的处理
- 建立一个启动器ServerBootstrap,方便我们启动netty服务器
- 绑定启动器的轮询组bootstrap.group(boss,worker)
- 指定使用Nio管道非阻塞.channel(NioServerSocketChannel.class)
- 配置管道初始化器.childHandler(new ChannelInitializer() {…}
- 在初始化器里配置一系列的处理器handler
- 然后绑定端口同步启动netty服务器
- 处理器里分为两个自定义的,一个是处理http的一个是处理ws消息的。
因为创建ws的时候第一次是发的一个http请求,所以我们利用这次http请求来完成加入聊天室的操作,让发起ws的时候url里带入昵称和聊天室名字的参数,然后握手之前先得到这些参数用来初始化,然后继续握手创建管道,然后把管道放入到这个聊天室的管道组,如果某个管道来了消息,那么要群发给管道里的每个人(要从管道知道是哪个聊天室的,以及这个用户名称,那么我们在初始化的时候将信息存入了管道的属性里去了attr)。
处理消息的时候是责任链模式,每个handler依次执行,直到某个handler处理掉这个消息
-
依赖
<!--netty的依赖集合,都整合在一个依赖里面了--> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.25.Final</version> </dependency>
-
NettyWebServer 创建Netty服务器
package com.example.netty_websocket_chatroom_demo.server; import com.example.netty_websocket_chatroom_demo.handler.ChatHandler; import com.example.netty_websocket_chatroom_demo.handler.HttpHandler; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; 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 NettyWebServer { public static void run(){ EventLoopGroup boss=new NioEventLoopGroup(); EventLoopGroup worker=new NioEventLoopGroup(); try { ServerBootstrap bootstrap=new ServerBootstrap(); bootstrap.group(boss,worker).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline=ch.pipeline(); //http编码解码器 pipeline.addLast(new HttpServerCodec()); //分块传输 pipeline.addLast(new ChunkedWriteHandler()); //块聚合 pipeline.addLast(new HttpObjectAggregator(1024*1024)); //进入聊天室的http处理器 pipeline.addLast(new HttpHandler()); //协议处理器 // pipeline.addLast(new WebSocketServerProtocolHandler("/ws")); //自定义的处理器 pipeline.addLast(new ChatHandler()); } }); ChannelFuture channelFuture=bootstrap.bind(8081).sync(); channelFuture.channel().closeFuture().sync(); }catch (Exception e){ e.printStackTrace(); }finally { boss.shutdownGracefully(); worker.shutdownGracefully(); } } }
-
HttpHandler用来处理升级协议的http请求并同时处理加入聊天室的操作
package com.example.netty_websocket_chatroom_demo.handler; import com.example.netty_websocket_chatroom_demo.constant.ChatroomConst; import com.example.netty_websocket_chatroom_demo.data.ChatRoom; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.*; import io.netty.channel.group.ChannelGroup; import io.netty.channel.group.DefaultChannelGroup; import io.netty.handler.codec.http.*; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker; import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory; import io.netty.util.AttributeKey; import io.netty.util.CharsetUtil; import io.netty.util.concurrent.ImmediateEventExecutor; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.util.Map; public class HttpHandler extends SimpleChannelInboundHandler<FullHttpRequest> { private WebSocketServerHandshaker handshaker; static { AttributeKey.newInstance(ChatroomConst.IP); AttributeKey.newInstance(ChatroomConst.USERNAME); AttributeKey.newInstance(ChatroomConst.CHATROOM); } @Override protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception { QueryStringDecoder decoder=new QueryStringDecoder(req.uri()); String chatroomName=decoder.parameters().get("chatroom").get(0); String nickName=decoder.parameters().get("nickname").get(0); if(chatroomName==null || "".equals(chatroomName) || nickName==null || "".equals(nickName) || !req.decoderResult().isSuccess()||!"websocket".equals(req.headers().get("Upgrade"))){ FullHttpResponse response=new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST); ChannelFuture future=ctx.channel().writeAndFlush(response); future.addListener(ChannelFutureListener.CLOSE); }else{ //获取IP String ip=((InetSocketAddress)ctx.channel().remoteAddress()).getAddress().getHostAddress(); if("0:0:0:0:0:0:0:1".equals(ip)){ ip="127.0.0.1"; } String username=nickName+"("+ip+")"; ctx.channel().attr(AttributeKey.valueOf(ChatroomConst.IP)).set(ip); ctx.channel().attr(AttributeKey.valueOf(ChatroomConst.USERNAME)).set(username); ctx.channel().attr(AttributeKey.valueOf(ChatroomConst.CHATROOM)).set(chatroomName); WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory( req.uri(), null, false); handshaker = wsFactory.newHandshaker(req); if (handshaker == null) { WebSocketServerHandshakerFactory .sendUnsupportedVersionResponse(ctx.channel()); } else { handshaker.handshake(ctx.channel(), req).sync(); ChannelGroup channelGroup=ChatRoom.map.get(chatroomName); if(channelGroup==null){ channelGroup=new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE); channelGroup.add(ctx.channel()); ChatRoom.map.put(chatroomName,channelGroup); }else{ TextWebSocketFrame textWebSocketFrame=new TextWebSocketFrame(username+" 加入了聊天室"); for(Channel channel:channelGroup){ channel.writeAndFlush(textWebSocketFrame); } channelGroup.add(ctx.channel()); } } } } @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { System.out.println("通道建立"); super.channelActive(ctx); } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { System.out.println("通道关闭"); String chatroom=(String) ctx.channel().attr(AttributeKey.valueOf(ChatroomConst.CHATROOM)).get(); if(chatroom!=null){ ChannelGroup channelGroup=ChatRoom.map.get(chatroom); channelGroup.remove(ctx.channel()); } super.channelInactive(ctx); } }
-
ChatHandler 聊天信息的处理器
package com.example.netty_websocket_chatroom_demo.handler; import com.example.netty_websocket_chatroom_demo.constant.ChatroomConst; import com.example.netty_websocket_chatroom_demo.data.ChatRoom; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.group.ChannelGroup; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.util.AttributeKey; import java.util.Date; public class ChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> { @Override protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception { String text=msg.text(); String username=(String) ctx.channel().attr(AttributeKey.valueOf(ChatroomConst.USERNAME)).get(); String chatroom=(String) ctx.channel().attr(AttributeKey.valueOf(ChatroomConst.CHATROOM)).get(); String id=ctx.channel().id().asLongText(); if(chatroom!=null){ ChannelGroup channelGroup= ChatRoom.map.get(chatroom); if(channelGroup!=null){ String time=new Date().toLocaleString(); TextWebSocketFrame other=new TextWebSocketFrame(time+" "+username+": "+text); TextWebSocketFrame me=new TextWebSocketFrame(time+" "+"我: "+text); for(Channel channel:channelGroup){ String channelId=channel.id().asLongText(); if(id.equals(channelId)){ channel.writeAndFlush(me); }else { channel.writeAndFlush(other); } } } } } }
-
ChatRoom 用来存放聊天室和每个聊天室里的channel
package com.example.netty_websocket_chatroom_demo.data; import io.netty.channel.group.ChannelGroup; import java.util.HashMap; import java.util.Map; /** * 存放每个聊天室和每个聊天室里的数据 */ public class ChatRoom { public static Map<String, ChannelGroup> map=new HashMap<>(); }
-
ChatroomConst 常量类
package com.example.netty_websocket_chatroom_demo.constant; /** * 常量 */ public interface ChatroomConst { String IP="IP"; String USERNAME="USERNAME"; String CHATROOM="CHATROOM"; }
-
测试页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>WebSocketTest</title> <script> socket=undefined /** * 往屏幕里输出信息 */ function addMsg(msg) { let content=document.getElementById('content') content.innerHTML=content.innerHTML+msg+"<br>" } function send() { let msg=document.getElementById('input').value if(msg){ if (!window.WebSocket) { return } if (socket.readyState == WebSocket.OPEN) { socket.send(msg) document.getElementById('input').value='' } else { alert('连接没有开启.') } }else{ alert('不允许空消息') } } /** * 加入聊天室 */ function enter() { nickname=document.getElementById('nickname').value chatroom=document.getElementById('chatroom').value url='ws://192.168.0.140:8081/ws?nickname='+nickname+'&chatroom='+chatroom if (!window.WebSocket) { window.WebSocket = window.MozWebSocket } if (window.WebSocket) { socket = new WebSocket(url) socket.onopen = function(event) { addMsg('聊天室'+chatroom+' 连接成功') console.log(event) } socket.onclose = function(event) { addMsg('连接被关闭') } socket.onmessage = function(event) { addMsg(event.data) } } else { alert('你的浏览器不支持 WebSocket!') } } /** * 退出聊天室 */ async function exit() { socket.close() addMsg("退出聊天室") } </script> </head> <body> <div id="content" style="width: 400px;height: 300px;"> </div> <div style="width: 400px;height: 200px"> <input type="text" id="nickname" placeholder="请输入要使用的昵称"><input type="text" id="chatroom" placeholder="请输入要加入的聊天室名"><button onclick="enter()">进入聊天室</button> <textarea id="input" placeholder="请输入短信内容" style="width: 100%;height: 80px"></textarea> <button onclick="send()">发送</button> <button onclick="exit()">退出聊天室</button> </div> </body> </html>
-
application.yml 项目配置
server: port: 80
-
完整maven pom.xml
<?xml version="1.0" encoding="UTF-8"?> <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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.4.0</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>netty_websocket_chatroom_demo</artifactId> <version>1.0.1-SNAPSHOT</version> <name>netty_websocket_chatroom_demo</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <!--netty的依赖集合,都整合在一个依赖里面了--> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.25.Final</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
本文来自博客园,作者:HumorChen99,转载请注明原文链接:https://www.cnblogs.com/HumorChen/p/18039682
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
· 三行代码完成国际化适配,妙~啊~