内容概览
本文主要讲解WebSocket的概念,产生原因,使用方法,并且配合演示案例:比特币价格的动态展示
WebSocket概念
WebSocket 的是一种网络通信协议。那么问题来了,我们已经有了 HTTP 协议,为什么还需要另一个协议?它能带来什么好处?
因为 HTTP 协议有一个缺陷:通信只能由客户端发起。
举例来说,在WebSocket概念出来之前,如果页面要不停地显示最新的价格,那么必须不停地刷新页面,或者用一段js代码每隔几秒钟发消息询问服务器数据。
而使用WebSocket技术之后,当服务器有了新的数据,会主动通知浏览器。当服务端有新的比特币价格之后,浏览器立马接收到消息。
HTTP 协议做不到服务器主动向客户端推送信息。
wiki百科上的定义
学新东西要搞懂基本概念,定义。要去找信息的源头,像编程这种舶来品,肯定是老外的香啦!
WebSocket是一种通信协议,可在单个TCP连接上进行全双工通信。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就可以建立持久性的连接,并进行双向数据传输。
由概念可以看出,webSocket的本质是种通信协议
首先,要明白WebSocket是一种通信协议,区别于HTTP协议,HTTP协议只能实现客户端请求,服务端响应的这种单项通信。
而WebSocket可以实现客户端与服务端的双向通讯,说白了,最大也是最明显的区别就是可以做到服务端主动将消息推送给客户端。
WebSocket 协议本质上是一个基于 TCP 的协议。
为了建立一个 WebSocket 连接,客户端浏览器首先要向服务器发起一个 HTTP 请求,这个请求和通常的 HTTP 请求不同,包含了一些附加头信息,其中附加头信息"Upgrade: WebSocket"表明这是一个申请协议升级的 HTTP 请求,服务器端解析这些附加的头信息然后产生应答信息返回给客户端,客户端和服务器端的 WebSocket 连接就建立起来了,双方就可以通过这个连接通道自由的传递信息,并且这个连接会持续存在直到客户端或者服务器端的某一方主动的关闭连接。
其余的特点有:
- 握手阶段采用 HTTP 协议。
数据格式轻量,性能开销小。客户端与服务端进行数据交换时,服务端到客户端的数据包头只有2到10字节,客户端到服务端需要加上另外4字节的掩码。HTTP每次都需要携带完整头部。 - 更好的二进制支持,可以发送文本,和二进制数据
- 没有同源限制,客户端可以与任意服务器通信
协议标识符是ws(如果加密,则是wss),请求的地址就是后端支持websocket的API。
几种与服务端实时通信的方法
我们都知道,不使用WebSocket与服务器实时交互,一般有两种方法。AJAX轮询和Long Polling长轮询。
AJAX轮询
AJAX轮询也就是定时发送请求,也就是普通的客户端与服务端通信过程,只不过是无限循环发送,这样,可以保证服务端一旦有最新消息,就可以被客户端获取。
Long Polling长轮询
Long Polling长轮询是客户端和浏览器保持一个长连接,等服务端有消息返回,断开。
然后再重新连接,也是个循环的过程,无穷尽也。。。
客户端发起一个Long Polling,服务端如果没有数据要返回的话,
会hold住请求,等到有数据,就会返回给客户端。客户端又会再次发起一次Long Polling,再重复一次上面的过程。
缺点
上边这两种方式都有个致命的弱点,开销太大,被动性。假设并发很高的话,这对服务端是个考验。
而WebSocket一次握手,持久连接,以及主动推送的特点可以解决上边的问题,又不至于损耗性能。
具体使用方法
一个典型的Websocket握手请求如下:
客户端请求
GET / HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: example.com
Origin: http://example.com
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
Sec-WebSocket-Version: 13
服务器回应
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: fFBooB7FAkLlXgRSz0BT3v4hq5s=
Sec-WebSocket-Location: ws://example.com/
Connection 必须设置 Upgrade,表示客户端希望连接升级。
Upgrade 字段必须设置 Websocket,表示希望升级到 Websocket 协议。
Sec-WebSocket-Key 是随机的字符串,服务器端会用这些数据来构造出一个 SHA-1 的信息摘要。把 “Sec-WebSocket-Key” 加上一个特殊字符串 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后计算 SHA-1 摘要,之后进行 BASE-64 编码,将结果做为 “Sec-WebSocket-Accept” 头的值,返回给客户端。如此操作,可以尽量避免普通 HTTP 请求被误认为 Websocket 协议。
Sec-WebSocket-Version 表示支持的 Websocket 版本。RFC6455 要求使用的版本是 13,之前草案的版本均应当弃用。
Origin 字段是可选的,通常用来表示在浏览器中发起此 Websocket 连接所在的页面,类似于 Referer。但是,与 Referer 不同的是,Origin 只包含了协议和主机名称。
啰嗦了那么多,终于要上代码了
前端页面
<%@ page language="java" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
<title>用WebSocket实时获知比特币价格</title>
</head>
<body>
<div style="width:400px;margin:20px auto;border:1px solid lightgray;padding:20px;text-align:center;">
当前比特币价格:¥<span style="color:#FF7519" id="price">10000</span>
<div style="font-size:0.9em;margin-top:20px">查看的人数越多,价格越高, 当前总共 <span id="total">1</span> 个人在线</div>
<div style="color:silver;font-size:0.8em;margin-top:20px">以上价格纯属虚构,如有雷同,so what?</div>
</div>
</body>
<script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>
<script type="text/javascript">
var websocket = null;
//判断当前浏览器是否支持WebSocket
if ('WebSocket' in window) {
websocket = new WebSocket("ws://localhost:8099/bitcoin/ws/bitcoinServer");
//连接成功建立的回调方法
websocket.onopen = function () {
websocket.send("客户端链接成功");
}
//接收到消息的回调方法
websocket.onmessage = function (event) {
setMessageInnerHTML(event.data);
}
//连接发生错误的回调方法
websocket.onerror = function () {
alert("WebSocket连接发生错误");
};
//连接关闭的回调方法
websocket.onclose = function () {
alert("WebSocket连接关闭");
}
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function () {
closeWebSocket();
}
}
else {
alert('当前浏览器 Not support websocket')
}
//将消息显示在网页上
function setMessageInnerHTML(innerHTML) {
var bitcoin = eval("("+innerHTML+")");
console.log(bitcoin)
$('#price').text(bitcoin.price);
$('#total').text(bitcoin.total);
/* document.getElementById('price').innerHTML = bitcoin.price;
document.getElementById('total').innerHTML = bitcoin.total; */
}
//关闭WebSocket连接
function closeWebSocket() {
websocket.close();
}
</script>
</html>
BitCoinDataCenter类
创建BitCoinDataCenter,使其继承HttpServlet.
标记为Servlet不是为了其被访问,而是为了便于伴随Tomcat一起启动,因为可以通过loadOnStartup一起就启动了
这个类实现了Runnable,可以在初始化方法里创建一个线程并调用之。
run 方法: 每个1-3秒就创建一个新价格,然后根据当前有多少人链接过来,进行调整价格,接着通过ServerManager广播出去。 这样浏览器就看到如如图所示的效果了
/**
* @author 立以宁
*2019年12月8日
*TODO
*/
@WebServlet(name ="BitCoinDataCenter",urlPatterns ="/BitCoinDataCenter",loadOnStartup = 1)
public class BitCoinDataCenter extends HttpServlet implements Runnable {
public void init() {
}
public void startUp() {
new Thread(this).start();
}
@Override
public void run() {
int bitPrice = 100000;
while (true) {
//每1-3秒产生一个新价格,就是要睡那么久
int duration = 1000 + new Random().nextInt(2000);
try {
Thread.sleep(duration);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//价格的波动范围在100000左右的50%
float random = 1 + (float) (Math.random()-0.5);//产生0.50-1.5
int newPrice = (int)(bitPrice*random);
//越多人观看,就价格越高
int total = ServerManager.getTotal();
newPrice = (int)(total*0.382*newPrice);
//广播出去
String messageFormat = "{\"price\":\"%d\",\"total\":%d}";
String message = String.format(messageFormat, newPrice,total);
ServerManager.broadCast(message);
}
}
}
创建BitCoinServer类
用注解@ServerEndpoint("/ws/bitcoinServer")把它标记为一个WebSocket Server
ws/bitcoinServer 表示有通过这个地址访问该服务
其中OnOpen发生的时候,即有链接过来的时候,会把当前WebSocket Server丢在ServerManager里管理起来,这样Tomcat才知道总共有哪些Server, 方便以后进行群发
/**
* @author 立以宁
*2019年12月8日
*TODO
*/
@ServerEndpoint("/ws/bitcoinServer")
public class BitCoinServer {
private Session session;
@OnOpen
public void onOpen(Session session) {
this.session = session;
ServerManager.add(this);
}
@OnClose
public void onClose(Session session) {
ServerManager.remove(this);
}
@OnError
public void onError(Session session, Throwable error) {
System.out.println("发生错误");
error.printStackTrace();
}
@OnMessage
public void onMessage(String message, Session session) {
System.out.println("来自客户端的消息:"+message);
}
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
}
ServerManager类
中维护了一个线程安全的集合servers, 用于因为浏览器发起连接请求而创建的BitCoinServer.
broadCast 方法遍历这个集合,让每个Server向浏览器发消息。
/**
* @author 立以宁
*2019年12月8日
*TODO
*/
public class ServerManager {
private static Collection<BitCoinServer> servers = Collections.synchronizedCollection(new ArrayList<BitCoinServer>());
//向集合中每一个server发送消息
public static void broadCast(String msg) {
for (BitCoinServer bitCoinServer:servers) {
try {
bitCoinServer.sendMessage(msg);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public static void add(BitCoinServer server) {
servers.add(server);
System.out.println("有连接加入!当前总连接数是:"+servers.size());
}
public static void remove(BitCoinServer server) {
servers.remove(server);
System.out.println("有连接退出!当前的总连接数:"+servers.size());
}
public static int getTotal() {
return servers.size();
}
}
后台效果图
总结
- 学习知识一定要去寻找源头,别人嚼过的东西肯定没味道了
- 学习新概念的时候一定要多提问,了解一个技术产生的背景,以及优缺点,这样才能灵活运用
- 我可真是个勤劳的搬运工,不说了,睡觉去了,狗命要紧( ̄▽ ̄)"
参考文章:
WebSocket 教程 - 阮一峰的网络日志
WebSocket其实没那么难
菜鸟教程
例子来源
这是一个学习java的草鸡棒的网站(๑•̀ㅂ•́)و✧
how2j
参考文档
WebSocket_API