Netty Websocket多人多房间聊天室Demo

Netty Websocket多人多房间聊天室Demo

描述:可任意输入自己的昵称和要加入的聊天室名,即可加入聊天室进行聊天,在同一个聊天室内的所有人的发言每个人都会立即收到

项目使用springboot,源码https://download.csdn.net/download/HumorChen99/15843430
https://gitee.com/HumorChen/netty_websocket_chatroom_demo

主要是这么些步骤:

  1. 建立两个事件轮询组NioEventLoopGroup,一个用来处理连接的建立,一个用来处理消息的处理
  2. 建立一个启动器ServerBootstrap,方便我们启动netty服务器
  3. 绑定启动器的轮询组bootstrap.group(boss,worker)
  4. 指定使用Nio管道非阻塞.channel(NioServerSocketChannel.class)
  5. 配置管道初始化器.childHandler(new ChannelInitializer() {…}
  6. 在初始化器里配置一系列的处理器handler
  7. 然后绑定端口同步启动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>
    
    
posted @ 2020-12-04 11:36  HumorChen99  阅读(1)  评论(0编辑  收藏  举报  来源