版权声明:转载请注明原创地址,否则依法追究法律责任 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
- 首先检查路径是否正确,对应的@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中,有的可能会存在一些不兼容的问题,但都是比较小的,可以另行百度尝试解决。