webSocket握手协议
客户端到服务端:
GET /demo HTTP/1.1
Host: example.com
Connection: Upgrade
Sec-WebSocket-Key2: 12998 5 Y3 1 .P00
Upgrade: WebSocket
Sec-WebSocket-Key1: 4@1 46546xW%0l 1 5
Origin: http://example.com
[8-byte security key]
服务端到客户端:
HTTP/1.1 101 WebSocket Protocol Handshake
Upgrade: WebSocket
Connection: Upgrade
WebSocket-Origin: http://example.com
WebSocket-Location: ws://example.com/demo
[16-byte hash response]
1.添加maven依赖
tomcat版本要7.0.65及以上,不然不支持webSocket
<!-- spring版本号 -->
<spring.version>4.3.9.RELEASE</spring.version>
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
<version>2.4</version>
<classifier>jdk15</classifier>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.8.5</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.8.5</version>
</dependency>
<!-- 添加对webSocket的支持 -->
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.1</version>
<scope>provided</scope>
</dependency>
<!-- 如果需要在MVC模式中被其他类调用,需要配置Configurator属性:用于调用一些自定义的配置算法,如拦截连接握手或可用于被每个端点实例调用的任意的方法和算法;对于服务加载程序,该接口必须提供默认配置器加载平台。 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>${spring.version}</version>
</dependency>
2.Spring-mvc.xml文件中添加
<bean id="mappingJacksonHttpMessageConverter" class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>text/html;charset=UTF-8</value>
</list>
</property>
</bean>
3.前端代码 包含心跳重连
添加webSocket的js引用
<script src="${pageContext.request.contextPath}/js/sockjs-0.3.min.js"></script>
/*Websocket*/
var websocket;
var userId = $('#userId').val();
var storeId = $('#storeId').val();
var host = window.location.host;
if(storeId!=0){
if('WebSocket' in window) {
console.log("此浏览器支持websocket");
reconnect(userId,storeId);
} else if('MozWebSocket' in window) {
alert("此浏览器只支持MozWebSocket");
} else {
alert("此浏览器只支持SockJS");
}
//websocket连接打开
websocket.onopen = function(evnt) {
heartCheck.start();
$("#socketMsg").html("链接服务器成功!")
};
//接收到服务器消息时调用方法
websocket.onmessage = function(evnt) {
heartCheck.reset();
var resultMsg = JSON.parse(evnt.data)
//调用播放语音的方法
var ele= document.getElementById("eleme");
var meituan = document.getElementById("meituan");
if(resultMsg.message.indexOf("饿了么")!=-1){
ele.play();
}else if(evnt.data.indexOf("美团")!=-1){
meituan.play();
}
//调用打印方法
var orderId = resultMsg.orderId;
if(orderId!='' && orderId!=null && orderId != undefined){
print(orderId);
}
};
websocket.onerror = function(evnt) {
reconnect(userId,storeId);
$('#socketMsg').html("与服务器连接异常!");
};
websocket.onclose = function(evnt) {
reconnect(userId,storeId);
$("#socketMsg").html("与服务器断开了链接!")
}
}
//心跳重连
var heartCheck = {
timeout: 30000,//60ms
timeoutObj: null,
serverTimeoutObj: null,
reset: function(){
clearTimeout(this.timeoutObj);
clearTimeout(this.serverTimeoutObj);
this.start();
},
start: function(){
var self = this;
this.timeoutObj = setTimeout(function(){
websocket.send("HeartBeat");
self.serverTimeoutObj = setTimeout(function(){
//如果onclose会执行reconnect,我们执行ws.close()就行了.如果直接执行reconnect 会触发onclose导致重连两次
websocket.close();
}, self.timeout)
}, this.timeout)
},
}
//重连方法
function reconnect(userId,storeId){
websocket = new WebSocket("ws://"+host+"/koms/websocket/remindOrder/"+userId+"/"+storeId);
}
4.服务端代码
其中包含一些业务代码,需要做到订单自动打印,打印的控制是放在HttpSession里面,当时做的时候碰到一个问题,当一个用户在同一个浏览器里面打开多个窗口的时候,消息会重复发多次. 就会重复打印。
登录一个页面之后,当前页面不关闭,直接复制本页面,这样我的服务端的Set集合里面对同一个用户保存了多个webSocket的session,这样我在服务端给客户端发信息的时候,同一个用户在同一个浏览器就发了多次。
后来的解决方案是,将webSocket的session保存到HttpSession中,每一个HttpSession发送一条信息,然后跳出,这样就不会存在同一个HTTPSession发多个消息了。同一个用户登录登录不同的浏览器,这样httpSession就不一样,我们业务里面允许就没有做处理
在Websocket中获取httpsession
import javax.servlet.http.HttpSession;
import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;
import javax.websocket.server.ServerEndpointConfig.Configurator;
/*
* 获取HttpSession
*
*/
public class GetHttpSessionConfigurator extends Configurator {
@Override
public void modifyHandshake(ServerEndpointConfig sec,
HandshakeRequest request, HandshakeResponse response) {
// TODO Auto-generated method stub
HttpSession httpSession=(HttpSession) request.getHttpSession();
sec.getUserProperties().put(HttpSession.class.getName(),httpSession);
}
}
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpSession;
import javax.websocket.EndpointConfig;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import com.cn.framework.constant.SessionListener;
@Component
@ServerEndpoint(value="/websocket/remindOrder/{userId}/{storeId}",configurator=GetHttpSessionConfigurator.class)
public class OrderRemind {
//日志记录
private Logger logger = LoggerFactory.getLogger(OrderRemind.class);
//记录每个用户下多个终端的连接
private static Map<Long, Set<OrderRemind>> userSocket = new HashMap<>();
//需要session来对用户发送数据, 获取连接特征userId
private Session session;
private Long userId;
private int storeId;
//httpsession
public static HttpSession httpSession = null;
/**
* @Title: onOpen
* @Description: websocekt连接建立时的操作
* @param @param userId 用户id
* @param @param session websocket连接的session属性
* @param @throws IOException
*/
@OnOpen
public void onOpen(@PathParam("userId") Long userId,@PathParam("storeId") int storeId,Session session,EndpointConfig config) throws IOException{
this.session = session;
this.userId = userId;
this.storeId = storeId;
//将webSocket的对象放到HttpSession集合中
httpSession= (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
Set<OrderRemind> webSocketSessions = (Set) httpSession.getAttribute("webSocketObject");
if(webSocketSessions!=null){
webSocketSessions.add(this);
}else{
webSocketSessions = new HashSet<>();
webSocketSessions.add(this);
}
httpSession.setAttribute("webSocketObject", webSocketSessions);
//根据该用户当前是否已经在别的终端登录进行添加操作
if (userSocket.containsKey(this.userId)) {
Set<OrderRemind> set = userSocket.get(this.userId);
System.out.println(set.size());
logger.error("当前用户id:{},门店:{},weSocketSession:{},已有其他终端登录",this.userId,this.storeId,this.session.hashCode());
userSocket.get(this.userId).add(this); //增加该用户set中的连接实例
}else {
logger.error("当前用户id:{},门店:{},weSocketSession:{},第一个终端登录",this.userId,this.storeId,this.session.hashCode());
Set<OrderRemind> addUserSet = new HashSet<>();
addUserSet.add(this);
userSocket.put(this.userId, addUserSet);
}
logger.error("用户{}登录的终端个数是为{}",userId,userSocket.get(this.userId).size());
logger.error("当前在线用户数为:{},所有终端个数为:{}",userSocket.size(),SessionListener.sessions.size());
}
/**
* @Title: onClose
* @Description: 连接关闭的操作
*/
@OnClose
public void onClose(){
//连接关闭的时候 httpsession中移除webSocket对象
Set<OrderRemind> webSocketSessions = (Set) httpSession.getAttribute("webSocketObject");
webSocketSessions.remove(this);
httpSession.setAttribute("webSocketObject",webSocketSessions);
//移除当前用户终端登录的websocket信息,如果该用户的所有终端都下线了,则删除该用户的记录
if (userSocket.get(this.userId).size() == 0) {
userSocket.remove(this.userId);
}else{
userSocket.get(this.userId).remove(this);
}
logger.error("用户{}登录的终端个数是为{}",this.userId,userSocket.get(this.userId).size());
logger.error("当前在线用户数为:{},所有终端个数为:{}",userSocket.size(),SessionListener.sessions.size());
}
/**
* @Title: onMessage
* @Description: 收到消息后的操作
* @param @param message 收到的消息
* @param @param session 该连接的session属性
*/
@OnMessage
public void onMessage(String message, Session session) {
logger.error("收到来自用户id为:{}的消息:{}",this.userId,message);
if(session ==null) logger.debug("session null");
//心跳重连机制
try {
session.getBasicRemote().sendText("{\"message\":\"连接正常!\"}");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* @Title: onError
* @Description: 连接发生错误时候的操作
* @param @param session 该连接的session
* @param @param error 发生的错误
*/
@OnError
public void onError(Session session, Throwable error){
logger.error("用户id为:{}的连接发送错误",this.userId);
error.printStackTrace();
}
/**
* @Title: sendMessageToUser
* @Description: 发送消息给用户下的所有终端
* @param @param userId 用户id
* @param @param message 发送的消息
* @param @return 发送成功返回true,反则返回false
*/
public Boolean sendMessageToUser(Map<String,Object> remindMessage){
//订单门店
int storeId = (Integer)remindMessage.get("store_id");
//订单渠道
int channelId = (Integer)remindMessage.get("channel_id");
//品牌
int brandId = (Integer)remindMessage.get("brand_id");
//订单号
Integer orderId = (Integer)remindMessage.get("order_id");
logger.error("sendMessageToUser:{ storeId:"+storeId+" ---- channelId"+channelId+" ---- brandId"+brandId+"}");
String message = "";
if(channelId==1){
message = "{\"message\":\"您有新的饿了么订单,请及时处理\",\"orderId\":\""+orderId+"\"}";
}else if(channelId ==2){
message = "{\"message\":\"您有新的美团外卖订单,请及时处理\",\"orderId\":\""+orderId+"\"}";
}
final String sendMessage = message;
//获取到所有的session 遍历所有的session ,取得所有session里面的webSocket对象 匹配userid和storeId
Map<String, HttpSession> sessions = SessionListener.sessions;
//遍历所有的session
Set<String> keySets = sessions.keySet();
for (String key : keySets) {
HttpSession session =(HttpSession)sessions.get(key);
Set<OrderRemind> orderReminds = (Set)session.getAttribute("webSocketObject");
try {
if(orderReminds==null || orderReminds.size()==0){
continue;
}
for (OrderRemind orderRemind : orderReminds) {
if(storeId==orderRemind.storeId){
logger.error("发送消息到客户端:{ userId:"+userId+" ---- orderId"+orderId+"---orderRemind.storeId:"+orderRemind.storeId+"}");
orderRemind.session.getBasicRemote().sendText(sendMessage);
}
break;
}
} catch (Exception e) {
e.printStackTrace();
}
}
/* Set<Long> keys = userSocket.keySet();
for (Long userId : keys) {
Set<OrderRemind> orderRemindes = userSocket.get(userId);
for (OrderRemind orderRemind : orderRemindes) {
//属于该门店的用户均发送推送消息
if(orderRemind.storeId==storeId){
try {
logger.error("发送消息到客户端:{ userId:"+userId+" ---- orderId"+orderId+"---orderRemind.storeId:"+orderRemind.storeId+"}");
orderRemind.session.getBasicRemote().sendText(message);
} catch (IOException e) {
logger.error(" 给用户id为:{}发送消息失败",orderRemind.userId);
e.printStackTrace();
}
}
}
}*/
return false;
}
}
结尾:仅供自己学习,记录问题和参考,若有带来误解和不便请见谅,共勉!