1.引言
我们知道web项目是基于http协议,http是应用层的协议,它的特点是无状态,什么叫做无状态呢?从编程模型的角度看,http的请求响应模型是一请求一响应,两次请求之间毫无关系!这也是为什么会需要cookie、session这样的技术,实现用户登录记住用户状态的原因。
当然,我们今天不探讨cookie、session,我想跟你分享的是关于websocket在实际应用中的一些实践,我们来搞清楚websocket是什么?以及websocket的一些实际应用场景。
在开始websocket详细内容前,我们先来看一些你熟悉的一些应用场景
- 扫码登录
- 扫码支付
- 走在吃货街道上,时不时听到商店里传出:你有新的订单了,外卖嘛对吧
这些场景我们是不是非常熟悉!比如说我一会写完文章,要通过公众号去发布,首先第一件事情就是要登录公众号,打开公众号登录页,它是这样的
你看到了,靠右边是不是有一个二维码,我只要拿出手机扫一扫该二维码,然后在手机上确认登录,我就能登录地道程序员公众号了。
比如掘金平台个人首页的小铃铛:
你看到了,一旦有朋友关注,点赞,亦或搜藏文章,都会及时收到官方通知。
要实现像这样的应用场景,你是不是有想过应该如何实现呢?让我们开始今天的分享吧!
2.案例
2.1.获取数据的推拉模式
从数据传递编程模型上来说,获取数据有两种方式
- pull拉模式
- push推模式
拉模式是谁需要谁主动,客户端需要数据,便主动向服务端发送获取数据的请求,等待服务端的响应;推模式则由对端主动推送,只需要告诉对端,有数据以后发给我呀!就行了。
关于拉模式,推模式,在编程实现中对应了我们熟悉的
- 拉模式:轮询、长轮询
- 推模式:长连接
我们看到轮询,是需要客户端主动发出请求,很有可能服务端在收到请求的时候,并没有准备好数据,于是响应客户端,你等会儿再来!客户端只能过会儿再发出请求,周而复始,来来回回浪费了不少资源,而且还不能及时获取到数据,这便是轮询的特点!我们上面举例微信公众平台扫描登录,用的即是轮询,打开浏览器检测网络请求就可以发现了。
那么长轮询呢?长轮询是当客户端发出获取数据的请求,服务端收到请求一看发现数据没准备好呢,好家伙!那我先不响应你!于是客户端收不到服务端的响应,只能等着,哪怕天荒地老也得等,一直等待服务端有了数据响应为止。我们熟悉的nacos、apollo配置中心组件,在客户端、服务端传递配置变更的时候即用了长轮询的方式。
不管轮询,还是长轮询,都是基于http,由客户端主动发起请求的编程模型,都存在获取数据实时性的问题,没有办法做到实时性。这个时候,长连接终于站了起来说,要实时性,那你选我呀!
长连接,即是要在客户端,服务端对端之间建立起连接,实现全双工的通信方式,谁都可以随时发言,还能同时发言,这也就是我们今天的主角websocket,它要做的事情。
2.2.什么是websocket
websocket是应用层的协议, 它是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议 。通过websocket客户端(浏览器)与服务端只需要完成一次握手,对端之间就可以创建持久性的连接,进行全双工通信(数据双向传递)。下图来源于菜鸟教程
从上图我们直观看到了基于http的轮询,与基于websocket的长连接通信方式上的差异。为了看到我们说的websocket一次握手,我们再看一个图
我们看到websocket,它其实是借助http实现的升级Upgrade,即一次握手的由来。
2.3.websocket应用案例
今天大多数小伙伴都是基于springboot开发应用,那么我们就来看一下在springboot中如何应用实践websocket。案例非常简单,模拟在线客户系统,实现在浏览器,与服务端的双向通信。且代码我都写了注释,非常容易看明白。
2.3.1.引入依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2.3.2.开启websocket支持
java
/**
* 启用spring boot 支持webSocket
*
* @author ThinkPad
* @version 1.0
*/
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
2.3.3.编写websocket服务端
java
/**
* 客服服务端:WebSocket Server
*
* @author ThinkPad
* @version 1.0
*/
@Component
@Slf4j
@ServerEndpoint("/kf/{sid}")
public class KfWebSocketServer {
/**
* 在线用户数量
*/
private static int clientCount = 0;
/**
* 存放客户端连接对象
*/
private static CopyOnWriteArraySet<KfWebSocketServer> clientSet = new CopyOnWriteArraySet<KfWebSocketServer>();
/**
* 客户端连接会话
*/
private Session session;
/**
* 客户端标识
*/
private String sid = "";
/**
* 连接建立成功调用方法
* @param session
* @param sid
*/
@OnOpen
public void onOpen(Session session, @PathParam("sid") String sid) {
this.session = session;
this.sid = sid;
// 将客户端连接,放入集合
clientSet.add(this);
// 客户端连接数 计数加一
addClientCount();
log.info("新客户建立连接标识:{},当前在线人数:{}", sid, getClientCount());
}
/**
* 连接关闭调用方法
*/
@OnClose
public void onClose() {
// 从集合中移除当前客户端
clientSet.remove(this);
// 在线客户端数 计数减一
subClientCount();
log.info("客户端连接断开标识:{},当前在线人数:{}", sid, getClientCount());
}
/**
* 收到客户端信息调用方法
* @param message
* @param session
*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("收到来自客户消息标识:{},消息:{}", sid, message);
try {
sendMessage("来自服务端响应:" + message);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 服务端、客户端连接发生错误
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("发生错误");
error.printStackTrace();
}
/**
* 服务端推送消息
* @param message
* @throws IOException
*/
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
/**
* 将消息推送给指定客户端
* @param message
* @param sid
* @throws IOException
*/
public static void sendInfo(String message, @PathParam("sid") String sid) throws IOException {
for (KfWebSocketServer item : clientSet) {
try {
if (item.sid.equals(sid)) {
item.sendMessage("来自服务端响应:" + message);
break;
}
} catch (IOException e) {
continue;
}
}
}
/**
* ===============================================================
*/
public static synchronized int getClientCount() {
return clientCount;
}
public static synchronized void addClientCount() {
KfWebSocketServer.clientCount++;
}
public static synchronized void subClientCount() {
KfWebSocketServer.clientCount--;
}
}
2.3.4.编写controller
java
/**
* 客服controller
*
* @author ThinkPad
* @version 1.0
*/
@Controller
@RequestMapping("/kf")
public class KfController {
/**
* 进入客户主页面
* @param userId
* @return
*/
@GetMapping("/index/{userId}")
public ModelAndView index(@PathVariable String userId) {
ModelAndView mav = new ModelAndView("/index");
mav.addObject("userId", userId);
return mav;
}
/**
* 推送数据到客户端
* @param cid
* @param message
* @return
*/
@ResponseBody
@RequestMapping("/push/{cid}")
public Map push(@PathVariable String cid, String message) {
Map<String,Object> result = new HashMap<>(16);
try {
KfWebSocketServer.sendInfo(message, cid);
result.put("code", cid);
result.put("msg", message);
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
}
2.3.5.编写前端页面
html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<title>在线客服系统,欢迎您!</title>
</head>
<body>
<input type="hidden" th:value="${userId}" id="userId"/>
消息:<input id="sendMsg" type="text" />
<button onclick="send()">发送消息</button>
<hr/>
<div id="msg"></div>
</body>
<script type="text/javascript">
// 用户Id,服务端地址
var userId = document.getElementById("userId").value;
var ws_address = "ws://localhost:8888/kf/" + userId;
var kf = {};
// 检查浏览器是否支持webSocket
if('WebSocket' in window) {
// 创建webSocket对象
kf.wSocket = new WebSocket(ws_address);
} else {
alert('抱歉,您的浏览器不支持webSocket!')
}
// 回调方法:连接成功
kf.wSocket.onopen = function (ev) {
document.getElementById('msg').innerHTML += '与服务端连接成功!<br/>';
}
// 回调方法:连接失败
kf.wSocket.onerror = function (ev) {
document.getElementById('msg').innerHTML += '与服务端连接失败!<br/>';
}
// 回调方法:收到消息
kf.wSocket.onmessage = function (ev) {
document.getElementById('msg').innerHTML += ev.data + '<br/>';
}
// 回调方法:关闭连接
kf.wSocket.onclose = function (ev) {
document.getElementById('msg').innerHTML += '与服务端连接关闭!<br/>';
}
// 监听窗口关闭事件,当窗口关闭时,主动去关闭webSocket连接
// 防止连接还没断开就关闭窗口,server端发生异常
window.onbeforeunload = function() {
kf.wSocket.close();
}
// 发送消息
function send(){
var message = document.getElementById('sendMsg').value;
kf.wSocket.send(message);
}
</script>
</html>
2.3.6.测试结果
前端
后端