1. 程式人生 > 其它 >websocket 代理tcp_netty實現websocket請求實戰

websocket 代理tcp_netty實現websocket請求實戰

技術標籤:websocket 代理tcpwebsocket: bad handshakewebsocket握手失敗websocket握手失敗200websocket握手失敗404window.location.href 加請求頭

描述

WebSocket是html5開始瀏覽器和服務端進行全雙工通訊的網路技術。在該協議下,與服務端只需要一次握手,之後建立一條快速通道,開始互相傳輸資料,實際是基於TCP雙向全雙工,比http半雙工提高了很大的效能,常用於網路線上聊天室等。繼續netty例項的學習,本期內容主要做WebSocket例項的實現和相關協議的驗證。

WebSocket與http比較

httpWebSocket
半雙工,可以雙向傳輸,不能同時傳輸全雙工
訊息冗長繁瑣,訊息頭,訊息體,換行...對代理、防火牆、路由器透明
http輪詢實現推送請求量大,而comet採用長連線無頭部、Cookie等
-ping/pong幀保持鏈路啟用
-特點:服務端可以主動傳遞給客戶端,不需要輪詢

程式碼示例和執行結果

服務端

服務端比較簡單,是啟動一個埠來處理請求。

主程式

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.stream.ChunkedWriteHandler;
import org.junit.Test;

public class NettyWebSocketServer {

public void run(final int port) {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();

try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline()
.addLast(new HttpServerCodec())
.addLast(new HttpObjectAggregator(65535))
.addLast(new ChunkedWriteHandler())
.addLast(new WebSocketServerHandler());
}
});
Channel ch = bootstrap.bind(port).sync().channel();
System.out.println("websocket @" + port);
ch.closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
}
}

@Test
public void runServer() {
run(23123);
}
}

請求處理handler

核心處理WebSocket請求,重點地方加了註釋

import io.netty.buffer.ByteBufUtil;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http.websocketx.*;

import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;

public class WebSocketServerHandler extends SimpleChannelInboundHandler<Object> {
private WebSocketServerHandshaker handshaker;

@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
//首次請求之後先進行握手,通過http請求來實現
if (msg instanceof FullHttpRequest) {
handleHttpRequest(ctx, (FullHttpRequest) msg);
} else if (msg instanceof WebSocketFrame) {
handleWebSocketRequest(ctx, (WebSocketFrame) msg);
}
}

private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest request) {
//解碼http失敗返回
if (!request.decoderResult().isSuccess()) {
sendResponse(ctx, request, new DefaultFullHttpResponse(request.protocolVersion(), HttpResponseStatus.BAD_REQUEST, ctx.alloc().buffer()));
return;
}

if (!HttpMethod.GET.equals(request.method())) {
sendResponse(ctx, request, new DefaultFullHttpResponse(request.protocolVersion(), HttpResponseStatus.FORBIDDEN, ctx.alloc().buffer()));
return;
}

//引數分別是ws地址,子協議,是否擴充套件,最大frame長度
WebSocketServerHandshakerFactory factory = new WebSocketServerHandshakerFactory(getWebSocketLocation(request), null, true, 5 * 1024 * 1024);
handshaker = factory.newHandshaker(request);
if (handshaker == null) {
WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
} else {
handshaker.handshake(ctx.channel(), request);
}
}

//SSL支援採用wss://
private String getWebSocketLocation(FullHttpRequest request) {
String location = request.headers().get(HttpHeaderNames.HOST) + "/websocket";
return "ws://" + location;
}

private void handleWebSocketRequest(ChannelHandlerContext ctx, WebSocketFrame frame) {
//關閉
if (frame instanceof CloseWebSocketFrame) {
handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());
return;
}

//握手 PING/PONG
if (frame instanceof PingWebSocketFrame) {
ctx.write(new PongWebSocketFrame(frame.content().retain()));
return;
}

//文字接收和傳送
if (frame instanceof TextWebSocketFrame) {
String recv = ((TextWebSocketFrame) frame).text();
String text = "[email protected]" + LocalDateTime.now() + "\n recv:" + recv;

//這裡加了個迴圈,每隔一秒服務端主動傳送內容給客戶端
for (int i = 0; i < 5; i++) {
String res = i + "-" + text;
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
ctx.writeAndFlush(new TextWebSocketFrame(res));
}
System.out.println(recv);
return;
}

if (frame instanceof BinaryWebSocketFrame) {
ctx.write(frame.retain());
}
}

@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}

private void sendResponse(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse resp) {
HttpResponseStatus status = resp.status();
if (status != HttpResponseStatus.OK) {
ByteBufUtil.writeUtf8(resp.content(), status.toString());
HttpUtil.setContentLength(req, resp.content().readableBytes());
}
boolean keepAlive = HttpUtil.isKeepAlive(req) && status == HttpResponseStatus.OK;
HttpUtil.setKeepAlive(req, keepAlive);
ChannelFuture future = ctx.write(resp);
if (!keepAlive) {
future.addListener(ChannelFutureListener.CLOSE);
}
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}

客戶端

客戶端使用了html來實現,可以直接在瀏覽器開啟,程式碼比較簡單,不做多的介紹。本地使用注意ws地址和服務端的一致。

<html>
<head>
<meta charset="UTF-8">
head>
<body>
<form onsubmit="return false;">
<input type="text" name="message" value="test"/>
<br/>
<input type="button" value="send", onclick="send(this.form.message.value)"/>
<hr/>
resp:
<br/>
<textarea id="responseText" style="width: 200px;height:300px;">textarea>
form>

form>
body>
<script type="text/javascript">var socket;if (!window.WebSocket) {
window.WebSocket = window.MozWebSocket;}if (window.WebSocket) {
socket = new WebSocket("ws://127.0.0.1:23123/websocket");
socket.onmessage = function (ev) {var ta = document.getElementById('responseText');
ta.value = '';
ta.value = ev.data;}
socket.onopen = function (ev) {var ta = document.getElementById('responseText');
ta.value = 'start open websocket ...';}
socket.onclose = function (ev) {var ta = document.getElementById('responseText');
ta.value = '';
ta.value = 'close websocket ...';}}else {alert('not support websocket !')}function send(msg) {if (!window.WebSocket) {return;}if (socket.readyState == WebSocket.OPEN) {
socket.send(msg);}else {alert('connect fail !')}}script>
html>

試驗結果

  1. 請求抓包說明

    7e904f008a400bc1ecd3bf91b07e5ede.png

    ws請求

    如圖中,首先是TCP三次握手,然後HTTP請求,建立WebSocket連線。之後客戶端請求一次,然後服務端每隔1秒傳送內容到客戶端。

    90b892c93cf26c89c7598681383ad80c.png

    keepAlive


    上圖是空閒時候的keepAlive呼叫

  2. 客戶端呼叫

    66781c342a07e35f436c7511be59c313.png

    服務端關閉的時候效果

    677e816609b4c1f796f54f0c2b286c6a.png

    服務端主動傳送的效果

    上圖是服務端主動傳送的效果,隨著內容不同資料會變化。

參考資料

[1]示例原始地址:github
[2]《Netty權威指南(第二版)》P213

結語

以上就是本期的內容,後面有時間會繼續netty相關例項的文章,確實在網路方面有很多新的收穫。