WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
现在,很多网站为了实现推送技术,所用的技术都是轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。
而比较新的技术去做轮询的效果是Comet。这种技术虽然可以双向通信,但依然需要反复发出请求。而且在Comet中,普遍采用的长链接,也会消耗服务器资源。
在这种情况下,HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。
1.引入包(gradle管理)
compile 'org.springframework.boot:spring-boot-starter-websocket:2.0.4.RELEASE'
2.配置
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* 简介:
*
* @Author: hzj
* @Date: 2019/7/8 0008 17:44
*/
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
3.代码实现
import com.getoncar.common.exception.BaseException;
import com.getoncar.common.exception.CustomException;
import com.getoncar.entity.Agent_Entity;
import com.getoncar.entity.Message_Entity;
import com.getoncar.host.agent.config.websupport.ResponseVo;
import com.getoncar.model.AgentSendDialogueRequest;
import com.getoncar.model.MessageModel;
import com.getoncar.service.MessageAgentService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import net.sf.json.JSONObject;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.EOFException;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static com.getoncar.service.MessageAgentService.oneMessageCache;
/**
* 简介:
*
* @Author: hzj
* @Date: 2019/7/9 0009 9:05
*/
@Slf4j
@Api(description ="websocket对话中心-hzj")
@RequestMapping(value = "/websocketController")
@ServerEndpoint(value = "/websocket/{userId}")
@Component //此注解千万千万不要忘记,它的主要作用就是将这个监听器纳入到Spring容器中进行管理
@RestController
public class WebSocketController {
//静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
public static int onlineCount = 0;
//concurrent包的线程安全Set[ConcurrentHashMap],用来存放每个客户端对应的MyWebSocket对象。
public static ConcurrentHashMap webSocketSet = new ConcurrentHashMap<>();
//与某个客户端的连接会话,需要通过它来给客户端发送数据
public Session session;
//接收参数中的用户ID
public Long userId;
//接收用户中的平台类型
public Long platformType;
public Session getSession() {
return session;
}
public void setSession(Session session) {
this.session = session;
}
private static ExecutorService executorService = Executors.newCachedThreadPool();
private static MessageAgentService messageService;
@Autowired
public void setMessageService(MessageAgentService messageService) {
WebSocketController.messageService = messageService;
}
@ApiOperation(value ="服务器群发消息")
@ApiImplicitParams(
@ApiImplicitParam(paramType = "query",name = "content",value = "消息内容",dataType = "String")
)
@GetMapping("/ServerSendInfo")
public void ServerSendInfo(String content) throws IOException {
sendInfos("这是服务器给你推送的消息:"+content);
}
/**
* 连接建立成功调用的方法
* 接收url中的参数
*/
@OnOpen
public void onOpen(Session session,@PathParam("userId") Long userId) {
log.info("进入websocket连接");
if(webSocketSet.containsKey(userId+"")){
log.info("重复登陆-"+userId);
try {
WebSocketController before = webSocketSet.get(userId+"");
if(before.getSession().isOpen()){
log.info("发送被迫下线通知");
before.getSession().getBasicRemote().sendText("你的会话再另一地方登陆,被迫下线");
before.getSession().close();//关闭会话
}
webSocketSet.remove(before);
log.info("重复登陆"+userId+"连接"+before.getSession().getId()+"被服务器主动关闭!当前在线人数为" + getOnlineCount());
} catch (IOException e) {
e.printStackTrace();
return;
}
}
int maxSize = 200 * 1024; // 200K
// 可以缓冲的传入二进制消息的最大长度
session.setMaxBinaryMessageBufferSize(maxSize);
// 可以缓冲的传入文本消息的最大长度
session.setMaxTextMessageBufferSize(maxSize);
this.session = session;
this.userId = userId;
this.platformType = platformType;
webSocketSet.put(userId+"",this); //加入set中
addOnlineCount(); //在线数加1
log.info("有新连接加入!当前在线人数为" + getOnlineCount() + " userId==== " + userId + " platformType==== " + platformType);
// try {
// sendMessage("连接成功");
// } catch (IOException e) {
// log.error("websocket IO异常");
// }
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(CloseReason reason) {
try {
webSocketSet.remove(this); //从set中删除
webSocketSet.remove(this.userId+""); //从set中删除
System.out.println("连接关闭***************"+this.userId);
subOnlineCount(); //在线数减1
log.info("有一连接"+this.session.getId()+"关闭!当前在线人数为" + getOnlineCount());
log.info("连接"+this.session.getId()+"关闭原因:"+reason.getCloseCode()+"-"+reason.toString());
}catch (Exception e){
log.info("异常情况");
e.printStackTrace();
}
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session) {
//{"car_dealer_id": 198,"car_resource_id": 88, "content": "H你好啊","token":"56bd2cbf1e1349f29cdbbbc54ffc1b95"}
log.info("来自客户端"+session.getId()+"的消息:" + message);
if(message.equals("_0_")){
log.info("心跳");
System.err.println("心跳");
}else {
Runnable t = new Runnable() {
@Override
public void run() {
JSONObject json = JSONObject.fromObject(message);
AgentSendDialogueRequest asdr = (AgentSendDialogueRequest)JSONObject.toBean(json,AgentSendDialogueRequest.class);
Long receiverId = 0L;
Agent_Entity agent_entity = messageService.getAgentByAccessToken(asdr.getToken());
if(agent_entity != null) {
Long agent_id = agent_entity.getId();
asdr.setFromAgentId(agent_id);
receiverId = (asdr.getCar_dealer_id());//接收者id
try {
if (session.isOpen()) { //先确认 session是否已经打开 使用session.isOpen() 为true 则发送消息。
sendInfo(receiverId, JSONObject.fromObject(asdr).toString()); //把发送者userId的消息发给-->receiverId接收者id
}
} catch (EOFException e) {
log.info("报错EOFException" + e.getMessage());
System.err.println("报错EOFException" + e.getMessage());
} catch (IOException e) {
log.info("报错IOException" + e.getMessage());
e.printStackTrace();
}finally {
//入库操作
}
}
}
};
executorService.submit(t);
}
}
/**
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("发生错误" + error);
error.printStackTrace();
}
public void sendMessage(String message) throws IOException {
synchronized (this.session) {
this.session.getBasicRemote().sendText(message);
}
}
/**
* 私发
*
* @param message
* @throws IOException
*/
public static void sendInfo(Long userId, String message) throws IOException {
for (WebSocketController item : webSocketSet.values()) {
try {
if (item.userId.equals(userId)) {
item.sendMessage(message);
}
} catch (IOException e) {
continue;
}
}
}
/**
* 群发自定义消息
*/
public static void sendInfos(String message) throws IOException {
log.info(message);
for (WebSocketController item : webSocketSet.values()) {
try {
item.sendMessage(message);
} catch (IOException e) {
continue;
}
}
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
WebSocketController.onlineCount++;
}
public static synchronized void subOnlineCount() {
WebSocketController.onlineCount--;
}
}
1.代码实现
PC端聊天窗口
适用浏览器:360、FireFox、Chrome、Opera、傲游、搜狗、世界之窗. 不支持Safari、IE8及以下浏览器。
HTML、css、js资料下载地址
链接:https://pan.baidu.com/s/1VXYBiz5vKrMmlvWpqYw-4A
提取码:2xtq
开发过程如有疑问可交流讨论 WX:18637815946