WebSocket實現線上聊天及常見BUG解決[圖文詳解]
阿新 • • 發佈:2019-02-06
前言
最近在開發時碰到這樣一個需求:使用者瀏覽我們的官網時,存在一個問題反饋的入口,當管理員在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
- 首先檢查路徑是否正確,對應的@ServerPoint註解是否和webSocketTest一致
- 檢查訪問的是本地還是外地伺服器,建議將localhost統一換成伺服器地址
- Gson的jar是否在pom檔案或者手動匯入過
-
WebSocket connection to 'ws://localhost:8080/CollabEdit/echo' failed: Error during WebSocket handshake: Unexpected response code: 404
這個問題也是在除錯成功之前一直困擾我的問題,最終定位到是Tomcat依賴的WebSocketjar包版本過低,解決方案先提供以下兩種:
- 將專案直接部署在Tomcat8.0及以上的版本執行
- 將依賴的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中,有的可能會存在一些不相容的問題,但都是比較小的,可以另行百度嘗試解決。