1.什么是WebSocket协议
WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。
WebSocket协议是建立在tcp协议上的一种通讯协议,代表其是面向稳定连接的通讯协议,可以保证通信双方数据传输的稳定和可靠。因为其又是全双工通信协议,因此其天生就具备通信双方一段向另一端的主动推送数据的能力。
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
2.WebSocket的特点
2.1 全双工通讯
通讯双方都可以通过WebSocket连接向对端发送数据,实现双向通讯,其通讯模式被成为全双工通讯。
传统的socket通讯或http通讯都只支持一方发送数据,另一方响应数据,即从连接建立的那一刻,客户端和服务端的关系就已经确立,数据传输在同一时刻只能是单向的,其通讯模式被称为半双工通讯
2.2 延迟低
由于WebSocket的连接建立后会,通讯双方会一直保持连接,因此其不需要每次在发送数据前还要与对方再次建立连接。与http或socket每次通讯前都建立连接的方式相比,其通讯延迟要低得多,因为省去了建立连接所消耗的时间。
2.3 通讯控制开销小
WebSocket相比于http连接,其头部信息要小得多,在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。在不包含扩展的情况下,对于服务器到客户端的内容,此头部大小只有2至10字节(和数据包长度有关);对于客户端到服务器的内容,此头部还需要加上额外的4字节的掩码。相对于HTTP请求每次都要携带完整的头部,此项开销显著减少了。
2.4 有状态
由于WebSocket是面向连接的通讯协议,所以其是有状态的,即只有通讯双方建立了连接才可以进行通讯,而不是随时都可以进行数据传输。
3.实现消息推送的推拉模型
3.1 拉模型
拉模型是指消息的接收端主动向消息的发送端定期发出请求获取消息,消息接收端在收到消息后对请求进行处理。
-
优点:
- 实现简单,只需简单的设置定时器定期向服务端拉取消息即可
- 消息接收端可按需主动拉取消息,接收端可以自己控制拉去频率,当接收端负载过高时可选择暂不拉取或降低拉取频率,等到负载低时再主动拉取
-
缺点:
- 消息推送会不及时,只有接收端主动拉去消息后,消息才会推送给接收端
- 当拉去频率过大时,会对消息推送端造成很大的服务压力,且在没有消息可推送时会造成大量的请求浪费
3.2 推模型
推模型是指消息发送端主动向消息接收端推送消息,消息接收端在收到推送消息后对请求进行处理。
-
优点:
- 消息推送及时,推送端可以在消息产生后就向接收端推送消息,无需等待接收端主动拉取
- 消息推送的网络消耗小,由于是消息产生端向接收端主动推送消息,因此无需造成过大的网络消耗,也避免了发送大量的无用请求
-
缺点:
- 实现相较于主动拉取模型较为复杂。
- 当接收端负载过高,处理不过来消息时,发送端依然会向接收端推送数据,加大了接收端的压力。
两种消息推送模型各有利弊,在实际业务中应结合场景来进行选择或进行个性化改造以适应业务。
4.基于WebSocket的简单消息推送实现
本实现消息推送端使用java语言打开WebSocket服务,消息接收端也使用java语言开发进行连接,实际场景中可根据需求的语言自行实现发送端和接收端
依赖pom
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>1.5.3</version>
</dependency>
- 消息发送端
WebSocketUtil.java
package org.mf.socket;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.java_websocket.WebSocket;
import org.java_websocket.framing.CloseFrame;
import org.java_websocket.handshake.ClientHandshake;
import org.java_websocket.server.WebSocketServer;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.SynchronousQueue;
/**
* 类名/接口名
*
* @author mengfei
* @date 2023年09月11日 13:36
*/
public class WebSocketUtil {
private static final Logger LOGGER = LogManager.getLogger(WebSocketUtil.class);
/**
* 自定义server容器,用来记录连接的webSocket
*/
private static CustomWebServer CUSTOM_WEB_SERVER = null;
/**
* 启动websocket服务
* @param port 端口号
* @throws Exception
*/
public static void startServer(Integer port) {
//设置jvm退出时的清理线程
setClearWebSocketTask();
//创建同步阻塞队列,用来获取webSocket启动结果
SynchronousQueue<WebSocketStartResult> synchronousQueue = new SynchronousQueue<>();
CUSTOM_WEB_SERVER = new CustomWebServer(port, synchronousQueue);
CUSTOM_WEB_SERVER.start();
//线程一直阻塞,直到拿到启动结果
WebSocketStartResult startResult = null;
try {
startResult = synchronousQueue.take();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (startResult.getStartStatus() == 2) {
//启动失败,抛出异常
Exception startException = (Exception) startResult.getStartResult();
LOGGER.error("websocket server启动失败", startException);
throw new RuntimeException(startException);
}
LOGGER.info("websocket server启动成功,端口号:{}", port);
}
private static void setClearWebSocketTask() {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
stopServer();
}
});
thread.setName("websocket关闭线程");
thread.setDaemon(true);
Runtime.getRuntime()
.addShutdownHook(thread);
}
public static void stopServer() {
if (CUSTOM_WEB_SERVER != null) {
try {
CUSTOM_WEB_SERVER.stop();
} catch (InterruptedException e) {
LOGGER.error("websocket服务关闭失败", e);
throw new RuntimeException(e);
}
}
}
/**
* 向websocket连接中推送消息,方法加锁,防止多个线程同时推送消息造成错误
* @param messageBody
* @return
*/
public static synchronized void writeMessage(String messageBody) {
if (CUSTOM_WEB_SERVER == null) {
return;
}
ConcurrentHashMap<WebSocket, Integer> connAndStatus = CUSTOM_WEB_SERVER.getConns();
for (Map.Entry<WebSocket, Integer> webSocketAnsStatusEntry : connAndStatus.entrySet()) {
WebSocket conn = webSocketAnsStatusEntry.getKey();
try {
//发送消息
JSONObject jsonObject = new JSONObject();
jsonObject.put("messageBody", messageBody);
conn.send(jsonObject.toJSONString());
} catch (Exception e) {
LOGGER.error("websocket消息推送失败", e);
}
}
};
/**
* websocket启动结果
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
private static class WebSocketStartResult {
/**
* socket server启动状态
*/
private Integer startStatus;
/**
* socket server启动结果信息
* 如果status为1,result中存放启动成功字符串
* 如果status为2,result中存放启动的异常
*/
private Object startResult;
}
/**
* 自定义websocket服务
*/
private static class CustomWebServer extends WebSocketServer {
/**
* 用于通知websocket server启动结果
*/
private SynchronousQueue<WebSocketStartResult> synchronousQueue;
/**
* 记录webSocket的启动状态,0-已创建未启动,1-启动成功,2-启动失败
*/
private Integer startState = 0;
/**
* 存储所有socket连接(使用线程安全的map集合,防止在往连接中推送消息时发生不必要的错误,比如连接已经关闭但还是向其推送消息)
*/
@Getter
private ConcurrentHashMap<WebSocket, Integer> conns = new ConcurrentHashMap<>();
public CustomWebServer(int port, SynchronousQueue<WebSocketStartResult> synchronousQueue) {
super(new InetSocketAddress(port));
this.synchronousQueue = synchronousQueue;
}
/**
* websocket conn打开的回调
* @param conn The <tt>WebSocket</tt> instance this event is occurring on.
* @param handshake The handshake of the websocket instance
*/
@Override
public void onOpen(WebSocket conn, ClientHandshake handshake) {
LOGGER.info("websocket 连接创建,连接:{}", conn);
//将连接加入map
conns.put(conn, 0);
}
/**
* websocket conn收到消息的回调,目前仅处理认证消息,其他消息返回错误
* @param conn The <tt>WebSocket</tt> instance this event is occurring on.
* @param message The UTF-8 decoded message that was received.
*/
@Override
public void onMessage(WebSocket conn, String message) {
LOGGER.info("websocket 接收到消息,连接:{},消息:{}", conn, message);
//在此处处理接收的message
}
/**
* websocket conn被关闭的回调
* @param conn The <tt>WebSocket</tt> instance this event is occurring on.
* @param code The codes can be looked up here: {@link CloseFrame}
* @param reason Additional information string
* @param remote Returns whether or not the closing of the connection was initiated by the remote
* host.
*/
@Override
public void onClose(WebSocket conn, int code, String reason, boolean remote) {
LOGGER.info("websocket 连接关闭,连接:{},状态:{},关闭原因:{},是否是远端关闭:{}", conn, code, reason, remote);
//从连接map中移除
conns.remove(conn);
}
/**
* websocketServer发生异常的回调
* @param conn Can be null if there error does not belong to one specific websocket. For example
* if the servers port could not be bound.
* @param ex The exception causing this error
*/
@Override
public void onError(WebSocket conn, Exception ex) {
LOGGER.error("websocket 异常,连接:{},exception:{}", conn, ex);
// 在这里,您可以处理任何错误情况
if (startState == 0) {
//修改启动状态
startState = 2;
//将启动结果的异常放入请求队列中
WebSocketStartResult webSocketStartResult = new WebSocketStartResult(2, ex);
try {
synchronousQueue.put(webSocketStartResult);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
/**
* websocketServer启动成功的回调
*/
@Override
public void onStart() {
LOGGER.info("websocket server启动成功");
WebSocketStartResult webSocketStartResult = new WebSocketStartResult(1, "启动成功");
try {
//修改启动状态
startState = 1;
//将启动结果塞入队列中
synchronousQueue.put(webSocketStartResult);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public static void main(String[] args) throws InterruptedException {
startServer(9999);
while (true) {
Thread.sleep(1000);
writeMessage("123");
}
}
}
main方法启动后,会在服务器9999端口创建WebSocket监听,1s后向已连接的客户端推送消息
- 消息接收端
SocketClient.java
package org.mf.socket;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* WebSocket连接客户端
*
* @author mengfei
* @date 2023年09月25日 18:01
*/
public class SocketClient {
private static final Logger logger = LogManager.getLogger(SocketClient.class);
private static MyWebSocketClient CLIENT = null;
public static Boolean createConnection(String address, Integer port) throws ExecutionException, InterruptedException {
if (CLIENT != null) {
CLIENT.setNotKeepAlive();
CLIENT.closeBlocking();
}
FutureTask futureTask = new FutureTask<>(new Callable<Object>() {
@Override
public Object call() throws Exception {
try {
CLIENT = new MyWebSocketClient(new URI("ws://" + address +":" + port));
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
CLIENT.connect();
return "success";
}
});
futureTask.run();
Object result = futureTask.get();
return true;
}
public static void sendMsg(String msg) throws URISyntaxException {
CLIENT.send(msg);
}
public static class MyWebSocketClient extends WebSocketClient {
/**
* webSocket连接断开后是否自动重连
*/
private volatile boolean keepAlive = true;
private URI serverURI;
public MyWebSocketClient(URI serverUri) {
super(serverUri);
this.serverURI = serverUri;
}
@Override
public void onOpen(ServerHandshake handshakedata) {
System.out.println("已打开连接");
}
@Override
public void onMessage(String message) {
logger.info("收到sdk推送消息:{}", message);
//用来处理推送过来的消息
}
@Override
public void onClose(int code, String reason, boolean remote) {
System.out.println("连接已关闭: " + reason);
if (keepAlive) {
//如果设置维持长连接则重新连接
FutureTask futureTask = new FutureTask<>(new Callable<Object>() {
@Override
public Object call() throws Exception {
CLIENT = new MyWebSocketClient(serverURI);
CLIENT.connect();
return "success";
}
});
try {
futureTask.get();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
}
@Override
public void onError(Exception ex) {
ex.printStackTrace();
}
public void setNotKeepAlive() {
keepAlive = false;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
createConnection("127.0.0.1", 9999);
}
}
main方法启动后会向本机9999端口创建WebSocket连接,当服务端有消息发送来的时候会在MyWebSocketClient的onMessage方法中进行处理。
完整代码大家可以在我的资源中进行免费下载