文章目录
为什么需要 WebSocket
初次接触 WebSocket 的人,都会问同样的问题:我们已经有了 HTTP 协议,为什么还需要另一个协议?它能带来什么好处?
答案很简单,因为 HTTP 协议有一个缺陷:通信只能由客户端发起。
举例来说,我们想了解今天的天气,只能是客户端向服务器发出请求,服务器返回查询结果。HTTP 协议做不到服务器主动向客户端推送信息。这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。我们只能使用"轮询":每隔一段时候,就发出一个询问,了解服务器有没有新的信息。最典型的场景就是聊天室。轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。因此,工程师们一直在思考,有没有更好的方法。WebSocket 就是这样发明的。
WebSocket简介
WebSocket 协议在2008年诞生,2011年成为国际标准。所有浏览器都已经支持了。
它的最大特点就是:服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,
属于服务器推送技术的一种。
其他特点:
- 建立在 TCP 协议之上,服务器端的实现比较容易。
- 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
- 数据格式比较轻量,性能开销小,通信高效。
- 可以发送文本,也可以发送二进制数据。
- 没有同源限制,客户端可以与任意服务器通信。
- 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。
ws://ip:端口/path
集成WebSocket
pom.xml引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
WebSocketConfig
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
WebSocketServer
消息群发
@ServerEndpoint("/sendall")
@Component
@Slf4j
//群发消息
public class SendAllSocketServer {
/**
* 存放所有在线的客户端
*/
private static Map<String, Session> clients = new ConcurrentHashMap<>();
/**
* 客户端连接
*
* @param session
*/
@OnOpen
public void onOpen(Session session) {
log.info("有新的客户端连接了: {}", session.getId());
// 将新用户存入在线的组
clients.put(session.getId(), session);
}
/**
* 客户端关闭
*
* @param session session
*/
@OnClose
public void onClose(Session session) {
log.info("有用户断开了, id为:{}", session.getId());
// 将掉线的用户移除在线的组里
clients.remove(session.getId());
}
/**
* 发生错误
*
* @param throwable e
*/
@OnError
public void onError(Throwable throwable) {
throwable.printStackTrace();
}
/**
* 收到客户端发来消息
*
* @param message 消息内容
*/
@OnMessage
public void onMessage(String message) {
log.info("服务端收到客户端发来的消息: {}", message);
this.sendAll(message);
}
/**
* 群发消息
*
* @param message 消息内容
*/
private void sendAll(String message) {
for (Map.Entry<String, Session> sessionEntry : clients.entrySet()) {
sessionEntry.getValue().getAsyncRemote().sendText(message);
}
}
}
测试
WebSocket在线测试:http://www.websocket-test.com/
- 客户端
- 服务端
两个客户端均接受到消息。
消息一对一发送
@ServerEndpoint("/sendone")
@Component
@Slf4j
//消息一对一发送
public class SendOneSocketServer {
private static Map<String, Session> clients = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(Session session) {
log.info("有新的客户端上线: {}", session.getId());
clients.put(session.getId(), session);
}
@OnClose
public void onClose(Session session) {
log.info("有用户断开了, id为:{}", session.getId());
// 将掉线的用户移除在线的组里
clients.remove(session.getId());
}
@OnError
public void onError(Session session, Throwable throwable) {
if (clients.get(session.getId()) != null) {
log.info("发生了错误,移除客户端: {}", session.getId());
clients.remove(session.getId());
}
throwable.printStackTrace();
}
@OnMessage
public void onMessage(Session session, String message) {
log.info("收到客户端发来的消息: {}", message);
this.sendOne(session, message);
}
/**
* 发送消息
*
* @param session
* @param message 消息内容
*/
private void sendOne(Session session, String message) {
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
测试
-
客户端
-
服务端
只有当前客户端接受到消息。
A发送消息到B
pom.xml添加依赖
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.46</version>
</dependency>
@ServerEndpoint("/sendto/{userId}")
@Component
@Slf4j
public class SendToSocketServer {
/**
* 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
*/
private static int onlineCount = 0;
/**
* concurrent包的线程安全Set,用来存放每个客户端对应的WebSocket对象。
*/
private static ConcurrentHashMap<String, SendToSocketServer> clients = new ConcurrentHashMap<>();
/**
* 与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
private Session session;
/**
* 接收userId
*/
private String userId = "";
/**
* 当前在线人数
*
* @return
*/
public static synchronized int getOnlineCount() {
return onlineCount;
}
/**
* 在线数加1
*/
public static synchronized void addOnlineCount() {
SendToSocketServer.onlineCount++;
}
/**
* 在线数减1
*/
public static synchronized void subOnlineCount() {
SendToSocketServer.onlineCount--;
}
/**
* 客户端建立连接
*
* @param session
* @param userId 用户ID
*/
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId) {
this.session = session;
this.userId = userId;
if (clients.containsKey(userId)) {
clients.remove(userId);
clients.put(userId, this);
} else {
clients.put(userId, this);
addOnlineCount();
}
log.info("用户连接:" + userId + ",当前在线人数为:" + getOnlineCount());
try {
sendMessage("连接成功");
} catch (IOException e) {
log.error("用户:" + userId + ",网络异常!!!!!!");
}
}
/**
* 客户端关闭连接
*/
@OnClose
public void onClose() {
if (clients.containsKey(userId)) {
clients.remove(userId);
subOnlineCount();
}
log.info("用户退出:" + userId + ",当前在线人数为:" + getOnlineCount());
}
/**
* 收到客户端发来消息(A->B)
*
* @param message 消息内容<测试参数:{"toUserId":"B"}>
*/
@OnMessage
public void onMessage(String message) {
log.info("用户消息:" + userId + ",报文:" + message);
if (StringUtils.isNotBlank(message)) {
try {
// 解析发送的报文
JSONObject jsonObject = JSON.parseObject(message);
// 追加发送人(防止串改)
jsonObject.put("fromUserId", this.userId);
String toUserId = jsonObject.getString("toUserId");
// 传送给对应toUserId用户的websocket
if (StringUtils.isNotBlank(toUserId) && clients.containsKey(toUserId)) {
clients.get(toUserId).sendMessage(jsonObject.toJSONString());
} else {
log.error("请求的userId:" + toUserId + "不在该服务器上");
}
} catch (Exception e) {
e.printStackTrace();
}
}
// // 群发消息
// for (Entry<String, SendToSocketServer> sendToSocketServerEntry : clients.entrySet()) {
// sendToSocketServerEntry.getValue().session.getAsyncRemote().sendText(message);
// }
}
/**
* 发生错误
*
* @param error
*/
@OnError
public void onError(Throwable error) {
log.error("用户错误:" + this.userId + ",原因:" + error.getMessage());
error.printStackTrace();
}
/**
* 服务器主动推送消息
*
* @param message 消息内容
* @throws IOException
*/
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
/**
* 发送自定义消息
*
* @param message 消息内容
* @param userId 用户ID
* @throws IOException
*/
public static void sendInfo(String message, @PathParam("userId") String userId) throws IOException {
log.info("发送消息到:" + userId + ",报文:" + message);
if (StringUtils.isNotBlank(userId) && clients.containsKey(userId)) {
clients.get(userId).sendMessage(message);
} else {
log.error("用户" + userId + ",不在线!");
}
}
}
测试
-
客户端
参数:{"toUserId":"B"}
-
服务端
客户端A发送消息到客户端B。
消息推送
推送新信息,可以在自己的Controller写个方法调用SendToSocketServer.sendInfo();即可
@RestController
public class SocketController {
@GetMapping("/index")
public ResponseEntity<String> index() {
return ResponseEntity.ok("请求成功!");
}
@GetMapping("/sendto/{toUserId}")
public ResponseEntity<String> pushMessage(String message, @PathVariable String toUserId) throws IOException {
SendToSocketServer.sendInfo(message, toUserId);
return ResponseEntity.ok("消息发送成功!");
}
}
测试
- 服务端
@EnableWebSocket
基于@EnableWebSocket 注解完成基本的socket通信以及socket握手权限,不涉及socket存储处理以及发送的逻辑代码。
添加 @EnableWebSocket 注解 设置socket服务注册
@Configuration
@EnableWebSocket
public class OrdersocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(orderSocketHandler(), "/ws/order")// 注册socket地址
.addInterceptors(new HandShakeInterceptor())// 拦截器验证权限
.setAllowedOrigins("*");// 通信允许的域名,这里使用*,表示匹配所有
}
@Bean
public OrderSocketHandler orderSocketHandler() {
return new OrderSocketHandler();
}
}
通信接口
@Slf4j
public class OrderSocketHandler extends TextWebSocketHandler {
public static final Map<String, WebSocketSession> clients = new ConcurrentHashMap<>();
/**
* 客户端连接
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
log.info("websocket建立连接:{}", session);
log.info("token: " + session.getAttributes().get("token"));
clients.put(session.getId(), session);
}
/**
* 客户端关闭
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
log.info("WebSocket服务端关闭:{} " + status);
clients.remove(session.getId());
}
/**
* 收到客户端发来消息
*/
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
String msg = message.getPayload();
log.info("WebSocket接收到ws请求:{}", msg);
}
/**
* 发生错误
*/
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
log.info("WebSocket服务端连接异常:{}" + exception.getMessage());
}
public static Map<String, WebSocketSession> getClients() {
return clients;
}
}
权限验证,根据具业务验证权限
public class HandShakeInterceptor extends HttpSessionHandshakeInterceptor {
/*
* 在WebSocket连接建立之前的操作,以鉴权为例
*/
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Map<String, Object> attributes) throws Exception {
ServletServerHttpRequest serverRequest = (ServletServerHttpRequest) request;
String token = serverRequest.getServletRequest().getParameter("token");
if (token != null) {
// 此处将token传递到OrderSocketHandler,必须写否则拿不到数据
attributes.put("token", token);
return super.beforeHandshake(request, response, wsHandler, attributes);
} else {
return false;
}
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Exception ex) {
// 省略根据业务处理
}
}
代码托管:springboot_websocket