WebSocket
什么是WebSocket
WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。
实现原理
在实现websocket连线过程中,需要通过浏览器发出websocket连线请求,然后服务器发出回应,这个过程通常称为“握手” 。在 WebSocket API,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。
优点
在以前的消息推送机制中,用的都是 Ajax 轮询(polling),在特定的时间间隔由浏览器自动发出请求,将服务器的消息主动的拉回来,这种方式是非常消耗资源的,因为它本质还是http请求,而且显得非常笨拙。而WebSocket 在浏览器和服务器完成一个握手的动作,在建立连接之后,服务器可以主动传送数据给客户端,客户端也可以随时向服务器发送数据。
WebSocket 和 Socket 区别
WebSocket:
- websocket通讯的建立阶段是依赖于http协议的。最初的握手阶段是http协议,握手完成后就切换到websocket协议,并完全与http协议脱离了。
- 建立通讯时,也是由客户端主动发起连接请求,服务端被动监听。
- 通讯一旦建立连接后,通讯就是“全双工”模式了。也就是说服务端和客户端都能在任何时间自由得发送数据,非常适合服务端要主动推送实时数据的业务场景。
- 交互模式不再是“请求-应答”模式,完全由开发者自行设计通讯协议。
- 通信的数据是基于“帧(frame)”的,可以传输文本数据,也可以直接传输二进制数据,效率高。当然,开发者也就要考虑封包、拆包、编号等技术细节。
Socket:
服务端监听通讯,被动提供服务;客户端主动向服务端发起连接请求,建立起通讯。
每一次交互都是:客户端主动发起请求(request),服务端被动应答(response)。
服务端不能主动向客户端推送数据。
通信的数据是基于文本格式的。二进制数据(比如图片等)要利用base64等手段转换为文本后才能传输。
1、依赖
<!--SpringBoot整合WebSocket-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!--json转换工具 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.49</version>
</dependency>
2、服务端
(1)配置类
@Component
public class WebSocketConfig {
/**
* ServerEndpointExporter 作用
*
* 这个Bean会自动注册使用@ServerEndpoint注解声明的websocket endpoint
*
* @return
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
(2)具体代码实现
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.concurrent.ConcurrentHashMap;
/**
*
* @ServerEndpoint 这个注解有什么作用?
*
* 这个注解用于标识作用在类上,它的主要功能是把当前类标识成一个WebSocket的服务端
* 注解的值用户客户端连接访问的URL地址
*
*/
@Slf4j
@Component
@ServerEndpoint("/websocket/{name}")
public class WebSocket {
/**
* 与某个客户端的连接对话,需要通过它来给客户端发送消息
*/
private Session session;
/**
* 标识当前连接客户端的用户名
*/
private String name;
/**
* 用于存所有的连接服务的客户端,这个对象存储是安全的
*/
private static ConcurrentHashMap<String,WebSocket> webSocketSet = new ConcurrentHashMap<>();
@OnOpen
public void OnOpen(Session session, @PathParam(value = "name") String name){
this.session = session;
this.name = name;
// name是用来表示唯一客户端,如果需要指定发送,需要指定发送通过name来区分
webSocketSet.put(name,this);
log.info("[WebSocket] 连接成功,当前连接人数为:={}",webSocketSet.size());
}
@OnClose
public void OnClose(){
webSocketSet.remove(this.name);
log.info("[WebSocket] 退出成功,当前连接人数为:={}",webSocketSet.size());
}
@OnMessage
public void OnMessage(String message){
log.info("[WebSocket] 收到消息:{}",message);
//判断是否需要指定发送,具体规则自定义
if(message.indexOf("TOUSER") == 0){
String name = message.substring(message.indexOf("TOUSER")+6,message.indexOf(";"));
AppointSending(name,message.substring(message.indexOf(";")+1,message.length()));
}else{
GroupSending(message);
}
}
/**
* 群发
* @param message
*/
public void GroupSending(String message){
for (String name : webSocketSet.keySet()){
try {
webSocketSet.get(name).session.getBasicRemote().sendText(message);
}catch (Exception e){
e.printStackTrace();
}
}
}
/**
* 指定发送
* @param name
* @param message
*/
public void AppointSending(String name,String message){
try {
webSocketSet.get(name).session.getBasicRemote().sendText(message);
}catch (Exception e){
e.printStackTrace();
}
}
}
(3)使用
@RestController
@RequestMapping("/socket")
public class SocketController {
@Resource
private WebSocket webSocket;
@GetMapping("/send")
public void send(){
HashMap<String, String> map = new HashMap<>();
map.put("key1","value1");
map.put("key2","value2");
String s = JSON.toJSONString(map);
//群发信息
webSocket.GroupSending(s);
}
}
3、HTML+JS 实现客户端
<body>
<input type="button" name="WebSocket" onclick="f()" value="WebSocket连接">
<script>
function f() {
var websocket = null;
var host = document.location.host;
var username = "rewind";
// console.log(username)
//判断当前浏览器是否支持WebSocket
if ('WebSocket' in window) {
console.log("浏览器支持Websocket"+username)
// ws://localhost:8080/websocket/rewind
websocket = new WebSocket('ws://'+host+'/websocket/'+username);
} else {
console.log('当前浏览器 Not support websocket')
}
//连接发生错误的回调方法
websocket.onerror = function() {
console.log("WebSocket连接发生错误")
setMessageInnerHTML("WebSocket连接发生错误");
};
//连接成功建立的回调方法
websocket.onopen = function() {
console.log("WebSocket连接成功")
setMessageInnerHTML("WebSocket连接成功");
}
//接收到消息的回调方法
websocket.onmessage = function(event) {
console.log("接收到消息的回调方法")
console.log("这是后台推送的消息:"+event.data);
//websocket.close();
//console.log("webSocket已关闭!")
}
//连接关闭的回调方法
websocket.onclose = function() {
setMessageInnerHTML("WebSocket连接关闭");
}
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function() {
closeWebSocket();
}
//关闭WebSocket连接
function closeWebSocket() {
websocket.close();
}
//将消息显示在网页上
function setMessageInnerHTML(innerHTML) {
document.getElementById('message').innerHTML += innerHTML + '<br/>';
}
}
</script>
</body>
document.location 这个对象包含了当前URL的信息
location.host 获取port号
location.hostname 设置或获取主机名称
location.href 设置或获取整个URL
location.port设置或获取URL的端口号
location.search 设置或获取href属性中跟在问号后面的部分
-------------------------------------------------------------------------------
js中window.location的应用
URL即:统一资源定位符 (Uniform Resource Locator, URL)
完整的URL由这几个部分构成:
scheme://host:port/path?query#fragment
scheme:通信协议
常用的http,ftp,maito等
host:主机
服务器(计算机)域名系统 (DNS) 主机名或 IP 地址。
port:端口号
整数,可选,省略时使用方案的默认端口,如http的默认端口为80。
path:路径
由零或多个"/"符号隔开的字符串,一般用来表示主机上的一个目录或文件地址。
query:查询
可选,用于给动态网页(如使用CGI、ISAPI、PHP/JSP/ASP/ASP.NET等技术制作的网页)传递参数,可有多个参数,用"&"符号隔开,每个参数的名和值用"="符号隔开。
fragment:信息片断
字符串,用于指定网络资源中的片断。例如一个网页中有多个名词解释,可使用fragment直接定位到某一名词解释。(也称为锚点.)
4、后台实现客户端
(1)依赖
<!--websocket作为客户端-->
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>1.3.5</version>
</dependency>
(2)配置类
import lombok.extern.slf4j.Slf4j;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.drafts.Draft_6455;
import org.java_websocket.handshake.ServerHandshake;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import java.net.URI;
/**
* @Description: 配置websocket后台客户端
*/
@Slf4j
@Component
public class WebSocketConfig {
@Bean
public WebSocketClient webSocketClient() {
try {
WebSocketClient webSocketClient = new WebSocketClient(new URI("ws://localhost:8085/websocket/test"),new Draft_6455()) {
@Override
public void onOpen(ServerHandshake handshakedata) {
log.info("[websocket] 连接成功");
}
@Override
public void onMessage(String message) {
log.info("[websocket] 收到消息={}",message);
}
@Override
public void onClose(int code, String reason, boolean remote) {
log.info("[websocket] 退出连接");
}
@Override
public void onError(Exception ex) {
log.info("[websocket] 连接错误={}",ex.getMessage());
}
};
webSocketClient.connect();
return webSocketClient;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
(3)客户端发送消息
1、首先写一个接口,里面有指定发送和群发消息两个方法。
2、实现发送的接口,区分指定发送和群发由服务端来决定(小编在服务端写了,如果带有TOUSER标识的,则代表需要指定发送给某个websocket客户端)
3、最后采用get方式用浏览器请求,也能正常发送消息
public interface WebSocketService {
/**
* 群发
* @param message
*/
void groupSending(String message);
/**
* 指定发送
* @param name
* @param message
*/
void appointSending(String name,String message);
}
import org.java_websocket.client.WebSocketClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @Description: websocket接口实现类
*/
@Component
public class ScoketClient implements WebSocketService{
@Autowired
private WebSocketClient webSocketClient;
@Override
public void groupSending(String message) {
// 这里我加了6666-- 是因为我在index.html页面中,要拆分用户编号和消息的标识,只是一个例子而已
// 在index.html会随机生成用户编号,这里相当于模拟页面发送消息
// 实际这样写就行了 webSocketClient.send(message)
webSocketClient.send(message+"---6666");
}
@Override
public void appointSending(String name, String message) {
// 这里指定发送的规则由服务端决定参数格式
webSocketClient.send("TOUSER"+name+";"+message);
}
}
import com.example.socket.code.ScoketClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Description: 测试后台websocket客户端
*/
@RestController
@RequestMapping("/websocket")
public class IndexController {
@Autowired
private ScoketClient webScoketClient;
@GetMapping("/sendMessage")
public String sendMessage(String message){
webScoketClient.groupSending(message);
return message;
}
}
5、单元测试报错
在SpringBoot项目中集成了WebSocket,在进行单元测试的时候,出现了以下错误:
javax.websocket.server.ServerContainer not available
SpringBootTest在启动的时候不会启动服务器,所以WebSocket自然会报错,这个时候需要添加选项webEnvironment,以便提供一个测试的web环境。如下:
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)