WebSocket实现在线聊天及常见BUG解决[图文详解]

版权声明:转载请注明原创地址,否则依法追究法律责任 https://blog.csdn.net/weixin_38964895/article/details/82108048

前言

       最近在开发时碰到这样一个需求:用户浏览我们的官网时,存在一个问题反馈的入口,当管理员在PC端的时候可以直接回复,当管理员不在的时候,进行微信推送,管理员在微信端和客户进行一对一的在线问题解答,由于这个功能块的收益客户较小,最终技术选型采用WebSocket实现在线聊天,同时监控管理员是否在线,以便进行微信推送。

正文

  • 后台源码
  • 前台源码
  • 成果展示
  • 常见BUG及解决方案

后台源码

1. applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:mvc="http://www.springframework.org/schema/mvc"
	xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">
	
	<!-- 1.自动扫描 -->
	<context:component-scan base-package="com"></context:component-scan>
	<!-- 2.动态资源访问 -->
	<mvc:annotation-driven></mvc:annotation-driven>
	<!-- 3.静态资源访问-->
	<mvc:default-servlet-handler/>
	<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<!-- 4.视图解析器 -->
		<property name="prefix" value="/WEB-INF/views/"></property>
		<property name="suffix" value=".jsp"></property>
	</bean>
	
	<bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
		<property name="order" value="1"></property>
		<property name="mediaTypes">
			<map>
				<entry key="json" value="application/json"></entry>
				<entry key="xml" value="application/xml"></entry>
				<entry key="htm" value="text/htm"></entry>
			</map>
		</property>
		
		<property name="defaultViews">
			<list>
				<bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"></bean>
			</list>
		</property>
	</bean>
	<!-- 文件上传 -->
	<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
		<!-- 20*1024*1024=20971520 -->
		<property name="maxUploadSize" value="20971520"></property>
		<property name="defaultEncoding" value="UTF-8"></property>
		<property name="resolveLazily" value="true"></property>
	</bean>
</beans>

2. VO类

package com.chart.dto;
 
public class MessageDto {
	
	private String messageType;
	private String data;
	public String getMessageType() {
		return messageType;
	}
	public void setMessageType(String messageType) {
		this.messageType = messageType;
	}
	public String getData() {
		return data;
	}
	public void setData(String data) {
		this.data = data;
	}
	
}

3.Contoller

package com.test.controller;
 
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
 
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
 
import com.test.socket.WebSocketTest;
 
@Controller
@RequestMapping("chatWebsocket")
public class ChartWebsocketController {
	@RequestMapping("login")
	public void login(String username,HttpServletRequest request,HttpServletResponse response) throws Exception{
		HttpSession session=request.getSession();
		session.setAttribute("username", username);
		WebSocketTest.setHttpSession(session);
		request.getRequestDispatcher("/socketChart.jsp").forward(request, response);
	}
	@RequestMapping("loginOut")
	public void loginOut(HttpServletRequest request,HttpServletResponse response) throws Exception{
		HttpSession session=request.getSession();
		session.removeAttribute("username");
		request.getRequestDispatcher("/socketChart.jsp").forward(request, response);
	}
}

4. WebSocket核心

package com.test.socket;
 
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
 
import javax.servlet.http.HttpSession;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

import com.chart.dto.MessageDto;
import com.google.gson.Gson;
 
/**
 * @ServerEndpoint
 */
@ServerEndpoint("/websocketTest")
public class WebSocketTest {
	private static int onlineCount = 0;
	//存放所有登录用户的Map集合,键:每个用户的唯一标识(用户名)
	private static Map<String,WebSocketTest> webSocketMap = new HashMap<String,WebSocketTest>();
	//session作为用户简历连接的唯一会话,可以用来区别每个用户
	private Session session;
	//httpsession用以在建立连接的时候获取登录用户的唯一标识(登录名),获取到之后以键值对的方式存在Map对象里面
	private static HttpSession httpSession;
	
	public static void setHttpSession(HttpSession httpSession){
		WebSocketTest.httpSession=httpSession;
	}
	/**
	 * 连接建立成功调用的方法
	 * @param session
	 * 可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据
	 */
	@OnOpen
	public void onOpen(Session session) {
		Gson gson=new Gson();
		this.session = session;
		webSocketMap.put((String) httpSession.getAttribute("username"), this);
		addOnlineCount(); // 
		MessageDto md=new MessageDto();
		md.setMessageType("onlineCount");
		md.setData(onlineCount+"");
		sendOnlineCount(gson.toJson(md));
		System.out.println(getOnlineCount());
	}
	/**
	 * 向所有在线用户发送在线人数
	 * @param message
	 */
	public void sendOnlineCount(String message){
		for (Entry<String,WebSocketTest> entry  : webSocketMap.entrySet()) {
			try {
				entry.getValue().sendMessage(message);
			} catch (IOException e) {
				continue;
			}
		}
	}
	
	/**
	 * 连接关闭调用的方法
	 */
	@OnClose
	public void onClose() {
		for (Entry<String,WebSocketTest> entry  : webSocketMap.entrySet()) {
			if(entry.getValue().session==this.session){
				webSocketMap.remove(entry.getKey());
				break;
			}
		}
		//webSocketMap.remove(httpSession.getAttribute("username"));
		subOnlineCount(); // 
		System.out.println(getOnlineCount());
	}
 
	/**
	 * 服务器接收到客户端消息时调用的方法,(通过“@”截取接收用户的用户名)
	 * 
	 * @param message
	 *            客户端发送过来的消息
	 * @param session
	 *            数据源客户端的session
	 */
	@OnMessage
	public void onMessage(String message, Session session) {
		Gson gson=new Gson();
		System.out.println("收到客户端的消息:" + message);
		StringBuffer messageStr=new StringBuffer(message);
		if(messageStr.indexOf("@")!=-1){
			String targetname=messageStr.substring(0, messageStr.indexOf("@"));
			String sourcename="";
			for (Entry<String,WebSocketTest> entry  : webSocketMap.entrySet()) {
				//根据接收用户名遍历出接收对象
				if(targetname.equals(entry.getKey())){
					try {
						for (Entry<String,WebSocketTest> entry1  : webSocketMap.entrySet()) {
							//session在这里作为客户端向服务器发送信息的会话,用来遍历出信息来源
							if(entry1.getValue().session==session){
								sourcename=entry1.getKey();
							}
						}
						MessageDto md=new MessageDto();
						md.setMessageType("message");
						md.setData(sourcename+":"+message.substring(messageStr.indexOf("@")+1));
						entry.getValue().sendMessage(gson.toJson(md));
					} catch (IOException e) {
						e.printStackTrace();
						continue;
					}
				}
				
			}
		}
		
	}
 
	/**
	 * 发生错误时调用
	 * 
	 * @param session
	 * @param error
	 */
	@OnError
	public void onError(Session session, Throwable error) {
		error.printStackTrace();
	}
 
	/**
	 * 这个方法与上面几个方法不一样。没有用注解,是根据自己需要添加的方法。
	 * 
	 * @param message
	 * @throws IOException
	 */
	public void sendMessage(String message) throws IOException {
		this.session.getBasicRemote().sendText(message);
		// this.session.getAsyncRemote().sendText(message);
	}
 
	public static synchronized int getOnlineCount() {
		return onlineCount;
	}
 
	public static synchronized void addOnlineCount() {
		WebSocketTest.onlineCount++;
	}
 
	public static synchronized void subOnlineCount() {
		WebSocketTest.onlineCount--;
	}
}

前台源码

<%@ page language="java" contentType="text/html" pageEncoding="utf-8"%>
<%
	String path = request.getContextPath();
	String basePath = request.getScheme() + "://"
			+ request.getServerName() + ":" + request.getServerPort()
			+ path + "/";
%>
<!DOCTYPE html>  
<html>  
<head>  
    <meta charset="utf-8">  
    <meta name="viewport" content="initial-scale=1,maxmum-scale=1,minimumscale=1" />
    <title>HTML5模拟微信聊天界面</title>  
    <script type="text/javascript" src="jslib/jquery.min.js"></script>
    <style>  
  /**重置标签默认样式*/   
        * {   
            margin: 0;   
            padding: 0;   
            list-style: none;   
            font-family: '微软雅黑' ;
            font-size:0.16rem;  
        }   
        body,html{
       		height:100%;
       		width:100%;
        }
       /*  body{
        	position:absolute;
        	top:0px;
        } */
        #container {   
            width: 100%;   
            height: 100%;   
            background: #eee;    
        }   
        .header {   
       		width:92%;
            background: white; 
            border:2px solid #ccc;
            border-radius:5px;  
            overflow:hidden;   
            color: #000;   
            line-height: 34px;   
            font-size: 20px; 
            margin:0 0.1rem;  
            padding:0.1rem;   
        }   
        .footer {   
            width: 96%;  
            height: 0.5rem;
            background: #666;   
            position: fixed;   
            bottom: 0;   
            padding: 0.1rem;  
        }   
        .footer input {   
            width: 80%;   
            height: 0.45rem;   
            outline: none;   
            font-size: 0.2rem;   
            text-indent: 0.1rem;   
           
            border-radius: 0.06rem;   
            
        }   
        .footer span {   
            display: inline-block;   
            width: 13%;   
            margin-left:2%;
            height: 0.45rem;   
            background: #ccc;   
            font-weight: 900;   
            line-height: 0.45rem;   
            cursor: pointer;   
            text-align: center;   
            border-radius: 0.06rem;   
        }   
        .footer span:hover {   
            color: #fff;   
            background: #999;   
        }   
        #user_face_icon {   
            display: inline-block;   
            background: white;   
            width: 60px;   
            height: 60px;   
            border-radius: 30px;   
            position: absolute;   
            bottom: 6px;   
            left: 14px;   
            cursor: pointer;   
            overflow: hidden;   
        }   
        img {   
            width: 70px;   
            height: 60px;   
        }   
        .content {   
        	height:780px;
            font-size: 0.2rem;   
            width: 98%; 
            overflow: auto;   
            padding: 0.05rem; 
            padding-bottom: 0.1rem;  
        }  

        .content li {   
            margin-top: 10px;   
            padding-left: 10px;   
            width: 95%;   
            display: block;   
            clear: both;   
            overflow: hidden;   
        }   
        .content li img {   
            float: left;   
        }   
        .content li span{   
            background: #7cfc00;   
            padding: 10px;   
            border-radius: 10px;   
            display:inline-block;
            max-width: 310px;   
            border: 1px solid #ccc;   
            box-shadow: 0 0 3px #ccc;  
            word-wrap:break-word;
            white-space:normal; 
        }   
        .content li img.imgleft {    
            float: left;    
        }   
        .content li img.imgright {    
            float: right;    
        }   
        .content li span.spanleft {    
            float: left;   
            background: #fff;   
        }   
        .content li span.spanright {    
            float: right;   
            background: #7cfc00;   
        }   
        .info{
        	overflow:hidden;
        }
        .info .detail-img {
			text-align: center;
		}
		
		.info .detail-img img {
			height: 20%;
			width: 15%;
			cursor: pointer;
		}
        .detail-title h3{
        	font-size:0.18rem;
        	text-align:center;
        }
        .origin{
        	text-align:center;
        }
        .origin>div{
        	display:inline-block;
        }
        .left{
        	float:left!important;
        }
         .right{
        	float:right!important;
        }
    </style>  
    <script>  
    var wd = document.documentElement.clientWidth*window.devicePixelRatio/10.8;
	$("html").css({"font-size":wd+'px'});
    
    var websocket = null;
	//判断当前浏览器是否支持WebSocket
	if ('WebSocket' in window) {
		websocket = new WebSocket('ws://localhost:8080/WebSocketDemo/websocketTest');
	} else {
		alert('当前浏览器 Not support websocket')
	}
	//连接发生错误的回调方法
	websocket.onerror = function() {
		alert("WebSocket连接发生错误");
	};
        window.onload = function(){   
           // var arrIcon = ['img/asker.bmp','img/tl.png'];   
            var iNow = -1;    //用来累加改变左右浮动  
            var num = 0;     //控制头像改变
            var btn = document.getElementById('btn');   
            //var icon = document.getElementById('user_face_icon').getElementsByTagName('img');
            var text = document.getElementById('textByWx');   
            var content = document.getElementsByTagName('ul')[0];   
           // var img = content.getElementsByTagName('img');   
            var span = content.getElementsByTagName('span'); 
            var username = ${toUser};  
  
            btn.onclick = function(){   
                if(text.value ==''){   
                    alert('不能发送空消息');   
                }else {   
                	var message = document.getElementById('textByWx').value;
            		
            		console.log(username);
            		websocket.send(username+"@"+message);
            		
                    content.innerHTML += '<li class="one"><span>'+message+'</span></li>'; 
                   /*  content.innerHTML += '<li><img src="'+arrIcon[0]+'"><span>'+message+'</span></li>';  */
                    iNow++;
                    console.log(message)
                    for(var i=0;i<$(".content li").length;i++){
        				if($(".content li").eq(i).attr('class').match(/one/)){
	        			 	console.log(1)
	                    	$(".content li").eq(i).find('span').addClass("right");
	                    }else{
	                    	console.log(2)
	                   		$(".content li").eq(i).find('span').addClass("left");
	                    }
        			}
                }  
                    text.value = '';   
				     // 内容过多时,将滚动条放置到最底端   
						content.scrollTop=content.scrollHeight; 
						console.log(content.scrollTop) ;
						console.log(content.scrollHeight) ;

                  
            }
            websocket.onmessage = function(event) {
            	var messageJson=eval("("+event.data+")");
        		if(messageJson.messageType=="message"){
        			console.log(messageJson)
        			content.innerHTML += '<li class="two"><span>'+messageJson.data+'</span></li>';
        			console.log(typeof(messageJson.data));
        			var m = username.toString();
        			
        			var te = messageJson.data;
        			for(var i=0;i<$(".content li").length;i++){
        				if($(".content li").eq(i).attr('class').match(/one/)){
	        			 	console.log(3)
	                    	$(".content li").eq(i).find('span').addClass("right");
	                    }else{
	                    	console.log(3)
	                   		$(".content li").eq(i).find('span').addClass("left");
	                    }
        			}
        			 
        			//content.innerHTML += '<li><img src="'+arrIcon[1]+'"><span>'+messageJson.data+'</span></li>';
        			 //$('img').addClass('imgleft');   
        			 //$('span').addClass('spanleft');  
        		}
        		content.scrollTop=content.scrollHeight; 
            }
            
           
        }   
    </script>  
</head>  
<body>  
    <div id="container">  
        <div class="header">  
                                  
            <!-- <input id="username" type="text"/> -->
            <!-- <span style="float: left;">报表和自助取数平台</span> -->  
            <span class="time" style="float: right;">欢迎 ${username}</span>  
            <div class="info">
	        	<div class="title">
					<div class="row">
						<div class="detail-title text-center">
							<h3>${detalMap.tReportFeedback.reportName}</h3>
						</div>
					</div>
				</div>
				<div class="row text-center origin">
					<div id="createTime">${detalMap.tReportFeedback.createTime}</div>
					<div id="userNm">提问人:XXX</div>
					<div id="orgNm">机构:XXXXXXXX</div>
				</div>
				<div class="detail-img text-center">
					<p>
						<img src="./image.do?imgPath=${detalMap.tReportFeedback.picPath}"
							class="" alt="pic" title="pic">
					</p>
				</div>
	        </div> 
        </div>  
       
         <ul class="content"></ul>  
      	<div class="footer">  
             
            <input id="textByWx" type="text" placeholder="说点什么吧...">  
            <span id="btn">发送</span>  
        </div>  
    </div>  
</body>  
</html>  

成果展示

常见BUG及解决方案

  • 建立连接成功,马上提示WebSocket连接关闭

       Tomcat版本需要8.0及以上,版本过低的没有WebSocket的相关Jar或者不支持WebSocket

  • 无法找到ws://localhost:8080/WebSocketDemo/webSocketTest

  1. 首先检查路径是否正确,对应的@ServerPoint注解是否和webSocketTest一致
  2. 检查访问的是本地还是外地服务器,建议将localhost统一换成服务器地址
  3. Gson的jar是否在pom文件或者手动导入过
  • WebSocket connection to 'ws://localhost:8080/CollabEdit/echo' failed: Error during WebSocket handshake: Unexpected response code: 404

     这个问题也是在调试成功之前一直困扰我的问题,最终定位到是Tomcat依赖的WebSocketjar包版本过低,解决方案先提供以下两种:

  1. 将项目直接部署在Tomcat8.0及以上的版本运行
  2. 将依赖的WebSocket的jar从Tomcat8.0及以上中手动挑选出,部署在项目中,然后部署到低版本就没有问题了。我在实践中采取的是:Tomcat8.0的jar打成war,部署在Tomcat7.0上,可以成功启动
  • 发送的消息在接收方窗口没有接收到

      请注意看WebSocket核心的如下代码:

String targetname=messageStr.substring(0, messageStr.indexOf("@"));
String sourcename="";
    for (Entry<String,WebSocketTest> entry  : webSocketMap.entrySet()) {
		//根据接收用户名遍历出接收对象
		if(targetname.equals(entry.getKey())){
			try {
				for (Entry<String,WebSocketTest> entry1  : webSocketMap.entrySet()) {
					//session在这里作为客户端向服务器发送信息的会话,用来遍历出信息来源
					if(entry1.getValue().session==session){
						sourcename=entry1.getKey();
					}
				}
				MessageDto md=new MessageDto();
				md.setMessageType("message");		            
                md.setData(sourcename+":"+message.substring(messageStr.indexOf("@")+1));
				entry.getValue().sendMessage(gson.toJson(md));
    }

       也就是说,接收消息的一方,必须在Session中是存在的,可以简单的理解为一个容器,用户一旦登陆,就会进入该容器,当需要发送消息时,会按照接收方的username或其他等同信息(id/number...)去容器寻找,找到就会将对应的消息发送给接收方

       这个Demo虽然是依赖与Tomcat,但是WebSocket也是支持WebLogic的,最终我们也是将该Demo部署在WebLogic中,有的可能会存在一些不兼容的问题,但都是比较小的,可以另行百度尝试解决。

猜你喜欢

转载自blog.csdn.net/weixin_38964895/article/details/82108048