1. websocket应用场景
1.消息聊天
场景描述:这是一个需求协作平台,项目中,拥有评论的区域模块,要求对产品经理,和与需求等公司内部人员对评论信息,是大家时时能够看见。
实现过程:以需求的id+用户id 作为唯一的key值,建立一个websocket连接,建立简介的时候,需要讲发送的消息传输到客户端,消息存储在表中。这些消息,都是需要存根的。
2.定时任务向客户端推送消息
场景描述:在项目中我们需要想用户推送一些理财的产品,用于客户去购买。这个过程是我们将一些及时理财的产品通过定时任务给客户推送。
实现过程,首先根据登录的用户,简历一个websocket连接,使用唯一的用户id+token的形式建立 唯一的key值。
当用户长时间不登录,token失效,从而导致,连接中断,不能让连接一直持续,会消耗大量的网路连接的资源。
涉及到机密的消息,或者是机密的数据,需要进行加密传输,前后端,制定好一种加密的方式,实现数据的发送,若不涉及,则不需要加密传输。加密方式建议对称加密模式。
2. websocket 和 http 协议的区别。
WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。
首先客户端先发起请求,这是一个http协议的模式,然后和服务端建立连接,连接一但建立,协议模式切换为websocket协议模式,此时是双工通信。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200110101626110.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MjExMzcxNQ==,size_16,color_FFFFFF,t_70)
参考博客:https://blog.csdn.net/qq_27409289/article/details/81814272。
3. 项目实现代码和图片,小demo后续更新。
1.项目微服务的架构。这个是一个单独的消息模块服务。
1. pom文件应用必须jar包。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2.定义一个config包,里面添加两个类
package com.gtja.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
@Bean
public WebSocketEndpointConfigure newConfigure() {
return new WebSocketEndpointConfigure();
}
}
package com.gtja.config;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import javax.websocket.server.ServerEndpointConfig;
public class WebSocketEndpointConfigure extends ServerEndpointConfig.Configurator implements ApplicationContextAware {
private static volatile BeanFactory context;
@Override
public <T> T getEndpointInstance(Class<T> clazz) throws InstantiationException {
return context.getBean(clazz);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
WebSocketEndpointConfigure.context = applicationContext;
}
}
3.编写websocket代码
package com.gtja.websocket;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.gtja.config.WebSocketEndpointConfigure;
import com.gtja.entity.Message;
import com.gtja.service.MessageService;
import com.gtja.util.DateUtil;
import com.gtja.vo.FileVo;
import com.gtja.vo.MessageVo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* @ServerEndpoint 注解是一个类层次的注解,它的功能主要是将目前的类定义成一个websocket服务器端,
* 注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端
*/
@Component
@ServerEndpoint(value = "/websocket/{token}",configurator= WebSocketEndpointConfigure.class)
public class WebSocket {
protected Logger logger = LoggerFactory.getLogger(this.getClass());
//静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static int onlineCount = 0;
//concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。若要实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key可以为用户标识 userId
private static Map<String, Session> clients = new ConcurrentHashMap<String, Session>();
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private String token;
//根据消息的发送 去推送在线连接的session连接 需求id 当前登录人userId
private static Map<String, List<String>> pushMap = new ConcurrentHashMap<String, List<String>>();
//messageService消息的service方法 ,消息的查询,新增,也可以发送通知等。
@Autowired
private MessageService messageService;
/**
* 连接建立成功调用的方法
* @param session 可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
@OnOpen
public void onOpen(@PathParam("token") String token,Session session) throws IOException {
//userid +"-"+ 需求id
System.out.println("================"+token);
String[] split = token.split("_");
clients.put(split[0], session);
addOnlineCount();
System.out.println("================"+split[0]);
System.out.println("连接数="+onlineCount);
System.out.println("================"+split[1]);
List<String> list = pushMap.get(split[1]);
if(list==null || list.size()==0){
//没有连接
List<String> list1 = new ArrayList<>();
list1.add(split[0]);
pushMap.put(split[1],list1);
}else {
if(!list.contains(split[0])){
list.add(split[0]);
}
}
this.logger.info("有新连接加入!当前在线人数为"+ getOnlineCount());
//判断 需求id 推送需求
List<MessageVo> messages1 = messageService.queryMessage(split[1]);
for(MessageVo messageVo:messages1){
if(messageVo.getFileList() !=null && !"".equals(messageVo.getFileList())&&messageVo.getFileName() !=null && !"".equals(messageVo.getFileName())){
List<String> url = Arrays.asList(messageVo.getFileList().split(","));
List<String> name = Arrays.asList(messageVo.getFileName().split(","));
List<FileVo> fileList1 = new ArrayList<>();
for(int i=0;i<name.size();i++){
FileVo fileVo = new FileVo();
fileVo.setName(name.get(i));
fileVo.setUrl(url.get(i));
fileList1.add(fileVo);
}
messageVo.setFileList1(fileList1);
}
if(messageVo.getImageList() !=null && !"".equals(messageVo.getImageList())&&(messageVo.getImageName() !=null && !"".equals(messageVo.getImageName()))){
List<String> url = Arrays.asList(messageVo.getImageList().split(","));
List<String> name = Arrays.asList(messageVo.getImageName().split(","));
List<FileVo> fileList1 = new ArrayList<>();
for(int i=0;i<name.size();i++){
FileVo fileVo = new FileVo();
fileVo.setName(name.get(i));
fileVo.setUrl(url.get(i));
fileList1.add(fileVo);
}
messageVo.setImageList1(fileList1);
}
}
Map<String,Object> paramMap = new HashMap<>();
paramMap.put("message",messages1);
String demandDate= JSON.toJSONString(paramMap);
//添加数据后推送时时的消息
WebSocket.sendInfo(demandDate,split[1]);
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(@PathParam("token") String token){
//userid +"-"+ 需求id
String[] split = token.split("_");
clients.remove(split[0]);
subOnlineCount();
List<String> list = pushMap.get(split[1]);
if(list!=null && list.size()>0){
//存在连接
if(list.contains(split[0])){
list.remove(split[0]);
if(list.size()==0){
pushMap.remove(split[1]);
}
}
}
this.logger.info("有一连接关闭!当前在线人数为"+ getOnlineCount());
}
/**
* 收到客户端消息后调用的方法
*
* @param session 可选的参数
*/
@OnMessage
public void onMessage(String messages, Session session) throws IOException {
//json格式的数据 装换为json对象
JSONObject jsonTo = JSONObject.parseObject(messages);
//取出消息的发送人
Message message = new Message();
message.setUserId( jsonTo.getString("userId"));
message.setUserName(jsonTo.getString("userName"));
message.setDemandId(jsonTo.getString("demandId"));
message.setMessageData(jsonTo.getString("messageData"));
message.setCreateTime(DateUtil.getCurrentDate());
message.setType(jsonTo.getString("type"));
String imageList = jsonTo.getString("imageList");
if(imageList != null && !"null".equals(imageList) && imageList.contains("http")){
String[] split = imageList.substring(1,imageList.length()-1) .split(",");
String urls="";
String imgNames="";
for ( String s:split ){
if(s.startsWith("\"url\"")){
urls += s.substring(7, s.length() - 2)+",";
}
if(s.startsWith("{\"imgName\"")){
imgNames += s.substring(12, s.length() - 2)+",";
}
}
message.setImageList(urls.substring(0,urls.length()-1));
message.setImageName(imgNames.substring(0,imgNames.length()-1));
}
String fileList = jsonTo.getString("fileList");
if(fileList != null && !"null".equals(fileList) && fileList.contains("http")){
String[] split1 = fileList.substring(1,fileList.length()-1) .split(",");
String files="";
String fileNames="";
for ( String s:split1 ){
if(s.startsWith("\"url\"")){
files += s.substring(7, s.length() - 2)+",";
}
if(s.startsWith("{\"fileName\"")){
fileNames += s.substring(13, s.length() - 2)+",";
}
}
message.setFileList(files.substring(0,files.length()-1));
message.setFileName(fileNames.substring(0,fileNames.length()-1));
}
messageService.createMessage(message);
//添加一条消息 判断消息里面是否包含 @姓名
//根据需求id 去查询所拥有的消息
List<MessageVo> messages1 = messageService.queryMessage(message.getDemandId());
for(MessageVo messageVo:messages1){
if(messageVo.getFileList() !=null && !"".equals(messageVo.getFileList())&&messageVo.getFileName() !=null && !"".equals(messageVo.getFileName())){
List<String> url = Arrays.asList(messageVo.getFileList().split(","));
List<String> name = Arrays.asList(messageVo.getFileName().split(","));
List<FileVo> fileList1 = new ArrayList<>();
for(int i=0;i<name.size();i++){
FileVo fileVo = new FileVo();
fileVo.setName(name.get(i));
fileVo.setUrl(url.get(i));
fileList1.add(fileVo);
}
messageVo.setFileList1(fileList1);
}
if(messageVo.getImageList() !=null && !"".equals(messageVo.getImageList())&&(messageVo.getImageName() !=null && !"".equals(messageVo.getImageName()))){
List<String> url = Arrays.asList(messageVo.getImageList().split(","));
List<String> name = Arrays.asList(messageVo.getImageName().split(","));
List<FileVo> fileList1 = new ArrayList<>();
for(int i=0;i<name.size();i++){
FileVo fileVo = new FileVo();
fileVo.setName(name.get(i));
fileVo.setUrl(url.get(i));
fileList1.add(fileVo);
}
messageVo.setImageList1(fileList1);
}
}
Map<String,Object> paramMap = new HashMap<>();
paramMap.put("message",messages1);
String demandDate= JSON.toJSONString(paramMap);
//添加数据后推送时时的消息
WebSocket.sendInfo(demandDate,message.getDemandId());
}
/**
* 发生错误时调用
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error){
this.logger.error("来自客户端的消息:");
error.printStackTrace();
}
/**
* 服务端发送自定义消息给客户端
*/
public static void sendInfo(String messages,String demandId) throws IOException {
List<String> list = pushMap.get(demandId);
//取出 userid集合
for(String userId:list){
for (String token : clients.keySet()) {
if (token.equals(userId)){
clients.get(token).getAsyncRemote().sendText(messages);
}
}
}
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
WebSocket.onlineCount++;
}
public static synchronized void subOnlineCount() {
WebSocket.onlineCount--;
}
public static synchronized Map<String, Session> getClients() {
return clients;
}
}
4. 项目的优化。
1.当连接人数过大时,是长连接是非常占用带宽的,我们可以定义一个定时任务,检测30分钟内,不活跃的用户,使该用户强制下线,发送一个消息给前端,定义个消息的类型,是强制下线的现行,让前端跳出弹窗,让用户重新连接。
2.用户连接了,但是可能因为网络的原因中断了,我们可以记录一张在线的用户表,判断在线的用户,前端定义一个心跳检测的请求,发送心跳,当我们接受到心跳后,判断连接中该用户是否存在,不存在,就查询表中是否在线,所在线,那么就是重新建立连接。心跳检测是很有必要的。
3.项目中的消息数据,可以存储在(可以用redis或者mongdb)缓存数据库中,若在mysql或者oracle中,频繁的访问数据库,对其他的业务也有一定的影响。
5. 面试中我遇到了面试官问我webSocket的应用。
1.首先我们想说一下为什么要用webSocket。
首先,我的项目是一个后台需求管理的软件,类似于jira,禅道,和tb。我们需求详情页面有一个需求讨论的模块,这个地方要实现用户和用户之间消息的互通,用户A发送消息,用户B就能够接收到,不用刷新页面。那么现在你就要说webSocket是一个双工的连接,我们长叫他长连接,和http不同,原因在上面问题2中。
2.然后你就要和面试官说了,你是怎么在项目中实现的。
首先,建立连接,是通过 需求编号-userId 生成token的形式,然后我们在@open方法中,接受了
token和session ,存储到对应的map集合中。然后通过需求编号查询去查这个需求里面所有的消息,然后去推送给前端。最好结合问题4回答。代码如上问题3
------有疑问欢迎在评论去区回答,可以详细和你说怎么去写,也可以提供源码。