1.目标:
一直想用netty做一个基于websocket的功能,但是实战前想系统的学习下什么是websocket协议,并且netty是如何做到和websocket结合的。基于这个目的,把自己此时此刻的心路历程记录下来,或许随着时间的推移,我能有更好的理解。
2.websocket连接三部曲
-
首先要完成 TCP 三次握手,浏览器要和服务器端建立稳定的连接。
-
HTTP 握手(升级协议)—— 最关键的一步:
客户端会先发一个普通的 HTTP 请求,但这个请求里藏了“暗号”。
GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Origin: http://example.com Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13Upgrade: websocket:这个请求头的作用非常直接,它明确地告诉服务器:“我不想继续用 HTTP 协议了,我想切换到
websocket协议。” - 在响应头中:当服务器同意升级时,它会在响应头中也带上Upgrade: websocket,意思是:“好的,我确认,我们将要切换到websocket协议。”Connection: Upgrade:告诉服务器或者中间代理,请注意,当前这个连接需要特殊处理,不要把它当作普通的 HTTP 连接来管理,我们要在这个连接上进行协议升级。
请求头 客户端发送时代表 服务器响应时代表 Upgrade: websocket我想升级到 WebSocket 协议。 我同意升级到 WebSocket 协议。 Connection: Upgrade请在当前连接上完成这次升级。 我同意在当前连接上完成升级。 服务端响应:
Server response: HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= Sec-WebSocket-Protocol: chat服务器一看请求 发现客户端想搞 WebSocket,如果自己支持。它就会返回一个特殊的代码 101(意思是:Switching Protocols,切换协议)。
-
第三步:全双工通信(自由聊天)
握手一旦成功,那条 TCP 连接就不再走 HTTP 协议了,而是变成了 WebSocket 协议
3.websocket一些细节
如果使用netty作为websocket服务端,那到底是谁负责完成握手和协议转换的呢?
回答:首先Netty得先按照HTTP进行解析,识别出是 WebSocket 请求后,动态替换成 WebSocket 解析器。 所以channelPipeline中需要先放入 HttpServerCodec,负责把字节流翻译成 HTTP 请求。因为 WebSocket 握手本质上是一个 HTTP 请求,所以Netty可以用这个编解码器HttpServerCodec把网卡收到的二进制数据(010101...)翻译成 Java 对象 HttpRequest。
也就是说:
-
第一阶段:刚连接时(HTTP 模式)
当客户端刚连上来发送握手请求时,数据流经过 Pipeline,首先遇到的是
HttpServerCodec。
- 它的作用是把二进制字节流解析成
HttpRequest。 - 这时候,数据还是 HTTP 包,根本不是 WebSocket Frame。
pipeline中会放入WebSocketServerProtocolHandler,专门用来识别websocket协议,完成握手,切换协议。
- 第二阶段:握手瞬间(关键变身)
当 HttpRequest 传到 WebSocketServerProtocolHandler 时,它发现这是一个握手请求(带有 Upgrade: websocket),就会进行协议升级,服务端会:
-
它会自动帮你校验握手信息。
-
发送握手响应(101 Switching Protocols)。
-
高能操作: 握手一旦成功,它会自动把 Pipeline 里那些“没用”的 HTTP 编解码器(比如
HttpServerCodec)移除掉,换上 WebSocket 的编解码器。
我们在初始化pipeline的时候,经常会如下操作:
pipeline.addLast(new HttpServerCodec()); // HTTP 编解码
pipeline.addLast(new HttpObjectAggregator(65536)); // 把 HTTP 消息聚合成完整的一个
// 核心在这里!
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
// 之后就是你自己的业务 Handler
pipeline.addLast(new MyBusinessHandler());
一旦 WebSocketServerProtocolHandler 完成了握手,上面的 HttpServerCodec 就失效了。此后传给 MyBusinessHandler 的不再是 HttpRequest,而是 TextWebSocketFrame 或 BinaryWebSocketFrame。
WebSocketServerProtocolHandler是怎么发挥作用的
WebSocketServerProtocolHandler 见名知意是一个websocket服务端协议的处理器。它作为一个inboud handler。在添加到pipeline的时候,会在它的前面添加一个WebSocketServerProtocolHandshakeHandler
public void handlerAdded(ChannelHandlerContext ctx) {
ChannelPipeline cp = ctx.pipeline();
if (cp.get(WebSocketServerProtocolHandshakeHandler.class) == null) {
// Add the WebSocketHandshakeHandler before this one.
cp.addBefore(ctx.name(), WebSocketServerProtocolHandshakeHandler.class.getName(),
new WebSocketServerProtocolHandshakeHandler(serverConfig));
}
if (serverConfig.decoderConfig().withUTF8Validator() && cp.get(Utf8FrameValidator.class) == null) {
// Add the UFT8 checking before this one.
cp.addBefore(ctx.name(), Utf8FrameValidator.class.getName(),
new Utf8FrameValidator(serverConfig.decoderConfig().closeOnProtocolViolation()));
}
}
WebSocketServerProtocolHandshakeHandler:它就是用来处理握手和协议转换的,底层是根据请求的不同版本,使用不同的WebSocketServerHandshaker 来处理握手。
public WebSocketServerHandshaker newHandshaker(HttpRequest req) {
CharSequence version = req.headers().get(HttpHeaderNames.SEC_WEBSOCKET_VERSION);
if (version != null) {
if (version.equals(WebSocketVersion.V13.toHttpHeaderValue())) {
// Version 13 of the wire protocol - RFC 6455 (version 17 of the draft hybi specification).
return new WebSocketServerHandshaker13(
webSocketURL, subprotocols, decoderConfig);
} else if (version.equals(WebSocketVersion.V08.toHttpHeaderValue())) {
// Version 8 of the wire protocol - version 10 of the draft hybi specification.
return new WebSocketServerHandshaker08(
webSocketURL, subprotocols, decoderConfig);
} else if (version.equals(WebSocketVersion.V07.toHttpHeaderValue())) {
// Version 8 of the wire protocol - version 07 of the draft hybi specification.
return new WebSocketServerHandshaker07(
webSocketURL, subprotocols, decoderConfig);
} else {
return null;
}
} else {
// Assume version 00 where version header was not specified
return new WebSocketServerHandshaker00(webSocketURL, subprotocols, decoderConfig);
}
}
有 三个版本,现在基本都用13版本
- WebSocketServerHandshaker13
- WebSocketServerHandshaker08
- WebSocketServerHandshaker07
WebSocketServerHandshaker13继承自WebSocketServerHandshaker,它是一个抽象类,用来统一处理握手 handshake。
public final ChannelFuture handshake(Channel channel, FullHttpRequest req,
HttpHeaders responseHeaders, final ChannelPromise promise) {
if (logger.isDebugEnabled()) {
logger.debug("{} WebSocket version {} server handshake", channel, version());
}
FullHttpResponse response = newHandshakeResponse(req, responseHeaders);
ChannelPipeline p = channel.pipeline();
if (p.get(HttpObjectAggregator.class) != null) {
p.remove(HttpObjectAggregator.class);
}
if (p.get(HttpContentCompressor.class) != null) {
p.remove(HttpContentCompressor.class);
}
ChannelHandlerContext ctx = p.context(HttpRequestDecoder.class);
final String encoderName;
if (ctx == null) {
// this means the user use an HttpServerCodec
ctx = p.context(HttpServerCodec.class);
if (ctx == null) {
promise.setFailure(
new IllegalStateException("No HttpDecoder and no HttpServerCodec in the pipeline"));
response.release();
return promise;
}
p.addBefore(ctx.name(), "wsencoder", newWebSocketEncoder());
p.addBefore(ctx.name(), "wsdecoder", newWebsocketDecoder());
encoderName = ctx.name();
} else {
p.replace(ctx.name(), "wsdecoder", newWebsocketDecoder());
encoderName = p.context(HttpResponseEncoder.class).name();
p.addBefore(encoderName, "wsencoder", newWebSocketEncoder());
}
channel.writeAndFlush(response).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (future.isSuccess()) {
ChannelPipeline p = future.channel().pipeline();
p.remove(encoderName);
promise.setSuccess();
} else {
promise.setFailure(future.cause());
}
}
});
return promise;
}
这里面有几个关键性的步骤:
1.FullHttpResponse response = newHandshakeResponse(req, responseHeaders); 这个交给子类来实现,构建响应来切换协议
2.在pipeline中塞入WS 解码器和WS 编码器
3.不同的开发者配置 Netty 的方式不一样(有人用 HttpServerCodec,有人分开用 HttpRequestDecoder 和 HttpResponseEncoder),这段代码兼容这两种情况
4.发完最后那个101 握手响应之后,就可以把http编码器移除了。
这就是“过河拆桥”的艺术。
- 握手响应(101) 本质上还是一个 HTTP 格式的报文。
- 所以,我们必须保留
HttpServerCodec或HttpResponseEncoder,直到这个 101 响应彻底发完。 - 一旦发完(
operationComplete),旧的 HTTP 工具类就彻底成了累赘。Netty 啪的一声把它们从 Pipeline 里踢出去(p.remove(encoderName))。等最后一份 HTTP 报文(101 响应)发出去的一瞬间,瞬间撤掉所有 HTTP 相关的组件。