Netty实现聊天室

文章目录

注:更多netty相关文章请访问博主专栏: netty专栏

本文内容基于上一篇博客 netty实现WebSocket协议,一些基本使用请参考该博客。

本例实现的功能:

  • 有新成员加入时,群广播消息,欢迎加入
  • 有成员退出时,群广播消息,退出
  • 每个成员都可以发送消息,消息广播给群内的每个人

完整的服务器代码如下:

package com.example;

import com.alibaba.fastjson.JSONObject;
import io.netty.bootstrap.ServerBootstrap;
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.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http.websocketx.*;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.stream.ChunkedFile;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.util.CharsetUtil;
import io.netty.util.concurrent.GlobalEventExecutor;

import javax.activation.MimetypesFileTypeMap;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.net.URLDecoder;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.*;
import java.util.regex.Pattern;

/**
 * netty实现聊天室
 */
public class MyChatRoomServer {

    int port;

    public MyChatRoomServer(int port) {
        this.port = port;
    }

    public void start() {
        ServerBootstrap bootstrap = new ServerBootstrap();
        EventLoopGroup boss = new NioEventLoopGroup();
        EventLoopGroup work = new NioEventLoopGroup();
        try {
            bootstrap.group(boss, work)
                    .handler(new LoggingHandler(LogLevel.DEBUG))
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChatRoomServerInitializer());

            ChannelFuture f = bootstrap.bind(new InetSocketAddress(port)).sync();
            System.out.println("http server started . port : " + port);
            f.channel().closeFuture().sync();

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            boss.shutdownGracefully();
            work.shutdownGracefully();
        }
    }

    public static void main(String[] args) {
        MyChatRoomServer server = new MyChatRoomServer(8080);// 8081为启动端口
        server.start();
    }
}

class ChatRoomServerInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel channel) {
        ChannelPipeline pipeline = channel.pipeline();
        pipeline.addLast(new HttpServerCodec())// http 编解器
                // http 消息聚合器  512*1024为接收的最大contentlength
                .addLast("httpAggregator", new HttpObjectAggregator(512 * 1024))
                // 支持异步发送大的码流(大的文件传输),但不占用过多的内存,防止java内存溢出
                .addLast("http-chunked", new ChunkedWriteHandler())
                .addLast(new ChatRoomRequestHandler());// 请求处理器
    }
}

class ChatRoomRequestHandler extends SimpleChannelInboundHandler<Object> {

    private WebSocketServerHandshaker handshaker;

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("收到消息:" + msg);
        if (msg instanceof FullHttpRequest) {
            //以http请求形式接入,但是走的是websocket
            handleHttpRequest(ctx, (FullHttpRequest) msg);
        } else if (msg instanceof WebSocketFrame) {
            //处理websocket客户端的消息
            handlerWebSocketFrame(ctx, (WebSocketFrame) msg);
        }
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        //添加连接
        System.out.println("客户端加入连接:" + ctx.channel());
        ChannelSupervise.addChannel(ctx.channel());

        TextWebSocketFrame tws = new TextWebSocketFrame(
                "欢迎 " + ctx.channel().id().asShortText() + "; 当前在线总人数:" + ChannelSupervise.count());
        ChannelSupervise.send2All(tws);

    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        //断开连接
        System.out.println("客户端断开连接:" + ctx.channel());
        ChannelSupervise.removeChannel(ctx.channel());

        TextWebSocketFrame tws = new TextWebSocketFrame(
                "再见 " + ctx.channel().id().asShortText() + "; 当前在线总人数:" + ChannelSupervise.count());
        ChannelSupervise.send2All(tws);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }

    /*
    对WebSocket请求进行处理
     */
    private void handlerWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {
        // 判断是否关闭链路的指令
        if (frame instanceof CloseWebSocketFrame) {
            handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());
            return;
        }
        // 判断是否ping消息,如果是,则构造pong消息返回。用于心跳检测
        if (frame instanceof PingWebSocketFrame) {
            ctx.channel().write(new PongWebSocketFrame(frame.content().retain()));
            return;
        }

        // 本例程仅支持文本消息,不支持二进制消息
        if (!(frame instanceof TextWebSocketFrame)) {
            System.out.println("本例程仅支持文本消息,不支持二进制消息");
            throw new UnsupportedOperationException(
                    String.format("%s frame types not supported", frame.getClass().getName()));
        }

        //处理客户端请求并返回应答消息
        String request = ((TextWebSocketFrame) frame).text();
        System.out.println(request);
        TextWebSocketFrame tws = new TextWebSocketFrame(ctx.channel().id().asShortText() + ":" + request);

        // 群发
        ChannelSupervise.send2All(tws);
    }

    /**
     * 唯一的一次http请求。
     * 该方法用于处理websocket握手请求
     */
    private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) {
        //如果HTTP解码失败,返回异常。要求Upgrade为websocket,过滤掉get/Post
        if (!req.decoderResult().isSuccess() || (!"websocket".equals(req.headers().get("Upgrade")))) {
            //若不是websocket方式,则创建BAD_REQUEST(400)的req,返回给客户端
            sendHttpResponse(ctx, req, new DefaultFullHttpResponse(
                    HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST));
            return;
        }

//        构造握手响应返回,本机测试
        WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
                "ws://localhost:8080/websocket", null, false);
        //通过工厂来创建WebSocketServerHandshaker实例
        handshaker = wsFactory.newHandshaker(req);
        if (handshaker == null) {
            WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
        } else {
            /*
            通过WebSocketServerHandshaker来构建握手响应消息返回给客户端。
            同时将WebSocket相关编解码类添加到ChannelPipeline中,该功能需要阅读handshake的源码。
             */
            handshaker.handshake(ctx.channel(), req);
        }
    }

    /**
     * 拒绝不合法的请求,并返回错误信息
     */
    private static void sendHttpResponse(ChannelHandlerContext ctx,
                                         FullHttpRequest req, DefaultFullHttpResponse res) {
        // 返回应答给客户端
        if (res.status().code() != 200) {
            ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(), CharsetUtil.UTF_8);
            res.content().writeBytes(buf);
            buf.release();
            HttpUtil.setContentLength(res, res.content().readableBytes());
        }

        ChannelFuture f = ctx.channel().writeAndFlush(res);

        // 如果是非Keep-Alive,关闭连接
        if (!HttpUtil.isKeepAlive(req) || res.status().code() != 200) {
            f.addListener(ChannelFutureListener.CLOSE);
        }
    }
}

public class ChannelSupervise {
    /**
     * ChannelGroup是netty提供用于管理web于服务器建立的通道channel的,
     * 其本质是一个高度封装的set集合,在服务器广播消息时,
     * 可以直接通过它的writeAndFlush将消息发送给集合中的所有通道中去
     */
    private static ChannelGroup GlobalGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
    /**
     * ChannelMap维护的是channelID和channel的对应关系,用于向指定channel发送消息
     */
    private static ConcurrentMap<String, ChannelId> ChannelMap = new ConcurrentHashMap<>();

    public static void addChannel(Channel channel) {
        GlobalGroup.add(channel);
        ChannelMap.put(channel.id().asShortText(), channel.id());
    }

    public static void removeChannel(Channel channel) {
        GlobalGroup.remove(channel);
        ChannelMap.remove(channel.id().asShortText());
    }

    //找到某个channel来发送消息
    public static Channel findChannel(String id) {
        return GlobalGroup.find(ChannelMap.get(id));
    }

    public static void send2All(TextWebSocketFrame tws) {
        GlobalGroup.writeAndFlush(tws);
    }

    public static int count() {
        return GlobalGroup.size();
    }
}

客户端页面代码:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>my websocket client</title>
</head>
<body>
<textarea id="msgBoxs"></textarea><br>
待发送消息`:<input type="text" id="msg">
<input type="button" id="sendBtn" onclick="send()" value="发送">
<script type="application/javascript">
    var socket ;
    if(!window.WebSocket){
        window.WebSocket = window.MozWebSocket;
    }

    if(window.WebSocket){
        var msgBoxs = document.getElementById("msgBoxs")
        var msgBox = document.getElementById("msg")

        socket = new WebSocket("ws://localhost:8080/websocket")
        socket.onopen = function (evt) {
            console.log("Connection open ...");
            socket.send("Hello WebSocket!");
        }

        socket.onmessage = function (evt) {
            console.log("Received Message: ", evt.data)
            msgBoxs.value =  msgBoxs.value + "\n" + evt.data
        }

        socket.onclose = function (evt) {
            console.log("Connect closed.");
        }
    }else{
        alert("ERROR:您的浏览器不支持WebSocket!!");
    }

    function send() {
        var msg = msgBox.value
        socket.send(msg)
        //msgBox.value = ""
    }

</script>
</body>
</html>

依次打开三个浏览器,作为三个客户端,以channelID作为用户ID。
启动以后可以看到当有用户加入时,前面新加入的用户会受到欢迎的消息。每个用户都可以发送消息,消息会被广播到群里的所有人:
在这里插入图片描述

当关闭一个窗口时,其他窗口会收到再见消息。
注意这里最好在客户端检测退出关闭socket的请求,页面关闭前主动释放与服务器的链接,释放资源,并且其他用户可以第一时间感知到下线,否则需要等待TCP超时后才可以受到下线消息。

简易聊天室完成

注:更多netty相关文章请访问博主专栏: netty专栏

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: Age of Ai 设计师:meimeiellie 返回首页
实付 9.90元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值