@@ -20,7 +20,7 @@ | |||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | |||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> | |||
<java.version>1.8</java.version> | |||
<netty-socketio.version>1.7.12</netty-socketio.version> | |||
<netty-socketio.version>1.7.16</netty-socketio.version> | |||
</properties> | |||
<dependencies> | |||
@@ -35,11 +35,28 @@ | |||
<artifactId>spring-boot-starter-web</artifactId> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.springframework.boot</groupId> | |||
<artifactId>spring-boot-configuration-processor</artifactId> | |||
<optional>true</optional> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.springframework.boot</groupId> | |||
<artifactId>spring-boot-starter-test</artifactId> | |||
<scope>test</scope> | |||
</dependency> | |||
<dependency> | |||
<groupId>cn.hutool</groupId> | |||
<artifactId>hutool-all</artifactId> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.projectlombok</groupId> | |||
<artifactId>lombok</artifactId> | |||
<optional>true</optional> | |||
</dependency> | |||
</dependencies> | |||
<build> | |||
@@ -0,0 +1,69 @@ | |||
package com.xkcoding.websocket.socketio.config; | |||
import cn.hutool.core.collection.CollUtil; | |||
import org.springframework.stereotype.Component; | |||
import java.util.List; | |||
import java.util.Optional; | |||
import java.util.UUID; | |||
import java.util.concurrent.ConcurrentHashMap; | |||
/** | |||
* <p> | |||
* 模拟数据库 | |||
* </p> | |||
* | |||
* @package: com.xkcoding.websocket.socketio.config | |||
* @description: 模拟数据库 | |||
* @author: yangkai.shen | |||
* @date: Created in 2018-12-18 19:12 | |||
* @copyright: Copyright (c) 2018 | |||
* @version: V1.0 | |||
* @modified: yangkai.shen | |||
*/ | |||
@Component | |||
public class DbTemplate { | |||
/** | |||
* 模拟数据库存储 user_id <-> session_id 的关系 | |||
*/ | |||
public static final ConcurrentHashMap<String, UUID> DB = new ConcurrentHashMap<>(); | |||
/** | |||
* 获取所有SessionId | |||
* | |||
* @return SessionId列表 | |||
*/ | |||
public List<UUID> findAll() { | |||
return CollUtil.newArrayList(DB.values()); | |||
} | |||
/** | |||
* 根据UserId查询SessionId | |||
* | |||
* @param userId 用户id | |||
* @return SessionId | |||
*/ | |||
public Optional<UUID> findByUserId(String userId) { | |||
return Optional.ofNullable(DB.get(userId)); | |||
} | |||
/** | |||
* 保存/更新 user_id <-> session_id 的关系 | |||
* | |||
* @param userId 用户id | |||
* @param sessionId SessionId | |||
*/ | |||
public void save(String userId, UUID sessionId) { | |||
DB.put(userId, sessionId); | |||
} | |||
/** | |||
* 删除 user_id <-> session_id 的关系 | |||
* | |||
* @param userId 用户id | |||
*/ | |||
public void deleteByUserId(String userId) { | |||
DB.remove(userId); | |||
} | |||
} |
@@ -0,0 +1,47 @@ | |||
package com.xkcoding.websocket.socketio.config; | |||
/** | |||
* <p> | |||
* 事件常量 | |||
* </p> | |||
* | |||
* @package: com.xkcoding.websocket.socketio.config | |||
* @description: 事件常量 | |||
* @author: yangkai.shen | |||
* @date: Created in 2018-12-18 19:36 | |||
* @copyright: Copyright (c) 2018 | |||
* @version: V1.0 | |||
* @modified: yangkai.shen | |||
*/ | |||
public interface Event { | |||
/** | |||
* 聊天事件 | |||
*/ | |||
String CHAT = "chat" ; | |||
/** | |||
* 收到消息 | |||
*/ | |||
String CHAT_RECEIVED = "chat_received" ; | |||
/** | |||
* 拒收消息 | |||
*/ | |||
String CHAT_REFUSED = "chat_refused" ; | |||
/** | |||
* 广播消息 | |||
*/ | |||
String BROADCAST = "broadcast" ; | |||
/** | |||
* 群聊 | |||
*/ | |||
String GROUP = "group" ; | |||
/** | |||
* 加入群聊 | |||
*/ | |||
String JOIN = "join" ; | |||
} |
@@ -0,0 +1,53 @@ | |||
package com.xkcoding.websocket.socketio.config; | |||
import cn.hutool.core.util.StrUtil; | |||
import com.corundumstudio.socketio.SocketIOServer; | |||
import com.corundumstudio.socketio.annotation.SpringAnnotationScanner; | |||
import org.springframework.boot.context.properties.EnableConfigurationProperties; | |||
import org.springframework.context.annotation.Bean; | |||
import org.springframework.context.annotation.Configuration; | |||
/** | |||
* <p> | |||
* 服务器配置 | |||
* </p> | |||
* | |||
* @package: com.xkcoding.websocket.socketio.config | |||
* @description: 服务器配置 | |||
* @author: yangkai.shen | |||
* @date: Created in 2018-12-18 16:42 | |||
* @copyright: Copyright (c) 2018 | |||
* @version: V1.0 | |||
* @modified: yangkai.shen | |||
*/ | |||
@Configuration | |||
@EnableConfigurationProperties({WsConfig.class}) | |||
public class ServerConfig { | |||
@Bean | |||
public SocketIOServer server(WsConfig wsConfig) { | |||
com.corundumstudio.socketio.Configuration config = new com.corundumstudio.socketio.Configuration(); | |||
config.setHostname(wsConfig.getHost()); | |||
config.setPort(wsConfig.getPort()); | |||
//这个listener可以用来进行身份验证 | |||
config.setAuthorizationListener(data -> { | |||
// http://localhost:8081?token=xxxxxxx | |||
// 例如果使用上面的链接进行connect,可以使用如下代码获取用户密码信息,本文不做身份验证 | |||
String token = data.getSingleUrlParam("token"); | |||
// 校验token的合法性,实际业务需要校验token是否过期等等,参考 spring-boot-demo-rbac-security 里的 JwtUtil | |||
// 如果认证不通过会返回一个 Socket.EVENT_CONNECT_ERROR 事件 | |||
return StrUtil.isNotBlank(token); | |||
}); | |||
return new SocketIOServer(config); | |||
} | |||
/** | |||
* Spring 扫描自定义注解 | |||
*/ | |||
@Bean | |||
public SpringAnnotationScanner springAnnotationScanner(SocketIOServer server) { | |||
return new SpringAnnotationScanner(server); | |||
} | |||
} |
@@ -0,0 +1,31 @@ | |||
package com.xkcoding.websocket.socketio.config; | |||
import lombok.Data; | |||
import org.springframework.boot.context.properties.ConfigurationProperties; | |||
/** | |||
* <p> | |||
* WebSocket配置类 | |||
* </p> | |||
* | |||
* @package: com.xkcoding.websocket.socketio.config | |||
* @description: WebSocket配置类 | |||
* @author: yangkai.shen | |||
* @date: Created in 2018-12-18 16:41 | |||
* @copyright: Copyright (c) 2018 | |||
* @version: V1.0 | |||
* @modified: yangkai.shen | |||
*/ | |||
@ConfigurationProperties(prefix = "ws.server") | |||
@Data | |||
public class WsConfig { | |||
/** | |||
* 端口号 | |||
*/ | |||
private Integer port; | |||
/** | |||
* host | |||
*/ | |||
private String host; | |||
} |
@@ -0,0 +1,68 @@ | |||
package com.xkcoding.websocket.socketio.controller; | |||
import cn.hutool.core.lang.Dict; | |||
import cn.hutool.core.util.ReflectUtil; | |||
import cn.hutool.core.util.StrUtil; | |||
import com.xkcoding.websocket.socketio.handler.MessageEventHandler; | |||
import com.xkcoding.websocket.socketio.payload.BroadcastMessageRequest; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.springframework.beans.factory.annotation.Autowired; | |||
import org.springframework.web.bind.annotation.PostMapping; | |||
import org.springframework.web.bind.annotation.RequestMapping; | |||
import org.springframework.web.bind.annotation.RestController; | |||
import java.lang.reflect.Field; | |||
/** | |||
* <p> | |||
* 消息发送Controller | |||
* </p> | |||
* | |||
* @package: com.xkcoding.websocket.socketio.controller | |||
* @description: 消息发送Controller | |||
* @author: yangkai.shen | |||
* @date: Created in 2018-12-18 19:50 | |||
* @copyright: Copyright (c) 2018 | |||
* @version: V1.0 | |||
* @modified: yangkai.shen | |||
*/ | |||
@RestController | |||
@RequestMapping("/send") | |||
@Slf4j | |||
public class MessageController { | |||
@Autowired | |||
private MessageEventHandler messageHandler; | |||
@PostMapping("/broadcast") | |||
public Dict broadcast(BroadcastMessageRequest message) { | |||
if (isBlank(message)) { | |||
return Dict.create().set("flag", false).set("code", 400).set("message", "参数为空"); | |||
} | |||
messageHandler.sendToBroadcast(message); | |||
return Dict.create().set("flag", true).set("code", 200).set("message", "发送成功"); | |||
} | |||
/** | |||
* 判断Bean是否为空对象或者空白字符串,空对象表示本身为<code>null</code>或者所有属性都为<code>null</code> | |||
* | |||
* @param bean Bean对象 | |||
* @return 是否为空,<code>true</code> - 空 / <code>false</code> - 非空 | |||
* @since 4.1.10 | |||
*/ | |||
private boolean isBlank(Object bean) { | |||
if (null != bean) { | |||
for (Field field : ReflectUtil.getFields(bean.getClass())) { | |||
Object fieldValue = ReflectUtil.getFieldValue(bean, field); | |||
if (null != fieldValue) { | |||
if (fieldValue instanceof String && StrUtil.isNotBlank((String) fieldValue)) { | |||
return false; | |||
} else if (!(fieldValue instanceof String)) { | |||
return false; | |||
} | |||
} | |||
} | |||
} | |||
return true; | |||
} | |||
} |
@@ -0,0 +1,143 @@ | |||
package com.xkcoding.websocket.socketio.handler; | |||
import com.corundumstudio.socketio.AckRequest; | |||
import com.corundumstudio.socketio.SocketIOClient; | |||
import com.corundumstudio.socketio.SocketIOServer; | |||
import com.corundumstudio.socketio.annotation.OnConnect; | |||
import com.corundumstudio.socketio.annotation.OnDisconnect; | |||
import com.corundumstudio.socketio.annotation.OnEvent; | |||
import com.xkcoding.websocket.socketio.config.DbTemplate; | |||
import com.xkcoding.websocket.socketio.config.Event; | |||
import com.xkcoding.websocket.socketio.payload.BroadcastMessageRequest; | |||
import com.xkcoding.websocket.socketio.payload.GroupMessageRequest; | |||
import com.xkcoding.websocket.socketio.payload.SingleMessageRequest; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.springframework.beans.factory.annotation.Autowired; | |||
import org.springframework.stereotype.Component; | |||
import java.util.Optional; | |||
import java.util.UUID; | |||
/** | |||
* <p> | |||
* 消息事件处理 | |||
* </p> | |||
* | |||
* @package: com.xkcoding.websocket.socketio.handler | |||
* @description: 消息事件处理 | |||
* @author: yangkai.shen | |||
* @date: Created in 2018-12-18 18:57 | |||
* @copyright: Copyright (c) 2018 | |||
* @version: V1.0 | |||
* @modified: yangkai.shen | |||
*/ | |||
@Component | |||
@Slf4j | |||
public class MessageEventHandler { | |||
@Autowired | |||
private SocketIOServer server; | |||
@Autowired | |||
private DbTemplate dbTemplate; | |||
/** | |||
* 添加connect事件,当客户端发起连接时调用 | |||
* | |||
* @param client 客户端对象 | |||
*/ | |||
@OnConnect | |||
public void onConnect(SocketIOClient client) { | |||
if (client != null) { | |||
String token = client.getHandshakeData().getSingleUrlParam("token"); | |||
// 模拟用户id 和token一致 | |||
String userId = client.getHandshakeData().getSingleUrlParam("token"); | |||
UUID sessionId = client.getSessionId(); | |||
dbTemplate.save(userId, sessionId); | |||
log.info("连接成功,【token】= {},【sessionId】= {}", token, sessionId); | |||
} else { | |||
log.error("客户端为空"); | |||
} | |||
} | |||
/** | |||
* 添加disconnect事件,客户端断开连接时调用,刷新客户端信息 | |||
* | |||
* @param client 客户端对象 | |||
*/ | |||
@OnDisconnect | |||
public void onDisconnect(SocketIOClient client) { | |||
if (client != null) { | |||
String token = client.getHandshakeData().getSingleUrlParam("token"); | |||
// 模拟用户id 和token一致 | |||
String userId = client.getHandshakeData().getSingleUrlParam("token"); | |||
UUID sessionId = client.getSessionId(); | |||
dbTemplate.deleteByUserId(userId); | |||
log.info("客户端断开连接,【token】= {},【sessionId】= {}", token, sessionId); | |||
client.disconnect(); | |||
} else { | |||
log.error("客户端为空"); | |||
} | |||
} | |||
/** | |||
* 加入群聊 | |||
* | |||
* @param client 客户端 | |||
* @param request 请求 | |||
* @param roomId 群聊号 | |||
*/ | |||
@OnEvent(value = Event.JOIN) | |||
public void onJoinEvent(SocketIOClient client, AckRequest request, String roomId) { | |||
// 模拟用户id 和token一致 | |||
String userId = client.getHandshakeData().getSingleUrlParam("token"); | |||
log.info("用户:{} 已加入群聊:{}", userId, roomId); | |||
client.joinRoom(roomId); | |||
} | |||
@OnEvent(value = Event.CHAT) | |||
public void onChatEvent(SocketIOClient client, AckRequest request, SingleMessageRequest data) { | |||
Optional<UUID> toUser = dbTemplate.findByUserId(data.getToUid()); | |||
if (toUser.isPresent()) { | |||
log.info("用户 {} 刚刚私信了用户 {}:{}", data.getFromUid(), data.getToUid(), data.getMessage()); | |||
sendToSingle(toUser.get(), data); | |||
} else { | |||
client.sendEvent(Event.CHAT_REFUSED, "对方不在线"); | |||
} | |||
} | |||
@OnEvent(value = Event.CHAT) | |||
public void onGroupEvent(SocketIOClient client, AckRequest request, GroupMessageRequest data) { | |||
log.info("群号 {} 收到来自 {} 的群聊消息:{}", data.getGroupId(), data.getFromUid(), data.getMessage()); | |||
sendToGroup(data); | |||
} | |||
/** | |||
* 单聊 | |||
*/ | |||
public void sendToSingle(UUID sessionId, SingleMessageRequest message) { | |||
server.getClient(sessionId).sendEvent(Event.CHAT_RECEIVED, message); | |||
} | |||
/** | |||
* 广播 | |||
*/ | |||
public void sendToBroadcast(BroadcastMessageRequest message) { | |||
log.info("系统紧急广播一条通知:{}", message.getMessage()); | |||
for (UUID clientId : dbTemplate.findAll()) { | |||
if (server.getClient(clientId) == null) { | |||
continue; | |||
} | |||
server.getClient(clientId).sendEvent(Event.BROADCAST, message); | |||
} | |||
} | |||
/** | |||
* 群聊 | |||
*/ | |||
public void sendToGroup(GroupMessageRequest message) { | |||
server.getRoomOperations(message.getGroupId()).sendEvent(Event.GROUP, message); | |||
} | |||
} |
@@ -0,0 +1,33 @@ | |||
package com.xkcoding.websocket.socketio.init; | |||
import com.corundumstudio.socketio.SocketIOServer; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.springframework.beans.factory.annotation.Autowired; | |||
import org.springframework.boot.CommandLineRunner; | |||
import org.springframework.stereotype.Component; | |||
/** | |||
* <p> | |||
* websocket服务器启动 | |||
* </p> | |||
* | |||
* @package: com.xkcoding.websocket.socketio.init | |||
* @description: websocket服务器启动 | |||
* @author: yangkai.shen | |||
* @date: Created in 2018-12-18 17:07 | |||
* @copyright: Copyright (c) 2018 | |||
* @version: V1.0 | |||
* @modified: yangkai.shen | |||
*/ | |||
@Component | |||
@Slf4j | |||
public class ServerRunner implements CommandLineRunner { | |||
@Autowired | |||
private SocketIOServer server; | |||
@Override | |||
public void run(String... args) { | |||
server.start(); | |||
log.info("websocket 服务器启动成功。。。"); | |||
} | |||
} |
@@ -0,0 +1,24 @@ | |||
package com.xkcoding.websocket.socketio.payload; | |||
import lombok.Data; | |||
/** | |||
* <p> | |||
* 广播消息载荷 | |||
* </p> | |||
* | |||
* @package: com.xkcoding.websocket.socketio.payload | |||
* @description: 广播消息载荷 | |||
* @author: yangkai.shen | |||
* @date: Created in 2018-12-18 20:01 | |||
* @copyright: Copyright (c) 2018 | |||
* @version: V1.0 | |||
* @modified: yangkai.shen | |||
*/ | |||
@Data | |||
public class BroadcastMessageRequest { | |||
/** | |||
* 消息内容 | |||
*/ | |||
private String message; | |||
} |
@@ -0,0 +1,34 @@ | |||
package com.xkcoding.websocket.socketio.payload; | |||
import lombok.Data; | |||
/** | |||
* <p> | |||
* 群聊消息载荷 | |||
* </p> | |||
* | |||
* @package: com.xkcoding.websocket.socketio.payload | |||
* @description: 群聊消息载荷 | |||
* @author: yangkai.shen | |||
* @date: Created in 2018-12-18 16:59 | |||
* @copyright: Copyright (c) 2018 | |||
* @version: V1.0 | |||
* @modified: yangkai.shen | |||
*/ | |||
@Data | |||
public class GroupMessageRequest { | |||
/** | |||
* 消息发送方用户id | |||
*/ | |||
private String fromUid; | |||
/** | |||
* 群组id | |||
*/ | |||
private String groupId; | |||
/** | |||
* 消息内容 | |||
*/ | |||
private String message; | |||
} |
@@ -0,0 +1,34 @@ | |||
package com.xkcoding.websocket.socketio.payload; | |||
import lombok.Data; | |||
/** | |||
* <p> | |||
* 私聊消息载荷 | |||
* </p> | |||
* | |||
* @package: com.xkcoding.websocket.socketio.payload | |||
* @description: 私聊消息载荷 | |||
* @author: yangkai.shen | |||
* @date: Created in 2018-12-18 17:02 | |||
* @copyright: Copyright (c) 2018 | |||
* @version: V1.0 | |||
* @modified: yangkai.shen | |||
*/ | |||
@Data | |||
public class SingleMessageRequest { | |||
/** | |||
* 消息发送方用户id | |||
*/ | |||
private String fromUid; | |||
/** | |||
* 消息接收方用户id | |||
*/ | |||
private String toUid; | |||
/** | |||
* 消息内容 | |||
*/ | |||
private String message; | |||
} |
@@ -1,4 +1,8 @@ | |||
server: | |||
port: 8080 | |||
servlet: | |||
context-path: /demo | |||
context-path: /demo | |||
ws: | |||
server: | |||
port: 8081 | |||
host: localhost |