高性能 Netty 之 WebSocket 详解与整合 - 掘金

Netty With WebSocket

上篇文章我们讲了如何使用 Netty 来开发一个 Http 文件服务器,里面蕴含了关于如何使用Netty 提供的组件类来解析 Http 协议后进行请求的处理,然后再继续通过已有的组件来进行编解码和传输。

这篇文章主要讲的是,如何使用 Netty 整合WebSocket 的做一个 DEMO 文章。实际上,如 Http 一样,Netty 也对 WebSocket 封装提供了一些方便好用的组件,让你只要编码一下就可以使用了。这一点上,我再次体验到了 Netty 的高扩展性。

那么接下来,本文将会讲述一下内容

  1. 什么是 WebSocket
  2. WebSocketHttp 的区别。
  3. 我们为什么需要 WebSocket
  4. NettyWebSocket 之间的整合。

什么是 WebSocket

大部人都是这样描述的

WebSocketHTML5 提供的一种浏览器与服务器之间进行全双工通信的网络技术。

看起来其实挺难懂的,HTML5 我们懂,但是 全双工通信 我们就不懂了。所以接下来我们来解析一下这是什么是 WebSocket

根据我们之前学习的,我们知道 Socket 是传输层和应用之间的一种功能接口,通过这些接口我们就可以使用 TCP/IP 协议栈在传输层收发数据了。那么 WebSocket 对于这个有什么关联之处呢?从 WebSocket 的字面意思来看,我们可以拆分称为了 WebSocket。可能你可以 GET 的到,是不是 WebSocket 就像是运行在 Web 上面,负责 Http 上的 Socket 通信规范?

的确是!WebSocket 可以说基于 Http 协议的 Socket 通讯规范,提供跟 TCP Socket 类似的功能,它可以像 TCP Socket 一样调用下层协议栈,任意地收发数据。但是千万不要以为 WebSocketHttp 的一个升级版(原因下面会说)。实际上,WebSocket 是一种基于 TCP 轻量级网络通讯协议,地位与 Http 是平级的。

为什么我们需要 WebSocket ?

首先我们需要明白,同一领域的新事物的出现,它大概率不是为了颠覆其前者,一般是站在巨人的肩膀上继续完善。而 WebSocket 的出现,实际上就是为了弥补 Http 的缺陷。

根据 WebSocket 的介绍,我们知道它是全双工通信的网络技术。而 Http 它是一种半双工技术。Http 在这种技术下有两个特点

  1. 在客户端与服务端之间,同一时刻只能允许单向数据流
  2. 服务端不能主动向客户端发送数据,只能以请求-应答的方式"被动"回复来自客户端的请求。

半双工会给我们带来什么问题呢?如果你做过实时信息的话,可能你就比较苦逼了。一般来说,实时通讯是需要双方的互动的,也就是你给他行,他给你发也行。但是很明显,半双工只能是客户端发给服务端,服务端不能发给客户端。或许你会说,那我客户端隔一段时间就去询问一下服务端不行吗?

实时上,再没有 WebSocket 的情况下,一般采用的是“轮询”的方式来实现即使通讯,也就是不断地请求服务端。如果轮询的频率比较高,那么就可以近似地实现实时通信的效果

但轮询的缺点也很明显,反复发送无效查询请求耗费了大量的带宽和 CPU 资源,非常不经济。

所以,WebSocket 这种全双工通信

WebSocket 的特点

或许还是很多人有疑问:明明我看到 WebSocket 是基于 Http 来进行交互的,它的协议格式不就是和 Http 有大同小异的地方吗?其实并不完全是。我们知道 Http 其实是当下互联网通讯协议的老大,没有之一。但是 WebSocket 并没有沿用很多 Http 的东西,相反,它有如下的特点

  1. 首先WebSocket 采用了二进制帧结构,与 Http 的结构其实完全不一样。但是为了能够方便推广和应用,不得不搭一下“便车”,在使用习惯上尽量向 Http 靠拢,这就是它名字里 Web 的含义。(下文我会说明为什么是搭便车)
  2. 其次,WebSocket 没有像 Socket 那样使用 IP+端口的方式,而是沿用了 HttpURI 格式。但是 URL 的开头不是 Http,而是 wswss,分别是明文和加密的 WebSocket 协议。
  3. 再者,WebSocket 的默认端口还是使用了 80443。因为目前互联网上的服务器防火墙屏蔽了大多数的端口,只对 Http80443 放行,所以 WebSocket 就可以伪装Http 协议来穿透防火墙,与服务器建立连接。

WebSocket 交互的过程

在讲 WebSocket 的特点的时候,有可能你稍稍知道了一下内幕:WebSocket 实际上和 Http 关系不大,只是 WebSocketHttp 的“名声”腾飞的!

不急,让我们来看看 WebSocket 的交互顺序。下面是一张总图的交互图:

我们看到了,WebSocket 也有类似 TCP 的握手过程。它首先发出一个 HttpGet 请求,下面是报文的详细内容

复制代码

`GET /HTTP/1.1
Upgrader: websocket
Connection: Upgrade
Host: example.com
Origin: http://example.com
Set-WebSocket-Key: sNNdMgdc2VGJEKS
Set-WebSocket-Version: 13`

在报文中,我们值得关注几个字段

字段名

使用

Upgrade

设置为 WebSocket,表示需要跟服务端说明讲 Http 升级为 WebSocket 协议

Sec-WebSocket-key

Base64 编码的 16 字节随机数,用于验证是否是 WebSocket 而不是 Http 协议

Sec-WebSocket-Version

表示使用 WebSocket 协议版本号

然后当服务器接收了客户端的报文后,就开始解析报文了。这时候从报文它知道了这是一个 WebSocket 的请求。所以开始构造特殊的报文信息,报文的内容为

复制代码

`HTTP/1.1 101 Switching Protocols
Upgrader: websocket
Connection: Upgrade
Set-WebSocket-Accept: fFBooB7FAkKLlXgrSz0BT3v4hq5s
Set-WebSocket-Location: ws://examples.com/`

报文的字段依旧熟悉。但是我们发现了 101 Switching Protocols 的说明,这个是服务器返回的的 101 状态码,告诉客户端可以进行 WebSocket 全双工双向通信。这就相当于,接下来客户端和服务端都约定好了使用 WebSocket 来交互了,已经没了 Http 什么事了。

然后上面的返回的 Set-WebSocket-Accept 是用来验证客户端请求报文,同样也是为了防止误连接。具体做法是将客户端的 Set-WebSocket-Key 进行一个专用的 UUID,然后再计算 SHA-1 摘要。这样子,客户端同样会通过这样的计算来比对服务端的响应信息,避免认证失败。

握手完成,后续传输的数据就不再是 Http 报文,而是 WebSocket 格式的二进制帧了。

Netty 整合 WebSocket

首先依旧,我们使用 Netty 来实现一个服务端的启动类

WebSocketServer.java

复制代码

`public class WebSocketServer {
public void run(int port) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch)
throws Exception {
// http 的解码器
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("http-codec",
new HttpServerCodec());
// 负责将 Http 的一些信息例如版本
// 和 Http 的内容继承一个 FullHttpRequesst
pipeline.addLast("aggregator",
new HttpObjectAggregator(65536));
// 大文件写入的类
ch.pipeline().addLast("http-chunked",
new ChunkedWriteHandler());
// websocket 处理类
pipeline.addLast("handler",
new WebSocketServerHandler());
}
});
// 监听端口
Channel ch = b.bind(port).sync().channel();
ch.closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) throws Exception {
new WebSocketServer().run(8080);
}
}`

接下来,我们是实现处理逻辑的处理器 WebSocketServerHandler.java

复制代码

`public class WebSocketServerHandler extends SimpleChannelInboundHandler<Object> {
private static final Logger logger = Logger
.getLogger(WebSocketServerHandler.class.getName());

private WebSocketServerHandshaker handshaker;
@Override
public void messageReceived(ChannelHandlerContext ctx, Object msg)
throws Exception {
// 传统的HTTP接入(握手流程是走这里的)
if (msg instanceof FullHttpRequest) {
handleHttpRequest(ctx, (FullHttpRequest) msg);
}
// WebSocket接入
else if (msg instanceof WebSocketFrame) {
handleWebSocketFrame(ctx, (WebSocketFrame) msg);
}
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
private void handleHttpRequest(ChannelHandlerContext ctx,
FullHttpRequest req) throws Exception {
// 如果HTTP解码失败,返回HHTP异常
if (!req.getDecoderResult().isSuccess()
|| (!"websocket".equals(req.headers().get("Upgrade")))) {
sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1,
BAD_REQUEST));
return;
}
// 构造握手响应返回,目前是本机的地址
WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
"ws://localhost:8080/websocket", null, false);
handshaker = wsFactory.newHandshaker(req);
if (handshaker == null) {
WebSocketServerHandshakerFactory
.sendUnsupportedWebSocketVersionResponse(ctx.channel());
} else {
handshaker.handshake(ctx.channel(), req);
}
}
private void handleWebSocketFrame(ChannelHandlerContext ctx,
WebSocketFrame frame) {
// 判断是否是关闭链路的指令
if (frame instanceof CloseWebSocketFrame) {
handshaker.close(ctx.channel(),
(CloseWebSocketFrame) frame.retain());
return;
}
// 判断是否是Ping消息
if (frame instanceof PingWebSocketFrame) {
ctx.channel().write(
new PongWebSocketFrame(frame.content().retain()));
return;
}
// 本例程仅支持文本消息,不支持二进制消息
if (!(frame instanceof TextWebSocketFrame)) {
throw new UnsupportedOperationException(String.format(
"%s frame types not supported", frame.getClass().getName()));
}
// 返回应答消息
String request = ((TextWebSocketFrame) frame).text();
if (logger.isLoggable(Level.FINE)) {
logger.fine(String.format("%s received %s", ctx.channel(), request));
}
ctx.channel().write(
new TextWebSocketFrame(request

  • " , 欢迎使用Netty WebSocket服务,现在时刻:"
  • new java.util.Date().toString()));

}
private static void sendHttpResponse(ChannelHandlerContext ctx,
FullHttpRequest req, FullHttpResponse res) {
// 返回应答给客户端
if (res.getStatus().code() != 200) {
ByteBuf buf = Unpooled.copiedBuffer(res.getStatus().toString(),
CharsetUtil.UTF_8);
res.content().writeBytes(buf);
buf.release();
setContentLength(res, res.content().readableBytes());
}
// 如果是非Keep-Alive,关闭连接
ChannelFuture f = ctx.channel().writeAndFlush(res);
if (!isKeepAlive(req) || res.getStatus().code() != 200) {
f.addListener(ChannelFutureListener.CLOSE);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
cause.printStackTrace();
ctx.close();
}
}`

其实上面代码还是较为明确的。主要分为两个处理:

方法

说明

handleHttpRequest()

负责响应客户端的握手请求

handleWebSocketFrame()

负责处理 WebSocket 的消息

而在处理 handleWebSocketFrame() 主要负责几个操作:

  1. 是否是关闭链路的指令
  2. 是否是心跳消息
  3. 是否是消息内容

结语

这篇文章本来是想讲 NettyWebSocket 的整合的。但是我发现原来 WebSocket 的成长以及背后流行的原因远远没有没有想象的那么简单。所以决定深挖一下。总结一下文章内讲的内容

  1. WebSocket 相当于 Http 协议的一个长连接的补丁。它和 Http 存在的共性(连接握手使用 Get 请求),第一是为了解决 Http 不支持长连接的不足,解决了 Http 无法满足需求的短链接的特性。
  2. 在内部结构上,WebSocketHttp 有着众多的不同。为了提高效率,WebSocket 使用二进制帧,更加容易理解和传输。
  3. 使用 Netty 封装好的处理器能快速实现 WebSocket 的应用

完结!


原网址: 访问
创建于: 2023-09-20 14:48:06
目录: default
标签: 无

请先后发表评论
  • 最新评论
  • 总共0条评论