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"即是所有本地地址
port 80 WebSocket绑定端口号。如果为0,则使用随机端口(端口获取可见 多端点服务)
bossLoopGroupThreads 0 bossEventLoopGroup的线程数
workerLoopGroupThreads 0 workerEventLoopGroup的线程数
useCompressionHandler false 是否添加WebSocketServerCompressionHandler到pipeline
prefix “” 当不为空时,即是使用application.properties进行配置,详情在 通过application.properties进行配置
optionConnectTimeoutMillis 30000 与Netty的ChannelOption.CONNECT_TIMEOUT_MILLIS一致
optionSoBacklog 128 与Netty的ChannelOption.SO_BACKLOG一致
childOptionWriteSpinCount 16 与Netty的ChannelOption.WRITE_SPIN_COUNT一致
childOptionWriteBufferHighWaterMark 64*1024 与Netty的ChannelOption.WRITE_BUFFER_HIGH_WATER_MARK一致,但实际上是使用ChannelOption.WRITE_BUFFER_WATER_MARK
childOptionWriteBufferLowWaterMark 32*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一致
childOptionTcpNodelay true 与Netty的ChannelOption.TCP_NODELAY一致
childOptionSoKeepalive false 与Netty的ChannelOption.SO_KEEPALIVE一致
childOptionSoLinger -1 与Netty的ChannelOption.SO_LINGER一致
childOptionAllowHalfClosure false 与Netty的ChannelOption.ALLOW_HALF_CLOSURE一致
readerIdleTimeSeconds 0 与IdleStateHandler中的readerIdleTimeSeconds一致,并且当它不为0时,将在pipeline中添加IdleStateHandler
writerIdleTimeSeconds 0 与IdleStateHandler中的writerIdleTimeSeconds一致,并且当它不为0时,将在pipeline中添加IdleStateHandler
allIdleTimeSeconds 0 与IdleStateHandler中的allIdleTimeSeconds一致,并且当它不为0时,将在pipeline中添加IdleStateHandler
maxFramePayloadLength 65536 最大允许帧载荷长度

通过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处理的流程图(盗图一张)

netty-websocket-spring-boot-starter基于netty的轻量级的高性能socket服务器_第1张图片

你可能感兴趣的:(spring,websocket,服务器)