SpringBoot三十二:集成WebSocket

为什么需要 WebSocket

初次接触 WebSocket 的人,都会问同样的问题:我们已经有了 HTTP 协议,为什么还需要另一个协议?它能带来什么好处?

答案很简单,因为 HTTP 协议有一个缺陷:通信只能由客户端发起。

举例来说,我们想了解今天的天气,只能是客户端向服务器发出请求,服务器返回查询结果。HTTP 协议做不到服务器主动向客户端推送信息。这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。我们只能使用"轮询":每隔一段时候,就发出一个询问,了解服务器有没有新的信息。最典型的场景就是聊天室。轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。因此,工程师们一直在思考,有没有更好的方法。WebSocket 就是这样发明的。

WebSocket简介

WebSocket 协议在2008年诞生,2011年成为国际标准。所有浏览器都已经支持了。

它的最大特点就是:服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话, 属于服务器推送技术的一种。

其他特点:

  1. 建立在 TCP 协议之上,服务器端的实现比较容易。
  2. 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
  3. 数据格式比较轻量,性能开销小,通信高效。
  4. 可以发送文本,也可以发送二进制数据。
  5. 没有同源限制,客户端可以与任意服务器通信。
  6. 协议标识符是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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值