Service搭建教程
1,依赖引入,springweb的起步依赖虽然有websocket的相关依赖,但是并不完整,所以依然需要导入websocket的起步依赖,依赖版本要统一(我使用的是2.5.3)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
2,注解实现服务端
2.1,@ServerEndpoint("/xxx/xxx/xxx"),指定服务器连接地址,支持restful风格,"/"不能省。该注解加在自定义的一个WebSocketServer的类,每个连接过来都会创建一个WebSocketServer的对象。
为什么同一个用户连接过来不会再次创建一个WebSocketServer对象?
这个问题本身就有问题,建立连接和发送消息在websocket协议中是有一个状态码的,用来区分,同时,建立连接后,服务端会存储当前连接对象(推送消息到客户端就用这个对象的session),客户端也会存储当前连接对象(发消息就用这个对象发),这样就能一一对应了,如果同一个用户并发请求10000个过来创建连接,那就可能创建1w个连接对象(未加锁的情况下,可以以客户维度加锁),但是由于后端会用一个唯一识别码(例如客户号),去存储当前连接(使用concurrentHashMap,static修饰,让所有的WebSocketServer对象用的都是同一个Map),所以最终值存储了一个,那么客户端也只能用这个最终的连接对象进行发送消息。
websocketSession和httpSession不是同一个
2.2,@ServerEndpoint的对象,是多例,但是还是需要被spring管理,需要加上@Componet注解(否则可能无法启动报错,或者启动了也无法接收请求),虽然加上的注解@Componet注解默认是单例,但是spring特殊处理过,所以是多例,但是是被spring管理。
@ServerEndpoint("/jerry/123")
@Component
@Slf4j
public class WebSocketServer {
private Session session;
private final static ConcurrentHashMap<String, WebSocketServer> clients = new ConcurrentHashMap<>();
2.3,@ServerEndpoint的对象,满足以上两点,还是无法接收到请求,这个时候其实还需要注册一个@ServerEndpoint的管理器,使得连接过来的请求处理到端点。
@Configuration
@Slf4j
public class WebSocketServiceConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
3.@ServerEndpoint注解的对象使用到的注解
3.1@OnOpen
当客户端和服务端建立连接成功时,会触发通知到当前注解的方法,方法入参要求Session和url(@pathParam注解restful风格的入参,这个入参可以没有),这个入参Session和HttpSession不同,是有websocket包自己实现的session
@OnOpen
public void openSuccess(Session session) {
if (session == null) {
log.info("连接异常(过期),session为空");
return;
}
//获取session
this.session = session;
String businessNo = getBusinessNo(session);
if (StringUtils.isEmpty(businessNo)) {
log.error("没有对应的参数,不保存连接");
return;
}
//存储当前客户端连接对象
saveWebClient(businessNo);
}
private String getBusinessNo(Session session) {
Map<String, List<String>> requestParameterMap = session.getRequestParameterMap();
System.out.println("requestParameterMap = " + requestParameterMap);
List<String> businessList = MapUtils.getObject(requestParameterMap, "businessNo");
if (businessList == null) {
return null;
}
return businessList.get(0);
}
3.2@OnMessage
当客户端和服务端建立成功后,监听消息,通常在服务器端,用来监听客户端发送过来的信息,进行处理转发(WebSocket有4种消息类型,BinaryMessage,PingMessage,PongMessage,TextMessage)
例如,客户1给客户2发送消息,当两个客户都跟服务器建立了连接,那么客户1,发送的消息,进入后,需要将消息推送给客户2,
TextMessage
消息格式,1客户 to 2客户,则一般时要带一个对方的Id
{ "businessNo":"2134349800","msg":"你好呀!!!", "toBusinessNo":"2134349801" }
那么这里面就得有两个标识符了,客户1和2的标识符,这样才能知道客户2的session(clients.get(getWebClientKey(businessNo).session),才能给客户2推送消息
@OnMessage
public void onMessage(String message) {
//clients
if (StringUtils.isBlank(message)) {
return;
}
WebClientMsgDto webClientMsgDto = JSON.parseObject(message, WebClientMsgDto.class);
if (webClientMsgDto == null) {
return;
}
String businessNo = webClientMsgDto.getBusinessNo();
if (StringUtils.isBlank(businessNo)) {
return;
}
WebSocketServer webClient = getWebClient(businessNo);
if (webClient == null) {
return;
}
String toBusinessNo = webClientMsgDto.getToBusinessNo();
WebSocketServer toWebClient = getWebClient(toBusinessNo);
if (toWebClient == null){
//告知当前用户对方离线
webClient.sendMsg("对方不在线,请稍后再发送");
return;
}
String msg = webClientMsgDto.getMsg();
if (StringUtils.isEmpty(msg)){
webClient.sendMsg("消息不能为空");
return;
}
toWebClient.sendMsg(JSON.toJSONString(
WebClientMsgDto.builder()
.msg(msg)
.businessNo(toBusinessNo)
.toBusinessNo(businessNo)
.build()));
}
/**
* 存储当前客户端连接对象
*
* @param businessNo
*/
private void saveWebClient(String businessNo) {
clients.put(getWebClientKey(businessNo), this);
log.info("clients1:{}",clients);
}
private String getWebClientKey(Session session) {
return getBusinessNo(session) + WebClientSenceEnum.CHAT_ROOM.getSenceName();
}
3.3@OnClose
客户关闭连接,或者异超时等关闭连接,会触发该注解方法,当触发该方法时,我们主要还是处理些扫尾的工作,比如清除存储的Client(clients.remove(getWebClientKey(session));)
/**
* @param session
*/
@OnClose
public void onClose(Session session) {
System.out.println("session = " + session);
// 关闭时,要记得删除存储的链接对象
rmWebClient(session);
}
private void rmWebClient(Session session) {
clients.remove(getWebClientKey(session));
}
3.4@OnError
当连接报错时,会触发该注解方法,可以根据需要自己实现,一般有两个参数,1,session,2,Throwable
@OnError
public void onError(Session session, Throwable throwable) {
System.out.println("session = " + session);
System.out.println("throwable = " + throwable);
}