开始前,首先先学习一下概念room和namespace
官方地址链接 地址
简单的来说,socket会属于某一个room,如果没有指定那么就socket就会归属默认的room,每个room又会属于某一namespace下,默认namespace是/。
客户端连接时可以指定自己的socket的归属哪个namespace ,(这里有个坑,自己定义的namespace 一定是/xiuweiSapce,注意是/xiuweiSapce不是xiuweiSapce)比如使用
1 | var socket = io.connect("http://localhost:9099/xiuweiSpace?room=F006"); |
至于归属那个room就是服务端自己设置的了。
接着就是Netty-socketIo的api了,这里不多做叙述。分享一篇博客,链接 ,文章内容排版都比我写的好。
剩下的言归正传,这是使用redis并且使用Java客户端redission作为集群推送的方案原因是因为netty-soketio与redisson是同一个作者。(这里再次膜拜大神)。这里使用redis发布订阅功能,需要服务端主动推送消息的时候,不管消息发动到哪个实例都会通过发布订阅推送到其他实例上,最总获取到所有的socketClient推送消息。
接着一言不合上代码
引入所需要的pom
1 2 3 4 5 6 7 8 9 10 11 | <dependency> <groupId>com.corundumstudio.socketio</groupId> <artifactId>netty-socketio</artifactId> <version>1.7.17</version> </dependency> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.11.0</version> </dependency> |
相关的配置文件与配置类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | #WebSocket # host在本地测试可以设置为localhost或者本机IP,在Linux服务器跑可换成服务器IP lsmdjsj.websocket.host=socketio.host=localhost lsmdjsj.websocket.socket-port=9099 # 设置最大每帧处理数据的长度,防止他人利用大数据来攻击服务器 lsmdjsj.websocket.maxFramePayloadLength=1048576 # 设置http交互最大内容长度 lsmdjsj.websocket.maxHttpContentLength=1048576 # 协议升级超时时间(毫秒),默认10秒。HTTP握手升级为ws协议超时时间 lsmdjsj.websocket.upgradeTimeout=1000000 # Ping消息超时时间(毫秒),默认60秒,这个时间间隔内没有接收到心跳消息就会发送超时事件 lsmdjsj.websocket.pingTimeout=6000000 # Ping消息间隔(毫秒),默认25秒。客户端向服务器发送一条心跳消息间隔 lsmdjsj.websocket.pingInterval=25000 #redis lsmdjsj.redisson.address=redis://127.0.0.1:6379 lsmdjsj.redisson.password= lsmdjsj.redisson.database=5 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | @Data @Component @ConfigurationProperties(prefix = "lsmdjsj.websocket") public class WebSocketProperties { /** * socket 地址 */ private String host; /** * socket 端口 */ private Integer socketPort; /** * 最大每帧处理数据的长度 */ private String maxFramePayloadLength; /** * http交互最大内容长度 */ private String maxHttpContentLength; /** * Ping 心跳间隔(毫秒) */ private Integer pingInterval; /** * Ping消息超时时间(毫秒),默认60秒,这个时间间隔内没有接收到心跳消息就会发送超时事件 */ private Integer pingTimeout; /** * 协议升级超时时间(毫秒),默认10秒。HTTP握手升级为ws协议超时时间 */ private Integer upgradeTimeout; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | @Data @ConfigurationProperties(prefix = "lsmdjsj.redisson") public class RedissonProperty { private int timeout = 3000; private String address; private String password; private int connectionPoolSize = 5; private int connectionMinimumIdleSize=2; private int slaveConnectionPoolSize = 250; private int masterConnectionPoolSize = 250; private String[] sentinelAddresses; private String masterName; private int database = 1; } |
初始化netty-socketio的服务端和redisson
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | @Configuration @EnableConfigurationProperties(RedissonProperty.class) public class RedissonConfig { @Resource private RedissonProperty conf; @Bean(name="redission",destroyMethod="shutdown") public RedissonClient redission() { Config config = new Config(); config.setCodec(new org.redisson.client.codec.StringCodec()); if(conf.getSentinelAddresses()!=null && conf.getSentinelAddresses().length>0){ config.useSentinelServers() .setMasterName(conf.getMasterName()).addSentinelAddress(conf.getSentinelAddresses()) .setPassword(conf.getPassword()).setDatabase(conf.getDatabase()); }else{ SingleServerConfig serverConfig = config.useSingleServer() .setAddress(conf.getAddress()) .setTimeout(conf.getTimeout()) .setConnectionPoolSize(conf.getConnectionPoolSize()) .setConnectionMinimumIdleSize(conf.getConnectionMinimumIdleSize()) .setDatabase(conf.getDatabase()); if(StringUtils.isNotBlank(conf.getPassword())) { serverConfig.setPassword(conf.getPassword()); } } return Redisson.create(config); } } |
初始化netty-socketio
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | @Configuration public class SocketIoConfig { private static Logger logger = LoggerFactory.getLogger(SocketIoConfig.class); @Resource private RedissonClient redisson; @Resource private WebSocketProperties webSocketProperties; @Resource private NettyExceptionListener nettyExceptionListener; /** * 创建 StoreFactory * @return */ private RedissonStoreFactory createRedissonStoreFactory(){ logger.info("创建 RedissonStoreFactory 开始"); RedissonStoreFactory redissonStoreFactory = new RedissonStoreFactory(redisson); logger.info("创建 RedissonStoreFactory 结束"); return redissonStoreFactory; } @Bean public SocketIOServer getSocketIOServer(){ logger.info("创建 SocketIOServer 开始"); //Sokcket配置 参考 jdk SocketConfig socketConfig = new SocketConfig(); socketConfig.setTcpNoDelay(true); //在默认情况下,当调用close关闭socke的使用,close会立即返回, // 但是,如果send buffer中还有数据,系统会试着先把send buffer中的数据发送出去,然后close才返回. socketConfig.setSoLinger(0); com.corundumstudio.socketio.Configuration config = new com.corundumstudio.socketio.Configuration(); // 设置监听端口 config.setPort(webSocketProperties.getSocketPort()); // 协议升级超时时间(毫秒),默认10000。HTTP握手升级为ws协议超时时间 config.setUpgradeTimeout(webSocketProperties.getUpgradeTimeout()); // Ping消息间隔(毫秒),默认25000。客户端向服务器发送一条心跳消息间隔 config.setPingInterval(webSocketProperties.getPingInterval()); // Ping消息超时时间(毫秒),默认60000,这个时间间隔内没有接收到心跳消息就会发送超时事件 config.setPingTimeout(webSocketProperties.getPingTimeout()); // 推荐使用redisson config.setStoreFactory(createRedissonStoreFactory()); //异常处理 config.setExceptionListener(nettyExceptionListener); //手动确认 config.setAckMode(AckMode.MANUAL); // 握手协议参数使用JWT的Token认证方案 认证方案 config.setAuthorizationListener(data -> { /* HttpHeaders httpHeaders = data.getHttpHeaders(); String token = httpHeaders.get("Authorization");*/ return true; }); config.setSocketConfig(socketConfig); logger.info("创建 SocketIOServer 结束"); return new SocketIOServer(config); } /** * spring * @param socketServer * @return */ @Bean public SpringAnnotationScanner springAnnotationScanner(SocketIOServer socketServer) { return new SpringAnnotationScanner(socketServer); } @Bean public PubSubStore pubSubStore(SocketIOServer socketServer) { return socketServer.getConfiguration().getStoreFactory().pubSubStore(); } } |
启动socket服务,并且订阅事件,也可以自定义设置相应的namespace的相关事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | @Component @Order(1) public class SocketServerRunner implements CommandLineRunner { private static Logger logger = LoggerFactory.getLogger(SocketServerRunner.class); @Resource private SocketIOServer socketIOServer; @Resource private PubSubStore pubSubStore; @Resource private RedissonClient redisson; @Override public void run(String... args) throws Exception { logger.info("socketIOServer 启动"); socketIOServer.start(); //订阅消息 pubSubStore.subscribe(PubSubType.DISPATCH,data -> { Collection<SocketIOClient> clients = null; String room = data.getRoom(); String namespace = data.getNamespace(); Packet packet = data.getPacket(); String jsonData = packet.getData(); if(!StringUtils.isBlank(namespace)){ SocketIONamespace socketIONamespace = socketIOServer.getNamespace(namespace); if(StringUtils.isBlank(room)){ clients = socketIONamespace.getRoomOperations(room).getClients(); } }else{ clients = socketIOServer.getBroadcastOperations().getClients(); } if(!CollectionUtils.isEmpty(clients)){ for (SocketIOClient client : clients) { client.sendEvent(Constants.PUSH_MSG,jsonData); } } },DispatchMessage.class); addNameSpace(socketIOServer); } private void addNameSpace(SocketIOServer socketIOServer){ SocketIONamespace xiuweiSpace = socketIOServer.addNamespace(Constants.XIU_WEI_NAME_SPACE); xiuweiSpace.addConnectListener(client -> { Map<String,Object> clientMap = new HashMap<>(16); String nameSpace = client.getNamespace().getName(); String room = client.getHandshakeData().getSingleUrlParam("room"); String sessionId = client.getSessionId().toString(); logger.info("xiuweiSpace连接成功, room={},nameSpace={}, sessionId={}", room, nameSpace,sessionId); if(StringUtils.isNotBlank(room)){ client.joinRoom(room); clientMap.put("rooms",room); } clientMap.put("createTime", LocalDateTime.now().toString()); redisson.getBucket(Constants.KEY_ROOM_PREFIX+Constants.XIU_WEI_NAME_SPACE+sessionId).trySet(clientMap); }); xiuweiSpace.addDisconnectListener(client -> { logger.info("客户端:" + client.getSessionId() + "断开连接"); String sessionId = client.getSessionId().toString(); redisson.getBucket(Constants.KEY_ROOM_PREFIX+Constants.XIU_WEI_NAME_SPACE+sessionId).delete(); }); xiuweiSpace.addEventListener(Constants.XIU_WEI_EVINT,String.class,(client, data, ackSender) -> { client.sendEvent(Constants.XIU_WEI_EVINT,data); if (ackSender.isAckRequested()) { logger.info("xiuwei接受到的消息. message={}", data); } }); } } |
默认处理未指定namesapce的socketClient 这里使用标签
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | @Component public class MessageEventHandler { private static final Logger logger = LoggerFactory.getLogger(MessageEventHandler.class); public static ConcurrentMap<String, SocketIOClient> socketIOClientMap = new ConcurrentHashMap<>(); @Resource private RedissonClient redisson; @Resource private SocketIOServer socketIOServer; @OnConnect public void onConnect(SocketIOClient client){ Map<String,Object> clientMap = new HashMap<>(16); if(client!=null){ String room = client.getHandshakeData().getSingleUrlParam("room"); String nameSpace = client.getNamespace().getName(); String sessionId = client.getSessionId().toString(); logger.info("socket连接成功, room={}, sessionId={},namespace={}",room,sessionId,nameSpace); if(StringUtils.isNotBlank(room)){ client.joinRoom(room); clientMap.put("rooms",room); } clientMap.put("createTime", LocalDateTime.now().toString()); redisson.getBucket(Constants.KEY_ROOM_PREFIX+sessionId).trySet(clientMap); } } /** * 客户端关闭连接时触发 * * @param client */ @OnDisconnect public void onDisconnect(SocketIOClient client) { logger.info("客户端:" + client.getSessionId() + "断开连接"); } /** * 客户端事件 * * @param client 客户端信息 * @param request 请求信息 * @param msg 客户端发送数据 */ @OnEvent(value = "messageevent") public void onEvent(SocketIOClient client, AckRequest request, String msg) { logger.info("发来消息:" + msg); //回发消息 JSONObject jsonObject = JSON.parseObject(msg); String message = jsonObject.getString("message"); Collection<SocketIOClient> clients = socketIOServer.getBroadcastOperations().getClients(); for (SocketIOClient clientByRoom : clients) { clientByRoom.sendEvent("messageevent", client.getSessionId().toString()+": "+message); } } } |
最后主动推送客户端的controller
1 2 3 4 5 6 7 8 9 10 | @RequestMapping("/push") @ResponseBody public SinoHttpResponse<Boolean> pushMsgByService(@RequestBody ChatMessage chatMessage){ SocketIONamespace namespace = socketIOServer.getNamespace(chatMessage.getNamespace()); Collection<SocketIOClient> allClients = namespace.getAllClients(); for (SocketIOClient client : allClients) { client.sendEvent(chatMessage.getEventName(),chatMessage.getMessage()); } return SinoHttpResponse.success(true); } |
最后贴一下可以测试的html ,本人前端技术巨渣,这里使用jquery写的demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | <!DOCTYPE html> <html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <title>webSocket测试</title> <script src="//i2.wp.com/cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.js"></script> <!-- 新 Bootstrap 核心 CSS 文件 --> <link rel="stylesheet" href="//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.min.css"> <!-- 可选的Bootstrap主题文件(一般不用引入) --> <link rel="stylesheet" href="//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap-theme.min.css"> <!-- jQuery文件。务必在bootstrap.min.js 之前引入 --> <script src="//cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script> <!--<script type="text/javascript" src="js/jquery-1.7.2.js"></script>--> <!-- 最新的 Bootstrap 核心 JavaScript 文件 --> <script src="//cdn.bootcss.com/bootstrap/3.3.5/js/bootstrap.min.js"></script> <script type="text/javascript"> $(function(){ /** * 前端js的 socket.emit("事件名","参数数据")方法,是触发后端自定义消息事件的时候使用的, * 前端js的 socket.on("事件名",匿名函数(服务器向客户端发送的数据))为监听服务器端的事件 **/ var socket = io.connect("http://localhost:9099?room=F006"); var firstconnect = true; if(firstconnect) { console.log("第一次链接初始化"); //监听服务器连接事件 socket.on('connect', function(){ $("#tou").html("链接服务器成功!"); }); //监听服务器关闭服务事件 socket.on('disconnect', function(){ $("#tou").html("与服务器断开了链接!"); }); //监听服务器端发送消息事件 socket.on('messageevent', function(data) { $("#msg").html($("#msg").html() + "<br/>" + data); }); firstconnect = false; } else { socket.socket.reconnect(); } $('#send').bind('click', function() { send(); }); function send(){ if (socket != null) { debugger; var message = document.getElementById('message').value; var title = "message"; var obj = {message:message,title:title}; var str = JSON.stringify(obj); socket.emit("messageevent",str); } else { alert('未与服务器链接.'); } } }); </script> </head> <body> <div class="page-header" id="tou"> webSocket及时聊天Demo程序 </div> <div class="well" id="msg"> </div> <div class="col-lg"> <div class="input-group"> <input type="text" class="form-control" placeholder="发送信息..." id="message"> <span class="input-group-btn"> <button class="btn btn-default" type="button" id="send" >发送</button> </span> </div><!-- /input-group --> </div><!-- /.col-lg-6 --> </div><!-- /.row --><br><br> 后台主动发送消息:<button class="btn btn-default" type="button" onclick="mess()">发送</button> <br><br> <a href="/user/index">back to index</a> <script src="//i2.wp.com/js.biocloud.cn/jquery/1.11.3/jquery.min.js"></script> <script> function mess() { $.ajax({ type: "POST", data:{index : "echo"}, url: "/websocket/auditing", success: function (data) { //alert(data); } }); } </script> </body> </html> |
最后感谢大家的观看,这只是一个demo,会存在各种各样的问题,欢迎大家指正