精品伊人久久大香线蕉,开心久久婷婷综合中文字幕,杏田冲梨,人妻无码aⅴ不卡中文字幕

打開APP
userphoto
未登錄

開通VIP,暢享免費電子書等14項超值服

開通VIP
Netty4實戰第十一章:WebSockets
本章主要內容
WebSocket
ChannelHandler,Decoder和Encoder
啟動基于Netty的應用
測試WebSocket
現在很多地方都可以看到“real-time-web”這個術語。大部分用戶在訪問網站時希望能實時獲取信息。
Netty提供了很多WebSocket相關的支持,包括不同的版本。在你的應用中可以無腦使用這些技術。因為你不需要詳細了解那些底層協議相關的知識,只需要使用它提供的簡單的API即可。
本章我們將學習開發一個關于WebSocket的例子,來理解如何在實際項目中使用它。你也可以將例子中的一些代碼集成到自己的應用中,復用這些代碼。
一、挑戰
為了說明WebSocket的實時性,這個例子我們將會用WebSocket實現一個WEB版本的即時通訊應用,類似QQ那樣。這個應用很類似QQ中的群聊天功能,一個用戶發消息,其他用戶都能收到。
從上圖可以看出,一個用戶發了消息,然后服務端轉發給其他用戶。不過我們主要實現服務器端部分,因為客戶端我們會使用瀏覽器。現在就開始實現這個應用。
二、實現
WebSocket使用HTTP升級機制將HTTP連接轉成WebSocket連接。因此WebSocket應用都是先從HTTP開始,然后升級到WebSocket。升級發生的時間是應用指定的。有的是上來就升級,有的是訪問了指定的URL之后才升級。
這個例子中,當URL以/ws結尾時才開始升級到WebSocket,否則就返回一個網頁給客戶端。一旦升級之后就通過WebSocket傳輸數據。
主要的邏輯都通過ChannelHandler來實現,方便以后復用。下一小節就詳細介紹這些ChannelHandler。
2.1、處理HTTP請求
上一小節說過,服務端將同時處理HTTP請求和WebSocket請求,因為服務端也要返回HTML頁面,比如聊天室頁面,用于客戶端展示。
因此我們需要編寫一個ChannelInboundHandler,處理客戶端的HTTP請求,消息實體使用FullHttpRequest。
[java]
package com.nan.netty.chatroom;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.stream.ChunkedNioFile;
import java.io.RandomAccessFile;
public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
private final String wsUri;
public HttpRequestHandler(String wsUri) {
this.wsUri = wsUri;
}
@Override
public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
if (wsUri.equalsIgnoreCase(request.uri())) {
//如果是WebSocket請求,則保留數據并傳遞到下一個ChannelHandler
ctx.fireChannelRead(request.retain());
} else {
if (HttpUtil.is100ContinueExpected(request)) {
//收到100-continue,則返回給客戶端100
send100Continue(ctx);
}
boolean keepAlive;
ChannelFuture future;
try (RandomAccessFile file = new RandomAccessFile(this.getClass().getResource("/").getPath() + "../resources/index.html", "r")) {
HttpResponse response = new DefaultHttpResponse(request.protocolVersion(), HttpResponseStatus.OK);
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8");
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, file.length());
keepAlive = HttpUtil.isKeepAlive(request);
if (keepAlive) {
//如果需要keep-alive,則添加相應頭信息
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
}
ctx.write(response);
if (ctx.pipeline().get(SslHandler.class) == null) {
//不用加密使用零內存復制發送文件
future = ctx.writeAndFlush(new DefaultFileRegion(file.getChannel(), 0, file.length()));
} else {
future = ctx.writeAndFlush(new ChunkedNioFile(file.getChannel()));
}
}
//如果不是keep-alive,則關閉Channel
if (!keepAlive) {
future.addListener(ChannelFutureListener.CLOSE);
}
}
}
private static void send100Continue(ChannelHandlerContext ctx) {
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
ctx.writeAndFlush(response);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
上面實現的HttpRequestHandler坐了以下的事情:
首先檢查請求路徑是否是WebSocket,如果是WebSocket則使用retain()方法和fireChannelRead(msg)方法將數據傳遞下去。
如果客戶端發送的100,則返回100。
如果不是WebSocket請求,則返回一個HttpResponse,返回的內容就是一個HTML文件的內容。
當然,上邊這個ChannelHandler只處理了普通HTTP的請求,我們還需要處理WebSocket的請求,然后將收到的數據轉發,下一小節就來學習這個知識點。
2.2、處理WebSocket請求
Netty提供了6種不同的WebSocket類型,如下表所示。
名稱描述
BinaryWebSocketFrame
二進制數據類型
TextWebSocketFrame
文本數據類型
ContinuationWebSocketFrame
屬于前一個二進制類型或文本類型
CloseWebSocketFrame
關閉請求
PingWebSocketFrame
請求發送了PongWebSocketFrame
PongWebSocketFrame
響應發送了PingWebSocketFrame
而我們的應用,一般只需要處理下面四種類型:
CloseWebSocketFrame
PingWebSocketFrame
PongWebSocketFrame
TextWebSocketFrame
幸運的是,其實只需要處理TextWebSocketFrame,其他的可以使用Netty提供的WebSocketServerProtocolHandler處理,Netty幫助開發者簡化開發工作,下面的代碼就是處理TextWebSocketFrame的ChannelHandler。
[java]
package com.nan.netty.chatroom;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
public class TextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
private final ChannelGroup group;
public TextWebSocketFrameHandler(ChannelGroup group) {
this.group = group;
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
//當WebSocket連接成功
if (evt == WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE) {
//不需要再處理HTTP請求
ctx.pipeline().remove(HttpRequestHandler.class);
//告訴已經連接的客戶端有新的客戶端連接進來了
group.writeAndFlush(new TextWebSocketFrame("Client " + ctx.channel() + " joined"));
//將新的客戶端添加到ChannelGroup中
group.add(ctx.channel());
} else {
super.userEventTriggered(ctx, evt);
}
}
@Override
public void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
//消息轉發給每一個客戶端
group.writeAndFlush(msg.retain());
}
}
主要的業務邏輯已經實現完了,現在只需要再實現一個ChannelInitializer,用來初始化每個Channel。2.3、初始化ChannelPipeline
最后一步就是需要初始化ChannelPipeline,添加所有需要的ChannelHandler。前面的章節講過,只需要繼承ChannelInitializer,然后重寫initChannel(…)方法即可,如下面的代碼。
[java]
package com.nan.netty.chatroom;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.group.ChannelGroup;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
public class ChatServerInitializer extends ChannelInitializer<Channel> {
private final ChannelGroup group;
public ChatServerInitializer(ChannelGroup group) {
this.group = group;
}
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new ChunkedWriteHandler());
pipeline.addLast(new HttpObjectAggregator(64 * 1024));
pipeline.addLast(new HttpRequestHandler("/ws"));
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
pipeline.addLast(new TextWebSocketFrameHandler(group));
}
}
大部分情況下initChannel(...)方法都是用來設置新注冊Channel的ChannelPipeline,主要代碼就是將應用中需要的ChannelHandler添加到ChannelPipeline中。
我們來看看上邊的代碼中我們添加的那些ChannelHandler的作用。
名稱
描述
HttpServerCodec
解碼HTTP請求,編碼HTTP響應
ChunkedWriteHandler
用來寫文件內容
HttpObjectAggregator
解碼HttpRequest/HttpContent/LastHttpContent聚合成FullHttpRequest,
使用它你接收的使用FullHttpRequest
HttpRequestHandler
這個我們自定義的實現,普通HTTP請求返回HTML,WebSocket傳遞給
下一個ChannelHandler
WebSocketServerProtocolHandler
處理WebSocket請求的Ping/Pong/Close事件
TextWebSocketFrameHandler
我們自定義實現的處理WebSocket文本消息,轉發給其他客戶端
WebSocketServerProtcolHandler比較特殊一些,所以這里多做一些介紹。WebSocketServerProtocol只處理WebSocket的Ping/Pong/Close請求,所以可以通過它幫忙將應用升級到WebSocket協議。
通過執行握手連接,一旦成功后就可以修改ChannelPipeline,添加需要的編解碼器,把不需要的編解碼器移除。我們剛初始化后的ChannelPipeline如下圖。
一旦WebSocket握手完成,則就會發生變化。WebSocketServerProtocolHandler會將HttpRequestDecoder替換成WebSocketFrameDecoder,將HttpResponseEncoder替換成WebSocketFrameEncoder。除此之外它還會移除其他不需要的ChannelHandler,例如我們本例中HttpObjectAggregator,然后我們在實現代碼中移除了HttpRequestHandler,升級到WebSocket后的ChannelPipeline如下圖。
上面ChannelPipeline的更新是Netty背后幫我們完成的,這種方式還是很靈活的,并且可以將不同的任務分配到不同的ChannelHandler中。
三、編寫服務端啟動類和客戶端
主要的業務邏輯都已經完成,現在只需要按照Netty提供的固定套路,設置服務端啟動器然后啟動服務端即可。
[java]
package com.nan.netty.chatroom;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.util.concurrent.ImmediateEventExecutor;
import java.net.InetSocketAddress;
public class ChatServer {
//使用DefaultChannelGroup保存所有WebSocket客戶端
private final ChannelGroup channelGroup = new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
private final EventLoopGroup group = new NioEventLoopGroup();
private Channel channel;
/**
* 啟動服務端
*/
public ChannelFuture start(InetSocketAddress address) {
//服務端啟動器
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(group)
.channel(NioServerSocketChannel.class)
.childHandler(createInitializer(channelGroup));
ChannelFuture future = bootstrap.bind(address);
future.syncUninterruptibly();
channel = future.channel();
return future;
}
/**
* 創建Channel初始化器
*/
protected ChannelInitializer<Channel> createInitializer(ChannelGroup channelGroup) {
return new ChatServerInitializer(channelGroup);
}
/**
* 釋放資源
*/
public void destroy() {
if (channel != null) {
channel.close();
}
channelGroup.close();
group.shutdownGracefully();
}
/**
* 主方法
*/
public static void main(String[] args) {
//服務端監聽端口
int port = 9999;
final ChatServer endpoint = new ChatServer();
ChannelFuture future = endpoint.start(new InetSocketAddress(port));
Runtime.getRuntime().addShutdownHook(new Thread(() -> endpoint.destroy()));
future.channel().closeFuture().syncUninterruptibly();
}
}
服務端的代碼已經完成,現在我們實現一個客戶端。本例中我們使用瀏覽器作為客戶端,所以需要編寫一個HTML頁面,并使用JS的WebSocket API與服務端連接并通信。
[html]
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>聊天室</title>
</head>
<body style="display: flex; justify-content: center; align-items: center">
<form onsubmit="return false;">
<h3>WebSocket聊天室:</h3>
<div>
<textarea id="responseText" style="width: 500px; height: 300px;"></textarea>
</div>
<div>
<input type="text" name="message"  style="width: 430px" value="我是Netty">
<input type="button" value="發送消息" onclick="send(this.form.message.value)">
</div>
</form>
<script type="text/javascript">
var socket;
if (!window.WebSocket) {
window.WebSocket = window.MozWebSocket;
}
if (window.WebSocket) {
socket = new WebSocket("ws://localhost:9999/ws");
socket.onmessage = function(event) {
console.log(event);
var ta = document.getElementById('responseText');
ta.value = ta.value + '\n' + event.data
};
socket.onopen = function(event) {
var ta = document.getElementById('responseText');
ta.value = "連接開啟!";
};
socket.onclose = function(event) {
var ta = document.getElementById('responseText');
ta.value = ta.value + "連接被關閉";
};
} else {
alert("你的瀏覽器不支持 WebSocket!");
}
function send(message) {
if (!window.WebSocket) {
return;
}
if (socket.readyState == WebSocket.OPEN) {
socket.send(message);
} else {
alert("連接沒有開啟.");
}
}
</script>
</body>
</html>
這個HTML文件的存放位置要和HttpRequestHandler代碼中的路徑一致,不然會發生找不到文件異常,比如本例中,我使用的是Gradle工具,所以將這個文件放到了Resources目錄下。
現在就可以嘗試一下我們的聊天室了。首先啟動服務端,然后在瀏覽器中輸入http://localhost:9999/,最好使用Chrome瀏覽器,然后就能看到如下頁面。
然后再打開一個瀏覽器標簽,同樣訪問地址http://localhost:9999/,可以同樣看到上圖畫面,并且第一個打開的瀏覽器頁面會編程如下所示。
說明另一個客戶端已經成功連接,并且通知了已經存在的客戶端,當然,發消息功能也是沒問題。
四、加密
上面我們已經實現了聊天室的主要功能,但是可能有人會提出,消息內容很私密,需要我們需要在傳輸過程中加密。
一般來說,添加加密功能不能一件容易的工作,往往需要大改項目。但是因為我們使用的是Netty,只需要將SslHandler添加到ChannelPipeline即可。雖然說還需要改一點點代碼,但也是很簡單的修改,不過如果還需要配置SslContext,可能就會麻煩一些。
總體來說,使用Netty添加加密功能還是很簡單的,主要是ChannelPipeline的存在,這里再次感謝它。
因為我們需要將SslHandler添加到ChannelPipeline中,所以需要改造一下ChatServerInitializer,這里我們繼承ChatServerInitializer再開發一個有加密功能的初始化類。
[java]
package com.nan.netty.chatroom;
import io.netty.channel.Channel;
import io.netty.channel.group.ChannelGroup;
import io.netty.handler.ssl.SslHandler;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
public class SecureChatServerInitializer extends ChatServerInitializer {
private final SSLContext context;
public SecureChatServerInitializer(ChannelGroup group, SSLContext context) {
super(group);
this.context = context;
}
@Override
protected void initChannel(Channel ch) throws Exception {
//父類原樣初始化
super.initChannel(ch);
SSLEngine engine = context.createSSLEngine();
engine.setUseClientMode(false);
//加密解密的SslHandler需要放到首位
ch.pipeline().addFirst(new SslHandler(engine));
}
}
安全的ChannelInitializer邏輯很簡單,就是將SslHandler添加到ChannelPipeline第一個位置。下一步呢,我們就是需要在啟動服務端時候使用這個新的SecureChatServerInitializer,并且在構造的時候將SSLContext傳入,但是SSLContext是需要SSL證書的,這里為了方便就不向專業結構申請了,直接使用JDK自帶的keytool工具生成自簽名證書,當然這個證書是不被瀏覽器認可的,所以不能用在生產環境中。
我這邊使用Windows系統,Linux或其他系統基本操作一致。打開CMD窗口,執行下面命令。
[java]
keytool -genkey -keysize 2048 -validity 365 -keyalg RSA -keypass netty123 -storepass netty123 -keystore netty.jks
keytool就是JDK自帶的工具,大家應該把JAVA安裝目錄下面的bin目錄加到環境變量中了吧,參數意義如下。
-keysize 2048 密鑰長度2048位
-validity 365 證書有效期365天
-keyalg RSA 使用RSA非對稱加密算法
-keypass netty123 密鑰的訪問密碼
-storepass netty123 密鑰庫的訪問密碼
-keystore netty.jks 指定生成的密鑰庫文件
然后需要填寫一些信息,最后確認一步輸入“y”即可,如下圖。
有的情況下會出現無權限訪問文件的問題,就要看看你的用戶有沒有目錄權限了,Windows系統的同學可以打開具有管理員權限的CMD窗口解決這種問題。
然后將生成的證書復制到指定目錄,我這里還是放到項目的Resources目錄下,然后編寫安全版本的啟動類。
[java]
package com.nan.netty.chatroom;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.group.ChannelGroup;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.security.KeyStore;
public class SecureChatServer extends ChatServer {
private final SSLContext context;
public SecureChatServer(SSLContext context) {
this.context = context;
}
@Override
protected ChannelInitializer<Channel> createInitializer(ChannelGroup group) {
//Channel初始化使用安全版本的
return new SecureChatServerInitializer(group, context);
}
public static void main(String[] args) {
int port = 9999;
SSLContext sslContext = null;
//讀取SSL證書文件,配置SSLContext
try (InputStream ksInputStream = new FileInputStream(SecureChatServer.class.getResource("/").getPath() + "../resources/netty.jks")) {
KeyStore ks = KeyStore.getInstance("JKS");
ks.load(ksInputStream, "netty123".toCharArray());
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(ks, "netty123".toCharArray());
sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), null, null);
} catch (Exception e) {
e.printStackTrace();
}
final SecureChatServer endpoint = new SecureChatServer(sslContext);
ChannelFuture future = endpoint.start(new InetSocketAddress(port));
Runtime.getRuntime().addShutdownHook(new Thread(() -> endpoint.destroy()));
future.channel().closeFuture().syncUninterruptibly();
}
}
現在還有最后一步,將之前的HTML文件里面的WebSocket連接修改為wss://localhost:9999/ws,因為我們使用了Ssl連接,那自然WebSocket協議也要修改成Ssl連接的。
[java]
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>聊天室</title>
</head>
<body style="display: flex; justify-content: center; align-items: center">
<form onsubmit="return false;">
<h3>WebSocket聊天室:</h3>
<div>
<textarea id="responseText" style="width: 500px; height: 300px;"></textarea>
</div>
<div>
<input type="text" name="message"  style="width: 430px" value="我是Netty">
<input type="button" value="發送消息" onclick="send(this.form.message.value)">
</div>
</form>
<script type="text/javascript">
var socket;
if (!window.WebSocket) {
window.WebSocket = window.MozWebSocket;
}
if (window.WebSocket) {
socket = new WebSocket("wss://localhost:9999/ws");
socket.onmessage = function(event) {
console.log(event);
var ta = document.getElementById('responseText');
ta.value = ta.value + '\n' + event.data
};
socket.onopen = function(event) {
var ta = document.getElementById('responseText');
ta.value = "連接開啟!";
};
socket.onclose = function(event) {
var ta = document.getElementById('responseText');
ta.value = ta.value + "連接被關閉";
};
} else {
alert("你的瀏覽器不支持 WebSocket!");
}
function send(message) {
if (!window.WebSocket) {
return;
}
if (socket.readyState == WebSocket.OPEN) {
socket.send(message);
} else {
alert("連接沒有開啟.");
}
}
</script>
</body>
</html>
好了,代碼都寫完了,現在我們啟動安全版的服務端,在瀏覽器輸入https://localhost:9999/,一般瀏覽器都會提示這是不安全的連接,因為我們的證書是自簽名的嘛。剩下的聊天功能和前面的一樣,這里就不詳說了。
五、總結
這一章我們主要學習了如何使用Netty提供的WebSocket開發一個聊天室功能。學習了如何處理HTTP請求,已經如何轉到WebSocket請求,并知道如何轉發數據。也應該明白了為什么WebSocket相比較HTTP比較重要,當然也大概能知道WebSocket的使用場景。
下一章中我們將學習Web 2.0其他的東西,這些東西可以增加你的應用更有魅力。下一章主要內容就是SPDY,看看它的優點,已經適合什么樣的業務場景。
本站僅提供存儲服務,所有內容均由用戶發布,如發現有害或侵權內容,請點擊舉報
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
Netty筆記:使用WebSocket協議開發聊天系統
WebSocket 實現 Web 端即時通信
Netty4.x中文教程系列(七)UDP協議
netty+mqtt
基于Google Protobuf的Netty編解碼技術
AttributeMap屬性
更多類似文章 >>
生活服務
分享 收藏 導長圖 關注 下載文章
綁定賬號成功
后續可登錄賬號暢享VIP特權!
如果VIP功能使用有故障,
可點擊這里聯系客服!

聯系客服

主站蜘蛛池模板: 曲松县| 绥芬河市| 广南县| 高台县| 阳曲县| 德清县| 龙江县| 莱芜市| 阳泉市| 华池县| 霞浦县| 孟村| 常宁市| 枣强县| 和田市| 东阳市| 荔浦县| 桦川县| 拉萨市| 德兴市| 刚察县| 兴化市| 大渡口区| 土默特左旗| 宜都市| 清徐县| 四子王旗| 大悟县| 乐都县| 云龙县| 高碑店市| 嘉定区| 赤壁市| 荆门市| 广水市| 镇平县| 玛曲县| 福海县| 贡觉县| 宣武区| 临泽县|