Netty With WebSocket
上篇文章我们讲了如何使用 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 的特点的时候,有可能你稍稍知道了一下内幕: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
+ " , 欢迎使用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() 主要负责几个操作:
- 是否是关闭链路的指令
- 是否是心跳消息
- 是否是消息内容
结语
这篇文章本来是想讲 Netty 与 WebSocket 的整合的。但是我发现原来 WebSocket 的成长以及背后流行的原因远远没有没有想象的那么简单。所以决定深挖一下。总结一下文章内讲的内容
WebSocket相当于Http协议的一个长连接的补丁。它和Http存在的共性(连接握手使用Get请求),第一是为了解决Http不支持长连接的不足,解决了Http无法满足需求的短链接的特性。- 在内部结构上,
WebSocket和Http有着众多的不同。为了提高效率,WebSocket使用二进制帧,更加容易理解和传输。 - 使用
Netty封装好的处理器能快速实现WebSocket的应用
完结!