maven 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
复制代码
代码准备
//webSocket相关配置
//链接地址
public static String WEBSOCKETPATHPERFIX = "/ws-push";
public static String WEBSOCKETPATH = "/endpointWisely";
//消息代理路径
public static String WEBSOCKETBROADCASTPATH = "/topic";
//前端发送给服务端请求地址
public static final String FORETOSERVERPATH = "/welcome";
//服务端生产地址,客户端订阅此地址以接收服务端生产的消息
public static final String PRODUCERPATH = "/topic/getResponse";
//点对点消息推送地址前缀
public static final String P2PPUSHBASEPATH = "/user";
//点对点消息推送地址后缀,最后的地址为/user/用户识别码/msg
public static final String P2PPUSHPATH = "/msg";
复制代码
接收前端消息实体
public class WiselyMessage {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
复制代码
后台发送消息实体
private String responseMessage;
public WiselyResponse(String responseMessage){
this.responseMessage = responseMessage;
}
public String getResponseMessage() {
return responseMessage;
}
public void setResponseMessage(String responseMessage) {
this.responseMessage = responseMessage;
}
}
复制代码
配置 websocket
@Configuration
// @EnableWebSocketMessageBroker注解用于开启使用STOMP协议来传输基于代理(MessageBroker)的消息,
这时候控制器(controller)
// 开始支持@MessageMapping,就像是使用@requestMapping一样。
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
//注册一个Stomp的节点(endpoint),并指定使用SockJS协议。
stompEndpointRegistry.addEndpoint(Constant.WEBSOCKETPATH).withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
//服务端发送消息给客户端的域,多个用逗号隔开
registry.enableSimpleBroker(Constant.WEBSOCKETBROADCASTPATH, Constant.P2PPUSHBASEPATH);
//定义一对一推送的时候前缀
registry.setUserDestinationPrefix(Constant.P2PPUSHBASEPATH);
//定义websoket前缀
registry.setApplicationDestinationPrefixes(Constant.WEBSOCKETPATHPERFIX);
}
}
复制代码
service
@Service
public class WebSocketService {
@Autowired
private SimpMessagingTemplate template;
/**
* 广播
* 发给所有在线用户
*
* @param msg
*/
public void sendMsg(WiselyResponse msg) {
template.convertAndSend(Constant.PRODUCERPATH, msg);
}
/**
* 发送给指定用户
* @param users
* @param msg
*/
public void send2Users(List<String> users, WiselyResponse msg) {
users.forEach(userName -> {
template.convertAndSendToUser(userName, Constant.P2PPUSHPATH, msg);
});
}
}
复制代码
控制器
@Controller
public class WsController {
@Resource
WebSocketService webSocketService;
@MessageMapping(Constant.FORETOSERVERPATH)
//@MessageMapping和@RequestMapping功能类似,用于设置URL映射地址,
浏览器向服务器发起请求,需要通过该地址。
@SendTo(Constant.PRODUCERPATH)
//如果服务器接受到了消息,就会对订阅了@SendTo括号中的地址传送消息。
public WiselyResponse say(WiselyMessage message) throws Exception {
List<String> users = Lists.newArrayList();
users.add("d892bf12bf7d11e793b69c5c8e6f60fb");
//此处写死只是为了方便测试,此值需要对应页面中订阅个人消息的userId
webSocketService.send2Users(users, new WiselyResponse("admin hello"));
return new WiselyResponse("Welcome, " + message.getName() + "!");
}
}
复制代码
页面
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>Spring Boot+WebSocket+广播式</title>
</head>
<body onload="disconnect()">
<noscript><h2 style="color: #ff0000">貌似你的浏览器不支持websocket</h2></noscript>
<div>
<div>
<button id="connect" onclick="connect();">连接</button>
<button id="disconnect" disabled="disabled" onclick="disconnect();">断开连接</button>
</div>
<div id="conversationDiv">
<label>输入你的名字</label><input type="text" id="name" />
<button id="sendName" onclick="sendName();">发送</button>
<p id="response"></p>
<p id="response1"></p>
</div>
</div>
<!--<script th:src="@{sockjs.min.js}"></script>
<script th:src="@{stomp.min.js}"></script>
<script th:src="@{jquery.js}"></script>-->
<script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
<script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
<script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
<script th:inline="javascript">
var stompClient = null;
//此值有服务端传递给前端,实现方式没有要求
var userId = [[${userId}]];
function setConnected(connected) {
document.getElementById('connect').disabled = connected;
document.getElementById('disconnect').disabled = !connected;
document.getElementById('conversationDiv').style.visibility = connected ? 'visible' : 'hidden';
$('#response').html();
}
function connect() {
var socket = new SockJS('/endpointWisely'); //1连接SockJS的endpoint是“endpointWisely”,与后台代码中注册的endpoint要一样。
stompClient = Stomp.over(socket);//2创建STOMP协议的webSocket客户端。
stompClient.connect({}, function(frame) {//3连接webSocket的服务端。
setConnected(true);
console.log('开始进行连接Connected: ' + frame);
//4通过stompClient.subscribe()订阅服务器的目标是'/topic/getResponse'发送过来的地址,与@SendTo中的地址对应。
stompClient.subscribe('/topic/getResponse', function(respnose){
showResponse(JSON.parse(respnose.body).responseMessage);
});
//4通过stompClient.subscribe()订阅服务器的目标是'/user/' + userId + '/msg'接收一对一的推送消息,其中userId由服务端传递过来,用于表示唯一的用户,通过此值将消息精确推送给一个用户
stompClient.subscribe('/user/' + userId + '/msg', function(respnose){
console.log(respnose);
showResponse1(JSON.parse(respnose.body).responseMessage);
});
});
}
function disconnect() {
if (stompClient != null) {
stompClient.disconnect();
}
setConnected(false);
console.log("Disconnected");
}
function sendName() {
var name = $('#name').val();
//通过stompClient.send()向地址为"/welcome"的服务器地址发起请求,与@MessageMapping里的地址对应。因为我们配置了registry.setApplicationDestinationPrefixes(Constant.WEBSOCKETPATHPERFIX);所以需要增加前缀/ws-push/
stompClient.send("/ws-push/welcome", {}, JSON.stringify({ 'name': name }));
}
function showResponse(message) {
var response = $("#response");
response.html(message);
}
function showResponse1(message) {
var response = $("#response1");
response.html(message);
}
</script>
</body>
</html>
复制代码
clipboard.png
点击连接控制台输出
clipboard.png
clipboard.png
clipboard.png
clipboard.png
clipboard.png
clipboard.png
clipboard.png
同时因为控制器有注解@SendTo所以会向@SendTo的地址广播消息,客户端订阅了广播地址所有控制台显示接收了消息
复制代码
clipboard.png
WebSocket 是目前比较成熟的技术了,WebSocket 协议为创建客户端和服务器端需要实时双向通讯的 webapp 提供了一个选择。其为 HTML5 的一部分,WebSocket 相较于原来开发这类 app 的方法来说,其能使开发更加地简单。大部分现在的浏览器都支持 WebSocket,比如 Firefox,IE,Chrome,Safari,Opera,并且越来越多的服务器框架现在也同样支持 WebSocket。
在实际的生产环境中,要求多个 WebSocket 服务器必须具有高性能和高可用,那么 WebSocket 协议就需要一个负载均衡层,NGINX 从 1.3 版本开始支持 WebSocket,其可以作为一个反向代理和为 WebSocket 程序做负载均衡。
WebSocket 协议与 HTTP 协议不同,但 WebSocket 握手与 HTTP 兼容,使用 HTTP 升级工具将连接从 HTTP 升级到 WebSocket。这允许 WebSocket 应用程序更容易地适应现有的基础架构。例如,WebSocket 应用程序可以使用标准 HTTP 端口 80 和 443,从而允许使用现有的防火墙规则。
WebSocket 应用程序可以在客户端和服务器之间保持长时间运行的连接,从而有助于开发实时应用程序。用于将连接从 HTTP 升级到 WebSocket 的 HTTP 升级机制使用 Upgrade 和 Connection 头。反向代理服务器在支持 WebSocket 时面临一些挑战。一个是 WebSocket 是一个逐跳协议,因此当代理服务器拦截客户端的升级请求时,需要向后端服务器发送自己的升级请求,包括相应的头文件。此外,由于 WebSocket 连接长期存在,与 HTTP 使用的典型短期连接相反,反向代理需要允许这些连接保持打开状态,而不是关闭它们,因为它们似乎处于空闲状态。
允许在客户机和后端服务器之间建立隧道,NGINX 支持 WebSocket。对于 NGINX 将升级请求从客户端发送到后台服务器,必须明确设置 Upgrade 和 Connection 标题。
Nginx 开启 websocket 代理功能的配置如下:
1)编辑nginx.conf,在http区域内一定要添加下面配置:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
map指令的作用:
该作用主要是根据客户端请求中$http_upgrade 的值,来构造改变$connection_upgrade的值,即根据变量$http_upgrade的值创建新的变量$connection_upgrade,
创建的规则就是{}里面的东西。其中的规则没有做匹配,因此使用默认的,即 $connection_upgrade 的值会一直是 upgrade。然后如果 $http_upgrade为空字符串的话,
那值会是 close。
2)编辑vhosts下虚拟主机的配置文件,在location匹配配置中添加如下内容:
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
示例如下:
upstream socket.kevin.com {
hash $remote_addr consistent;
server 10.0.12.108:9000;
server 10.0.12.109:9000;
}
location / {
proxy_pass http://socket.kevin.com/;
proxy_set_header Host $host:$server_port;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
复制代码
WebSocket 机制
WebSocket 是 HTML5 下一种新的协议。它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯的目的。它与 HTTP 一样通过已建立的 TCP 连接来传输数据,但是它和 HTTP 最大不同是:
1) WebSocket 是一种双向通信协议。在建立连接后,WebSocket 服务器端和客户端都能主动向对方发送或接收数据,就像 Socket 一样;
2)WebSocket 需要像 TCP 一样,先建立连接,连接成功后才能相互通信。
传统 HTTP 客户端与服务器请求响应模式如下图所示:
WebSocket 模式客户端与服务器请求响应模式如下图:
上图对比可以看出,相对于传统 HTTP 每次请求-应答都需要客户端与服务端建立连接的模式,
WebSocket 是类似 Socket 的 TCP 长连接通讯模式。一旦 WebSocket 连接建立后,后续数据都以帧序列的形式传输。
在客户端断开 WebSocket 连接或 Server 端中断连接前,不需要客户端和服务端重新发起连接请求。
海量并发及客户端与服务器交互负载流量大的情况下,极大的节省了网络带宽资源的消耗,有明显的性能优势,且客户端发送和接受消息是在同一个持久连接上发起,实时性优势明显。
相比 HTTP 长连接,WebSocket 有以下特点:
1)是真正的全双工方式,建立连接后客户端与服务器端是完全平等的,可以互相主动请求。而 HTTP 长连接基于 HTTP,是传统的客户端对服务器发起请求的模式。
2)HTTP 长连接中,每次数据交换除了真正的数据部分外,服务器和客户端还要大量交换 HTTP header,信息交换效率很低。Websocket 协议通过第一个 request 建立了 TCP 连接之后,之后交换的数据都不需要发送 HTTP header 就能交换数据,这显然和原有的 HTTP 协议有区别所以它需要对服务器和客户端都进行升级才能实现(主流浏览器都已支持 HTML5)。
此外还有 multiplexing、不同的 URL 可以复用同一个 WebSocket 连接等功能。这些都是 HTTP 长连接不能做到的。
WebSocket 与 Http 相同点
- 都是一样基于 TCP 的,都是可靠性传输协议。
- 都是应用层协议。
WebSocket 与 Http 不同点
- WebSocket 是双向通信协议,模拟 Socket 协议,可以双向发送或接受信息。HTTP 是单向的。
- WebSocket 是需要浏览器和服务器握手进行建立连接的。而 http 是浏览器发起向服务器的连接,服务
器预先并不知道这个连接。
WebSocket 与 Http 联系
WebSocket 在建立握手时,数据是通过 HTTP 传输的。但是建立之后,在真正传输时候是不需要 HTTP 协议的。
在 WebSocket 中,只需要服务器和浏览器通过 HTTP 协议进行一个握手的动作,然后单独建立一条 TCP 的通信通道进行数据的传送。
WebSocket 连接的过程
1)客户端发起 http 请求,经过 3 次握手后,建立起 TCP 连接;http 请求里存放 WebSocket 支持的版本号等信息,如:Upgrade、Connection、WebSocket-Version 等;
2)服务器收到客户端的握手请求后,同样采用 HTTP 协议回馈数据;
3)客户端收到连接成功的消息后,开始借助于 TCP 传输信道进行全双工通信。
下面再通过客户端和服务端交互的报文对比 WebSocket 通讯与传统 HTTP 的不同点:
1)在客户端,new WebSocket 实例化一个新的 WebSocket 客户端对象,请求类似 ws://yourdomain:port/path 的服务端 WebSocket URL,客户端 WebSocket 对象会自动解析并识别为 WebSocket 请求,并连接服务端端口,执行双方握手过程,客户端发送数据格式类似:
GET /webfin/websocket/
HTTP
/1
.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: xqBt3ImNzJbYqRINxEFlkg==
Origin: http:
//localhost
:8080
Sec-WebSocket-Version: 13
可以看到,客户端发起的 WebSocket 连接报文类似传统 HTTP 报文,Upgrade:websocket 参数值表明这是 WebSocket 类型请求,Sec-WebSocket-Key 是 WebSocket 客户端发送的一个 base64 编码的密文,要求服务端必须返回一个对应加密的 Sec-WebSocket-Accept 应答,否则客户端会抛出 Error during WebSocket handshake 错误,并关闭连接。
2)服务端收到报文后返回的数据格式类似:
HTTP/1
.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: K7DJLdLooIwIG
/MOpvWFB3y3FE8
=
Sec-WebSocket-Accept
的值是服务端采用与客户端一致的密钥计算出来后返回客户端的,HTTP/1.1 101 Switching Protocols
表示服务端接受 WebSocket 协议的客户端连接,经过这样的请求-响应处理后,两端的 WebSocket 连接握手成功, 后续就可以进行 TCP 通讯了。
在开发方面,WebSocket API 也十分简单:只需要实例化 WebSocket,创建连接,然后服务端和客户端就可以相互发送和响应消息。在 WebSocket 实现及案例分析部分可以看到详细的 WebSocket API 及代码实现。
腾讯云公网有日租类型七层负载均衡转发部分支持 Websocket,目前包括英魂之刃、银汉游戏等多家企业已接入使用。当出现不兼容问题时,请修改 websocket 配置,websocket server 不校验下图中圈出的字段:
比如一个使用 WebSocket 应用于视频的业务思路如下:
1)使用心跳维护 websocket 链路,探测客户端端的网红/主播是否在线
2)设置负载均衡 7 层的 proxy_read_timeout 默认为 60s
3)设置心跳为 50s,即可长期保持 Websocket 不断开
Nginx 代理 webSocket 经常中断的解决方法(也就是如何保持长连接)
现象描述:用 nginx 反代代理某个业务,发现平均 1 分钟左右,就会出现 webSocket 连接中断,然后查看了一下,是 nginx 出现的问题。
产生原因:nginx 等待第一次通讯和第二次通讯的时间差,超过了它设定的最大等待时间,简单来说就是超时!
解决方法 1
其实只要配置 nginx.conf 的对应 localhost 里面的这几个参数就好
proxy_connect_timeout;
proxy_read_timeout;
proxy_send_timeout;
解决方法 2
发心跳包,原理就是在有效地再读时间内进行通讯,重新刷新再读时间
配置示例:
http {
server {
location / {
root html;
index index.html index.htm;
proxy_pass http://webscoket;
proxy_http_version 1.1;
proxy_connect_timeout 4s; #配置点1
proxy_read_timeout 60s; #配置点2,如果没效,可以考虑这个时间配置长一点
proxy_send_timeout 12s; #配置点3
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
}
}
复制代码
关于上面配置 2 的解释
这个是服务器对你等待最大的时间,也就是说当你 webSocket 使用 nginx 转发的时候,用上面的配置 2 来说,如果 60 秒内没有通讯,依然是会断开的,所以,你可以按照你的需求来设定。
比如说,我设置了 10 分钟,那么如果我 10 分钟内有通讯,或者 10 分钟内有做心跳的话,是可以保持连接不中断的,详细看个人需求。
WebSocket 与 Socket 的关系
Socket 其实并不是一个协议,而是为了方便使用 TCP 或 UDP 而抽象出来的一层,是位于应用层和传输控制层之间的一组接口。当两台主机通信时,必须通过 Socket 连接,Socket 则利用 TCP/IP 协议建立 TCP 连接。TCP 连接则更依靠于底层的 IP 协议,IP 协议的连接则依赖于链路层等更低层次。
WebSocket 就像 HTTP 一样,则是一个典型的应用层协议。
总的来说:Socket 是传输控制层接口,WebSocket 是应用层协议。
评论