Netty + Redis + Websocket IM 分布式集群实现

本文档详细介绍了如何使用Netty、WebSocket和Redis构建一个IM即时通讯系统的分布式集群。配置类中涵盖了连接设置,收发消息Handler处理通信逻辑,Redis工具类用于存储和检索消息,同时利用Redis的订阅监听功能实现实时推送。此外,还包含启动类以运行服务及测试客户端,以及一个简单的HTML页面ws123.html用于测试WebSocket连接。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

常量表
Constants.java 

/**
 * 项目中的全局常量定义
 */
public interface Constants {

    String WEBSOCKET_STR = "websocket";
    String UPGRADE_STR = "Upgrade";
    int OK_CODE = 200;

    String HTTP_CODEC = "http-codec";
    String AGGREGATOR = "aggregator";
    String HTTP_CHUNKED = "http-chunked";
    String HANDLER = "handler";
    int MAX_CONTENT_LENGTH = 65536;
    int PORT = 8989;

    String WEB_SOCKET_URL = "ws://localhost:"+PORT+"/ws";

    //订阅者列表
    String IM_QUEUE_CHANNLID = "im-queue-channlid";
}

配置类:

NettyConfig.java

import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @program: Netty-WebSocket
 * @description: 工程的全局配置类
 **/
public class NettyConfig {

    /**
     * 存储每一个客户端接入进来时的channel对象
     */
    public final static ChannelGroup GROUP = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    /**
     * 本地存储一份map <channel, 1>
     */
    public final static Map<String,String> LOCALCHANNELMAP = new ConcurrentHashMap<>();

    /**
     * 本地存储一份map <tokey, channel>  送达到key,channel
     */
    public final static Map<String, List<String>> LOCALCHANNELLISTMAP = new ConcurrentHashMap<>();

}

 

收发消息 handler

MyWebsocketHandler.java

import com.xx.im.api.WebsocketServer;
import com.xx.im.api.config.Constants;
import com.xx.im.api.config.NettyConfig;
import com.xx.im.api.redis.JedisUtil;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http.websocketx.*;
import io.netty.util.CharsetUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Date;

/**
 * @program: Netty-WebSocket
 * @description: 接收处理并响应客户端WebSocket请求的核心业务处理类
 **/
@ChannelHandler.Sharable
public class MyWebsocketHandler extends SimpleChannelInboundHandler<Object> {

    private static Logger log = LoggerFactory.getLogger(MyWebsocketHandler.class);

    private WebSocketServerHandshaker handshaker;

    /**
     * 服务端处理客户端WebSocket请求的核心方法
     *
     * @param ctx ctx
     * @param msg msg
     * @throws Exception Exception
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
        // 处理客户端向服务端发起http握手请求的业务
        if (msg instanceof FullHttpRequest) {
            handHttpRequest(ctx, (FullHttpRequest) msg);
        }
        // 处理websocket连接
        else if (msg instanceof WebSocketFrame) {
            handWebsocketFrame(ctx, (WebSocketFrame) msg);
        }
    }

    /**
     * 处理客户端与服务端之间的websocket业务
     *
     * @param ctx   ctx
     * @param frame frame
     */
    private void handWebsocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {
        // 判断是否是关闭websocket的指令
        if (frame instanceof CloseWebSocketFrame) {
            handshaker.close(ctx.channel(), ((CloseWebSocketFrame) frame).retain());
            log.debug("接收到关闭websocket的指令");
        }

        // 判断是否是ping消息
        if (frame instanceof PingWebSocketFrame) {
            ctx.channel().write(new PongWebSocketFrame(frame.content().retain()));
            log.debug("接收到ping消息");
            return;
        }

        // 判断是否是二进制消息,如果是二进制消息,则抛出异常
        if (!(frame instanceof TextWebSocketFrame)) {
            log.error("目前不支持二进制消息");
            throw new UnsupportedOperationException("【" + this.getClass().getName() + "】不支持的消息");
        }

        // 获取客户端向服务端发送的消息
        String requestStr = ((TextWebSocketFrame) frame).text();
        log.debug("服务端收到客户端的消息: {}", requestStr);


        // 那个客户端发来的,继续返回给那个客户端
        //Channel channel = NettyConfig.GROUP.find(ctx.channel().id());
        //channel.writeAndFlush(tws);

        // 发布到redis 订阅列表中,进行广播
        String keychannel = ctx.channel().id().asLongText();
        ChannelId id = ctx.channel().id();
        JedisUtil.set(keychannel,requestStr);
        JedisUtil.set(keychannel+"Id",id);
        JedisUtil.pushMsg(keychannel);


        // 返回应答消息
//        String responseStr = new Date().toString()  + ctx.channel().id() +  " ===>>> " + requestStr;
//        TextWebSocketFrame tws = new TextWebSocketFrame(responseStr);
        // 群发,服务端向每个连接上来的客户端群发消息
        //NettyConfig.GROUP.writeAndFlush(tws);

//        log.debug("群发消息完成. 群发的消息为: {}", responseStr);
    }

    /**
     * 处理客户端向服务端发起http握手请求的业务
     *
     * @param ctx     ctx
     * @param request request
     */
    private void handHttpRequest(ChannelHandlerContext ctx, FullHttpRequest request) {
        String upgrade = request.headers().get(Constants.UPGRADE_STR);
        // 非websocket的http握手请求处理
        if (!request.decoderResult().isSuccess() || !Constants.WEBSOCKET_STR.equals(upgrade)) {
            sendHttpResponse(ctx, request,
                    new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST));
            log.warn("非websocket的http握手请求");
            return;
        }

        WebSocketServerHandshakerFactory wsFactory =  new WebSocketServerHandshakerFactory(Constants.WEB_SOCKET_URL, null, false);
        handshaker = wsFactory.newHandshaker(request);
        if (handshaker == null) {
            // 响应不支持的请求
            WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
            log.warn("不支持的请求");
        } else {
            handshaker.handshake(ctx.channel(), request);
            log.debug("正常处理");
        }
    }

    /**
     * 服务端主动向客户端发送消息
     *
     * @param ctx      ctx
     * @param request  request
     * @param response response
     */
    private void sendHttpResponse(ChannelHandlerContext ctx,
                                  FullHttpRequest request,
                                  DefaultFullHttpResponse response) {
        // 不成功的响应
        if (response.status().code() != Constants.OK_CODE) {
            ByteBuf buf = Unpooled.copiedBuffer(response.status().toString(), CharsetUtil.UTF_8);
            response.content().writeBytes(buf);
            buf.release();
            log.warn("不成功的响应");
        }

        // 服务端向客户端发送数据
        ChannelFuture channelFuture = ctx.channel().writeAndFlush(response);
        if (!HttpUtil.isKeepAlive(request) ||
                response.status().code() != Constants.OK_CODE) {
            // 如果是非Keep-Alive,或不成功都关闭连接
            channelFuture.addListener(ChannelFutureListener.CLOSE);
            log.info("websocket连接关闭");
        }
    }

    /**
     * 客户端与服务端创建连接的时候调用
     *
     * @param ctx ctx
     * @throws Exception Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        // 将channel添加到channel group中
        NettyConfig.GROUP.add(ctx.channel());
        NettyConfig.LOCALCHANNELMAP.put( ctx.channel().id().asLongText(),"1");//存在,并且在线
        log.info("客户端与服务端连接开启...");
    }

    /**
     * 客户端与服务端断开连接的时候调用
     *
     * @param ctx ctx
     * @throws Exception Exception
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        // 从channel group中移除这个channel
        NettyConfig.GROUP.remove(ctx.channel());
        NettyConfig.LOCALCHANNELMAP.remove(ctx.channel().id().asLongText());// 不存在,离线
        log.info("客户端与服务端关闭连接...");
    }

    /**
     * 服务端接收客户端发送过来的数据结束之后调用
     *
     * @param ctx ctx
     * @throws Exception Exception
     */
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        // 清空数据
        ctx.flush();

        log.info("flush数据 {}{}", ctx.name(),ctx.channel().id().asLongText());
    }

    /**
     * 工程出现异常的时候调用
     *
     * @param ctx   ctx
     * @param cause cause
     * @throws Exception Exception
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        // 打印异常堆栈
        cause.printStackTrace();
        // 主动关闭连接
        ctx.close();
        log.error("WebSocket连接异常");
    }
}
初始化通道 
MyWebsocketInitializer.java

import com.xx.im.api.config.Constants;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.stream.ChunkedWriteHandler;

public class MyWebsocketInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ch.pipeline().addLast(Constants.HTTP_CODEC, new HttpServerCodec());
        ch.pipeline().addLast(Constants.AGGREGATOR, new HttpObjectAggregator(Const
### Netty 框架 WebSocket 实现与应用 #### WebSocket 协议简介 WebSocket 是 HTML5 定义的一种全双工通信协议,相比传统的 HTTP 请求-响应模式更加高效。通过 WebSocket 可以实现实时的数据交换,在客户端和服务端之间建立持久连接,从而更好地节省服务器资源和带宽,并提供更实时的通讯能力[^1]。 #### 使用场景分析 对于需要频繁交互的应用程序来说,比如在线聊天室、股票行情更新等,采用 WebSocket 技术可以显著提升用户体验。因为 WebSockets 支持双向通信机制,允许客户端和服务端同时独立地向对方发送数据而不必等待另一方回应,非常适合处理即时通讯类需求[^3]。 #### NettyWebSocket实现方式 Netty 提供了一套完整的 API 来支持 WebSocket 功能开发。以下是创建一个简单的 WebSocket 服务器所需的几个关键步骤: 1. **初始化 ServerBootstrap** 配置并启动 NIO 线程组以及绑定监听端口。 2. **设置 ChannelInitializer** 添加必要的处理器到管道中,如 `HttpServerCodec` 和 `WebSocketServerProtocolHandler`,用于解析 HTTP 请求并将之升级为 WebSocket 连接。 3. **自定义业务逻辑 Handler** 编写继承自 `SimpleChannelInboundHandler<WebSocketFrame>` 或者其他适合类型的 handler 类来处理具体的业务逻辑,例如接收消息、广播给所有已连接用户等功能。 下面是一个基本的例子展示如何利用 Netty 构建 WebSocket 服务端: ```java import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; public class WebSocketServer { private final int port; public static void main(String[] args) throws Exception { new WebSocketServer(8080).start(); } public WebSocketServer(int port) { this.port = port; } public void start() throws InterruptedException { EventLoopGroup bossGroup = new NioEventLoopGroup(); // (1) EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); // (2) b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) // (3) .childHandler(new WebSocketServerInitializer()); ChannelFuture f = b.bind(port).sync(); // (4) System.out.println("Web socket server started at port " + port); f.channel().closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } } ``` 此代码片段展示了如何使用 Netty 创建一个 WebSocket 服务器实例,其中包含了线程池配置、通道工厂指定及事件循环分发等内容[^2]。 另外,如果想要构建更为复杂的企业级 IM 应用,则可能还需要考虑诸如集群部署方案等问题。此时可以通过引入 Redis Pub/Sub 特性或者其他分布式协调工具来帮助完成跨节点间的消息同步工作[^4]。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值