HTTP 協議的弊端
- HTTP 協議爲半雙工協議. 半雙工協議指數據可以在客戶端和服務端兩個方向上傳輸, 但是不能同時傳輸. 它意味這同一時刻, 只有一個方向上的數據傳輸; 客戶端發送請求, 服務器等待, 直到收到完整的請求. 然後發送迴應, 客戶端和服務器無法同時發送.
- HTTP 消息冗長而繁瑣. HTTP 消息包含消息頭、消息頭、換行符等, 通常情況下采用文本方式傳輸, 相比於其他的二進制通信協議, 冗長而繁瑣;
- 針對服務器推送的黑客攻擊. 例如長時間輪詢.
WebSocket 入門
webSocket 是 HTML5 開始提供的一種瀏覽器於服務器間進行全雙工通信的技術.
在 WebSocket API 中, 瀏覽器和服務器只需要做一個握手的動作, 然後, 瀏覽器和服務器之間就形成了一條快速通道, 兩者就可以直接相互傳送數據了. WebSocket 基於 TCP 雙向全雙工進行消息傳遞, 在同一時刻, 既可以發送消息, 也可以接收消息, 相比 HTTP 的半雙工協議, 性能得到很大提升.
WebSocket 的特點:
- 單一的 TCP 連接, 採用全雙工模式通信;
- 對代理、防火牆和路由器透明;
- 無頭部信息、Cookie和身份驗證;
- 無安全開銷;
- 通過
ping/pong
幀保持鏈路激活; - 服務器可以主動傳遞消息給客戶端, 不再需要客戶端輪詢.
WebSocket 連接建立
建立 webSocket 連接時, 需要通過客戶端或瀏覽器發出握手請求, 類似下面的 http 報文.
這個請求和通常的 HTTP 請求不同, 包含了一些附加頭信息, 其中附加頭信息 Upgrade:WebSocket
表明這是一個申請協議升級的 HTTP 請求.
服務器解析這些附加的頭信息, 然後生成應答信息返回給客戶端, 客戶端和服務端的 WebSocket 連接就建立起來了, 雙方可以通過這個連接通道自由的傳遞信息, 並且這個連接會持續存在直到客戶端或服務端的某一方主動關閉連接.
服務端返回給客戶端的應答消息, 類似如下報文
請求消息中的 Sec-WebSocket-Key
是隨機的, 服務端會用這些數據來構造出一個 SHA-1 的信息摘要, 把 Sec-WebSocket-Key
加上一個魔幻字符串 258EAFA5-E914-47DA-95CA-C5AB0DC85B11
. 使用 SHA-1 加密, 然後進行 BASE-64 編碼, 將結果做爲 Sec-WebSocket-Accept
頭的值, 返回給客戶端.
WebSocket 生命週期
握手成功之後, 服務端和客戶端就可以通過 messages
的方式進行通訊, 一個消息由一個或多個幀組成.
幀都有自己對應的類型, 屬於同一個消息的多個幀具有相同類型的數據. 從廣義上講, 數據類型可以是文本數據(UTF-8文字)、二進制數據和控制幀(協議級信令, 例如信號).
WebSocket 連接生命週期如下:
Netty WebSocket 協議開發
示例代碼
public class TimeServer {
public void bind(int port) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.handler(new LoggingHandler(LogLevel.DEBUG))
.childHandler(new ChildChannelHandler());
// 綁定端口, 同步等待成功
ChannelFuture f = b.bind(port).sync();
// 等待服務端監聽端口關閉
f.channel().closeFuture().sync();
} finally {
System.out.println("shutdownGracefully");
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast("http-codec", new HttpServerCodec());
ch.pipeline().addLast("aggregator", new HttpObjectAggregator(65536));
ch.pipeline().addLast("http-chunked", new ChunkedWriteHandler());
ch.pipeline().addLast("handler", new WebSOcketServerHandler());
}
}
private class WebSOcketServerHandler extends SimpleChannelInboundHandler<Object> {
private WebSocketServerHandshaker handshaker;
@Override
protected void messageReceived(ChannelHandlerContext ctx, Object msg) throws Exception {
// 傳統的 HTTP 接入
if (msg instanceof FullHttpRequest) {
System.out.println("傳統的 HTTP 接入");
handleHttpRequest(ctx, (FullHttpRequest) msg);
}
// WebSocket 接入
else if (msg instanceof WebSocketFrame) {
System.out.println("WebSocket 接入");
handleWebSocketFrame(ctx, (WebSocketFrame) msg);
}
}
private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) throws Exception {
// 如果 HTTP 解碼失敗, 返回HTTP異常
if (!req.getDecoderResult().isSuccess() || (!"websocket".equalsIgnoreCase(req.headers().get("Upgrade")))) {
sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.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();
ctx.channel().write(new TextWebSocketFrame(request + " , 歡迎使用 Netty WebSocket 服務, 現在時刻: "
+ new java.util.Date().toString()));
}
private 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();
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
}
}
HttpServerCodec
: 將請求和應答消息編碼或解碼爲 HTTP 消息.HttpObjectAggregator
: 它的目的是將 HTTP 消息的多個部分組合成一條完整的 HTTP 消息. Netty 可以聚合 HTTP 消息, 使用 FullHttpResponse
和 FullHttpRequest
到 ChannelPipeline
中的下一個 ChannelHandler
, 這就消除了斷裂消息, 保證了消息的完整.ChunkedWriteHandler
: 來向客戶端發送 HTML5 文件, 主要用於支持瀏覽器和服務端進行 WebSocket 通信.
第一次握手請求消息由 HTTP 協議承載, 所以它是一個 HTTP 消息, 執行 handleHttpRequest
方法來處理 WebSocket 握手請求. 通過判斷請求消息判斷是否包含 Upgrade
字段或它的值不是 websocket, 則返回 HTTP 400 響應.
握手請求校驗通過之後, 開始構造握手工廠, 創建握手處理類 WebSocketServerHandshaker
, 通過它構造握手響應消息返回給客戶端.
添加 WebSocket Encoder 和 WebSocket Decoder 之後, 服務端就可以自動對 WebSocket 消息進行編解碼了, 後面的 handler 可以直接對 WebSocket 對象進行操作.
handleWebSocketFrame
對消息進行判斷, 首先判斷是否是控制幀, 如果是就關閉鏈路. 如果是維持鏈路的 Ping
消息, 則構造 Pong
消息返回. 由於本例程的 WebSocket 通信雙方使用的都是文本消息, 所以對請求新消息的類型進行判斷, 而不是文本的拋出異常.
最後, 從 TextWebSocketFrame
中獲取請求消息字符串, 對它處理後通過構造新的 TextWebSocketFrame
消息返回給客戶端, 由於握手應答時, 動態增加了 TextWebSocketFrame
的編碼類, 所以可以直接發送 TextWebSocketFrame
對象.