老生常谈,干啥的?一个网络通信协议框架,自己可以各种自定义,具体的,网上一捞一大把。两大特性:NIO和零拷贝。
版本约定: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的设置。
<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;
}
}
}
}
当ServerEndpointExporter类通过Spring配置进行声明并被使用,它将会去扫描带有@ServerEndpoint注解的类 被注解的类将被注册成为一个WebSocket端点 所有的配置项都在这个注解的属性中 ( 如:@ServerEndpoint(“/ws”) )
当有新的连接进入时,对该方法进行回调 注入参数的类型:Session、HttpHeaders…
当有新的WebSocket连接完成时,对该方法进行回调 注入参数的类型:Session、HttpHeaders…
当有WebSocket连接关闭时,对该方法进行回调 注入参数的类型:Session
当有WebSocket抛出异常时,对该方法进行回调 注入参数的类型:Session、Throwable
当接收到字符串消息时,对该方法进行回调 注入参数的类型:Session、String
当接收到二进制消息时,对该方法进行回调 注入参数的类型:Session、byte[]
当接收到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.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的方式与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中信息一致性问题没说)
如果有做代理的话,注意代理超时,建议在客户端处理超时