WebSocket使用文档
导航
一. WebSocket简介
1.1 概述
-
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
-
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
-
在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。
-
现在,很多网站为了实现推送技术,所用的技术都是 Ajax 轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。
-
HTML5 定义的 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯,图示如下:
1.2 WebSocket实例
- WebSocket 协议本质上是一个基于 TCP 的协议。
- 为了建立一个 WebSocket 连接,客户端浏览器首先要向服务器发起一个 HTTP 请求,这个请求和通常的 HTTP 请求不同,包含了一些附加头信息,其中附加头信息"Upgrade: WebSocket"表明这是一个申请协议升级的 HTTP 请求,服务器端解析这些附加的头信息然后产生应答信息返回给客户端,客户端和服务器端的 WebSocket 连接就建立起来了,双方就可以通过这个连接通道自由的传递信息,并且这个连接会持续存在直到客户端或者服务器端的某一方主动的关闭连接
二. 通用服务端向浏览器发送消息示例
2.1 原理概述:
- 首先引入spring-websocket 的jar包,然后引入一个Config对WebSocket进行配置,然后再配置一个WebSocket的类,这个类的作用是__建立连接__,发送消息,关闭连接,异常处理,等作用;
- 它就相当于一个__通道__,浏览器的
url
指向了这个类的时候,它们就建立
了一个连接,这个浏览器所持有的身份Token(即username)作为一个通道中的一个用户,加入了这个用户池
; - 连接则加入用户池,断开连接则从用户池中移除,当然我们也可以查询出这个通道内的用户池里有多少个用户保持着连接状态;当发送消息的时候,会先
判断用户池
里是否有此用户,如果有才进行发送; - 当通道内某用户与此通道建立连接发生异常时,则进入异常的方法;我们可以在每个方法节点中扩展所需要的功能;比如我们也可以通过一个Controller引入这个WebSocket,调用里面的发送消息方法,则能进行服务器对浏览器的主动消息发送;
2.2 实战代码环境描述
- 运行环境:JDK1.8
- 工具:SVN IDEA
- 版本控制工具: Maven 3
- 项目环境: SpringBoot
- 效果图演示:
2.3 pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
引入websocket依赖
2.4 启动类:MySpringBootApplication.java
package com.demo;
import com.demo.controller.WebSocket;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
/**
* create by: anyu
*/
@SpringBootApplication
public class MySpringBootApplication {
public static void main(String[] args) {
// SpringApplication springApplication = new SpringApplication(MySpringBootApplication.class);
// ConfigurableApplicationContext configurableApplicationContext = springApplication.run(args);
// //解决WebSocket不能注入的问题
// WebSocket.setApplicationContext(configurableApplicationContext);
SpringApplication.run(MySpringBootApplication.class,args); //如果WebSocket要引入其他的类或者变量,则使用上面的,把注释去掉;这行的则进行注释;
}
//
// @Bean
// RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
// return restTemplateBuilder.build();
// }
}
这里被注释的内容后面再讲
2.5 application.yml:
#netty-websocket:
# host: 127.0.0.1
server:
port: 8055
# path: /
# port: 1880
配置了端口号,这里可不配置直接使用默认的也可以
2.6 配置类:WebSocketConfig
package com.demo.controller;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* websocket的配置
*/
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
对WebSocket的配置类,因为WebSocket是多例,所以每次需要New 一个新的;
2.7 消息载体:Message类
package com.demo.controller;
import lombok.Data;
import lombok.ToString;
import java.io.Serializable;
/**
* @create_by: zhanglei
* @craete_time 2019/7/18
*/
@Data
@ToString
public class Message implements Serializable {
private String message;
private String to;
}
这是一个实体类,它作为消息的载体使用; message :消息内容, to: 发送给谁
2.8 核心类:WebSocket(类名和路径可变)
package com.demo.controller;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* author:zhanglei
* ServerEndpoint的java类,能够接受客户端发送过来的信息和发送给客户端信息
*/
@Component
@ServerEndpoint("/websocket/{username}")
public class WebSocket {
private Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* 在线人数
*/
public static int onlineNumber = 0;
/**
* 以用户的姓名为key,WebSocket为对象保存起来
*/
private static Map<String, WebSocket> clients = new ConcurrentHashMap<String, WebSocket>();
/**
* 会话
*/
private Session session;
/**
* 用户名称
*/
private String username;
/* 注入@Value 的示例:*/
// private static String port;
//
// @Value("${server.port}")
// public void getPort(String port) {
// WebSocket.port = port;
// }
/* 注入@Autowired的示例:*/
// //此处是解决无法注入的关键
// private static ApplicationContext applicationContext;
// //你要注入的service或者dao
// private RestTemplate restTemplate;
//
// public static void setApplicationContext(ApplicationContext applicationContext) {
// WebSocket.applicationContext = applicationContext;
// }
/**
* 注意:
* 1. 在@OnOpen注解下的这个open方法中 直接使用@Value和@Autowired是无法注入的,需要像上面一样注入才能注入配置的port或者下面的restTemplate
* 2. 被注释的代码只是作为扩展参考,可以无需使用,直接删除即可;
* 3. 原理介绍: 当前端浏览器连接的时候,使用 ws://...连接,会直接进入@Onpen方法内;
* 4. 浏览器监听服务器返回的消息,是在@onMessage定义消息内容,通过引入此类,然后调用@OnMessage方法即可发送消息
*/
/**
* 建立连接
*
* @param session
*/
@OnOpen
public void onOpen(@PathParam("username") String username, Session session) {
logger.info("现在来连接的客户id:" + session.getId() + "用户名:" + username);
onlineNumber++;
// System.out.println(port);
// restTemplate= applicationContext.getBean(RestTemplate.class);
// System.out.println("--------------");
// System.out.println("端口号"+port);
// System.out.println("-------------");
// HttpHeaders headers = new HttpHeaders();
// MultiValueMap<String, String> map = new LinkedMultiValueMap<String,String>(); //map里面是请求体的内容
// map.add("token", "eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOiJ1aWQiLCJleHAiOjE1NjM3Mzc5Nzd9._NT8EQN9xOlRA1KMflV7vl9ByyBHI7rnG7emV0_CARA");
// HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<MultiValueMap<String, String>>(map, headers);
// ResponseEntity<String> response = restTemplate.postForEntity("http://10.4.7.214:9071/checkToken", request, String.class); //地址替换为自己的
// System.out.println(response.getBody());
// ResponseResult responseResult = JSON.parseObject(response.getBody(), ResponseResult.class);
// System.out.println(responseResult);
// System.out.println("------");
// System.out.println(responseResult.getResult().getUserAccount());
this.username = username;
this.session = session;
clients.put(username, this);
try {
sendMessageTo(JSON.toJSONString("连接已成功建立"),username);
} catch (IOException e) {
e.printStackTrace();
}
}
@OnError
public void onError(Session session, Throwable error) {
logger.info("服务端发生了错误" + error.getMessage());
}
/**
* 连接关闭
*/
@OnClose
public void onClose() {
clients.remove(username);
onlineNumber--;
}
/**
* 收到客户端的消息 对单发送
*
* @param message 消息
* @param session 会话
*/
@OnMessage
public void onMessage(String message, Session session) {
try {
JSONObject jsonObject = JSON.parseObject(message);
String textMessage = jsonObject.getString("message");
String tousername = jsonObject.getString("to");
sendMessageTo(JSON.toJSONString(textMessage), tousername);
} catch (Exception e) {
logger.info("发生了错误了");
}
}
public void sendMessageTo(String message, String ToUserName) throws IOException {
for (WebSocket item : clients.values()) {
if (item.username.equals(ToUserName)) {
item.session.getAsyncRemote().sendText(message);
break;
}
}
}
public void sendMessageAll(String message, String FromUserName) throws IOException {
for (WebSocket item : clients.values()) {
item.session.getAsyncRemote().sendText(message);
}
}
}
onlineNumber: 此通道内的连接人数。 clients它是一个集合,里面保存的数据就是连接的用户身份username; username: 代表用户名称(Token等,唯一身份标志,建立连接和接收消息都是这个);
@OnOpen 创建连接则触发此注解下的方法; @OnClose 关闭连接触发的方法 @OnError发生错误触发的方法 @OnMessage 服务器给浏览器发送消息触发的方法
注释注销的内容,是有关于WebScoekt的引入;在@OnClose中无法直接通过@Autowired直接引入其他类或者@Value直接注入其他属性。因为Spring是单例模式,而WebSocket是多例模式,每次都会重新创建一个新的连接;直接使用@Value会报空,直接使用@Autowired引入其他类并使用其方法可能会导致异常。我们可以通过启动类将configurableApplicationContext装配进入webSocket,通过注释的内容可为每个WebSocket通道 单独创建;
2.9 接口类
package com.demo.controller;
import com.alibaba.fastjson.JSON;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import javax.websocket.*;
@RestController
public class QuickController {
@Autowired
private WebSocket webSocket;
@RequestMapping("/sendMsg")
@ResponseBody
public String sendMsg() throws Exception{
Message message=new Message();
message.setMessage("北京交通委提醒你:道路千万条,安全第一条,行车不规范,行人两行泪");
message.setTo("用户a");
String s = JSON.toJSONString(message);
webSocket.onMessage(s,null);
return "SUCCESS";
}
}
此接口提供了一个sendMsg方法,它将消息内容和接收消息的人封装为一个Message对象,传入WebSocket的OnMessage方法内;通过OnMessage对浏览器发送消息;
2.10 静态页面测试:
<!DOCTYPE html>
<html>
<head>
<title>websocket</title>
<script type="text/javascript" src="http://ajax.microsoft.com/ajax/jquery/jquery-1.4.min.js"></script>
<script src="http://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
<script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
</head>
<body>
<div style="margin: auto;text-align: center">
<h1>Welcome to websocket</h1>
</div>
<br/>
<div style="margin: auto;text-align: center">
<select id="onLineUser">
<option>--所有--</option>
</select>
<input id="text" type="text" />
<button onclick="send()">发送消息</button>
</div>
<br>
<div style="margin-right: 10px;text-align: right">
<button onclick="closeWebSocket()">关闭连接</button>
</div>
<hr/>
<div id="message" style="text-align: center;"></div>
<input type="text" value="用户a" id="username" style="display: none" />
</body>
<script type="text/javascript">
var webSocket;
var commWebSocket;
if ("WebSocket" in window)
{
webSocket = new WebSocket("ws://localhost:8055/websocket/"+document.getElementById('username').value);
//连通之后的回调事件
webSocket.onopen = function()
{
//webSocket.send( document.getElementById('username').value+"已经上线了");
console.log("已经连通了websocket");
setMessageInnerHTML("已经连通了websocket");
};
//接收后台服务端的消息
webSocket.onmessage = function (evt)
{
var received_msg = evt.data;
console.log("数据已接收:" +received_msg);
var obj = JSON.parse(received_msg);
console.log("可以解析成json:"+obj.messageType);
//1代表上线 2代表下线 3代表在线名单 4代表普通消息
if(obj.messageType==1){
//把名称放入到selection当中供选择
var onlineName = obj.username;
var option = "<option>"+onlineName+"</option>";
$("#onLineUser").append(option);
setMessageInnerHTML(onlineName+"上线了");
}
else if(obj.messageType==2){
$("#onLineUser").empty();
var onlineName = obj.onlineUsers;
var offlineName = obj.username;
var option = "<option>"+"--所有--"+"</option>";
for(var i=0;i<onlineName.length;i++){
if(!(onlineName[i]==document.getElementById('username').value)){
option+="<option>"+onlineName[i]+"</option>"
}
}
$("#onLineUser").append(option);
setMessageInnerHTML(offlineName+"下线了");
}
else if(obj.messageType==3){
var onlineName = obj.onlineUsers;
var option = null;
for(var i=0;i<onlineName.length;i++){
if(!(onlineName[i]==document.getElementById('username').value)){
option+="<option>"+onlineName[i]+"</option>"
}
}
$("#onLineUser").append(option);
console.log("获取了在线的名单"+onlineName.toString());
}
else{
setMessageInnerHTML(obj.textMessage);
}
};
//连接关闭的回调事件
webSocket.onclose = function()
{
console.log("连接已关闭...");
setMessageInnerHTML("连接已经关闭....");
};
}
else{
// 浏览器不支持 WebSocket
alert("您的浏览器不支持 WebSocket!");
}
//将消息显示在网页上
function setMessageInnerHTML(innerHTML) {
document.getElementById('message').innerHTML += innerHTML + '<br/>';
}
function closeWebSocket() {
//直接关闭websocket的连接
webSocket.close();
}
function send() {
var selectText = $("#onLineUser").find("option:selected").text();
if(selectText=="--所有--"){
selectText = "All";
}
else{
setMessageInnerHTML(document.getElementById('username').value+"对"+selectText+"说:"+ $("#text").val());
}
var message = {
"message":document.getElementById('text').value,
"username":document.getElementById('username').value,
"to":selectText
};
webSocket.send(JSON.stringify(message));
$("#text").val("");
}
</script>
</html>
这个HTML页面由来自网上改编,所以有些代码不适用,只作为接收测试使用。实际按需求增删改;
2.11 测试说明:
- 对应类的代码应该全部挨个建立完毕
- 先运行启动类,然后浏览器访问1.8 的Html页面,然后按F12 进入调试;
- 访问Controller的接口,然后消息就会发送到浏览器页面;
- 效果如下:
三. 在SpringCloud 项目中的注意点:
- 当我们使用websocket在SpringCloud中时,由于websocket是长连接,gateway网关无法正常转发。我们可以使用如下配置正常转发:
- 代码为:
/** * 说明: * 1. 对Socket微服务的websocket的转发配置,建立通道时的转发地址需要写入下方uri然后加入Bean * 2. 前端指向的接口url和这里配置的转发url除了端口号(前端指向9041|GateWay|网关 ---- 后端转发指向9055|Socket-service)不一致,后面部分都必须一致; * 3. 使用网关进行连接时,需要有UserToken登录,非登录状态不能指向websocket转发地址,若需不登录进行连接则进入AuthorizeFilter配置过滤; * @param builder * @return */ @Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { return builder.routes() .route(r -> r.path("socket-service") .uri("https://localhost:9055/api/msgsocket/") .uri("https://localhost:9055/api/msgsocket2/") .uri("https://localhost:9055/api/pushCommon/") .uri("https://localhost:9055/api/msgsocket3/") ) .build(); }
这里的 /msgsocket 这些路径是映射的访问uri;如果这些接口需要从gateway跳转,则需要在此处加上;