一:WebSocket原理
1、要谈WebSocket就不得不提起HTTP连接
WebSocket是HTML5出的东西(协议,就是大家一起约定好的东西),也就是说HTTP协议没有变化,或者说没关系,但HTTP是不支持持久连接的(长连接,循环连接的不算)首先HTTP有1.1和1.0之说,也就是所谓的keep-alive,把多个HTTP请求合并为一个,但是Websocket其实是一个新协议,跟HTTP协议基本没有关系,只是为了兼容现有浏览器的握手规范而已,也就是说它是HTTP协议上的一种补充。有交集,但是并不是全部。当然他们都属于网络的7层协议中的应用层。
另外Html5是指的一系列新的API,或者说新规范,新技术。Http协议本身只有1.0和1.1,而且跟Html本身没有直接关系。
通俗来说,你可以用HTTP协议传输非Html数据,就是这样。再简单来说,层级不一样。
此外,ws协议是全双工协议,意味着不光服务器向客户端推送,客户端也可以像服务器发送请求。
2、Websocket是什么样的协议,具体有什么优点
首先,Websocket是一个持久化的协议,相对于HTTP这种非持久的协议来说。
1) HTTP的生命周期通过Request来界定,也就是一个Request 一个Response,那么在HTTP1.0中,这次HTTP请求就结束了。
在HTTP1.1中进行了改进,使得有一个keep-alive,也就是说,在一个HTTP连接中,可以发送多个Request,接收多个Response。
但是请记住 Request = Response , 在HTTP中永远是这样,也就是说一个request只能有一个response。而且这个response也是被动的,不能主动发起。
2)首先Websocket是基于HTTP协议的,或者说借用了HTTP的协议来完成一部分握手。在握手阶段是一样的。
在三次握手进行通信的过程中,Websocket协议要比HTTP协议的握手请求中,多了几个东西。
这个就是Websocket的核心了,告诉Apache、Nginx等后端服务器:发起的是Websocket协议,即协议是ws:// 而不是http://。
之后,Sec-WebSocket-Key 是一个Base64 encode的值,这个是浏览器随机生成的,告诉服务器:我要验证你是不是真的是Websocket。
然后,Sec_WebSocket-Protocol 是一个用户定义的字符串,用来区分同URL下,不同的服务所需要的协议。
最后,Sec-WebSocket-Version 是告诉服务器所使用的Websocket Draft(协议版本),在最初的时候,Websocket协议还在 Draft 阶段,各种奇奇怪怪的协议都有,而且还有很多期奇奇怪怪不同的东西,什么Firefox和Chrome用的不是一个版本之类的,当初Websocket协议太多可是一个大难题。不过现在还好,已经定下来啦~大家都使用的一个东西。
3、Websocket的作用
在讲Websocket之前,我就顺带着讲下 long poll 和 ajax轮询 的原理。
首先是 ajax轮询 ,ajax轮询 的原理非常简单,让浏览器隔个几秒就发送一次请求,询问服务器是否有新信息。相当于java代码的while循环,不段的发起请求,如果客户端连接数量过多的话,对服务器压力无疑是个重大的考验。
其次是 long poll,其实原理跟 ajax轮询 差不多,都是采用轮询的方式,不过采取的是阻塞模型(就是请求到服务器后,一直不返回相应,也不超时),直到有消息才返回,返回完之后,客户端再次建立连接,周而复始。
通过上面这个例子,我们可以看出,这两种方式都不是最好的方式,需要很多资源。
一种需要更快的速度,一种需要更多的'电话'。这两种都会导致'电话'的需求越来越高。
HTTP还是一个无状态协议。通俗的说就是,服务器因为每天要接待太多客户了,是个健忘鬼,你一挂电话,他就把你的东西全忘光了,把你的东西全丢掉了。你第二次还得再告诉服务器一遍。
所以在这种情况下出现了,Websocket出现了。他解决了HTTP的这几个难题。
首先,被动性,当服务器完成协议升级后(HTTP->Websocket),服务端就可以主动推送信息给客户端啦。所以我们其实一直建立者连接,一旦有消息了,服务器就会推送给客户端。
只需要经过一次HTTP请求,就可以做到源源不断的信息传送了。(在程序设计中,这种设计叫做回调,即:你有信息了再来通知我,而不是我傻乎乎的每次跑来问你)
这样的协议解决了上面同步有延迟,而且还非常消耗资源的这种情况。
那么为什么他会解决服务器上消耗资源的问题呢?
其实我们所用的程序是要经过两层代理的,即HTTP协议在Nginx等服务器的解析下,然后再传送给相应的Handler(JAVA等)来处理。
简单地说,我们有一个非常快速的接线员(Nginx),他负责把问题转交给相应的客服(Handler)。
本身接线员基本上速度是足够的,但是每次都卡在客服(Handler)了,老有客服处理速度太慢。,导致客服不够。
Websocket就解决了这样一个难题,建立后,可以直接跟接线员建立持久连接,有信息的时候客服想办法通知接线员,然后接线员在统一转交给客户。
这样就可以解决客服处理速度过慢的问题了。
同时,在传统的方式上,要不断的建立,关闭HTTP协议,由于HTTP是非状态性的,每次都要重新传输identity info(鉴别信息),来告诉服务端你是谁。
虽然接线员很快速,但是每次都要听这么一堆,效率也会有所下降的,同时还得不断把这些信息转交给客服,不但浪费客服的处理时间,而且还会在网路传输中消耗过多的流量/时间。
但是Websocket只需要一次HTTP握手,所以说整个通讯过程是建立在一次连接/状态中,也就避免了HTTP的非状态性,服务端会一直知道你的信息,直到你关闭请求,这样就解决了接线员要反复解析HTTP协议,还要查看identity info的信息。
同时由客户主动询问,转换为服务器(推送)有信息的时候就发送(当然客户端还是等主动发送信息过来的。。),没有信息的时候就交给接线员(Nginx),不需要占用本身速度就慢的客服(Handler)了。
至于怎么在不支持Websocket的客户端上使用Websocket。。答案是:不能。
但是可以通过上面说的 long poll 和 ajax 轮询来模拟出类似的效果。
二:代码实现
首先是jar包引入,springboot可以很容易的实现websocket协议的配置(springboot版本2.0以上)
org.springframework.boot
spring-boot-starter-websocket
接下来就是配置启动文件了
package cn.chinotan.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* @program: test
* @description: WebSocketConfig启动配置
* @author: xingcheng
* @create: 2019-05-30 19:33
**/
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
紧接着就是业务逻辑处理
package cn.chinotan.websocket;
import lombok.extern.slf4j.Slf4j;
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.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @program: test
* @description: WebSocketServer 服务器
* @author: xingcheng
* @create: 2019-05-30 19:34
**/
@ServerEndpoint("/websocket/{sid}")
@Component
@Slf4j
public class WebSocketServer {
/**
* 静态变量,用来记录当前在线连接数。线程安全。
*/
private static AtomicInteger onlineCount = new AtomicInteger(0);
/**
* concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
*/
private static CopyOnWriteArraySet webSocketSet = new CopyOnWriteArraySet();
/**
* 与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
private Session session;
/**
* 接收sid
*/
private String sid = "";
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("sid") String sid) {
this.session = session;
// 加入set中
webSocketSet.add(this);
// 在线数加1
addOnlineCount();
log.info("有新窗口开始监听:" + sid + ",当前在线人数为" + getOnlineCount());
this.sid = sid;
sendMessage("连接成功: " + sid);
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
//从set中删除
webSocketSet.remove(this);
//在线数减1
subOnlineCount();
log.info("有一连接关闭!当前在线人数为" + getOnlineCount());
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("收到来自窗口" + sid + "的信息:" + message);
//群发消息
for (WebSocketServer item : webSocketSet) {
item.sendMessage(message);
}
}
/**
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("发生错误");
error.printStackTrace();
}
/**
* 实现服务器主动推送
*/
public void sendMessage(String message) {
try {
this.session.getBasicRemote().sendText(message);
} catch (IOException e) {
log.error("消息推送失败");
e.printStackTrace();
}
}
/**
* 群发自定义消息
*/
public static void sendInfo(String message, String sid) {
log.info("推送消息到窗口" + sid + ",推送内容:" + message);
for (WebSocketServer item : webSocketSet) {
//这里可以设定只推送给这个sid的,为null则全部推送
if (sid == null) {
item.sendMessage(message);
} else if (item.sid.equals(sid)) {
item.sendMessage(message);
}
}
}
public static int getOnlineCount() {
return onlineCount.get();
}
public static void addOnlineCount() {
WebSocketServer.onlineCount.addAndGet(1);
}
public static void subOnlineCount() {
WebSocketServer.onlineCount.decrementAndGet();
}
}
可以看到,通过springboot的注解,可以很容易的实现ws协议的编写,类似于controller,并且还有对应的生命周期监听注解
最后服务器端,我们配置一个推送请求,通过平常的http协议编写,模拟服务器想客户端推送过程
package cn.chinotan.controller;
import cn.chinotan.websocket.WebSocketServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.util.List;
/**
* @program: test
* @description: websocket控制器
* @author: xingcheng
* @create: 2019-06-01 15:13
**/
@RestController
@RequestMapping("/webSocket")
public class WebSocketController {
@Autowired
WebSocketServer webSocketServer;
@GetMapping("/server/push/{sid}")
public void push(@PathVariable("sid") Integer sid) {
if (0 == sid) {
WebSocketServer.sendInfo(sid + ":全员注意啦!", null);
} else {
WebSocketServer.sendInfo(sid + ":注意啦! -> " + (sid + 1), String.valueOf(sid + 1));
}
}
}
最后,我们编写客户端的代码,采用html
WebSocket测试页面
接下来就可以进行测试了:
首先,我们启动在谷歌浏览器上启动三个窗口,连接地址分别是file:///Users/xingcheng/Downloads/websocket.html?sid=21,file:///Users/xingcheng/Downloads/websocket.html?sid=22,file:///Users/xingcheng/Downloads/websocket.html?sid=23,代表3个不同的客户端
来看服务器和客户端的交互过程
服务器:
客户端:
可以看到,连接的过程全部监听到了
接下来模拟服务器的推送过程:群聊,访问连接:http://localhost:9000/webSocket/server/push/0
服务器:
客户端:
最后,验证单聊:22客户端通过服务器给23发送消息,推送地址:http://localhost:9000/webSocket/server/push/22
客户端: