什么是websocket
长链接技术介绍
说到websocket,必须讲到在它之前的各种长链接技术,比如轮循,长轮循,sse等。长链接顾名思义,就是让客户端浏览器与服务器端保持长久的连接,并能持续通讯,它还有一个特点,就是反向ajax,或叫服务器推技术。也就是说,服务器端也能通过这些手段实现向客户端推送的技术,比如,在现实应用中,看到的股票数据实时更新,这是通过这种技术来实现的。因为服务器端无法主动的向客户端推送数据,只能通过客户端连接上服务器端,然后被动地推送数据,这些连接到服务器端或者服务器端向客户端发送数据的方法就可以分成很多种,比如最简单的就是通过ajax隔一段时间发送http请求。
像轮循,长轮循等技术并不能实现真正意义上的实时,它是模拟型的实时,它发送的是完整的http请求。下面来具体说一下每个技术的特点。
轮循
轮循,也叫短轮循,英文名也叫Polling。它很简单,只是用ajax隔一段时间,可能是1秒,2秒,时间自己设定,向服务器发送请求。这种方案会频繁地与服务器通讯,每次通讯都是发送完整的http请求,如果服务器经常有数据变动,有回应还好,有时候发送的请求都是没有意义,都是在等服务器端的回应,而服务器又没有任何改变,所以这种方式很消耗网络资源,很低效。
长轮循
长轮循是对定时轮询的改进和提高,目地是为了降低无效的网络传输。这种方式也是通过ajax请求发送数据到服务器端,服务器端一直hold住这个连接,直到有数据到达,通过这种机制来减少无效的客户端和服务器间的交互,比如可以通过这种方式实现简易型的聊天室,但是,如果服务端的数据变更非常频繁的话,或者说访问的人非常多的时候,这种机制和定时轮询比较起来没有本质上的性能的提高。
HTML5 服务器推送事件
英文名也叫HTML5 Server Sent Events (SSE) / EventSource。SSE是html5规范的一部分,它是一种流技术,它的规范由两部分组成,第一个部分是服务器端与浏览器端之间的通讯协议,第二部分则是在浏览器端提供 JavaScript 使用的 EventSource 对象。服务器端的响应的内容类型是“text/event-stream”,响应文本的内容可以看成是一个事件流,它能够持续不断地向服务器端推送数据。不过这种技术很难跨域,且对IE的支持并不好,但也不能代表这种技术是没用或过时的,用它结合PostgreSQL的notify,或者Redis的pub/sub可以轻易构建聊天室。
websocket
上述的几种方法不代表就是过时没用的,相反,在某一程度上,它们还在应用中,只是,现在我们要来介绍一种更为好,更实时的技术,它叫websocket。它也是一种协议,它是基于tcp协议的,它跟http协议同级,它在浏览器层次发挥作用,可以由http协议升级为ws协议,就像是http加个安全通道升级为https协议一样。它的原理是这样的,由于它是一个协议,它不用发送跟http同样多的头信息,它比较轻量,速度快。为了建立一个 WebSocket 连接,客户端浏览器首先要向服务器发起一个 HTTP 请求,这个请求和通常的 HTTP 请求不同,包含了一些附加头信息,其中附加头信息”Upgrade: WebSocket”表明这是一个申请协议升级的 HTTP 请求,服务器端解析这些附加的头信息然后产生应答信息返回给客户端,客户端和服务器端的 WebSocket 连接就建立起来了,双方就可以通过这个连接通道自由的传递信息,并且这个连接会持续存在直到客户端或者服务器端的某一方主动的关闭连接。
在github.com或trello.com等应用就可以看到websocket的使用。比如,github上的:
请求
Request URL:wss://live.github.com/_sockets/NzQwNjQzOjA4NmI3MGI3ODE2N2JmNGI2OTkwNTI1MzA3NjVjNjYxOjgxYTFjMzVlYTE0NDBkYTUxYjllNTc2NmNjYmE1MDg0ZWY2M2ZiZDQ1NWFmOTM5MWIwMmNlYTMzOGZlYWIwMzY=--46b941101badcb9affe775bd52bf902d4b57468c
Request Method:GET
Status Code:101 Switching Protocols
响应头信息
Response Headers
Connection:Upgrade
Sec-WebSocket-Accept:ihEYOEOsteVV84Y2koOeMRELVT8=
Server:GitHub.com
Upgrade:websocket
请求头信息
Request Headers
Connection:Upgrade
Sec-WebSocket-Extensions:permessage-deflate; client_max_window_bits
Sec-WebSocket-Key:+wcmQ7sbHbIF7K/sGpkOKw==
Sec-WebSocket-Version:13
Upgrade:websocket
SpringBoot整合
pom
<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>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.73</version>
</dependency>
群发
package com.dev.websocket.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @Description : 群发消息 //描述
*/
@Slf4j
@ServerEndpoint("/result")
@Component
public class SendObjectMessage {
/** 记录当前在线连接数 */
private static AtomicInteger onlineCount = new AtomicInteger(0);
private static CopyOnWriteArraySet<SendObjectMessage> socket = new CopyOnWriteArraySet<>();
/**
* 与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
private Session session;
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session) {
this.session = session;
socket.add(this);
onlineCount.incrementAndGet(); // 在线数加1
try {
sendMessage("连接成功!");
} catch (IOException e) {
e.printStackTrace();
}
log.info("有新连接加入,当前在线人数为:{}", onlineCount.get());
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(Session session) {
socket.remove(this);
// 在线数减1
onlineCount.decrementAndGet();
log.info("用户退出,当前在线人数为:{}", onlineCount.get());
}
/**
* 收到客户端消息后调用的方法
*
* @param message
* 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session) throws IOException {
if (message != null && !message.isEmpty()) {
for (SendObjectMessage sendObjectMessage : socket) {
sendObjectMessage.sendMessage(message);
}
}
log.info("服务端收到客户端的消息:{}", message);
}
@OnError
public void onError(Throwable error) {
log.error("发生错误");
error.printStackTrace();
}
/**
* 服务端发送消息给客户端
*/
public void sendMessage(String message) throws IOException {
//如果开启@Async异步需要加锁,否则就会报错
synchronized (session){
this.session.getBasicRemote().sendText(message);
}
}
/**
* 自定义群发
*/
public static void sendInfo(String message){
if (CollectionUtils.isEmpty(socket)){
return;
}
if (message != null && message.isEmpty()) {
return;
}
for (SendObjectMessage sendObjectMessage : socket) {
try {
sendObjectMessage.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>websocket通讯2</title>
</head>
<script type="text/javascript">
let websocket = null;
function openSocket() {
//判断当前浏览器是否支持WebSocket, 主要此处要更换为自己的地址
if ('WebSocket' in window) {
let url = "http://localhost:80/result";
url = url.replace("https", "ws").replace("http", "ws");
if (websocket == null) {
websocket = new WebSocket(url);
}
//连接发生错误的回调方法
websocket.onerror = function() {
setMessageInnerHTML("error");
};
//连接成功建立的回调方法
websocket.onopen = function(event) {
console.log("websocket已打开");
}
//接收到消息的回调方法
websocket.onmessage = function(event) {
setMessageInnerHTML(event.data);
}
//连接关闭的回调方法
websocket.onclose = function() {
setMessageInnerHTML("close");
}
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function() {
websocket.close();
}
//将消息显示在网页上
function setMessageInnerHTML(innerHTML) {
document.getElementById('message').innerHTML += innerHTML + '<br/>';
}
}else {
console.log("您的浏览器不支持websocket!!!")
}
}
//关闭连接
function closeWebSocket() {
if (websocket != null){
websocket.close();
websocket = null;
}
}
//发送消息
function send() {
const message = document.getElementById('text').value;
websocket.send('{"message":"' + message + '"}');
}
</script>
<body>
msg:<input id="text" type="text" />
<p>【操作】:<button type="button" onclick="openSocket()">openSocket</button></p>
<p>【操作】:<button type="button" onclick="send()">发送消息</button></p>
<p>【操作】:<button type="button" onclick="closeWebSocket()">Close</button></p>
<div id="message"></div>
</body>
</html>
点对点
package com.dev.websocket.config;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
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.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @Description : 点对点消息 //描述
*/
@Slf4j
@ServerEndpoint("/client/{userId}")
@Component
public class SendMessage {
/** 记录当前在线连接数 */
private static AtomicInteger onlineCount = new AtomicInteger(0);
private static volatile ConcurrentHashMap<String, SendMessage> socket = new ConcurrentHashMap<>();
/**
* 与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
private Session session;
/**
* 接收userId
*/
private String userId = "";
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId) {
this.session = session;
this.userId = userId;
if (socket.containsKey(userId)) {
socket.remove(userId);
socket.put(userId, this);
} else {
socket.put(userId, this);
onlineCount.incrementAndGet(); // 在线数加1
}
try {
sendMessage("连接成功!");
} catch (IOException e) {
e.printStackTrace();
}
log.info("用户:{}加入连接,当前在线人数为:{}", userId, onlineCount.get());
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(Session session) {
if (socket.containsKey(userId)) {
socket.remove(userId);
// 在线数减1
onlineCount.decrementAndGet();
}
log.info("用户:{}退出,当前在线人数为:{}", session.getId(), onlineCount.get());
}
/**
* 收到客户端消息后调用的方法
*
* @param message
* 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session) throws IOException {
if (message != null && !message.isEmpty()) {
JSONObject object = JSON.parseObject(message);
object.put("userId", userId);
String toUserId = object.getString("toUserId");
if (message != null && !message.isEmpty() && socket.containsKey(toUserId)) {
socket.get(toUserId).sendMessage(object.toJSONString());
} else {
log.error("请求的用户:{}不在该服务器上", toUserId);
}
}
log.info("服务端收到客户端[{}]的消息:{}", userId, message);
}
@OnError
public void onError(Throwable error) {
log.error("发生错误");
error.printStackTrace();
}
/**
* 服务端发送消息给客户端
*/
private void sendMessage(String message) throws IOException {
session.getBasicRemote().sendText(message);
}
}
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>websocket通讯</title>
</head>
<script type="text/javascript">
let websocket = null;
function openSocket() {
//判断当前浏览器是否支持WebSocket, 主要此处要更换为自己的地址
if ('WebSocket' in window) {
let url = "http://localhost:80/client/" + document.getElementById('userId').value;
url = url.replace("https", "ws").replace("http", "ws");
if (websocket == null) {
websocket = new WebSocket(url);
}
//连接发生错误的回调方法
websocket.onerror = function() {
setMessageInnerHTML("error");
};
//连接成功建立的回调方法
websocket.onopen = function(event) {
console.log("websocket已打开");
}
//接收到消息的回调方法
websocket.onmessage = function(event) {
setMessageInnerHTML(event.data);
}
//连接关闭的回调方法
websocket.onclose = function() {
setMessageInnerHTML("close");
}
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function() {
websocket.close();
}
//将消息显示在网页上
function setMessageInnerHTML(innerHTML) {
document.getElementById('message').innerHTML += innerHTML + '<br/>';
}
}else {
console.log("您的浏览器不支持websocket!!!")
}
}
//关闭连接
function closeWebSocket() {
if (websocket != null){
websocket.close();
websocket = null;
}
}
//发送消息
function send() {
const message = document.getElementById('text').value;
const toUserId = document.getElementById('toUserId').value;
websocket.send('{"toUserId":"' + toUserId + '","message":"' + message + '"}');
}
</script>
<body>
msg:<input id="text" type="text" />
toUserId:<input id="toUserId" type="text" />
userId:<input id="userId" type="text" />
<p>【操作】:<button type="button" onclick="openSocket()">openSocket</button></p>
<p>【操作】:<button type="button" onclick="send()">发送消息</button></p>
<p>【操作】:<button type="button" onclick="closeWebSocket()">Close</button></p>
<div id="message"></div>
</body>
</html>
WebSocketConfig
package com.dev.websocket.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* @Description : websocket配置 //描述
*/
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
映射
package com.dev.websocket;
import com.dev.websocket.config.SendObjectMessage;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
@SpringBootApplication
@Controller
@EnableScheduling
public class WebsocketApplication {
public static void main(String[] args) {
SpringApplication.run(WebsocketApplication.class, args);
}
private static List<String> list = Arrays.asList("有内鬼,终止交易!","年轻人,不讲武德,我劝你耗子尾汁!","加油,干饭人!","早安,打工人!");
@GetMapping("/index")
public String index(){
return "index";
}
@GetMapping("/message")
public String message(){
return "message";
}
@Scheduled(cron = "0/10 * * * * ?")
private void taskRun(){
Random random = new Random();
SendObjectMessage.sendInfo(list.get(random.nextInt(list.size())));
}
}
测试群发
启动项目,打开浏览器输入http://localhost/message进入
点击openSocket按钮
与此同时后台
打开后收到后台定时任务发送的自定义消息
在msg input输入框内输入消息后点击发送消息
点击Close按钮关闭连接
测试点对点
打开两个浏览器页面分别输入http://localhost/index
分别在userId 输入框里输入1和2后点击openSocket
给客户端id为1发送消息
给客户端id为2回复消息