一. 什么是WebSocket?
WebSocket 是一种网络传输协议,可在单个TCP连接上进行全双工通信,位于OSI模型的应用层。
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就可以建立持久性的连接,并进行双向数据传输。
二. 怎么建立WebSocket连接?
浏览器在 TCP 三次握手建立连接之后,都统一使用 HTTP 协议先进行一次通信。
- 如果此时是普通的 HTTP 请求,那后续双方就还是老样子继续用普通 HTTP 协议进行交互。
- 如果这时候是想建立 WebSocket 连接,就会在 HTTP 请求里带上一些特殊的header 头,如下:
Connection: Upgrade
Upgrade: WebSocket
Sec-WebSocket-Key: T2a6wZlAwhgQNqruZ2YUyg==\r\n
后续流程如下:
三. 使用Springboot整合WebSocket
- 添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
- 实现WebSocket的文本消息处理器
@Component
@Slf4j
public class HttpAuthHandler extends TextWebSocketHandler {
/**
* socket 建立成功事件
*
* @param session
* @throws Exception
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
Object sessionId = session.getAttributes().get("session_id");
if (sessionId != null) {
// 用户连接成功,放入在线用户缓存
WsSessionManager.add(sessionId.toString(), session);
} else {
throw new RuntimeException("用户登录已经失效!");
}
}
/**
* 接收消息事件
*
* @param session
* @param message
* @throws Exception
*/
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
// 获得客户端传来的消息
String payload = (String) message.getPayload();
Object sessionId = session.getAttributes().get("session_id");
log.info("server 接收到 {} 发送的 {}", sessionId, payload);
session.sendMessage(new TextMessage("server 发送给 " + sessionId + " 消息 " + payload + " " + LocalDateTime.now().toString()));
}
/**
* socket 断开连接时
*
* @param session
* @param status
* @throws Exception
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
Object sessionId = session.getAttributes().get("session_id");
if (sessionId != null) {
// 用户退出,移除缓存
WsSessionManager.remove(sessionId.toString());
}
}
}
- 实现WebSocket握手拦截器
@Component
@Slf4j
public class MyInterceptor implements HandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
log.info("握手开始");
String hostName = request.getRemoteAddress().getHostName();
String sessionId = hostName + String.valueOf((int)(Math.random()*1000));
if (Strings.isNotBlank(sessionId)) {
// 放入属性域
attributes.put("session_id", sessionId);
log.info("用户 session_id {} 握手成功!", sessionId);
return true;
}
log.info("用户登录已失效");
return false;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
log.info("握手结束");
}
}
- 创建WebSocket配置类
@Configuration
@EnableWebSocket
public class WebsocketConfig implements WebSocketConfigurer {
@Autowired
private HttpAuthHandler httpAuthHandler;
@Autowired
private MyInterceptor myInterceptor;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry
.addHandler(httpAuthHandler, "/websocket") // ws://域名/websocket
.addInterceptors(myInterceptor)
.setAllowedOrigins("*");
}
}
- 使用Apifox测试连接
建立websocket连接输入:ws://localhost:8080/websocket
在服务器控制台查看日志:
四. 使用netty整合WebSocket
- 添加依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.76.Final</version>
</dependency>
- 编写netty启动类
public class NettyWebSocketStarter {
private static EventLoopGroup bossGroup = new NioEventLoopGroup(1);
private static EventLoopGroup workerGroup = new NioEventLoopGroup();
public static void main(String[] args) {
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup);
serverBootstrap.channel(NioServerSocketChannel.class).handler(new LoggingHandler(LogLevel.DEBUG)).childHandler(new ChannelInitializer() {
@Override
protected void initChannel(Channel channel) throws Exception {
ChannelPipeline pipeline = channel.pipeline();
// 对 http 协议支持
pipeline.addLast(new HttpServerCodec());
// 聚合解码 httpRequest/httpContent/lastHttpContent到fullHttpRequest
// 保证接受的 http 请求完整
pipeline.addLast(new HttpObjectAggregator(64 * 1024));
// 心跳 long readerIdleTime, long writerIdleTime, long allIdleTime, TimeUnit unit
// readerIdleTime 读超时时间,即测试端一定时间内未接收到测试端发来消息
// writerIdleTime 写超时时间,即测试端一定时间内想被发送消息
// allIdleTime 所有超时时间
pipeline.addLast(new IdleStateHandler(6L, 0, 0, TimeUnit.SECONDS));
pipeline.addLast(new HeartBeatHandler());
// 将http协议升级为websocket协议
pipeline.addLast(new WebSocketServerProtocolHandler("/webscoket", null, true, 64 * 1024, true, true, 10000L));
pipeline.addLast(new WebSocketHandler());
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
System.out.println("netty 启动成功");
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
System.err.println("启动 netty 失败" + e.getMessage());
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
- 实现心跳Handler
public class HeartBeatHandler extends ChannelDuplexHandler {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent e) {
if (e.state() == IdleState.READER_IDLE) // 心跳超时
ctx.close();
else if (e.state() == IdleState.WRITER_IDLE)
ctx.writeAndFlush("heart");
}
}
}
- 实现WebSocketHandler
public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
/**
* 通道就绪后触发,一般用于初始化
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("有新的连接加入...");
}
/**
* 通道关闭后触发,一般用于释放资源
* @param ctx
* @throws Exception
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("有连接断开...");
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame textWebSocketFrame) throws Exception {
System.out.printf("来自用户的消息:%s\n", textWebSocketFrame.text());
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
// 认证
if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete e) {
System.out.println("握手成功...");
String uri = e.requestUri();
System.out.println("uri: " + uri);
}
}
}
- 使用Apifox测试连接
建立websocket连接输入:ws://localhost:8080/websocket
在服务器控制台查看日志: