前言:
WebSocket是一种网络通信协议。RFC6455 定义了它的通信标准。
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
我们平时开发的程序,基本上是短连接,服务器端一般是属于被动回复(即一应一答式,客户端发送一次请求,服务器端反馈一次。服务器不会主动去回复信息给客户端)但是问题来了,服务器被动回复消息导致很多业务无法实现,怎么办呢?比如我有一个业务,用户下单并且支付成功后,需要通知店家开始准备该订单所需要的东西,有什么办法可以解决呢?有人说轮询,有人说长连接。
轮询确实可以实现,但是轮询是有一定的时间差,如果实时性要求高的订单,这个时间差就比较不适合了,并且轮询也是极其消耗性能的。
这样长连接好像成为了不错的选择,但是长连接也有不好,例如服务器维护一个长连接会增加开销,对带宽也有一定的占用。
那么久该到我们的WebSocket出场了。首先,WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。其次,为了建立一个 WebSocket 连接,客户端浏览器首先要向服务器发起一个 HTTP 请求,这个请求和通常的 HTTP 请求不同,包含了一些附加头信息,其中附加头信息”Upgrade: WebSocket”表 明这是一个申请协议升级的 HTTP 请求,服务器端解析这些附加的头信息然后产生应答信息返回给客户端,客户端和服务器端的 WebSocket 连接就建立起来了,双方就可以通过这个连接通道自由的传递信息,并且这个连接会持续存在直到客户端或者服务器端的某一方主动的关闭连接。
这就可以很好的实现 在我们下单之后,服务器会主动推送消息到店家,店家收到消息后就去准备用户所需要的物品。实时性得到了保证。
介绍了那么多,接下来我们准备开始学习WebSocket吧。
开发环境:
win10+IntelliJ IDEA +JDK1.8
springboot版本:springboot 1.5.14 ——2.0后的springboot增加了挺多新特性,暂时先不做了解
项目结构:
项目完整案例:
https://github.com/LuckyToMeet-Dian-N/springboot_Learn_9
配置文件:
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.14.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.properties:
server.port=80
# 是否使用缓存,开发阶段最填false,方便使用ctrl+shift+F9 进行重新编译,无需重启服务
spring.thymeleaf.cache=false
# 检查该模板是否存在
spring.thymeleaf.check-template-location=true
# 模板中内容的类型
spring.thymeleaf.content-type=text/html
# 启动 MVC 对Thymeleaf 视图的解析
spring.thymeleaf.enabled=true
# 模板的字符集
spring.thymeleaf.encoding=UTF-8
# 从解析中排除的视图名称的逗号分隔列表,没有的话就空咯
spring.thymeleaf.excluded-view-names=
# 使用的是什么类型模板
spring.thymeleaf.mode=HTML5
# 在构建URL时可以预览查看名称的前缀。就是路径在哪
spring.thymeleaf.prefix=classpath:/templates/
# 在构建URL时附加到视图名称的后缀。就是我们用rest风格,不同加文件后缀名。自己加上去
spring.thymeleaf.suffix=.html
服务端代码:
controller层:主要用于主动推送消息
package com.example.demo.controller;
import com.example.demo.socket.MyWebSocket;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.io.IOException;
/**
* @Description: 消息发送类。服务端主动发送
* @Author: Gentle
* @date 2018/9/5 19:30
*/
@RestController
public class SocketController {
@Resource
MyWebSocket myWebSocket;
@RequestMapping("many")
public String helloManyWebSocket(){
//向所有人发送消息
myWebSocket.sendMessage("你好~!");
return "发送成功";
}
@RequestMapping("one")
public String helloOneWebSocket(String sessionId) throws IOException {
//向某个人发送消息
myWebSocket.sendMessage(sessionId,"你好~!,单个用户");
return "发送成功";
}
}
第二个controller:主要用来跳转页面的,就一个页面
package com.example.demo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @Description: 跳转到连接页面
* @Author: Gentle
* @date 2018/9/5 20:05
*/
@Controller
public class PageController {
@RequestMapping(value = "page")
public String gg(){
return "wen";
}
}
WebSocket类:这里有坑,注意
package com.example.demo.socket;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* @Description: websocket简单入门。此类可以向个人发送消息。但是不适合向,因为这里无法适合业务。可以自行改造Map来控制
* @Author: Gentle
* @date 2018/9/5 18:43
*/
@Component
@ServerEndpoint(value = "/ws/myWebSocket")
@Slf4j
public class MyWebSocket {
//每个客户端都会有相应的session,服务端可以发送相关消息
private Session session;
//J.U.C包下线程安全的类,主要用来存放每个客户端对应的webSocket连接,为什么说他线程安全。在文末做简单介绍
private static CopyOnWriteArraySet<MyWebSocket> copyOnWriteArraySet = new CopyOnWriteArraySet<MyWebSocket>();
/**
* 打开连接。进入页面后会自动发请求到此进行连接
* @param session
*/
@OnOpen
public void onOpen(Session session) {
this.session = session;
copyOnWriteArraySet.add(this);
log.info("websocket有新的连接, 总数:"+ copyOnWriteArraySet.size());
}
/**
* 用户关闭页面,即关闭连接
*/
@OnClose
public void onClose() {
copyOnWriteArraySet.remove(this);
log.info("websocket连接断开, 总数:"+ copyOnWriteArraySet.size());
}
/**
* 测试客户端发送消息,测试是否联通
* @param message
*/
@OnMessage
public void onMessage(String message) {
log.info("websocket收到客户端发来的消息:"+message);
}
/**
* 出现错误
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("发生错误:" + error.getMessage(), session.getId());
error.printStackTrace();
}
/**
* 用于发送给客户端消息(群发)
* @param message
*/
public void sendMessage(String message) {
//遍历客户端
for (MyWebSocket webSocket : copyOnWriteArraySet) {
log.info("websocket广播消息:" + message);
try {
//服务器主动推送
webSocket.session.getBasicRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 用于发送给指定客户端消息,这里写的不好。。不管了
*
* @param message
*/
public void sendMessage(String sessionId, String message) throws IOException {
Session session = null;
MyWebSocket tempWebSocket = null;
for (MyWebSocket webSocket : copyOnWriteArraySet) {
if (webSocket.session.getId().equals(sessionId)) {
tempWebSocket = webSocket;
session = webSocket.session;
break;
}
}
if (session != null) {
tempWebSocket.session.getBasicRemote().sendText(message);
} else {
log.warn("没有找到你指定ID的会话:{}", sessionId);
}
}
/**
* springboot内置tomcat的话,需要配一下这个。。如果没有这个对象,无法连接到websocket
* 别问为什么。。很坑。。。
* @return
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
//这个对象说一下,貌似只有服务器是tomcat的时候才需要配置,具体我没有研究
return new ServerEndpointExporter();
}
}
注意:这里一定要有这个@bean,如果没有,客户端无法连接到websocket
@Bean
public ServerEndpointExporter serverEndpointExporter() {
//这个对象说一下,貌似只有服务器是tomcat的时候才需要配置,具体我没有研究
return new ServerEndpointExporter();
}
客户端代码:这里是构建起来连接
<!DOCTYPE html >
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title>Title</title>
</head>
<body>
本页面是自动连接websocket
<script type="text/javascript">
var socket;
if (typeof (WebSocket) == "undefined") {
console.log("遗憾:您的浏览器不支持WebSocket");
} else {
console.log("恭喜:您的浏览器支持WebSocket");
//实现化WebSocket对象
//指定要连接的服务器地址与端口建立连接
//注意ws、wss使用不同的端口。我使用自签名的证书测试,
//无法使用wss,浏览器打开WebSocket时报错
//ws对应http、wss对应https。
socket = new WebSocket("ws://localhost/ws/myWebSocket");
//连接打开事件
socket.onopen = function() {
console.log("Socket 已打开");
socket.send("消息发送测试(From Client)");
};
//收到消息事件
socket.onmessage = function(msg) {
alert(msg.data);
};
//连接关闭事件
socket.onclose = function() {
console.log("Socket已关闭");
};
//发生了错误事件
socket.onerror = function() {
alert("Socket发生了错误");
}
//窗口关闭时,关闭连接
window.unload=function() {
socket.close();
};
}
</script>
</body>
</html>
代码就上面那么多了。其实真的不多。毕竟封装的太好了,对开发简化了很多。
测试程序:
首先打开3个页面,输入http://localhost/page跳转到页面,开启2个webSocket连接
控制台显示:
接下来我们在请求全部推送的请求:再开一个页面输入:http://localhost/many触发群发消息
针对个人用户,输入:http://localhost/one?sessionId=0 这里做的不是很好,ID不是我们真正意义上的用户iD,是一个用户加入后的自增id。 (待后续优化)
基本到这里就结束了。websocket基本入门。别走开,接下来还有个彩蛋喔!
彩蛋:
上面,我们接触了J.U.C的一些类,那么们来看看这个类是怎么玩的:
private static CopyOnWriteArraySet<MyWebSocket> copyOnWriteArraySet = new CopyOnWriteArraySet<MyWebSocket>();
我们先进入源码来接触一下,先看这里:实现了序列化接口这个不必说,继承自抽象的Set也不需要说。关键是,他使用的是另一个安全容器里的方法。
我们主要看add()方法,他调用的是另一个类中的方法。那我们在往下看
线程安全的add()方法。他在添加的时候,已经加了锁。
可能很多人比较习惯用synchronized来保证线程安全,当然这是正确的。只是synchronized有点重,即使后面坐了很多很多优化,依旧有些重,这里不做往下了。在往下篇幅就有点长了。
总结:
今天学习了webSocket的入门,初步知道了怎么使用。但是这里仅仅只是入门,入了门后可以改成适合自己所需要业务的代码。当然,我们也开始慢慢接触JUC力的类。这只是个初步,后面会慢慢“入侵”JUC,了解java在并发方面是做的多么优秀。最后祝大家学习进步,工作顺利。