1. 程式人生 > >WebSocket實現線上聊天及常見BUG解決[圖文詳解]

WebSocket實現線上聊天及常見BUG解決[圖文詳解]

前言

       最近在開發時碰到這樣一個需求:使用者瀏覽我們的官網時,存在一個問題反饋的入口,當管理員在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中,有的可能會存在一些不相容的問題,但都是比較小的,可以另行百度嘗試解決。