1. websocket介绍
WebSocket是一种计算机通信协议,用于在客户端和服务器之间建立持久性的全双工通信连接。它提供了一种在单个TCP连接上进行双向通信的方式,允许服务器主动向客户端发送数据,而不需要客户端首先发送请求。这与传统的HTTP请求-响应模型不同,后者需要客户端发送请求并等待服务器响应。
WebSocket协议的主要特点包括:
- 双向通信:WebSocket允许服务器主动向客户端发送数据,而不仅仅是响应客户端的请求。这使得实时应用程序(如聊天应用程序、多人游戏等)的开发更加容易,因为服务器可以即时地将更新推送给客户端。
- 持久连接:WebSocket连接在客户端和服务器之间保持活动状态,而不像HTTP连接那样在每个请求之后关闭。这消除了为每个请求建立新连接的开销,减少了网络流量和延迟。
- 低开销:由于WebSocket使用较少的头部信息,并且不需要在每个请求中进行完整的握手,所以它具有较低的开销。这使得它在网络带宽有限的环境下更加高效。
- 跨域支持:与传统的AJAX请求不同,WebSocket允许在不同域之间进行跨域通信。这使得开发者可以构建具有分布式架构的应用程序,其中前端和后端可以部署在不同的域上。
WebSocket的工作流程如下:
- 客户端通过HTTP协议向服务器发送一个特殊的请求,其中包含用于升级连接为WebSocket的标头。
- 服务器收到请求后,如果支持WebSocket协议,将返回一个包含特殊标头的响应,用于升级连接。
- 一旦连接升级成功,客户端和服务器之间的通信将使用WebSocket协议进行,而不是HTTP。
- 客户端和服务器可以通过发送帧(frames)来进行双向通信,每个帧可以包含文本、二进制数据或控制信息。
2. websocket连接过程
客户端依靠发起HTTP握手,告诉服务端进行WebSocket协议通讯,并告知WebSocket协议版本。服务端确认协议版本,升级为WebSocket协议。之后如果有数据需要推送,会主动推送给客户端。
- 请求头Request Headers
GET /test HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: tFGdnEL/5fXMS9yKwBjllg==
Origin: http://example.com
Sec-WebSocket-Protocol: v10.stomp, v11.stomp, v12.stomp
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Sec-WebSocket-Version: 13
首先客户端(如浏览器)发出带有特殊消息头(Upgrade、Connection)的请求到服务器,服务器判断是否支持升级,支持则返回响应状态码101,表示协议升级成功,对于WebSocket就是握手成功。
其中关键的字段就是Upgrade
,Connection
,告诉 Apache 、 Nginx 等服务器:注意啦,发起的是Websocket协议,不再 使用原先的HTTP。
其中,Sec-WebSocket-Key
当成是请求id就好了。
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HaA6EjhHRejpHyuO0yBnY4J4n3A=
Sec-WebSocket-Extensions: permessage-deflate;client_max_window_bits=15
Sec-WebSocket-Protocol: v12.stomp
Sec-WebSocket-Accept的字段值是由握手请求中的Sec-WebSocket-Key的字段值生成的。成功握手确立WebSocket连接之后,通信时不再使用HTTP的数据帧,而采用WebSocket独立的数据帧。
3. 代码实现
(1): 创建了一个ServerBootstrap实例,用于引导服务器的启动配置。然后,通过serverBootstrap的一系列方法来配置服务器的选项和处理器。
(2): 使用group方法将主线程组(mainGroup)和工作线程组(workerGroup)设置为服务器的线程组。然后,通过channel方法设置服务器的通道类型为NioServerSocketChannel。接着,使用option方法设置一些选项,如SO_BACKLOG和SO_KEEPALIVE。
(3): 通过handler方法给serverBootstrap添加一个LoggingHandler处理器,用于处理日志。
(4): 通过childHandler方法给serverBootstrap添加一个ChannelInitializer,用于初始化SocketChannel的处理管道。
(5): 在管道的初始化过程中,首先添加了一个IdleStateHandler,用于处理客户端的心跳超时。然后,添加了HttpServerCodec,用于处理HTTP协议的编码和解码。接着,使用ChunkedWriteHandler以块方式写入数据。
然后,使用HttpObjectAggregator将HTTP数据聚合为完整的请求。接下来,添加了一个自定义的HttpHeadersHandler,用于保存用户的IP地址。
(6): 添加了WebSocketServerProtocolHandler,将HTTP协议升级为WebSocket协议,并保持长连接。最后,添加了一个自定义的业务逻辑处理器(NETTY_WEB_SOCKET_SERVER_HANDLER)。
(7): 通过调用serverBootstrap.bind(WEBSOCKET_PORT).sync()来启动服务器并绑定指定的端口。这将阻塞当前线程,直到服务器成功启动。
@Slf4j
@Configuration
public class NettyWebSocketServer {
public static final int WEBSOCKET_POET = 8090;
public static final NettyWebSocketServerHandler NETTY_WEB_SOCKET_SERVER_HANDLER = new NettyWebSocketServerHandler();
//创建线程池执行器
//主事件循环组(mainGroup)负责接受客户端连接请求,并将连接分配给工作事件循环组(workerGroup)中的线程进行处理。通常情况下,主事件循环组只需要一个线程即可。
private EventLoopGroup mainGroup = new NioEventLoopGroup();
//工作事件循环组(workerGroup)负责实际的I/O操作,例如读取、写入和处理数据。
// 工作事件循环组的线程数通过NettyRuntime.availableProcessors()来设置为可用的处理器核心数,这样可以充分利用系统资源。
private EventLoopGroup workerGroup = new NioEventLoopGroup(NettyRuntime.availableProcessors());
/**
* 启动 ws sever
*/
@PostConstruct
public void start() throws InterruptedException{
run();
}
/**
* 销毁
*/
public void destroy() {
Future<?> futureMainGroup = mainGroup.shutdownGracefully();
Future<?> futureWorkerGroup = workerGroup.shutdownGracefully();
futureMainGroup.syncUninterruptibly();
futureWorkerGroup.syncUninterruptibly();
log.info("关闭 ws sever 成功!!!");
}
public void run() throws InterruptedException{
//服务器启动引导对象
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(mainGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128)
.option(ChannelOption.SO_KEEPALIVE, true)
.handler(new LoggingHandler(LogLevel.INFO)) // 为 serverBootstrap 添加日志处理器
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
//30 秒客户端没有向服务端发送心跳则关闭连接
pipeline.addLast(new IdleStateHandler(30, 0, 0));
// 因为使用http协议, 需要 http解码器
pipeline.addLast(new HttpServerCodec());
// 以块方式写, 添加 chunkedWriter 处理器
pipeline.addLast(new ChunkedWriteHandler());
/**
* 说明:
* 1. http数据在传输过程中是分段的,HttpObjectAggregator可以把多个段聚合起来;
* 2. 这就是为什么当浏览器发送大量数据时,就会发出多次 http请求的原因
*/
pipeline.addLast(new HttpObjectAggregator(8192));
//保存用户ip
pipeline.addLast(new HttpHeadersHandler());
/**
* 说明:
* 1. 对于 WebSocket,它的数据是以帧frame 的形式传递的;
* 2. 可以看到 WebSocketFrame 下面有6个子类
* 3. 浏览器发送请求时: ws://localhost:7000/hello 表示请求的uri
* 4. WebSocketServerProtocolHandler 核心功能是把 http协议升级为 ws 协议,保持长连接;
* 是通过一个状态码 101 来切换的
*/
pipeline.addLast(new WebSocketServerProtocolHandler("/"));
//自定义handler, 处理业务逻辑
pipeline.addLast(NETTY_WEB_SOCKET_SERVER_HANDLER);
}
});
serverBootstrap.bind(WEBSOCKET_POET).sync();
System.out.println("启动成功");
}
}
明白了websocket的升级过程,对netty的处理的就比较简单了。websocket初期是通过http请求,进行升级,建立双方的连接。
1.所以编解码器需要用到HttpServerCodec
。
2.WebSocketServerProtocolHandler
是netty进行websocket升级的处理器。在这期间会抹除http相关的信息,比如请求头啥的。如果想获取相关信息,需要在这之前获取。
3.HttpHeadersHandler
是我们自己的处理器。赶在websocket升级之前,获取用户的ip地址,然后保存到channel的附件里。
4.NettyWebSocketServerHandler
是我们的业务处理器,里面处理客户端的事件。
5.IdleStateHandler
实现心跳检测。
- 测试
发送 ws请求
4. 获取用户ip与 token
/**
* 获取IP
* 从HTTP请求的头部信息中获取相关参数,并保存到NettyUtil中的属性中。
*/
public class HttpHeadersHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(io.netty.channel.ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof FullHttpRequest) {
FullHttpRequest request = (FullHttpRequest) msg;
UrlBuilder urlBuilder = UrlBuilder.ofHttp(request.uri());
//获取token
String token = Optional.ofNullable(urlBuilder.getQuery()).map(k -> k.get("token")).map(CharSequence::toString).orElse("");
NettyUtil.setAttribute(ctx.channel(), NettyUtil.TOKEN, token);
//获取请求路径
request.setUri(urlBuilder.getPath().toString());
HttpHeaders headers = request.headers();
String ip = headers.get("X-Real-IP");
if(StringUtils.isEmpty(ip)){//如果没经过nginx,就直接获取远端地址
InetSocketAddress address = (InetSocketAddress) ctx.channel().remoteAddress();
ip = address.getAddress().getHostAddress();
}
NettyUtil.setAttribute(ctx.channel(), NettyUtil.IP, ip);
ctx.pipeline().remove(this);
ctx.fireChannelRead(request);
}else{
ctx.fireChannelRead(msg);
}
}
}
ChannelHandlerContext是Netty中的一个类,用于表示一个通道处理器上下文。它提供了与通道关联的处理器链的访问,并提供了一些方法来执行常见的通道操作,例如读取数据、写入数据、关闭通道等。ChannelHandlerContext还提供了处理通道中断事件和处理异常的方法。在使用Netty进行网络编程时,通常会在ChannelHandlerContext上添加处理器来处理通道上的各种事件。
/**
* netty工具类
*/
public class NettyUtil {
public static AttributeKey<String> TOKEN = AttributeKey.valueOf("token");
public static AttributeKey<String> IP = AttributeKey.valueOf("ip");
public static AttributeKey<Long> UID = AttributeKey.valueOf("uid");
public static AttributeKey<WebSocketServerHandshaker> HANDSHAKER_ATTR_KEY = AttributeKey.valueOf(WebSocketServerHandshaker.class, "HANDSHAKER");
public static <T> void setAttribute(Channel channel, AttributeKey<T> key, T value) {
Attribute<T> attr = channel.attr(key);
attr.set(value);
}
public static <T> T getAttr(Channel channel, AttributeKey<T> ip) {
return channel.attr(ip).get();
}
}