SpringBoot2.X + Netty + WebSocket 整合

转载请表明出处 https://blog.csdn.net/Amor_Leo/article/details/107089890 谢谢

SpringBoot2.X + Netty + WebSocket 整合

  • 简介
  • pom
  • yml
  • 实体类
  • 整合
    • config
    • netty server
    • handler
    • 页面
    • 接口
      • controller
      • service
    • 实践

简介

Netty 是一个基于NIO(Nonblocking I/O,非阻塞IO)的客户、服务器端的异步事件驱动的网络应用程序框架。

pom

 
        org.springframework.boot
        spring-boot-starter-parent
        2.3.1.RELEASE
         
    

    
        1.8
    

    
        
            org.springframework.boot
            spring-boot-starter-web
        
        
        
            io.netty
            netty-all
            4.1.50.Final
        
        
        
            cn.hutool
            hutool-all
            5.3.8
        
        
        
            com.google.guava
            guava
            29.0-android
        
        
        
            org.projectlombok
            lombok
            1.18.12
            provided
        
        
        
            org.apache.commons
            commons-lang3
            3.10
        
        
        
            com.alibaba
            fastjson
            1.2.72
        

        
            org.springframework.boot
            spring-boot-starter-test
            test
            
                
                    org.junit.vintage
                    junit-vintage-engine
                
            
        
    

yml

server:
  port: 8080
webSocket:
  netty:
    port: 8000
    path: /ws

实体类

/**
 * @author LHL
 */
@Data
public class MessageRequest {

	/**
	 * 多个handle 通过编写路由handler 通过该属性判断是哪一个handler执行
	 * */
	private String channel;

	/**
	 * 消息时间
	 */
	protected String dateTime;

	/**
	 * 发送者id
	 */
	private String senderId;

	/**
	 * 消息接收方id
	 */
	private String receiverId;

	/**
	 * 发送方头像
	 */
	private String sendPortrait;

	/**
	 * 接收方头像
	 */
	private String receiverPortrait;

	/**
	 * 消息接收方昵称
	 */
	private String nickName;

	/**
	 * 发送者昵称
	 */
	private String senderName;

	/**
	 * 发送时间
	 * */
	private String sendTime;

	/**
	 * 消息id,用于撤回
	 */
	private String msgId;

	/**
	 * 消息实体json字符串
	 */
	private String msgEntity;

}

/**
 * @author LHL
 */
@Data
public class MessageResponse {

	/**
	 * 管道类型
	 */
	private String channel;

	/**
	 * 消息id
	 */
	private String msgId;

	/**
	 * 发送者Id
	 */
	private String senderId;

	/**
	 * 接受者id
	 */
	private String receiverId;

	/**
	 * 发送者对象
	 */
	private Object sender;

	/**
	 * 接受者对象
	 */
	private Object receiver;

	/**
	 * 响应消息
	 */
	private String msgEntity;

	/**
	 * 消息时间
	 */
	private String dateTime;

	/**
	 * 200成功
	 */
	private Integer code;

	/**
	 * 0不是回执,1是回执
	 */
	private Integer isReceipt;

	public MessageResponse(){}

	public MessageResponse(MessageRequest messageRequest){
		this.isReceipt = 1;
		this.code = 500;
		this.channel = messageRequest.getChannel();
		this.senderId = messageRequest.getSenderId();
		this.receiverId = messageRequest.getReceiverId();
		this.msgEntity = messageRequest.getMsgEntity();
	}
}

整合

config

/**
 * @author LHL
 */
public class NettyConfig {

    /**
     * 定义一个channel组,管理所有的channel
     * GlobalEventExecutor.INSTANCE 是全局的事件执行器,是一个单例
     */
    private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    /**
     * 存放用户与Chanel的对应信息,用于给指定用户发送消息
     */
    private static ConcurrentHashMap<String, Channel> userChannelMap = new ConcurrentHashMap<>();

    private NettyConfig() {
    }

    /**
     * 获取channel组
     *
     * @return
     */
    public static ChannelGroup getChannelGroup() {
        return channelGroup;
    }

    /**
     * 获取用户channel map
     *
     * @return
     */
    public static ConcurrentHashMap<String, Channel> getUserChannelMap() {
        return userChannelMap;
    }
}

netty server

/**
 * @author LHL
 */
@Component
public class NettyServer {

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

    /**
     * webSocket协议名
     */
    private static final String WEBSOCKET_PROTOCOL = "WebSocket";

    /**
     * 端口号
     */
    @Value("${webSocket.netty.port}")
    private int port;

    /**
     * webSocket路径
     */
    @Value("${webSocket.netty.path}")
    private String webSocketPath;

    @Autowired
    private WebSocketHandler webSocketHandler;

    private EventLoopGroup bossGroup;

    private EventLoopGroup workGroup;

    /**
     * 启动
     *
     * @throws InterruptedException
     */
    private void start() throws InterruptedException {
        //主线程组,用于接收客户端的链接,但不做任何处理
        bossGroup = new NioEventLoopGroup();
        //定义从线程组,主线程组会把任务转给从线程组进行处理
        workGroup = new NioEventLoopGroup();
        ServerBootstrap bootstrap = new ServerBootstrap();
        // bossGroup辅助客户端的tcp连接请求, workGroup负责与客户端之前的读写操作
        bootstrap.group(bossGroup, workGroup);
        // 设置NIO类型的channel NIO双向通道
        bootstrap.channel(NioServerSocketChannel.class);
        // 设置监听端口
        bootstrap.localAddress(new InetSocketAddress(port));
        /*
         * option是设置 bossGroup,childOption是设置workerGroup
         * netty 默认数据包传输大小为1024字节, 设置它可以自动调整下一次缓冲区建立时分配的空间大小,避免内存的浪费    最小  初始化  最大 (根据生产环境实际情况来定)
         * 使用对象池,重用缓冲区
         */
        bootstrap.option(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator(64, 10496, 1048576));
        bootstrap.childOption(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator(64, 10496, 1048576));
        // 连接到达时会创建一个通道 初始化器,chanel注册后会执行里面相应的初始化方法
        bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {

            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
                // 流水线管理通道中的处理程序(Handler),用来处理业务
                // webSocket协议本身是基于http协议的,所以这边也要使用http编解码器
                ch.pipeline().addLast(new HttpServerCodec());

                //ch.pipeline().addLast(new StringDecoder(CharsetUtil.UTF_8));
                //ch.pipeline().addLast(new StringEncoder(CharsetUtil.UTF_8));
                ch.pipeline().addLast(new ObjectEncoder());
                // 以块的方式来写的处理器
                ch.pipeline().addLast(new ChunkedWriteHandler());
                /*
                请求分段,聚合请求进行完整的请求或响应
                说明:
                1、http数据在传输过程中是分段的,HttpObjectAggregator可以将多个段聚合
                2、这就是为什么,当浏览器发送大量数据时,就会发送多次http请求
                 */
                ch.pipeline().addLast(new HttpObjectAggregator(8192));

                //对httpMessage进行聚合,聚合成FullHttpReq 或者 FullHttpRes
                ch.pipeline().addLast(new HttpObjectAggregator(1024*64));
                //websocket服务器协议,用于指定给客户端链接路由:ws,会帮忙处理握手动作,以及心跳
                //处理心跳检测
                ch.pipeline().addLast(new IdleStateHandler(6*10,0,0));

                /*
                说明:
                1、对应webSocket,它的数据是以帧(frame)的形式传递
                2、浏览器请求时 ws://localhost:8000/xxx 表示请求的uri
                3、核心功能是将http协议升级为ws协议,保持长连接
                */
                ch.pipeline().addLast(new WebSocketServerProtocolHandler(webSocketPath, WEBSOCKET_PROTOCOL, true, 65536 * 10));

                // 自定义的handler,处理业务逻辑
                ch.pipeline().addLast(webSocketHandler);

            }
        });

        //启动
        //绑定端口,并设置为同步方式,是一个异步的chanel
        //配置完成,开始绑定server,通过调用sync同步方法阻塞直到绑定成功
        ChannelFuture channelFuture = bootstrap.bind().sync();

        log.info("Server started and listen on:{}", channelFuture.channel().localAddress());

        /*
         * 关闭
         * 获取某个客户端所对应的chanel,关闭并设置同步方式
         * 对关闭通道进行监听
         */
        channelFuture.channel().closeFuture().sync();
    }

    /**
     * 在程序关闭前
     * 释放资源
     */
    @PreDestroy
    public void destroy() throws InterruptedException {
        if (bossGroup != null) {
            bossGroup.shutdownGracefully().sync();
        }
        if (workGroup != null) {
            workGroup.shutdownGracefully().sync();
        }
    }

    /**
     * 在创建Bean时运行
     * 需要开启一个新的线程来执行netty server 服务器
     */
    @PostConstruct()
    public void init() {
        new Thread(() -> {
            try {
                start();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

handler

/**
 * @author LHL
 * @Description: @Sharable 注解用来说明ChannelHandler是否可以在多个channel直接共享使用
 */
@Component
@ChannelHandler.Sharable
public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

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

    private WebSocketServerHandshaker handshaker;

    /**
     * webSocket路径
     */
    @Value("${webSocket.netty.path}")
    private String webSocketPath;


    /**
     * channel注册
     *
     * @param ctx ChannelHandlerContext
     */
    @Override
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
        log.info("channelRegistered channel注册");
        super.channelRegistered(ctx);
    }


    /**
     * channel注册
     *
     * @param ctx ChannelHandlerContext
     */
    @Override
    public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
        log.info("channelUnregistered channel注册");
        super.channelUnregistered(ctx);
    }

    /**
     * 客户端与服务端第一次建立连接时 执行
     *
     * @param ctx ChannelHandlerContext
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        log.info(date + " " + ctx.channel().remoteAddress() + " 客户端连接成功!");
        ctx.writeAndFlush("连接成功!");
    }

    /**
     * 一旦连接,第一个被执行 客户端连接成功后执行的回调方法
     *
     * @param ctx ChannelHandlerContext
     */
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        log.info("handlerAdded 被调用" + ctx.channel().id().asLongText());
        // 添加到channelGroup 通道组
        NettyConfig.getChannelGroup().add(ctx.channel());
    }

    /**
     * 目前走这个方法
     * 接收到消息执行的回调方法
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        //http请求和tcp请求分开处理
        if (msg instanceof HttpRequest) {
            handleHttpRequest(ctx, (FullHttpRequest) msg);
        } else if (msg instanceof WebSocketFrame) {
            handlerWebSocketFrame(ctx, (WebSocketFrame) msg);
        }
    }

    /**
     * 如果不重写channelRead
     * 则走这个方法
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame frame) throws Exception {
        // //接收到的消息
        //log.info("服务器 收到[" + ctx.channel().remoteAddress() + "]消息:" + frame.text());

        // //获取请求消息对象
        //MessageRequest messageReq = JSONObject.parseObject(frame.text(), MessageRequest.class);
        //MessageResponse messageResp = new MessageResponse(messageReq);
        //String uid = messageReq.getSenderId();
        // //发送者id为空
        //if(StringUtils.isBlank(messageReq.getSenderId())){
        //    messageResp.setMsgEntity("发送用户不存在!");
        //    ctx.channel().writeAndFlush(new TextWebSocketFrame(JSONUtil.toJsonStr(messageResp)));
        //    return;
        //}
        // // 获取用户ID,关联channel
        //bind(ctx, uid);

        // //回复消息
        //messageResp.setIsReceipt(1);
        //messageResp.setCode(200);
        //ctx.channel().writeAndFlush(new TextWebSocketFrame(JSONUtil.toJsonStr(messageResp)));
    }


    /**
     * 第一次请求是http请求,请求头包括ws的信息
     */
    public void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest request)
            throws Exception {
        // 如果HTTP解码失败,返回HTTP异常
        if (null != request) {
            if (request instanceof HttpRequest) {
                HttpMethod method = request.method();
                // 如果是websocket请求就握手升级
                if (webSocketPath.equalsIgnoreCase(request.uri())) {
                    log.info(" req instanceof HttpRequest");
                    WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
                            webSocketPath, null, false);
                    handshaker = wsFactory.newHandshaker(request);
                    if (handshaker == null) {
                        WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
                    }
                    handshaker.handshake(ctx.channel(), request);
                }
            }
        }
    }

    /**
     * websocket消息处理
     * (只支持文本)
     */
    public void handlerWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) throws Exception {
        // 关闭请求
        if (frame instanceof CloseWebSocketFrame) {
            handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());
            return;
        }
        // ping请求
        if (frame instanceof PingWebSocketFrame) {
            ctx.channel().write(new PongWebSocketFrame(frame.content().retain()));
            return;
        }
        // 只支持文本格式,不支持二进制消息
        if (frame instanceof TextWebSocketFrame) {
            //接收到的消息
            log.info("服务器 收到[" + ctx.channel().remoteAddress() + "]消息:" + ((TextWebSocketFrame) frame).text());

            //获取请求消息对象
            MessageRequest messageReq = JSONObject.parseObject(((TextWebSocketFrame) frame).text(), MessageRequest.class);
            MessageResponse messageResp = new MessageResponse(messageReq);
            String uid = messageReq.getSenderId();
            //发送者id为空
            if (StringUtils.isBlank(messageReq.getSenderId())) {
                messageResp.setMsgEntity("发送用户不存在!");
                ctx.channel().writeAndFlush(new TextWebSocketFrame(JSONUtil.toJsonStr(messageResp)));
                return;
            }
            // 获取用户ID,关联channel
            bind(ctx, uid);

            // 回复消息
            messageResp.setIsReceipt(1);
            messageResp.setCode(200);
            ctx.channel().writeAndFlush(new TextWebSocketFrame(JSONUtil.toJsonStr(messageResp)));
        }

    }

    /**
     * channel读取数据完毕
     *
     * @param ctx ChannelHandlerContext
     */
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        log.info("channel读取数据完毕");
        super.channelReadComplete(ctx);
    }

    /**
     * channel可写事件更改
     *
     * @param ctx ChannelHandlerContext
     */
    @Override
    public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
        log.info("channel可写事件更改");
        super.channelWritabilityChanged(ctx);
    }


    /**
     * 这里是保持服务器与客户端长连接  进行心跳检测 避免连接断开
     *
     * @param ctx
     * @param evt
     */
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent stateEvent = (IdleStateEvent) evt;
            switch (stateEvent.state()) {
                //读空闲(服务器端)
                case READER_IDLE:
                    String date1 = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
                    log.info(date1 + "【" + ctx.channel().remoteAddress() + "】读空闲(服务器端)");
                    break;
                //写空闲(客户端)
                case WRITER_IDLE:
                    String date2 = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
                    log.info(date2 + "【" + ctx.channel().remoteAddress() + "】写空闲(客户端)");
                    break;
                case ALL_IDLE:
                    String date3 = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
                    log.info(date3 + "【" + ctx.channel().remoteAddress() + "】读写空闲");
                    break;
                default:
                    break;
            }
        }
    }


    /**
     * 客户端与服务端 断连时 执行
     * 客户端下线
     *
     * @param ctx ChannelHandlerContext
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        log.info(date + " " + ctx.channel().remoteAddress() + " 客户端下线!");
    }

    /**
     * 断开连接
     *
     * @param ctx ChannelHandlerContext
     */
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        log.info("handlerRemoved 被调用" + ctx.channel().id().asLongText());
        // 删除通道
        NettyConfig.getChannelGroup().remove(ctx.channel());
        removeUserId(ctx);
    }

    /**
     * 抛出异常
     *
     * @param ctx   ChannelHandlerContext
     * @param cause 异常
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.info("异常:{}", cause.getMessage());
        // 删除通道
        NettyConfig.getChannelGroup().remove(ctx.channel());
        removeUserId(ctx);
        ctx.close();
    }

    /**
     * 删除用户与channel的对应关系
     *
     * @param ctx ChannelHandlerContext
     */
    private void removeUserId(ChannelHandlerContext ctx) {
        AttributeKey<String> key = AttributeKey.valueOf("userId");
        String userId = ctx.channel().attr(key).get();
        NettyConfig.getUserChannelMap().remove(userId);
    }

    /**
     * 根据用户id判断是否存在用户
     *
     * @param userId 用户id
     * @return Channel
     */
    public static boolean exist(String userId) {
        return NettyConfig.getUserChannelMap().containsKey(userId);
    }

    /**
     * 根据用户id获取对应的channel
     *
     * @param userId 用户id
     * @return Channel
     */
    public static Channel getChannel(String userId) {
        return NettyConfig.getUserChannelMap().get(userId);
    }

    /**
     * 用户绑定channel
     *
     * @param ctx ChannelHandlerContext
     * @param uid 用户id
     */
    public void bind(ChannelHandlerContext ctx, String uid) {
        if (!exist(uid)) {
            NettyConfig.getUserChannelMap().put(uid, ctx.channel());
            // 将用户ID作为自定义属性加入到channel中,方便随时channel中获取用户ID
            AttributeKey<String> key = AttributeKey.valueOf("userId");
            ctx.channel().attr(key).setIfAbsent(uid);
        }
    }

}

页面


<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title>Netty-Websockettitle>
    <script type="text/javascript">
        var socket;
        if (!window.WebSocket) {
            window.WebSocket = window.MozWebSocket;
        }
        if (window.WebSocket) {
            socket = new WebSocket("ws://127.0.0.1:8000/ws");
            socket.onmessage = function (event) {
                var ta = document.getElementById('responseText');
                let resData = JSON.parse(event.data);
                console.log(resData);
                ta.value += "用户: " + resData.senderId + "\t\t消息: " + resData.msgEntity + "\r\n";
            };
            socket.onopen = function (event) {
                var ta = document.getElementById('responseText');
                ta.value = "Netty-WebSocket服务器。。。。。。连接  \r\n";
            };
            socket.onclose = function (event) {
                var ta = document.getElementById('responseText');
                ta.value = "Netty-WebSocket服务器。。。。。。关闭 \r\n";
            };
        } else {
            alert("您的浏览器不支持WebSocket协议!");
        }

        function send(uid, message) {
            if (!window.WebSocket) {
                return;
            }
            let param={
                senderId: uid,
                msgEntity: message
            }
            if (socket.readyState == WebSocket.OPEN) {
                let resData = JSON.stringify(param);
                console.log(resData);
                socket.send(resData);
            } else {
                alert("WebSocket 连接没有建立成功!");
            }

        }

    script>
head>
<body>
<form onSubmit="return false;">
    <div>
        <label>IDlabel><input type="text" name="uid" value="${uid!!}"/> <br/>
        <label>TEXTlabel><input type="text" name="message" value="这里输入消息"/> <br/>
        <br/> <input type="button" value="发送ws消息"
                     onClick="send(this.form.uid.value, this.form.message.value)"/>
    div>
    <hr color="black"/>
    <h3>服务端返回的应答消息h3>
    <textarea id="responseText" style="width: 1024px;height: 300px;">textarea>
form>
body>
html>

接口

controller

/**
 * @author LHL
 */
@RestController
@RequestMapping("/push")
public class PushController {

    @Autowired
    private IPushService pushService;

    /**
     * 推送给所有用户
     *
     * @param msg 消息
     */
    @PostMapping("/pushAll")
    public void pushToAll(@RequestParam("msg") String msg) {
        pushService.pushMsgToAll(msg);
    }

    /**
     * 推送给指定用户
     *
     * @param request 消息信息
     */
    @PostMapping("/pushOne")
    public void pushMsgToOne(@RequestBody MessageRequest request) {
        pushService.pushMsgToOne(request);
    }

}

service

/**
 * @author LHL
 */
public interface IPushService {
    /**
     * 推送给指定用户
     *
     * @param userId
     * @param msg
     */
    void pushMsgToOne(MessageRequest request);

    /**
     * 推送给所有用户
     *
     * @param msg
     */
    void pushMsgToAll(String msg);
}
/**
 * @author LHL
 */
@Service
public class PushServiceImpl implements IPushService {

    @Override
    public void pushMsgToOne(MessageRequest request) {
        Channel channel = null;
        if (exist(request.getSenderId())) {
            channel = getChannel(request.getSenderId());
            MessageResponse response = new MessageResponse(request);
            response.setIsReceipt(1);
            response.setCode(200);
            channel.writeAndFlush(new TextWebSocketFrame(JSONUtil.toJsonStr(response)));
        }
    }

    @Override
    public void pushMsgToAll(String msg) {
        NettyConfig.getChannelGroup().writeAndFlush(new TextWebSocketFrame(msg));
    }


    /**
     * 根据用户id判断是否存在用户
     *
     * @param userId 用户id
     * @return Channel
     */
    public static boolean exist(String userId) {
        return NettyConfig.getUserChannelMap().containsKey(userId);
    }


    /**
     * 根据用户id获取对应的channel
     *
     * @param userId 用户id
     * @return Channel
     */
    public static Channel getChannel(String userId) {
        return NettyConfig.getUserChannelMap().get(userId);
    }

    /**
     * 用户绑定channel
     *
     * @param ctx ChannelHandlerContext
     * @param uid 用户id
     */
    public void bind(ChannelHandlerContext ctx, String uid) {
        NettyConfig.getUserChannelMap().put(uid, ctx.channel());
        // 将用户ID作为自定义属性加入到channel中,方便随时channel中获取用户ID
        AttributeKey<String> key = AttributeKey.valueOf("userId");
        ctx.channel().attr(key).setIfAbsent(uid);
    }
}

实践

打开页面 http://localhost:8080/index.html
SpringBoot2.X + Netty + WebSocket 整合_第1张图片
SpringBoot2.X + Netty + WebSocket 整合_第2张图片

你可能感兴趣的:(JAVA)