Socket(长连接,一直连接,资源耗费大):
所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象
一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制
从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议根进行交互的接口
Http:
超文本传输协议(Hypertext Transfer Protocol,HTTP)是一个简单的请求-响应协议,它通常运行在TCP之上
它指定了客户端可能发送给服务器什么样的消息以及得到什么样的响应
一次请求,一次响应(轮询操作)
WebSocket:
WebSocket是一种在单个TCP连接上进行全双工通信的协议
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据
WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输
工作原理:
(1)先握手(Http请求)
(2)进行长连接(Socket连接)
(3)最后分手(Http请求,服务端断开)
(4)长连接断开
引入Websocket、StringUtils、fastjson相关依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.4</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.41</version>
</dependency>
yml相关配置:
# 日志级别设置
logging:
level:
root: info # 最基础的日志输出级别
com.kd.opt: debug # 指定包下的日志输出级别
org.springframework.web: debug # 指定类下的日志输出级别
file:
name: D:\\Java\\idea workspace\\websocket_demo1\\log.txt # 服务器中,日志打印位置
后端相关代码(WebSocketConfig.java):
package com.kd.opt.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* 开启WebSocket支持
*
* @author 小辰哥哥
*/
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
后端相关代码(CorsConfig.java):
package com.kd.opt.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
@Configuration
public class CorsConfig extends WebMvcConfigurationSupport {
/**
* 解决跨域问题(新版本2.4.2)
*
* @param registry
* @author zhouziyu
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
// 设置允许跨域请求的域名
.allowedOriginPatterns("*")
// 是否允许证书(cookies)
.allowCredentials(true)
// 设置允许的方法
.allowedMethods("*")
// 跨域允许时间
.maxAge(3600);
}
}
后端相关代码(WebSocketServer.java):
package com.kd.opt.config;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
/**
* (1)WebSocket是类似客户端服务端的形式(采用ws协议),这里的WebSocketServer其实就相当于一个ws协议的Controller
* (2)直接@ServerEndpoint("/websocket/{userId}") 、@Component启用即可,然后在里面实现@OnOpen开启连接,@onClose关闭连接,@onMessage接收消息等方法
* (3)新建一个ConcurrentHashMap webSocketMap 用于接收当前userId的WebSocket,方便IM之间对userId进行推送消息
* (4)集群版(多个ws节点)还需要借助mysql或者redis等进行处理,改造对应的sendMessage方法即可
*
* @author 小辰哥哥
*/
@ServerEndpoint("/websocket/{userId}")
@Component
public class WebSocketServer {
// 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的
private static int onlineCount = 0;
// concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象
private static ConcurrentHashMap<String, WebSocketServer> webSocketMap = new ConcurrentHashMap<>();
// 与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
// 接收userId
private String userId = "";
// 日志打印
private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketServer.class);
/**
* 方法一:连接建立
*
* @param session
* @param userId
* @author 小辰哥哥
*/
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId) {
this.session = session;
this.userId = userId;
// Map集合中是否包含userId
if (webSocketMap.containsKey(userId)) {
webSocketMap.remove(userId);
webSocketMap.put(userId, this);
} else {
webSocketMap.put(userId, this);
// 在线数加1
addOnlineCount();
}
LOGGER.debug("用户连接:" + userId + ",当前在线人数为:" + getOnlineCount());
try {
// 服务端主动推送消息
sendMessage("服务端主动向您推送消息$$$连接成功");
} catch (IOException e) {
LOGGER.error("用户连接:" + userId + ",网络异常");
}
}
/**
* 方法二:断开连接
*
* @author 小辰哥哥
*/
@OnClose
public void onClose() {
if (webSocketMap.containsKey(userId)) {
webSocketMap.remove(userId);
// 在线人数减1
subOnlineCount();
}
LOGGER.debug("用户退出:" + userId + ",当前在线人数为:" + getOnlineCount());
}
/**
* 方法三:客户端发送过来的消息
*
* @param message
* @param session
* @author 小辰哥哥
*/
@OnMessage
public void onMessage(String message, Session session) {
LOGGER.debug("当前用户:" + userId + ",报文:" + message);
// isNotEmpty(str) 等价于 str != null && str.length > 0
// isNotBlank(str) 等价于 str != null && str.length > 0 && str.trim().length > 0
if (StringUtils.isNotBlank(message)) {
try {
// 解析客户端发送过来的报文
JSONObject jsonObject = JSON.parseObject(message);
// 追加发送人
jsonObject.put("fromUserId", this.userId);
// 获取接收人
String toUserId = jsonObject.getString("toUserId");
// 获取报文中info信息
String info = jsonObject.getString("info");
// 传送给对应接收人的websocket
if (StringUtils.isNotBlank(toUserId) && webSocketMap.containsKey(toUserId)) {
webSocketMap.get(toUserId).sendMessage("发送消息$$$" + jsonObject.getString("contentText"));
} else {
if ("心跳包".equals(info)) {
LOGGER.debug("心跳包检测中,当前在线人数为:" + getOnlineCount());
} else {
// 接收人不在线/不存在
LOGGER.error("请求的userId:" + toUserId + "不在该服务器上");
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 方法四:实现服务器主动推送
*
* @param message
* @throws IOException
* @author 小辰哥哥
*/
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
/**
* 方法五:发生错误时执行的回调函数
*
* @param session
* @param error
* @author 小辰哥哥
*/
@OnError
public void onError(Session session, Throwable error) {
LOGGER.error("用户错误:" + this.userId + ",原因:" + error.getMessage());
error.printStackTrace();
}
/**
* 方法六:发送自定义消息
*
* @param message
* @param userId
* @throws IOException
* @author 小辰哥哥
*/
public static void sendInfo(String message, @PathParam("userId") String userId) throws IOException {
LOGGER.info("发送消息到:" + userId + ",报文:" + message);
if (StringUtils.isNotBlank(userId) && webSocketMap.containsKey(userId)) {
webSocketMap.get(userId).sendMessage(message);
} else {
LOGGER.error("用户" + userId + ",不在线!");
}
}
/**
* 方法七:获取当前在线人数
*
* @return
* @author 小辰哥哥
*/
public static synchronized int getOnlineCount() {
return onlineCount;
}
/**
* 方法八:在线人数加1
*
* @author 小辰哥哥
*/
public static synchronized void addOnlineCount() {
WebSocketServer.onlineCount++;
}
/**
* 方法九:在线人数减1
*
* @author 小辰哥哥
*/
public static synchronized void subOnlineCount() {
WebSocketServer.onlineCount--;
}
}
后端相关代码(WebsocketController.java):
package com.kd.opt.controller;
import com.kd.opt.config.WebSocketServer;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
@RestController
public class WebsocketController {
@RequestMapping(value = "/serverSendMessage",method = RequestMethod.GET)
public String serverSendMessage() throws IOException {
WebSocketServer.sendInfo("我喜欢你$$$小辰哥哥","521");
return "success";
}
}
Vue相关代码(WebSocketUtil.js):
/**
* WebSocketUtil Vue前端工具包
*
* @author 小辰哥哥
*/
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);
export default new Vuex.Store({
state: {
websock: null,
eventlist: [],
lockReconnect: false
},
getters: {
onEvent(state) {
return function (a) {
a = state.eventlist;
return a;
}
}
},
mutations: {
WebSocketInit(state, url) {
// state.websock.binaryType = "arraybuffer";
var that = this;
state.websock = new WebSocket(url);
/**
* 方法一:连接建立
*
* @author 小辰哥哥
*/
state.websock.onopen = function () {
console.log("WebSocket连接成功");
// 成功建立连接后,开始进行心跳检测
heartCheck.reset().start();
};
/**
* 方法二:断开连接
*
* @author 小辰哥哥
*/
state.websock.onclose = function () {
console.log("WebSocket断开连接");
};
/**
* 方法三:服务端向客户端推送消息
*
* @author 小辰哥哥
* @param callBack
*/
state.websock.onmessage = function (callBack) {
// 如果获取到消息,说明连接是正常的,开始进行心跳检测
heartCheck.reset().start();
// 接收到的后端推送消息
let datas = callBack.data;
console.log("WebSocket服务端向客户端推送:" + datas);
// 分割后的集合(根据'$$$'字符串进行切割数据)
let datasList = [];
datasList = datas.split('$$$');
state.eventlist = datasList;
};
/**
* 方法四:发生错误时执行的回调函数
*
* @author 小辰哥哥
*/
state.websock.onerror = function () {
console.log("WebSocket断开连接,网络连接异常");
state.websock.close();
reconnect(url);
};
/**
* 方法五:断线重连
*
* @author 小辰哥哥
* @param url
*/
function reconnect(url) {
// 判断是否重连的标志(state.lockReconnect)
if (state.lockReconnect) {
return;
}
state.lockReconnect = true;
// 没连接上会一直重连,设置延迟避免请求过多
setTimeout(function () {
console.info("WebSocket尝试重连中..." + new Date());
state.lockReconnect = false;
that.commit('WebSocketInit', url);
}, 5000);
}
// 心跳检测, 每隔一段时间检测连接状态,如果处于连接中,就向server端主动发送消息,来重置server端与客户端的最大连接时间,如果已经断开了,发起重连。
var heartCheck = {
timeout: 300000,
serverTimeoutObj: null,
reset: function () {
clearTimeout(this.timeoutObj);
clearTimeout(this.serverTimeoutObj);
state.websocket_connected_count = 0;
return this;
},
start: function () {
var self = this;
this.serverTimeoutObj = setInterval(function () {
if (state.websock.readyState == 1) {
console.log("WebSocket连接正常,发送消息保持连接");
state.websock.send('{"info":"心跳包"}');
// 如果获取到消息,说明连接是正常的,开始进行心跳检测
heartCheck.reset().start();
} else {
console.log("WebSocket断开连接,尝试重连");
reconnect(url);
}
}, this.timeout)
}
};
},
WebSocketSend(state, p) {
console.log("WebSocket客户端向服务端发送:" + JSON.stringify(p.jsonData.msg));
// 发送消息
state.websock.send(JSON.stringify(p.jsonData.msg));
},
WebSocketClose(state) {
console.log("WebSocket主动关闭连接");
// 发送消息
state.websock.close();
}
},
actions: {
WebSocketInit({
commit}, url) {
commit('WebSocketInit', url)
},
WebSocketSend({
commit}, p) {
p.type = 3;
commit('WebSocketSend', p)
},
WebSocketClose({
commit}) {
commit('WebSocketClose')
}
}
})
Vue相关代码(在src目录下,main.js):
import Vue from 'vue';
import websocket from './Base/WebSocketUtil.js';
Vue.prototype.$websocket = websocket;
Vue相关代码(路由,index.js):
import user1 from "@/components/user1";
import user2 from "@/components/user2";
{
path: '/user1',
name: 'user1',
component: user1,
},
{
path: '/user2',
name: 'user2',
component: user2,
},
Vue相关代码(user1.vue):
<template>
<div>
<div style="width: 500px">
<p style="text-align: center">用户1</p>
<el-input placeholder="请输入内容" v-model="text"></el-input>
<el-button type="primary" @click="send">发送</el-button>
<el-button type="primary" @click="flush">发送2</el-button>
<el-button type="primary" @click="exit">退出</el-button>
</div>
</div>
</template>
<script>
export default {
name: 'user1',
created() {
// Websocket建立连接
this.$websocket.dispatch("WebSocketInit", "ws://localhost:8080/websocket/521");
},
computed: {
alertCont() {
return this.$websocket.getters.onEvent('Websocket');
}
},
watch: {
alertCont: function (a) {
console.log("服务端推送消息(用户1):" + a[0] + "," + a[1]);
},
},
data() {
return {
text: ''
}
},
methods: {
exit() {
// Websocket断开连接
this.$websocket.state.websock.close();
},
flush() {
this.$axios({
url: '/serverSendMessage',
method: 'GET'
}).then(data => {
console.log(data.data);
}).catch(error => {
console.log(error);
})
},
send() {
// 客户端向服务端发送消息(JSON字符串形式)
this.$websocket.state.websock.send('{"toUserId":"520","contentText":"' + this.text + '"}');
}
}
}
</script>
<style scoped>
</style>
Vue相关代码(user2.vue):
<template>
<div>
<div style="width: 500px">
<p style="text-align: center">用户2</p>
<el-input placeholder="请输入内容" v-model="text"></el-input>
<el-button type="primary" @click="send">发送</el-button>
<el-button type="primary" @click="exit">退出</el-button>
</div>
</div>
</template>
<script>
export default {
name: "user2",
created() {
// Websocket建立连接
this.$websocket.dispatch("WebSocketInit", "ws://localhost:8080/websocket/520");
},
computed: {
alertCont() {
return this.$websocket.getters.onEvent('WebSocket');
}
},
watch: {
alertCont: function (a) {
console.log("服务端推送消息(用户2):" + a[0] + "," + a[1]);
},
},
data() {
return {
text: ''
}
},
methods: {
exit() {
// Websocket断开连接
this.$websocket.state.websock.close();
},
send() {
// 客户端向服务端发送消息(JSON字符串形式)
this.$websocket.state.websock.send('{"toUserId":"521","contentText":"' + this.text + '"}');
}
}
}
</script>
<style scoped>
</style>
<style>
</style>
开始测试(建立连接):
开始测试(发送消息):
开始测试(服务器推送消息):
开始测试(断开连接):
每天一个提升小技巧!!!