1.为什么有了http还需要websocket
为 HTTP 协议有一个缺陷:通信只能由客户端发起,如果服务器有连续的状态变化,只能使用"轮询"
WebSocket 服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息(这个功能可以做个聊天器哦)
默认端口也是80和443 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL
关于短连接 ,长连接
长连接: HTTP1.1规定了默认保持长连接(HTTP persistent connection ,也有翻译为持久连接),数据传输完成了保持TCP连接不断开(不发RST包、不四次握手),等待在同域名下继续用这个通道传输数据;相反的就是短连接。
短连接: Client方与server每进行一次报文收发交易时才进行通讯连接,交易完毕后立即断开连接。此方式常用于一点对多点通讯。
关键字:Keep-Alive
websocket 单机解决方案:
1。引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
@Component
@Slf4j
@ServerEndpoint("/iflytek/{userId}") //此注解相当于设置访问URL
public class IFlytekVoiceSocket {
private Session session;
private String userId;
private static CopyOnWriteArraySet<IFlytekVoiceSocket> iFlytekVoiceSockets = new CopyOnWriteArraySet<>();
private static ConcurrentHashMap<String, Session> sessionPool = new ConcurrentHashMap<String, Session>();
// 每当发生websocket连接时,会执行此方法,可以在这是缓存用户和session
@OnOpen
public void onOpen(Session session, @PathParam(value = "userId") String userId) {
try {
this.session = session;
this.userId = userId;
iFlytekVoiceSockets.add(this);
sessionPool.put(userId, session);
log.info("【websocket消息】有新的连接,总数为:" + iFlytekVoiceSockets.size()
+ "类型type:" + userId
+ "连接userId:" + userId);
} catch (Exception e) {
}
}
// 每当断开websocket连接时,会执行此方法,可以去除用户和session
@OnClose
public void onClose() {
try {
iFlytekVoiceSockets.remove(this);
sessionPool.remove(this.userId);
log.info("【websocket消息】连接断开,总数为:" + iFlytekVoiceSockets.size()
+ "类型type:" + userId
+ "断开userid:" + userId);
} catch (Exception e) {
}
}
@OnMessage
public void onMessage(String message) {
IFlytekVoice iFlytekVoice = JSONObject.parseObject(message, IFlytekVoice.class);
log.info("【iflytek】收到客户端消息:" + iFlytekVoice.toString());
LambdaQueryWrapper<SysBaseDict> sysBaseDictLambdaQueryWrapper = new LambdaQueryWrapper<>();
sysBaseDictLambdaQueryWrapper.eq(SysBaseDict::getDictCode, "iflytek");
System.out.println("iFlytekVoice " + iFlytekVoice);
ISysBaseDictService sysBaseDictService = (ISysBaseDictService) SpringContextUtils.getBean("sysBaseDictServiceImpl");
SysBaseDict one = sysBaseDictService.getOne(sysBaseDictLambdaQueryWrapper);
LambdaQueryWrapper<SysBaseDict> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SysBaseDict::getDictId, one.getId());
queryWrapper.eq(SysBaseDict::getItemCode, iFlytekVoice.getCmd());
SysBaseDict one1 = sysBaseDictService.getOne(queryWrapper);
System.out.println("one1 " + one1);
sessionPool.forEach((k, v) -> {
if ("saas".equals(k)) {
v.getAsyncRemote().sendText(one1.getItemValue());
}
});
}
}
分布式方案解决:
但是以上只能是单机服务下有效 ,在分布式应用下,由于每个服务的jvm隔离,sessionPool 存的也不一样,所以需要借助第三方工具实现,这里使用redis 发布订阅系统实现
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2 。注册一个redis消息监听配置类 RedisConfig
@Slf4j
@EnableCaching
@Configuration
public class RedisConfig {
/**
* redis 监听容器, 可以添加多个监听不同的频道,只需要把消息监听器和对应的监听处理器绑定 绑定的方式是监听容器的入参 监听处理器 的名称 和 下面监听处理器的 bean name 一致
*
* @param redisConnectionFactory redis 配置
* @return
*/
@Bean
public RedisMessageListenerContainer redisContainer(RedisConnectionFactory redisConnectionFactory,
RedisReceiver redisReceiver,
MessageListenerAdapter commonListenerAdapter,
MessageListenerAdapter iflyTekListenerAdapter) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(redisConnectionFactory);
container.addMessageListener(commonListenerAdapter, new ChannelTopic(GlobalConstants.REDIS_TOPIC_NAME));
// 添加绑定channle 名称
container.addMessageListener(iflyTekListenerAdapter, new ChannelTopic(GlobalConstants.IFLYTEK_TOPIC_NAME));
return container;
}
@Bean
MessageListenerAdapter commonListenerAdapter(RedisReceiver redisReceiver) {
MessageListenerAdapter messageListenerAdapter = new MessageListenerAdapter(redisReceiver, "onMessage");
messageListenerAdapter.setSerializer(jacksonSerializer());
return messageListenerAdapter;
}
/**
* 讯飞监听处理器 和消息监听容器绑定 发布用会调用RedisReceiver 的 onIflyTekMessage 方法
* @param redisReceiver
* @return
*/
@Bean
MessageListenerAdapter iflyTekListenerAdapter(RedisReceiver redisReceiver) {
MessageListenerAdapter messageListenerAdapter = new MessageListenerAdapter(redisReceiver, "onIflyTekMessage");
return messageListenerAdapter;
}
}
3
编写处理订阅消息的类
package org.jeecg.common.modules.redis.listener;
public interface IFlyTekRedisListener {
void onIFlyTekMessage(String message);
}
package org.jeecg.common.modules.redis.receiver;
import cn.hutool.core.util.ObjectUtil;
import lombok.Data;
import lombok.extern.log4j.Log4j2;
import org.jeecg.common.base.BaseMap;
import org.jeecg.common.constant.GlobalConstants;
import org.jeecg.common.modules.redis.listener.IFlyTekRedisListener;
import org.jeecg.common.modules.redis.listener.JeecgRedisListerer;
import org.jeecg.common.util.SpringContextHolder;
import org.springframework.stereotype.Component;
/**
* @author zyf
*/
@Component
@Data
@Log4j2
public class RedisReceiver {
/**
* 接受消息并调用业务逻辑处理器
*
* @param params
*/
public void onMessage(BaseMap params) {
Object handlerName = params.get(GlobalConstants.HANDLER_NAME);
JeecgRedisListerer messageListener = SpringContextHolder.getHandler(handlerName.toString(), JeecgRedisListerer.class);
if (ObjectUtil.isNotEmpty(messageListener)) {
messageListener.onMessage(params);
}
}
/**
* 接受语音通道消息逻辑处理器 从applicationcontext中获得类型iFlyTekVoiceHandler bean,调用其onIFlyTekMessage方法,这里是为了解耦
*
*/
public void onIflyTekMessage(String message) {
log.info("message"+message);
IFlyTekRedisListener iFlyTekVoiceHandler = SpringContextHolder.getHandler("iFlyTekVoiceHandler", IFlyTekRedisListener.class);
System.out.println("ifly"+iFlyTekVoiceHandler);
iFlyTekVoiceHandler.onIFlyTekMessage(message);
}
}
4.修改websocket 主要是onmessage
//将第三方发过来的消息转发至redis 发布订阅通道上
@OnMessage
public void onMessage(String message) {
this.sendMessage(message);
}
private void sendMessage(String message) {
RedisTemplate redisTemplate = (RedisTemplate) SpringContextUtils.getBean("redisTemplate");
redisTemplate.convertAndSend(IFLYTEK_TOPIC_NAME, message);
}
@NotNull
public IFlytekVoiceVO getiFlytekVoiceVO(String message) {
String msg= (String)JSON.parse(message);
System.out.println("msg"+msg);
IFlytekVoiceDTO iFlytekVoice = JSON.parseObject(msg, IFlytekVoiceDTO.class);
log.info("【iflytek】收到客户端消息:" + iFlytekVoice.toString());
LambdaQueryWrapper<SysBaseDict> sysBaseDictLambdaQueryWrapper = new LambdaQueryWrapper<>();
sysBaseDictLambdaQueryWrapper.eq(SysBaseDict::getDictCode, "iflytek");
ISysBaseDictService sysBaseDictService = (ISysBaseDictService) SpringContextUtils.getBean("sysBaseDictServiceImpl");
SysBaseDict sysBaseDict = sysBaseDictService.getOne(sysBaseDictLambdaQueryWrapper);
LambdaQueryWrapper<SysBaseDict> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SysBaseDict::getDictId, sysBaseDict.getId());
queryWrapper.eq(SysBaseDict::getItemCode, iFlytekVoice.getCmd());
SysBaseDict sysBaseDict1 = sysBaseDictService.getOne(queryWrapper);
IFlytekVoiceVO iFlytekVoiceVO;
if ("-10001".equals(iFlytekVoice.getCmd())) {
iFlytekVoiceVO = new IFlytekVoiceVO(500, "录音错误");
} else {
iFlytekVoiceVO = new IFlytekVoiceVO(200, sysBaseDict1.getItemValue());
}
return iFlytekVoiceVO;
}
}
iFlyTekVoiceHandler 实现IFlyTekRedisListener ,是的可以被RedisReceiver 调用
package org.jeecg.modules.message.websocket;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.extern.log4j.Log4j2;
import org.jeecg.common.modules.redis.listener.IFlyTekRedisListener;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.modules.system.entity.SysBaseDict;
import org.jeecg.modules.system.service.ISysBaseDictService;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
@Component(value = "iFlyTekVoiceHandler")
@Log4j2
public class IFlyTekVoiceHandler implements IFlyTekRedisListener {
@Resource
private IFlytekVoiceSocket iFlytekVoiceSocket;
@Override
public void onIFlyTekMessage(String message) {
try {
IFlytekVoiceVO iFlytekVoiceVO = iFlytekVoiceSocket.getiFlytekVoiceVO(message);
IFlytekVoiceSocket.sessionPool.forEach((k, v) -> {
v.getAsyncRemote().sendText(iFlytekVoiceVO.toString());
});
} catch (Exception e) {
e.printStackTrace();
IFlytekVoiceSocket.sessionPool.forEach((k, v) -> {
v.getAsyncRemote().sendText(new IFlytekVoiceVO(500, "服务器错误").toString());
});
}
}
}
其他modle类
package org.jeecg.modules.message.websocket;
public class IFlytekVoiceVO {
private Integer code;
private String msg;
public IFlytekVoiceVO(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
@Override
public String toString() {
return "{" +
"code=" + code +
", msg='" + msg + '\'' +
'}';
}
}
package org.jeecg.modules.message.websocket;
public class IFlytekVoiceDTO {
private String cmd;
private String content;
public String getCmd() {
return cmd;
}
public void setCmd(String cmd) {
this.cmd = cmd;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
@Override
public String toString() {
return "IFlytekVoiceDTO{" +
"cmd='" + cmd + '\'' +
", content='" + content + '\'' +
'}';
}
}