网络|基于Netty、WebSocket构建的高性能推送系统

WebSocket

       Websocket是 全双工协议(客户端服务端之间两个方向,只能同时双向传输)他是基于Http协议是半双工协议,(客户端服务端之间两个方向,只能单向传输)的基础上发展而来的,首次和后端服务通信采用Http协议,后续通信都是采用tcp协议,他的最大应用点在于可以主动的和客户端通信,区别以往只能由客户端主动发送的过程,可以使用该技术来完成很多场景,例如大屏展示,需要实时刷新数据,不可能由客户端不断的发送http请求,给服务端造成巨大的负担,而是由服务器主动更新。

在这里插入图片描述Http、Https、WebSocket、WebSockets

       WebSocket 和 HTTP 一样,都需要先通过request和response来完成第一次交互请求,不同的地方在于websocket 也有一些约定的通讯方式,http 通讯方式为 http 开头的方式, 例如 http://xxx.com/getDzx ,WebSocket 通讯方式则为 ws 开头的方式,例如 ws://xxx.com/getDzx

SSL:

  • HTTPS: https://xxx.com/getDzx
  • WEBSOCKETS: wss:/x.com/getDzx

       其实他们之间的关系用一张图,即可清楚的描述

在这里插入图片描述
场景简介

       同实时公交的场景一致,在完成了数据(定位数据等)采集后,需要把定位信息计算的结果实时刷新到客户端页面(移动端、pc端),实现效果如下图,图中的蓝色小车需要不断刷新位置。

在这里插入图片描述
技术难点&方案

  • 如何高性能刷新客户端,防止页面渲染卡顿?

        方案1:采用Http接口请求方式,定时请求后端服务(阻塞页面渲染,页面经常卡顿)

        方案2:采用多端口,基于netty支持各种协议(netty本身支持多种协议,包括Http、WebSocket、SSL等)

  • 多种传输协议都要能同时支持?

        方案1:采用多端口,基于netty支持各种协议(netty本身支持多种协议,包括Http、WebSocket、SSL等)

        方案2:只用一个端口,基于netty做动态的ChannelPipleLine删减,对不同的协议,需要特定的协议标识(例如:协议A,B,ChannelPipleLine上有他们对应的channelHandler,只要识别出是协议A,则添加协议A的channelHandler到ChannelPipleLine上,若下一时刻是协议B,则删除协议A的channelHandler,添加协议B相关channelHandler)

核心代码

       netty服务端BaseServer

/**
 * Description:
 * <p>
 * 采用模板设计模式,提供统一的netty server模板类,
 * 以便支持多端口
 * </p>
 *
 * @author dzx
 * @date 2019/10/27 17:59
 */
@Slf4j
public abstract class BaseServer {
    /**
     * 创建bossGroup线程组,监听客户端连接线程组
     */
    public EventLoopGroup bossGroup = null;

    /**
     * 创建workGroup线程组,处理客户端请求线程组
     */
    public EventLoopGroup workGroup = null;

    /**
     * 创建handlerGroup线程组,处理具体业务
     */
    public EventExecutorGroup handlerGroup = null;

    public ServerBootstrap serverBootstrap = null;


    /**
     * 启动服务端
     */
    public void start(int serverPort) {
        if (bossGroup != null) {
            return;
        }
        try {
            // 创建线程组,服务配置对象
            bossGroup = new NioEventLoopGroup(2);
            workGroup = new NioEventLoopGroup();
            handlerGroup = new DefaultEventExecutorGroup(Runtime.getRuntime().availableProcessors());
            serverBootstrap = new ServerBootstrap();

            // 配置缓存区大小,netty服务端类型,采用主从多线程模型
            serverBootstrap.channel(NioServerSocketChannel.class)
                    .group(bossGroup, workGroup)
                    .option(ChannelOption.SO_BACKLOG, 1024)
                    .option(ChannelOption.SO_RCVBUF, 16 * 1024)
                    .option(ChannelOption.SO_SNDBUF, 16 * 1024)
                    .childOption(ChannelOption.TCP_NODELAY, true)
                    .childOption(ChannelOption.SO_KEEPALIVE, true);

            // 设置处理器
            setChildrenHandler();

            // 绑定端口启动
            serverBootstrap.bind(serverPort).sync();
            log.info("Netty server bind port {}", serverPort);
        } catch (Exception e) {
            //异常关闭
            log.error("Netty access to resources fail", e);
            close();
        }
    }
    /**
     * 设置处理器抽象方法
     */
    public abstract void setChildrenHandler();

    /**
     * 关闭方法
     */
    public void close() {
        if (bossGroup != null) {
            bossGroup.shutdownGracefully();
            workGroup.shutdownGracefully();
            handlerGroup.shutdownGracefully();
        }
    }

}

       WebSocketServer 端

@Component
@Slf4j
public class WebSocketServer extends BaseServer {

    private final WebSocketHandler webSocketHandler;

    /**
     * 采用spring团队建议,构造注入
     */
    public WebSocketServer(
            WebSocketHandler webSocketHandler) {
        this.webSocketHandler = webSocketHandler;

    }


    @Override
    public void setChildrenHandler() {
        serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel ch) {
               ch.pipeline()
                        //将请求和应答消息编码或者解码为Http消息
                        .addLast(new HttpServerCodec())
                        //将http消息的多个部分组合成一条完整的http消息
                        .addLast(new HttpObjectAggregator(65536))
                        //像客户端发送文件
                        .addLast(new ChunkedWriteHandler())
                        //监听路径为/ws的请求 WebSocket支持的handler
                        .addLast(new WebSocketServerProtocolHandler("/ws"))
                        .addLast(handlerGroup, webSocketHandler);
            }
        });
    }

}

       WebSocketHandler端

/**
 * description:
 * <p>
 * webSocket的handler处理类
 * TextWebSocketFrame是用来获取请求WebSocket消息的,单位是frame(帧)
 * </p>
 *
 * @author : dzx
 * Date : 2019/11/1 11:36
 */
@Slf4j
@ChannelHandler.Sharable
@Component
public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {

        log.info("webServer 接收到msg=" + msg.text());
        //判断是否存在,此处msg接收到的是车辆的唯一识别号
        if (PackageHandlerUtil.container.containsKey(msg.text())) {
            PackageHandlerUtil.container.get(msg.text()).add(ctx);
        } else {
            List<ChannelHandlerContext> contexts = new ArrayList<>();
            contexts.add(ctx);
            PackageHandlerUtil.container.put(msg.text(), contexts);
        }
    }
    /**
     * 通道关闭方法
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        log.info("channelInactive....");
        Map<String, List<ChannelHandlerContext>> container1 = PackageHandlerUtil.container;
        //循环判断属于当前通道的ChannelHandlerContext,取出并删除
        for (Map.Entry<String, List<ChannelHandlerContext>> container : container1.entrySet()) {
            if (container.getValue().contains(ctx)) {
                container.getValue().remove(ctx);
                log.info(ctx.toString() + ", remove....");
            }
        }
        ctx.fireChannelInactive();
    }
    /**
     * 通道激活方法
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        log.info("channelActive....");
        ctx.fireChannelActive();
    }
}

       PackageHandlerUtil

@Slf4j
public class PackageHandlerUtil {
   /**
     * 此处设计的结构为String和List的ChannelHandlerContext
     * 以车辆的唯一识别码作为key,每次用户的会话信息根据同一辆车(同一个key)而被放入集合中
     * 若接收到某辆车的数据信息,便可以把该辆车下的list的所有会话信息都拿到
     * 利用这一的结构可以简单的做一个广播事件(类似redis的广播订阅)
     */
    public static Map<String, List<ChannelHandlerContext>> container = new ConcurrentHashMap<>(100000);
}

       广播代码示例

 //广播消息到该车辆的每个channelContext,TerminalAddress是车辆的唯一识别码
        if (PackageHandlerUtil.container.containsKey(busHeartBeat.getMsgHead().getTerminalAddress())) {
            for (ChannelHandlerContext context : PackageHandlerUtil.container.get(busHeartBeat.getMsgHead().getTerminalAddress())) {
            //使用websocket的时候需要以TextWebSocketFrame帧的形式发送消息
                context.writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(targetSite)));
            }
        }

        到此,我们的技术难点都逐一解决,但是可以发现,有一个很致命的问题,那就是单点存储,就单台服务器来说,会话信息无法海量的容纳,单凭该初始化数量就可以发现该问题,初始化就有10000台,若数量多呢,如何满足分布式的要求?

public static Map<String, List<ChannelHandlerContext>> container = new ConcurrentHashMap<>(100000);

       同时我们也发现WebSocket要做成集群的话,需要面临的问题很多,例如:

  • WebSocket的会话信息无法序列化会,无法远程存储
    java.io.NotSerializableException: 
    org.springframework.web.socket.adapter.standard.StandardWebSocketSession
  • WebSocket若做成分布式,如何共享消息,做广播推送,或订阅推送

       

以下是博主的几个方案:(以下会话消息都指ChannelHandlerContext)

  • 方案1:使用负载均衡(使用nginx,或最小连接,详细可以参考

网络|基于Netty构建的高性能车辆网项目实现(三)
技术,均摊各个节点会话压力,既然WebSocket的会话消息不能远程存储,只能单机存储,那么我们就依旧存储在单机上,只是要均摊,只能固定用户访问指定节点的服务器,nginx能完成客户端的指定服务器访问,至于硬件接入层方面,我们需要把采集的信息放入高性能中间件(Kafka/RabbitMq/RocketMq)中,由后端中的中间服务(消费mq消息,把消费信息发送给指定的用户),由于需要拆分netty和Websocket,此处实现的WebSocket使用springBoot中的,会话信息是(自定义的信息类,同样使用concurrentHashMap存储)。(推荐,但有一定的局限性,多少用户,什么用户固定访问一台什么样的机器,需要具体业务衡量)

  • 方案2:使用redis的消息订阅代替mq(不推荐,redis不是专门用于队列消费场景,消峰能力有限)

方案1图例

在这里插入图片描述

介于时间仓促,代码会适时贴出~

       前端代码

<html>
<head>
    <meta charset="UTF-8"></meta>
    <title>springboot项目WebSocket测试demo</title>
</head>
<body>
<h3>springboot项目websocket测试demo</h3>
<h4>测试说明</h4>
<h5>文本框中数据数据,点击‘发送测试’,文本框中的数据会发送到后台websocket,后台接受到之后,会再推送数据到前端,展示在下方;点击关闭连接,可以关闭该websocket;可以跟踪代码,了解具体的流程;代码上有详细注解</h5>
<br />
<input id="text" type="text" />
<button οnclick="send()">发送测试</button>
<hr />
<button οnclick="clos()">关闭连接</button>
<hr />
<div id="message"></div>
<script>
    var websocket = null;
    if('WebSocket' in window){
        websocket = new WebSocket("ws://buspos.bdxhcom.com/ws");
    }else{
        alert("您的浏览器不支持websocket");
    }
    websocket.onerror = function(){
        setMessageInHtml("send error!");
    }
    websocket.onopen = function(){
        setMessageInHtml("connection success!")
    }
    websocket.onmessage  = function(event){
        setMessageInHtml(event.data);
    }
    websocket.onclose = function(){
        setMessageInHtml("closed websocket!")
    }
    window.onbeforeunload = function(){
        clos();
    }
    function setMessageInHtml(message){
        document.getElementById('message').innerHTML += message;
    }
    function clos(){
        websocket.close(3000,"强制关闭");
    }
    function send(){
        var msg = document.getElementById('text').value;
        websocket.send(msg);
    }
</script>
</body>
</html>

参考资料

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值