1.mc-websocket-biz模块分析
/**
*
*/
package com.hst.mc.websocket.biz.advise;
import java.io.IOException;
import java.lang.reflect.Type;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;
import com.google.common.base.Joiner;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
/**
* @author lijing1
*
*/
@Slf4j
@RestControllerAdvice
public class RequestAopLogComponent implements RequestBodyAdvice {
@Override
public boolean supports(MethodParameter methodParameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
//Auto-generated method stub
return true;
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
//Auto-generated method stub
return inputMessage;
}
@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
RequestMapping requestMapping = parameter.getMethodAnnotation(RequestMapping.class);
log.info("请求地址====>{}", Joiner.on(',').join(requestMapping.value()));
log.info("请求参数====>{}", JSONUtil.toJsonStr(body));
return body;
}
@Override
public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
RequestMapping requestMapping = parameter.getMethodAnnotation(RequestMapping.class);
log.info("请求地址====>{}", Joiner.on(',').join(requestMapping.value()));
return body;
}
}
主要是实现父类方法,获取请求体的参数。
@ControllerAdvice 注解,可以用于定义@ExceptionHandler、@InitBinder、@ModelAttribute,并应用到所有@RequestMapping中
启动类注解分析:
package com.hst.mc.websocket.biz;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import com.hst.mc.common.swagger.annotation.EnableKnife4jSwagger2;
import com.hst.mc.websocket.biz.config.WebSocketConfig;
import com.pig4cloud.pig.common.feign.annotation.EnablePigFeignClients;
import com.pig4cloud.pig.common.job.annotation.EnablePigXxlJob;
import com.pig4cloud.pig.common.security.annotation.EnablePigResourceServer;
/**
* @author pig archetype
* <p>
* 项目启动类
*/
@EnablePigXxlJob //自定义的注解
@EnableKnife4jSwagger2 //自定义的注解
@EnablePigFeignClients //开启feign
@EnablePigResourceServer //自定义的注解
@EnableDiscoveryClient //向注册中心微服务客户端
@SpringBootApplication //启动类注解
public class WebsocketApplication {
public static void main(String[] args) {
SpringApplication.run(WebsocketApplication.class, args);
}
}
怎么写一个自定义注解(注解本质其实就是一个接口):
package com.pig4cloud.pig.common.job.annotation;
import com.pig4cloud.pig.common.job.XxlJobAutoConfiguration;
import org.springframework.context.annotation.Import;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 激活xxl-job配置
*
* @author lishangbu
* @date 2020/9/14
*/
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({ XxlJobAutoConfiguration.class })
public @interface EnablePigXxlJob {
}
主要读取的XxlJobAutoConfiguration配置类,进行初始化。
package com.pig4cloud.pig.common.job;
import com.pig4cloud.pig.common.job.properties.XxlJobProperties;
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
import com.xxl.job.core.util.NetUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import java.util.stream.Collectors;
/**
* xxl-job自动装配
*
* @author lishangbu
* @date 2020/9/14
*/
@Configuration(proxyBeanMethods = false)
@EnableAutoConfiguration
@ComponentScan("com.pig4cloud.pig.common.job.properties")
@Slf4j
public class XxlJobAutoConfiguration {
/**
* 服务名称 包含 XXL_JOB_ADMIN 则说明是 Admin
*/
private static final String XXL_JOB_ADMIN = "xxl-job-admin";
/**
* 配置xxl-job 执行器,提供自动发现 xxl-job-admin 能力
*
* @param xxlJobProperties xxl 配置
* @param discoveryClient 注册发现客户端
* @return XxlJobSpringExecutor
*/
@Bean
public XxlJobSpringExecutor xxlJobSpringExecutor(XxlJobProperties xxlJobProperties,
DiscoveryClient discoveryClient) {
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAppname(xxlJobProperties.getExecutor().getAppname());
xxlJobSpringExecutor.setAddress(xxlJobProperties.getExecutor().getAddress());
xxlJobSpringExecutor.setIp(xxlJobProperties.getExecutor().getIp());
xxlJobSpringExecutor.setPort(this.makePortAvaliable(xxlJobProperties.getExecutor().getPort()));
xxlJobSpringExecutor.setAccessToken(xxlJobProperties.getExecutor().getAccessToken());
xxlJobSpringExecutor.setLogPath(xxlJobProperties.getExecutor().getLogPath());
xxlJobSpringExecutor.setLogRetentionDays(xxlJobProperties.getExecutor().getLogRetentionDays());
// 如果配置为空则获取注册中心的服务列表 "http://pigx-xxl:9080/xxl-job-admin"
if (! StringUtils.hasText(xxlJobProperties.getAdmin().getAddresses())) {
String serverList = discoveryClient.getServices().stream().filter(s -> s.contains(XXL_JOB_ADMIN))
.flatMap(s -> discoveryClient.getInstances(s).stream()).map(instance -> String
.format("http://%s:%s/%s", instance.getHost(), instance.getPort(), XXL_JOB_ADMIN))
.collect(Collectors.joining(","));
xxlJobSpringExecutor.setAdminAddresses(serverList);
log.info("XxlJobAutoConfig adminAddresses:{}", serverList);
} else {
xxlJobSpringExecutor.setAdminAddresses(xxlJobProperties.getAdmin().getAddresses());
}
return xxlJobSpringExecutor;
}
/**
* 因为xxl-job默认写死的9999和gateway的端口冲突了
* 所以改写xxl-job, 如果大于0,则从port开始找合适的端口,如果小于等于0则从19999开始找端口
* 考虑到并发执行下的效果,所以使用default为随机端口
* 而又考虑到运维开放端口的可能新,随机范围进行限定(10000-10200)
* 最终传给xxl执行的就是确定的端口
*
* @auther FredG_zhoupf
*/
private int makePortAvaliable(Integer port) {
Integer defaultPort = 10000 + (int) (Math.random() * 200);
Integer targetPort = NetUtil.findAvailablePort(port > 0 ? port : defaultPort);
log.info("this executor onlinePort is {}", targetPort);
return targetPort;
}
}
2.websocket的配置分析
package com.hst.mc.websocket.biz.config;
import java.net.URI;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.drafts.Draft_6455;
import org.java_websocket.handshake.ServerHandshake;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Lazy;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
/**
* @author lijing1
*
*/
@Data
@Slf4j
@Configuration
public class WebSocketClientConfig {
@DependsOn({"serverEndpointExporter", "webSocketServer"}) //先加载webSocketServer后再在加载webSocketClient
@Lazy //springIoc的延迟加载注解
@Bean
public WebSocketClient webSocketClient() {
try {
//socket服务端的地址配置
WebSocketClient webSocketClient = new WebSocketClient(new URI("ws://192.168.3.79:7004/push/message/g999/c999"),new Draft_6455()) {
@Override
public void onOpen(ServerHandshake handshakedata) {
log.info("[websocket] 连接成功");
}
@Override
public void onMessage(String message) {
log.info("[websocket] 收到消息={}",message);
}
@Override
public void onClose(int code, String reason, boolean remote) {
log.info("[websocket] 退出连接");
}
@Override
public void onError(Exception ex) {
log.info("[websocket] 连接错误={}",ex.getMessage());
}
};
webSocketClient.connect();
return webSocketClient;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
package com.hst.mc.websocket.biz.service;
import java.io.IOException;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Resource;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;
import com.alibaba.nacos.common.utils.Objects;
import com.hst.mc.websocket.api.enums.MsgTypeEnum;
import com.hst.mc.websocket.api.enums.WebsocketCode;
import com.hst.mc.websocket.api.exception.WebsocketException;
import com.hst.mc.websocket.biz.config.WebSocketConfig;
import com.hst.mc.websocket.biz.config.WebSocketServerConfig;
import com.xkcoding.http.util.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
/**
* @author lijing1
*
* websocket服务端,多例的,一次websocket连接对应一个实例
*
* 本实例可以扩展为简单的按群组聊天式的互动
*
* 如下几个可以做成配置化,方便计费和用量控制 TODO
* 1,可以控制到总的连接数; onlineCount; 10000
* 2,后续可以控制到每个群组的最大连接数量;onlineCountPerGroup;(√²10000 ≈ 3000)
* 3,可以控制总的群组数量; onlineGroup; 300
*/
@Component
//@ServerEndpoint注解是一个类层次的注解,它的功能主要是将目前的类定义成一个websocket服务器端,注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端
@ServerEndpoint(value = "/push/message/{gid}/{cid}", configurator = WebSocketServerConfig.class)
@Slf4j
public class WebSocketServer {
@Resource
private WebSocketConfig config;
/** 用来记录当前在线连接数。设计成线程安全的。*/
private static AtomicInteger onlineCount = new AtomicInteger(0);
/** 用来记录当前在线群组。设计成线程安全的。*/
private static AtomicInteger onlineGroup = new AtomicInteger(0);
// TODO private static AtomicInteger onlineCountPerGroup = new AtomicInteger(0);
/** 用于保存uri对应的连接服务,{uri:WebSocketServer},设计成线程安全的 */
// private static ConcurrentHashMap<String, CopyOnWriteArraySet<WebSocketServer>> webSocketServerMAP = new ConcurrentHashMap<>();
private static ConcurrentHashMap<String, ConcurrentHashMap<String, WebSocketServer>> webSocketServerMAP = new ConcurrentHashMap<>();
public static ConcurrentHashMap<String, WebSocketServer> getServerByGid (String gid) {
return webSocketServerMAP.get(gid);
}
private Session session;// 与某个客户端的连接会话,需要通过它来给客户端发送数据
private String gid; //客户端消息发送者群组号
private String cid; //客户端消息发送者
/**
* 连接建立成功时触发,绑定参数
* @param session 可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据
* @param gid 群组id
* @param cid 客户端id
*/
@OnOpen
public void onOpen(Session session, @PathParam("gid") String gid, @PathParam("cid") String cid) {
log.info("连接进入:群组{},客户端{}", gid, cid);
// checkToken(session);
this.session = session;
this.gid = gid;
this.cid = cid;
ConcurrentHashMap<String, WebSocketServer> webSocketServerSet = webSocketServerMAP.get(gid);
if (MapUtil.isEmpty(webSocketServerSet)) {
webSocketServerSet = new ConcurrentHashMap<>();
}
webSocketServerSet.put(cid, Objects.isNull(webSocketServerSet.get(cid)) ? this : webSocketServerSet.get(cid));
webSocketServerMAP.put(gid, webSocketServerSet);
// if(webSocketServer != null){ //同样业务的连接已经在线,则把原来的挤下线。
// webSocketServer.session.getBasicRemote().sendText(uri + "重复连接被挤下线了");
// webSocketServer.session.close();//关闭连接,触发关闭连接方法onClose()
// }
// webSocketServerMAP.put(uri, this);//保存uri对应的连接服务
addOnlineCount(); // 在线数加1
if (webSocketServerMAP.containsKey(gid)) {
addOnlineGroup(); // 在线群组数加1
}
}
private void checkToken(Session session2) {
String token = (String) session.getUserProperties().get("token");
log.info("token is : {}", token);
if (!StrUtil.equals(token, config.getToken())) {
throw new WebsocketException(WebsocketCode.EMPTY_CLIENT);
}
}
/**
* 连接关闭时触发,注意不能向客户端发送消息了
*/
@OnClose
public void onClose() {
webSocketServerMAP.get(gid).remove(cid);
if(webSocketServerMAP.get(gid).size() == 0) {
webSocketServerMAP.remove(gid);
}
reduceOnlineCount(); // 在线数减1
if (!webSocketServerMAP.containsKey(gid)) {
reduceOnlineGroup(); // 在线群组数减1
}
}
/**
* 收到客户端消息后触发
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message) {
log.info("收到消息:{}", message);
try {
JSONObject dto = JSONUtil.parseObj(message);
Integer msgTypeInt = dto.getInt("msgType");
String cid = dto.getStr("cid");
String gid = dto.getStr("gid");
String data = dto.getStr("data");
switch (MsgTypeEnum.getByValue(msgTypeInt)) {
case POINT :
sendInfo(cid, gid, data);
break;
case GROUP :
sendInfo(gid, data);
break;
case ANNOUNCEMENT :
sendInfo(data);
break;
default:
break;
}
} catch (Exception ex) {
log.error("收到消息格式不正确", ex.getMessage());
}
}
/**
* 通信发生错误时触发
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
try {
log.info("群组{},客户端{}:通信发生错误,连接关闭", gid, cid);
// TODO 是否要删除这一组连接 webSocketServerMAP.remove(gid);
webSocketServerMAP.get(gid).remove(cid);
if(webSocketServerMAP.get(gid).size() == 0) {
webSocketServerMAP.remove(gid);
}
reduceOnlineCount(); // 在线数减1
if (!webSocketServerMAP.containsKey(gid)) {
reduceOnlineGroup(); // 在线群组数减1
}
}catch (Exception ex){
log.error(ex.getMessage());
}
}
/**
* 实现服务器主动推送
* @throws IOException 发送消息一次
*/
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
// this.session.getAsyncRemote().sendText(message);
}
/**
* 获取在线连接数
* @return
*/
public static int getOnlineCount() {
return onlineCount.get();
}
/**
* 获取在线连接数
* @return
*/
public static int getOnlineGroup() {
return onlineGroup.get();
}
/**
* 原子性操作,在线连接数加一
*/
public static void addOnlineCount() {
onlineCount.getAndIncrement();
}
/**
* 原子性操作,在线群组数加一
*/
public static void addOnlineGroup() {
onlineGroup.getAndIncrement();
}
/**
* 原子性操作,在线连接数减一
*/
public static void reduceOnlineCount() {
onlineCount.getAndDecrement();
}
/**
* 原子性操作,在线连接数减一
*/
public static void reduceOnlineGroup() {
onlineGroup.getAndDecrement();
}
/**
* 发送自定义消息
* */
public static void sendInfo(String cid, String gid, String message) {
ConcurrentHashMap<String, WebSocketServer> wsMap = webSocketServerMAP.get(gid);
if (MapUtil.isEmpty(wsMap)) {
log.warn("群组{}不存在", gid);
return;
}
if (Objects.isNull(wsMap.get("cid"))) {
log.warn("群组{}存在,但是客户端{}不存在", gid, cid);
return;
}
try {
log.info("推送消息到群组:{},客户端:{},推送内容:{}", gid, cid, message);
wsMap.get("cid").sendMessage(message);
} catch (Exception ex) {
log.error("推送消息到窗口失败:{}", ex.getMessage());
}
}
/**
* 发送群发消息
* */
public static void sendInfo(String gid, String message) {
log.info("开始推送消息到群组:{},推送内容:{}", gid, message);
ConcurrentHashMap<String, WebSocketServer> wsMap = webSocketServerMAP.get(gid);
if (MapUtil.isEmpty(wsMap)) {
log.warn("群组{}不存在", gid);
return;
}
for (Entry<String, WebSocketServer> set : webSocketServerMAP.get(gid).entrySet()) {
try {
log.info("推送消息到群组:{},推送内容:{}", gid, message);
set.getValue().sendMessage(message);
} catch (Exception ex) {
log.error("推送消息到窗口失败:{}", ex.getMessage());
continue;
}
}
}
/**
* 发送公告
* */
public static void sendInfo(String message) {
log.info("推送消息给所有端,推送内容:{}", message);
if (MapUtil.isEmpty(webSocketServerMAP)) {
log.error("不存在连接");
}
for (Entry<String, ConcurrentHashMap<String, WebSocketServer>> mapEntry : webSocketServerMAP.entrySet()) {
log.info("开始推送消息到群组:{}", mapEntry.getKey());
for (Entry<String, WebSocketServer> setEntry : mapEntry.getValue().entrySet()) {
try {
log.info("推送消息到窗口:{}", setEntry.getKey());
setEntry.getValue().sendMessage(message);
} catch (Exception ex) {
log.error("推送消息到窗口失败:{}", ex.getMessage());
continue;
}
}
log.info("结束开始推送消息到群组:{}", mapEntry.getKey());
}
}
}
/**
*
*/
package com.hst.mc.websocket.biz.config;
import java.util.List;
import java.util.Map;
import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
import cn.hutool.core.collection.CollectionUtil;
import lombok.extern.slf4j.Slf4j;
/**
* @author lijing1
*
*/
@Slf4j
@Configuration
public class WebSocketServerConfig extends ServerEndpointConfig.Configurator{
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
Map<String, Object> userProperties = sec.getUserProperties();
Map<String, List<String>> headers = request.getHeaders();
List<String> remoteIp = headers.get("token");
if(CollectionUtil.isNotEmpty(remoteIp)){
log.info("ws token:" + remoteIp.get(0));
userProperties.put("token", remoteIp.get(0));
}
}
}
这个被执行主要做的是将请求里面的token值存储到sec里面。
package com.hst.mc.websocket.biz.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Configuration;
/**
* @author lijing1
*
*/
import lombok.Data;
@Data
/**作用
SpringCloud 使用 @RefreshScope注解,实现配置文件的动态加载。
使用方法
修改配置文件后,不重启应用。
在需要读取配置文件的地方添加 @RefreshScope注解
发送POST请求:http://localhost:port/actuator/refresh。
然后在重新发送controller层的请求,发现配置文件的更新已经生效了。
*/
@RefreshScope
@Configuration
@ConfigurationProperties(prefix = "websocket")
public class WebSocketConfig {
private Integer maxOnlineCount = 100;
private Integer maxOnlineGroup = 200;
private String token = "hst123edu";
}
这是一个配置常量类。
package com.hst.mc.websocket.biz.controller;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import com.hst.mc.websocket.api.vo.WebsocketInfoVO;
import com.hst.mc.websocket.biz.config.WebSocketConfig;
import com.pig4cloud.pig.common.core.config.LocalConfiguration;
import com.pig4cloud.pig.common.core.util.R;
import io.swagger.annotations.Api;
/**
* @author lijing1
*
*/
@RestController
@RequestMapping(value = "/webSocketInfo")
@Api(value = "webSocketMessage", tags = "websocke信息")
@ApiSupport(order = 1)
public class WebSocketInfoController {
@Resource
private WebSocketConfig config;
@Resource
private LocalConfiguration localConfiguration;
@GetMapping("/getWebSocketInfo")
public R<WebsocketInfoVO> getWebSocketInfo() {
WebsocketInfoVO vo = new WebsocketInfoVO();
vo.setToken(config.getToken());
//不用传request也可以通过以下方法获取request
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
.getRequestAttributes()).getRequest();
vo.setProtol("ws");
vo.setIp(request.getRemoteHost());
vo.setPort(request.getRemotePort());
request.getServerPort();
int port = request.getLocalPort();
vo.setUrl("ws://" + localConfiguration.getIp() + ":" + port + "/push/message/{gid}/{cid}");
return R.ok(vo);
}
}