netty-websocket-spring-boot-starter基于netty的轻量级的高性能socket服务器

netty

老生常谈,干啥的?一个网络通信协议框架,自己可以各种自定义,具体的,网上一捞一大把。两大特性:NIO和零拷贝。

netty-websocket-spring-boot-starter

版本约定:0.9.5
基于此版演绎的,因为每个版本有轻微区别
本人已在生产运行超过一年之久。

官方文档:

直达网站https://gitee.com/Yeauty/netty-websocket-spring-boot-starter

这是个开源的框架。通过它,我们可以像spring-boot-starter-websocket一样使用注解进行开发,只需关注需要的事件(如OnMessage)。并且底层是使用Netty,netty-websocket-spring-boot-starter其他配置和spring-boot-starter-websocket完全一样,当需要调参的时候只需要修改配置参数即可,无需过多的关心handler的设置。

maven配置:

<dependency>
	<groupId>org.yeauty</groupId>
	<artifactId>netty-websocket-spring-boot-starter</artifactId>
	<version>0.9.5</version>
</dependency>

快速开始

引入如上的最新依赖
new一个ServerEndpointExporter对象,交给Spring IOC容器,表示要开启WebSocket功能,如下:

@Configuration
@Slf4j
public class WebSocketConfig {

	@Bean
	public ServerEndpointExporter serverEndpointExporter() {
		log.debug("===============================>>>>底层基于netty的webscoketSeriver启动,贼优雅!");
		return new ServerEndpointExporter();
	}
}

在端点类上加上@ServerEndpoint注解,并在相应的方法上加上@BeforeHandshake、@OnOpen、@OnClose、@OnError、@OnMessage、@OnBinary、@OnEvent注解,样例如下:

/**
 * 在端点类上加上@ServerEndpoint、@Component注解,并在相应的方法上加上@OnOpen、@OnClose、@OnError、@OnMessage注解(不想关注某个事件可不添加对应的注解):
 * 当ServerEndpointExporter类通过Spring配置进行声明并被使用,它将会去扫描带有@ServerEndpoint注解的类
 * 被注解的类将被注册成为一个WebSocket端点 所有的配置项都在这个注解的属性中 ( 如:@ServerEndpoint("/ws") )
 * readerIdleTimeSeconds 与IdleStateHandler中的readerIdleTimeSeconds一致,并且当它不为0时,将在pipeline中添加IdleStateHandler,
 *
 * @author 四叶草 All right reserved
 * @version 1.0
 * @Copyright 2019
 * @Created 2019年12月5日
 */
@ServerEndpoint(path = "/imserver/{token}", host = "${netty-websocket.host}", port = "${netty-websocket.port}", readerIdleTimeSeconds = "55")
@Component
@Slf4j
public class MyWebSocket {
    @Autowired
    private SocketService socketServiceImpl;

    /**
     * 当有新的连接进入时
     *
     * @param token                         用户网页的http的token
     *                                      用户id+前缀
     *                                      用户id
     * @param session
     * @param headers
     * @param req                           通过 通过@RequestParam实现请求中query的获取参数
     * @param reqMap
     * @param @PathVariable支持RESTful风格中获取参数
     * @param pathMap
     * @BeforeHandshake 注解,可在握手之前对连接进行关闭 在@BeforeHandshake事件中可设置子协议
     * 去掉配置端点类上的 @Component 更新Netty版本到 4.1.44.Final
     * 当有新的连接进入时,对该方法进行回调 注入参数的类型:Session、HttpHeaders...
     */
    @SuppressWarnings("rawtypes")
    @BeforeHandshake
    public void handshake(Session session, HttpHeaders headers, @RequestParam String req,
                          @RequestParam MultiValueMap reqMap, @PathVariable("token") String token, @PathVariable Map pathMap) {
        if (StringUtils.isEmpty(token)) {
            session.close();
        }
        String userId = token.split("\\|")[0];
        String redisToken = (String) SpringContextHolder.getBean(RedisUtil.class)
                .get(StringUtils.join(RedisNameConstants.t_user_token, userId));
        if (!(token).equals(redisToken)) {
            session.close();
        } else {
            // 设置协议stomp
//			session.setSubprotocols("stomp");

        }

    }

    /**
     * 当有新的WebSocket连接完成时,对该方法进行回调 , ParameterMap
     * parameterMap注入参数的类型:Session、HttpHeaders、ParameterMap
     *
     * @param session
     * @param headers
     * @throws IOException
     */
    @SuppressWarnings("rawtypes")
    @OnOpen
    public void onOpen(Session session, HttpHeaders headers, @RequestParam String req,
                       @RequestParam MultiValueMap reqMap, @PathVariable String arg, @PathVariable("token") String token,
                       @PathVariable Map pathMap) throws IOException {
        String userId = token.split("\\|")[0];
        try {
            if (!(token).equals(SpringContextHolder.getBean(RedisUtil.class)
                    .get(StringUtils.join(RedisNameConstants.t_user_token, userId)))) {
                session.close();
            } else {
                JSONObject jsonObject = new JSONObject();
                if (GlobalVariableConstant.initializeFlag[0] < 1) {
                    jsonObject.put("userId", userId);
                    jsonObject.put("msg", "撮合引擎还没初始化,请稍候...");
                    session.sendText(jsonObject.toString());
                    session.close();
                    return;
                } else {
                    session.setAttribute("token", token);
                    session.setAttribute("userId", userId);
                    log.debug("====把用户{},加入通道{},", userId, session.channel().id());
                    SychronizedMapUtil.editMap(GlobalUserUtil.channelMapByUserId, userId, session);

                    jsonObject.put("userId", userId);
                    jsonObject.put("msg", "恭喜您连接成功");
                    session.sendText(jsonObject.toString());
                }
            }
            log.debug("用户连接:" + userId + ",当前在线人数为:" + GlobalUserUtil.channelMapByUserId.size() + "    其中的用户的sessionId:"
                    + session.id());
        } catch (Exception e) {
            log.debug("========>>>>>用户:" + userId + ",网络异常!!!!!!");
        }
    }

    /**
     * 当有WebSocket连接关闭时,对该方法进行回调 注入参数的类型:Session
     *
     * @param session
     * @throws IOException
     */
    @OnClose
    public void onClose(Session session) throws IOException {
        if (session.getAttribute("userId") != null) {
            SychronizedMapUtil.delMap(GlobalUserUtil.channelMapByUserId, session.getAttribute("userId"));
            Set set = GlobalUserUtil.channelMapBySymbol.get("1");
            Set set2 = GlobalUserUtil.channelMapBySymbol.get("2");
            if (set != null) {
                set.remove(session.getAttribute("userId"));
            }
            if (set2 != null) {
                set2.remove(session.getAttribute("userId"));
            }
            log.debug("==============>>>>>>>>>>>>>>>{},用户退出,当前在线人数为:{}", session.getAttribute("userId"), GlobalUserUtil.channelMapByUserId.size());
        }

    }

    /**
     * 当有WebSocket抛出异常时,对该方法进行回调 注入参数的类型:Session、Throwable
     *
     * @param session
     * @param throwable
     */
    @OnError
    public void onError(Session session, Throwable throwable) {
        throwable.printStackTrace();
    }

    /**
     * 接收到字符串消息时,对该方法进行回调 注入参数的类型:Session、String
     *
     * @param session
     * @param message
     */
    @SuppressWarnings("unchecked")
    @OnMessage
    public void OnMessage(Session session, String message) {
        log.debug("用户消息:{},报文:{},session现有的主题:{},主题:{}", session.getAttribute("userId"), message, session.getAttribute("F39_PAN_KOU"), session.getAttribute("F39_PAN_KOU_GCC"));

        // 可以群发消息
        // 消息可异步保存到数据库、redis、MongoDB 等
        if (StringUtils.isNotBlank(message)) {
            try {
                // 解析发送的报文
                JSONObject jsonObject = JSON.parseObject(message);
                String type = jsonObject.getString("type");
                if (MonitorTypeConstants.TO_SUB.equals(type)) {
                 
                } else if (MonitorTypeConstants.TO_UNSUB.equals(type)) {
                 
                } else if (MonitorTypeConstants.GET_ALL_L_LINE.equals(type)) {
                    socketServiceImpl.pushKLineData(session, jsonObject);
                } else {
                    // webSocketMap.get(userId).sendMessage("你想干什么");
                }
                // }else{
                // System.out.println("请求的userId:"+message+"不在该服务器上");
                // 否则不在这个服务器上,发送到mysql或者redis
                // }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 当接收到二进制消息时,对该方法进行回调 注入参数的类型:Session、byte[]
     *
     * @param session
     * @param bytes
     */
    @OnBinary
    public void onBinary(Session session, byte[] bytes) {
        for (byte b : bytes) {
            log.debug("==========>>>>>>>>>>>{},", b);
        }
        session.sendBinary(bytes);
    }

    /**
     * 当接收到Netty的事件时,对该方法进行回调 注入参数的类型:Session、Object
     *
     * @param session
     * @param evt
     */
    @OnEvent
    public void onEvent(Session session, Object evt) {
        log.debug("==netty心跳事件===evt=>>>>{},来自===userId:{}", JSONObject.toJSONString(evt), session.channel().id());
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent idleStateEvent = (IdleStateEvent) evt;
            switch (idleStateEvent.state()) {
                case READER_IDLE:
                    log.debug("read idle");
                    //		socketServiceImpl.sendHeart(session);
                    break;
                case WRITER_IDLE:
                    log.debug("write idle");
                    break;
                case ALL_IDLE:
                    log.debug("all idle");
                    break;
                default:
                    break;
            }
        }
    }

}

注解说明:

@ServerEndpoint

当ServerEndpointExporter类通过Spring配置进行声明并被使用,它将会去扫描带有@ServerEndpoint注解的类 被注解的类将被注册成为一个WebSocket端点 所有的配置项都在这个注解的属性中 ( 如:@ServerEndpoint(“/ws”) )

@BeforeHandshake

当有新的连接进入时,对该方法进行回调 注入参数的类型:Session、HttpHeaders…

@OnOpen

当有新的WebSocket连接完成时,对该方法进行回调 注入参数的类型:Session、HttpHeaders…

@OnClose

当有WebSocket连接关闭时,对该方法进行回调 注入参数的类型:Session

@OnError

当有WebSocket抛出异常时,对该方法进行回调 注入参数的类型:Session、Throwable

@OnMessage

当接收到字符串消息时,对该方法进行回调 注入参数的类型:Session、String

@OnBinary

当接收到二进制消息时,对该方法进行回调 注入参数的类型:Session、byte[]

@OnEvent

当接收到Netty的事件时,对该方法进行回调 注入参数的类型:Session、Object

配置

所有的配置文件如下

属性默认值说明
path“/”WebSocket的path,也可以用value来设置
host“0.0.0.0”WebSocket的host,"0.0.0.0"即是所有本地地址
port80WebSocket绑定端口号。如果为0,则使用随机端口(端口获取可见 多端点服务)
bossLoopGroupThreads0bossEventLoopGroup的线程数
workerLoopGroupThreads0workerEventLoopGroup的线程数
useCompressionHandlerfalse是否添加WebSocketServerCompressionHandler到pipeline
prefix“”当不为空时,即是使用application.properties进行配置,详情在 通过application.properties进行配置
optionConnectTimeoutMillis30000与Netty的ChannelOption.CONNECT_TIMEOUT_MILLIS一致
optionSoBacklog128与Netty的ChannelOption.SO_BACKLOG一致
childOptionWriteSpinCount16与Netty的ChannelOption.WRITE_SPIN_COUNT一致
childOptionWriteBufferHighWaterMark64*1024与Netty的ChannelOption.WRITE_BUFFER_HIGH_WATER_MARK一致,但实际上是使用ChannelOption.WRITE_BUFFER_WATER_MARK
childOptionWriteBufferLowWaterMark32*1024与Netty的ChannelOption.WRITE_BUFFER_LOW_WATER_MARK一致,但实际上是使用 ChannelOption.WRITE_BUFFER_WATER_MARK
childOptionSoRcvbuf-1(即未设置)与Netty的ChannelOption.SO_RCVBUF一致
childOptionSoSndbuf-1(即未设置)与Netty的ChannelOption.SO_SNDBUF一致
childOptionTcpNodelaytrue与Netty的ChannelOption.TCP_NODELAY一致
childOptionSoKeepalivefalse与Netty的ChannelOption.SO_KEEPALIVE一致
childOptionSoLinger-1与Netty的ChannelOption.SO_LINGER一致
childOptionAllowHalfClosurefalse与Netty的ChannelOption.ALLOW_HALF_CLOSURE一致
readerIdleTimeSeconds0与IdleStateHandler中的readerIdleTimeSeconds一致,并且当它不为0时,将在pipeline中添加IdleStateHandler
writerIdleTimeSeconds0与IdleStateHandler中的writerIdleTimeSeconds一致,并且当它不为0时,将在pipeline中添加IdleStateHandler
allIdleTimeSeconds0与IdleStateHandler中的allIdleTimeSeconds一致,并且当它不为0时,将在pipeline中添加IdleStateHandler
maxFramePayloadLength65536最大允许帧载荷长度

通过application.yaml/properties进行配置

所有参数皆可使用${…}占位符获取application.properties/yaml中的配置。yaml文件如下

#socket端口
netty-websocket:
  host: 127.0.0.1
  path: /
  port: 8319

接下来即可在application.properties/yaml中配置

@ServerEndpoint(path = "/imserver/{token}", host = "${netty-websocket.host}", port = "${netty-websocket.port}", readerIdleTimeSeconds = "55")
@Component
@Slf4j
public class MyWebSocket {

自定义Favicon

配置favicon的方式与spring-boot中完全一致。只需将favicon.ico文件放到classpath的根目录下即可。如下:

src/
  +- main/
    +- java/
    |   + <source code>
    +- resources/
        +- favicon.ico

自定义错误页面

配置自定义错误页面的方式与spring-boot中完全一致。你可以添加一个 /public/error 目录,错误页面将会是该目录下的静态页面,错误页面的文件名必须是准确的错误状态或者是一串掩码,如下:

src/
  +- main/
    +- java/
    |   + <source code>
    +- resources/
        +- public/
            +- error/
            |   +- 404.html
            |   +- 5xx.html
            +- <other public assets>

多端点服务

在快速启动的基础上,在多个需要成为端点的类上使用@ServerEndpoint、@Component注解即可
可通过ServerEndpointExporter.getInetSocketAddressSet()获取所有端点的地址
当地址不同时(即host不同或port不同),使用不同的ServerBootstrap实例
当地址相同,路径(path)不同时,使用同一个ServerBootstrap实例
当多个端点服务的port为0时,将使用同一个随机的端口号
当多个端点的port和path相同时,host不能设为"0.0.0.0",因为"0.0.0.0"意味着绑定所有的host

集群部署思路

每一个客户端连接时都会有一个唯一标识,那么这时可在redis中存 uid : serverid(websocket服务器的唯一标识).这时当需要对某个客户端(或者或某个uid)进行推送时,就可以在redis中获取到相应的服务器信息 (当然,里面还要保证服务器中信息与redis中信息一致性问题没说)

注意点:

如果有做代理的话,注意代理超时,建议在客户端处理超时

最后的最后还是附上一张netty处理的流程图(盗图一张)

盗图一张

  • 4
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
### 回答1: Netty-WebSocket-Spring-Boot-Starter是一个用于将Websocket集成到Spring Boot应用程序中的库。它使用Netty作为底层框架,提供了一种快速和可靠的方式来处理异步通信。 这个库提供了一种简单的方法来创建Websocket端点,只需要使用注释和POJO类即可。在这些端点上可以添加动态的事件处理程序,以处理连接、断开连接和消息事件等。 此外,Netty-WebSocket-Spring-Boot-Starter还包括了一些安全性的特性,如基于令牌的授权和XSS保护,可以帮助您保持您的Websocket应用程序安全。 总的来说,Netty-WebSocket-Spring-Boot-Starter提供了一种快速和易于使用的方式来构建Websocket应用程序,使得它成为应用程序开发人员的有用工具。 ### 回答2: netty-websocket-spring-boot-starter 是一个开源的 Java Web 开发工具包,主要基于 Netty 框架实现了 WebSocket 协议的支持,同时集成了 Spring Boot 框架,使得开发者可以更加方便地搭建 WebSocket 服务器。 该工具包提供了 WebSocketServer 配置类,通过在 Spring Boot 的启动配置类中调用 WebSocketServer 配置类,即可启动 WebSocket 服务器。同时,该工具包还提供了多种配置参数,如端口号、URI 路径、SSL 配置、认证配置等等,可以根据业务需求进行自定义配置。 此外,该工具包还提供了一些可扩展的接口和抽象类,如 WebSocketHandler、ChannelHandlerAdapter 等,可以通过继承和实现这些接口和抽象类来实现业务逻辑的处理和拓展。 总的来说,netty-websocket-spring-boot-starter 提供了一个高效、简单、易用的 WebSocket 服务器开发框架,可以减少开发者的开发成本和工作量。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值