上篇文章我们讲了如何使用 Netty
来开发一个 Http
文件服务器,里面蕴含了关于如何使用Netty
提供的组件类来解析 Http
协议后进行请求的处理,然后再继续通过已有的组件来进行编解码和传输。
这篇文章主要讲的是,如何使用 Netty
整合WebSocket
的做一个 DEMO
文章。实际上,如 Http
一样,Netty
也对 WebSocket
封装提供了一些方便好用的组件,让你只要编码一下就可以使用了。这一点上,我再次体验到了 Netty
的高扩展性。
那么接下来,本文将会讲述一下内容
WebSocket
。WebSocket
与 Http
的区别。WebSocket
。Netty
和 WebSocket
之间的整合。WebSocket
大部人都是这样描述的
WebSocket
是HTML5
提供的一种浏览器与服务器之间进行全双工通信的网络技术。
看起来其实挺难懂的,HTML5
我们懂,但是 全双工通信
我们就不懂了。所以接下来我们来解析一下这是什么是 WebSocket
。
根据我们之前学习的,我们知道 Socket
是传输层和应用之间的一种功能接口,通过这些接口我们就可以使用 TCP/IP
协议栈在传输层收发数据了。那么 WebSocket
对于这个有什么关联之处呢?从 WebSocket
的字面意思来看,我们可以拆分称为了 Web
和 Socket
。可能你可以 GET
的到,是不是 WebSocket
就像是运行在 Web
上面,负责 Http
上的 Socket
通信规范?
的确是!WebSocket
可以说基于 Http
协议的 Socket
通讯规范,提供跟 TCP Socket
类似的功能,它可以像 TCP Socket
一样调用下层协议栈,任意地收发数据。但是千万不要以为 WebSocket
是 Http
的一个升级版(原因下面会说)。实际上,WebSocket
是一种基于 TCP
轻量级网络通讯协议,地位与 Http
是平级的。
WebSocket
?首先我们需要明白,同一领域的新事物的出现,它大概率不是为了颠覆其前者,一般是站在巨人的肩膀上继续完善。而 WebSocket
的出现,实际上就是为了弥补 Http
的缺陷。
根据 WebSocket
的介绍,我们知道它是全双工通信的网络技术。而 Http
它是一种半双工技术。Http
在这种技术下有两个特点
半双工会给我们带来什么问题呢?如果你做过实时信息的话,可能你就比较苦逼了。一般来说,实时通讯是需要双方的互动的,也就是你给他行,他给你发也行。但是很明显,半双工只能是客户端发给服务端,服务端不能发给客户端。或许你会说,那我客户端隔一段时间就去询问一下服务端不行吗?
实时上,再没有 WebSocket
的情况下,一般采用的是“轮询”的方式来实现即使通讯,也就是不断地请求服务端。如果轮询的频率比较高,那么就可以近似地实现实时通信的效果
但轮询的缺点也很明显,反复发送无效查询请求耗费了大量的带宽和 CPU
资源,非常不经济。
所以,WebSocket
这种全双工通信。
WebSocket
的特点或许还是很多人有疑问:明明我看到 WebSocket
是基于 Http
来进行交互的,它的协议格式不就是和 Http
有大同小异的地方吗?其实并不完全是。我们知道 Http
其实是当下互联网通讯协议的老大,没有之一。但是 WebSocket
并没有沿用很多 Http
的东西,相反,它有如下的特点
WebSocket
采用了二进制帧结构,与 Http
的结构其实完全不一样。但是为了能够方便推广和应用,不得不搭一下“便车”,在使用习惯上尽量向 Http
靠拢,这就是它名字里 Web
的含义。(下文我会说明为什么是搭便车)WebSocket
没有像 Socket
那样使用 IP+端口
的方式,而是沿用了 Http
的 URI
格式。但是 URL
的开头不是 Http
,而是 ws
和 wss
,分别是明文和加密的 WebSocket
协议。WebSocket
的默认端口还是使用了 80
和 443
。因为目前互联网上的服务器防火墙屏蔽了大多数的端口,只对 Http
的 80
和 443
放行,所以 WebSocket
就可以伪装成 Http
协议来穿透防火墙,与服务器建立连接。在讲 WebSocket
的特点的时候,有可能你稍稍知道了一下内幕:WebSocket
实际上和 Http
关系不大,只是 WebSocket
靠 Http
的“名声”腾飞的!
不急,让我们来看看 WebSocket
的交互顺序。下面是一张总图的交互图:
我们看到了,WebSocket
也有类似 TCP
的握手过程。它首先发出一个 Http
的 Get
请求,下面是报文的详细内容
复制代码
`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
}
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()
主要负责几个操作:
这篇文章本来是想讲 Netty
与 WebSocket
的整合的。但是我发现原来 WebSocket
的成长以及背后流行的原因远远没有没有想象的那么简单。所以决定深挖一下。总结一下文章内讲的内容
WebSocket
相当于 Http
协议的一个长连接的补丁。它和 Http
存在的共性(连接握手使用 Get
请求),第一是为了解决 Http
不支持长连接的不足,解决了 Http
无法满足需求的短链接的特性。WebSocket
和 Http
有着众多的不同。为了提高效率,WebSocket
使用二进制帧,更加容易理解和传输。Netty
封装好的处理器能快速实现 WebSocket
的应用完结!
原网址: 访问
创建于: 2023-09-20 14:48:06
目录: default
标签: 无
未标明原创文章均为采集,版权归作者所有,转载无需和我联系,请注明原出处,南摩阿彌陀佛,知识,不只知道,要得到
最新评论