spring基于netty实现websocket通信

1.maven引入

        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.8.Final</version>
        </dependency>

 2.服务启动类优化


@Slf4j
@Service
public class NettyServer implements Runnable {
    private int port;
    private Channel channel;
    private EventLoopGroup bossGroup;
    private EventLoopGroup workerGroup;


    public NettyServer() {
        this.port = 8000;
    }

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

    @PostConstruct
    public void init(){
        RedisUtil.deleteKeys(Global.KEY);

        Map<String,String> uploadMap= PropertyUtil.getPropertyMap(Const.REDIS_CONFIG);
        String nettyPort =uploadMap.get("netty.port");
        if(BaseUtil.isNotEmpty(nettyPort)){
            new Thread(new NettyServer(Integer.parseInt(nettyPort))).start();
        }else{
            new Thread(new NettyServer()).start();
        }
    }

    @PreDestroy
    public void destory() {
        System.out.println("destroy server resources");
        if (null == channel) {
            System.out.println("server channel is null");
        }else{
            channel.closeFuture().syncUninterruptibly();
        }
        try{
            bossGroup.shutdownGracefully();
            bossGroup = null;
        }catch (Exception e){

        }
        try{
            workerGroup.shutdownGracefully();
            workerGroup = null;
        }catch (Exception e){

        }
    }


    @Override
    public void run() {
        bossGroup = new NioEventLoopGroup();
        workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b =
                    new ServerBootstrap();
            b.option(ChannelOption.SO_REUSEADDR, true).option(ChannelOption.SO_BACKLOG, 1024)
                    .option(ChannelOption.SO_RCVBUF, 1024 * 128)
                    .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);

            b.childOption(ChannelOption.TCP_NODELAY, true)
                    .childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
                    .childOption(ChannelOption.SO_KEEPALIVE, true);

            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel e) throws Exception {
                            //Socket 连接心跳检测
                            e.pipeline().addLast("idleStateHandler", new IdleStateHandler(60, 0, 0));
                            e.pipeline().addLast(new HttpServerCodec());
                            e.pipeline().addLast(new ChunkedWriteHandler());
                            // HttpObjectAggregator:将HTTP消息的多个部分合成一条完整的HTTP消息
                            e.pipeline().addLast(new HttpObjectAggregator(65536));
                            // ChunkedWriteHandler:向客户端发送HTML5文件
                            e.pipeline().addLast(new ChunkedWriteHandler());
                            // 在管道中添加我们自己的接收数据实现方法
                            e.pipeline().addLast(new MyWebSocketServerHandler(port));
                            e.pipeline().addLast(new WebSocketServerProtocolHandler("/ws",null,true,65536*10));
                        }
                    });
            // 服务器绑定端口监听
            ChannelFuture f = b.bind(port).sync();
            if(f.isSuccess()){
                System.out.println("Netty 服务已启动"+port);
            }
            // 监听服务器关闭监听
            channel = f.channel();
            channel.closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

PropertyUtil读取的是配置文件,自行根据项目情况进行修改

public class Global {

    //所有连接的用户-后续对接
    public static ChannelGroup group = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
    //应用对应所有的链接
    public static Map<Long,Map<String, ChannelGroup>> appUserMap = new HashMap<>();

    public static Map<String, Channel> userMap = new HashMap<>();//id 对应 用户


    public static final String KEY = "USER_KEY";




}

根据情况设计存储的map.方便随时通信


@Component
@Slf4j
@ChannelHandler.Sharable
public class MyWebSocketServerHandler extends SimpleChannelInboundHandler<TextWebSocketFrame > {


    private int port;
    public MyWebSocketServerHandler() {
    }
    public MyWebSocketServerHandler(int port) {
        this.port = port;
    }
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println(ctx.channel().remoteAddress() + " channelRead " );
        Channel channel = ctx.channel();
        //首次连接是FullHttpRequest,处理参数
        if (null != msg && msg instanceof FullHttpRequest) {
            FullHttpRequest request = (FullHttpRequest) msg;
            String uri = request.getUri();
            Map paramMap=getUrlParams(uri);
            //如果url包含参数,需要处理
            if(uri.contains("?")){
                String newUri=uri.substring(0,uri.indexOf("?"));
                request.setUri(newUri);
            }
            String userSig = (String) paramMap.get("userSig");
            try{
                //鉴权
                JSONObject object = UserSignTokenUtil.validSign(userSig);
                UserChannelUtil.handshake(object,channel);
            }catch (NonePrintException e){
                super.channelRead(ctx, msg);
                CloseWebSocketFrame closeFrame = new CloseWebSocketFrame(1008, "服务端错误: " + e.getErrMsg());
                ctx.writeAndFlush(closeFrame).addListener(ChannelFutureListener.CLOSE);
                return;
            }
        }else if(msg instanceof TextWebSocketFrame){
            //正常的TEXT消息类型
            TextWebSocketFrame frame=(TextWebSocketFrame)msg;
            System.out.println("客户端收到服务器数据:" +frame.text());
           // sendAllMessage(frame.text());
        }
        super.channelRead(ctx, msg);
    }

    private void sendAllMessage(String message){
        //收到信息后,群发给所有channel
        Global.group.writeAndFlush( new TextWebSocketFrame(message));
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        System.out.println(ctx.channel().remoteAddress() + " channelRead0 " );
        String jsonString = msg.text();
        try{
            SocketRequest socketRequest = JSON.parseObject(jsonString, SocketRequest.class);
            if(BaseUtil.isEmpty(socketRequest.getM())){
                return;
            }
            SocketUtil.handleMessage(socketRequest,ctx);
        }catch (Exception e){
            log.error("websocket执行错误:{}",e.getMessage());
        }

        //ctx.channel().writeAndFlush(new TextWebSocketFrame("服务器时间" + LocalDateTime.now() + " " + msg.text()));
    }

    @Override
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
        System.out.println(ctx.channel().remoteAddress() + " channelRegistered " );
        System.out.println("与客户端建立连接,通道开启!");
    }
    @Override
    public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
        System.out.println(ctx.channel().remoteAddress() + " channelUnregistered " );
    }
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {

    }
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();
        UserChannelUtil.remove(channel);
    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.out.println(ctx.channel().remoteAddress() + " exceptionCaught :" + cause.getMessage() );
        super.exceptionCaught(ctx, cause);
    }


    private static Map getUrlParams(String url){
        Map<String,String> map = new HashMap<>();
        url = url.replace("?",";");
        if (!url.contains(";")){
            return map;
        }
        if (url.split(";").length > 0){
            String[] arr = url.split(";")[1].split("&");
            for (String s : arr){
                String key = s.split("=")[0];
                String value = s.split("=")[1];
                map.put(key,value);
            }
            return  map;
        }else{
            return map;
        }
    }
}

鉴权逻辑不需要可以直接忽略,如果需要可私信

3.统一请求方式

@Data
public class SocketRequest {

    //请求方法
    private String m;
    //请求参数
    private String data;

}

4.统一返回格式

@Data
public class SocketResult {

    //方法名称
    private String m;
    //错误信息
    private String errMsg;
    //错误code 0 正常
    private Integer errCode = 0;
    //数据
    private Object data;
}

public class SocketResultUtil {

    /**
     * 成功返回数据
     * @param o
     * @param m
     * @return
     */
    public static SocketResult success(Object o, String m) {
        SocketResult result = new SocketResult();
        result.setM(m);
        result.setErrMsg("");
        result.setErrCode(0);
        result.setData(o);
        return result;
    }

    /**
     * 失败返回数据
     * @param code
     * @param desc
     * @param m 请求方法
     * @return
     */
    public static SocketResult fail(Integer code, String desc,String m,Object o) {
        SocketResult result = new SocketResult();
        result.setM(m);
        result.setErrMsg(desc);
        result.setErrCode(code);
        result.setData(o);
        return result;
    }



    public static ChannelFuture sendFail(ChannelHandlerContext ctx, SocketError socketError, String m) {
        return sendFail(ctx,socketError.getCode(),socketError.getDesc(),m,null);
    }

    public static ChannelFuture sendFail(ChannelHandlerContext ctx,Integer code, String desc, String m,Object o) {
        ChannelFuture future = ctx.writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(fail(code,desc,m,o))));
        return future;
    }

    public static void sendParamFail(ChannelHandlerContext ctx,String desc, String m,Object o) {
        ctx.writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(fail(SocketError.PARAM_ERROR.getCode(),SocketError.PARAM_ERROR.getDesc()+desc, m,o))));
    }




}

5.消息处理demo

/**
     * 消息处理
     * @param socketRequest
     * @param ctx
     */
    public static void handleMessage(SocketRequest socketRequest, ChannelHandlerContext ctx) {
        if(!SocketParamCheck.check(socketRequest)){
            SocketResultUtil.sendFail(ctx, SocketError.PARAM_ERROR,"");
            return;
        }
        switch (socketRequest.getM()){
            //心跳
            case "ping":
                MessageSender.send(ctx.channel(),CmdEnums.heartbeat, TimeUtil.getUnixTime());
                break;
            //根据不同方法处理消息结果返回
            case "sendMessage":
                //这里处理消息
                break;
            default:
                SocketResultUtil.sendFail(ctx, SocketError.UN_KNOW_M,socketRequest.getM());
                break;
        }
    }

6.发送方法

public static void send(Channel channel, String m,Object o) {
        channel.writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(SocketResultUtil.success(o,m))));
    }
 /**
     * 发送消息到用户
     * @param userId
     * @param cmdEnums
     * @param object
     */
    public static void sendToUser(String userId, CmdEnums cmdEnums, Object object) {
        Channel channel = UserChannelUtil.getUserChannel(userId);
        if(UserChannelUtil.checkChannel(channel)){
            send(channel,cmdEnums,object);
        }
    }

注意:在分布式环境中,Global类中的连接管理需替换为Redis等分布式存储,可通过Channel.attr()绑定用户信息实现集群管理

### Spring Cloud 和 Netty 实现 WebSocket 通信 #### 配置依赖项 为了使应用程序能够支持 WebSocket 并通过 Netty 处理请求,在 `pom.xml` 文件中添加必要的依赖项: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <!-- 如果使用 Nacos --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> </dependency> ``` #### 创建 WebSocket Handler 类 创建一个名为 `MyWebSocketHandler.java` 的类来处理来自客户端的消息并响应它们。此类应扩展 `SimpleChannelInboundHandler<TextWebSocketFrame>`,以便可以轻松地读取传入的数据帧。 ```java import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; public class MyWebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> { @Override protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception { String content = msg.text(); // 对接收到的内容做进一步处理 // 发送回复给客户端 ctx.writeAndFlush(new TextWebSocketFrame("Server received: " + content)); } } ``` #### 启动器配置 编写启动程序以初始化服务器实例,并监听特定端口上的连接尝试。这通常是在主应用程序文件中的 `main()` 方法内完成的。 ```java import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; import reactor.ipc.netty.NettyContext; import reactor.ipc.netty.http.server.HttpServer; @Component public class WebSocketStarter { private final int port; public WebSocketStarter(@Value("${server.port}") int port) { this.port = port; } @Bean public Mono<NettyContext> start() { return HttpServer.create() .port(port) .newRouter(routes -> routes.get("/ws", (req, res) -> req.acceptWebSocket(webSocketInbound -> webSocketInbound.aggregateFrames().receive() .asString() .doOnNext(System.out::println) .then())) ) .bindNow(); } } ``` 上述代码片段展示了如何设置基于 Reactor Netty 库构建的基础 Web Socket 服务[^1]。请注意,这里假设已经安装了 Spring Boot 及其相关组件作为开发环境的一部分。 对于更复杂的场景,比如与消息队列集成,则可以在 `MyWebSocketHandler` 中加入相应的逻辑用于接收 MQ 推送的信息并向所有已建立连接的客户广播这些更新[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

亲亲果果果冻

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值