项目使用websocket总结

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

------有疑问欢迎在评论去区回答,可以详细和你说怎么去写,也可以提供源码。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值