Netty 高階應用
1. 編解碼器
概念:在網路應用中,需要實現某種編解碼器。將原始位元組資料與自定義訊息資料進行相互轉換。網路中都是以位元組碼的形式傳輸的。
對Netty而言,編解碼器由兩部分組成:編碼器、解碼器
- 編碼器:將訊息物件轉為位元組或其他序列形式在網路上傳輸
- 解碼器:負責將位元組或其他序列形式轉為指定的訊息物件
Netty的編解碼器實現了ChannelHandlerAdapter,也是一種特殊的ChannelHandler,所以依賴與ChannelPipeline,可以將多個編解碼器連結在一起,以實現複雜的轉換邏輯。
- 解碼器
- ByteToMessageDecoder:用於將位元組轉為訊息,需要檢查緩衝區是否有足夠的位元組
- ReplayingDecoder:繼承ByteToMessageDecoder,不需要檢查緩衝區是否有足夠的位元組,但是ReplayingDecoder速度略慢於ByteToMessageDecoder,同時不是所有的ByteBuf都支援。專案複雜性高則使用ReplayingDecoder,否則使用ByteToMessageDecode
- MessageToMessageDecoder:用於從一種訊息解碼為另一種訊息(如POJO到POJO)
解碼器示例:
public class DemoDecoder extends MessageToMessageDecoder<ByteBuf> {
@Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
String msg = byteBuf.toString(CharsetUtil.UTF_8);
list.add(msg);
}
}
通道里加入解碼器:
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new DemoDecoder());
socketChannel.pipeline().addLast(new DemoNettyServerHandle());
}
- 編碼器
- MessageToByteEncoder:將訊息轉為位元組
- MessageToMessageEncoder:用於從一種訊息編碼為另外一種訊息(例如POJO到POJO)
編碼器示例:
public class DemoEncoder extends MessageToMessageEncoder<String> {
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, String s, List<Object> list) throws Exception {
list.add(Unpooled.copiedBuffer(s,CharsetUtil.UTF_8));
}
}
- 編碼解碼器Codec
同時具備編碼與解碼功能
- ByteToMessageCodec
- MessageToMessageCodec
2. 基於Netty的HTTP伺服器開發
效果如圖:
程式碼如下:
public class NettyHttpServer {
private int port;
public NettyHttpServer(int port) {
this.port = port;
}
public static void main(String[] args) {
new NettyHttpServer(8090).run();
}
public void run(){
EventLoopGroup bossGroup=null;
EventLoopGroup workerGroup=null;
try{
bossGroup=new NioEventLoopGroup(1);
workerGroup=new NioEventLoopGroup();
ServerBootstrap serverBootstrap=new ServerBootstrap();
serverBootstrap.group(bossGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG,128)
.childOption(ChannelOption.SO_KEEPALIVE,Boolean.TRUE)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//新增編解碼器
socketChannel.pipeline().addLast(new HttpServerCodec());
socketChannel.pipeline().addLast(new NettyHttpServerHandler());
}
});
ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
public class NettyHttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, HttpObject httpObject) throws Exception {
if(httpObject instanceof HttpRequest){
DefaultHttpRequest request=(DefaultHttpRequest)httpObject;
if(request.uri().equals("/favicon.ico")){
//圖示不響應
return;
}
System.out.println("接收到請求:"+request.uri());
ByteBuf byteBuf = Unpooled.copiedBuffer("你好,我是服務端", CharsetUtil.UTF_8);
DefaultFullHttpResponse response=new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,HttpResponseStatus.OK,byteBuf);
//設定響應頭
response.headers().set(HttpHeaderNames.CONTENT_TYPE,"text/html;charset=utf-8");
response.headers().set(HttpHeaderNames.CONTENT_LENGTH,byteBuf.readableBytes());
channelHandlerContext.writeAndFlush(response);
}
}
}
3. 粘包和拆包
簡介:粘包和拆包是TCP網路程式設計中不可避免的,無論客戶端還是服務端,當我們讀取或傳送訊息的時候都要考慮TCP底層的粘包/拆包機制。
粘包產生的原因:
- 應用程式寫入的資料小於套接字緩衝區大小,網路卡將應用多次寫入的資料傳送到網路上
- 接收方不及時讀取套接字緩衝區資料
- TCP預設使用Nagle演算法,將小資料包合併
拆包產生的原因:
- 資料太大超過剩餘緩衝區的大小
- 資料太大超過MSS最大報文長度
粘包和拆包的解決方案
- 訊息長度固定,累計讀取到定長的報文就認為是一個完整的資訊
- 將換行符作為訊息結束符
- 將特殊的分隔符作為訊息的結束標誌
- 通過在訊息頭中定義長度欄位來標識訊息總長度
Netty中粘包和拆包的解決方案
Netty提供了4種解碼器來解決:
- 固定長度拆包器FixedLengthFrameDecoder
- 行拆包器LineBasedFrameDecoder,以換行符作為分隔符
- 分隔符拆包器DelimiterBasedFrameDecoder,通過自定義的分隔符進行拆分
- 基於資料包長度的拆包器LengthFieldBasedFrameDecoder,將應用層資料包的長度最為拆分一句。要求應用層協議中包含資料包的長度。
DelimiterBasedFrameDecoder示例:
ByteBuf byteBuf =
Unpooled.copiedBuffer("$".getBytes(StandardCharsets.UTF_8));
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(2048, byteBuf));
LengthFieldBasedFrameDecoder構造器引數講解:
public LengthFieldBasedFrameDecoder(
ByteOrder byteOrder,
int lengthFieldOffset,
int lengthFieldLength,
int lengthAdjustment,
int initialBytesToStrip,
boolean failFast)
-
byteOrder是指明Length欄位是大端序還是小端序,因為Netty要讀取Length欄位的值,所以大端小端要設定好,預設Netty是大端序ByteOrder.BIG_ENDIAN。
-
maxFrameLength是指最大包長度,如果Netty最終生成的資料包超過這個長度,Netty就會報錯。
-
lengthFieldOffset是指明Length的偏移位
-
lengthFieldLength是Length欄位長度
-
lengthAdjustment 這個引數很多時候設為負數,這是最讓小夥伴們迷惑的。下面我用一整段話來解釋這個引數
當Netty利用lengthFieldOffset(偏移位)和lengthFieldLength(Length欄位長度)成功讀出Length欄位的值後,Netty認為這個值是指從Length欄位之後,到包結束一共還有多少位元組,如果這個值是13,那麼Netty就會再等待13個Byte的資料到達後,拼接成一個完整的包。但是更多時候,Length欄位的長度,是指整個包的長度,如果是這種情況,當Netty讀出Length欄位的時候,它已經讀取了包的4個Byte的資料,所以,後續未到達的資料只有9個Byte,即13 - 4 = 9,這個時候,就要用lengthAdjustment來告訴Netty,後續的資料並沒有13個Byte,要減掉4個Byte,所以lengthAdjustment要設為 -4!!!
-
initialBytesToStrip,跳過的個數。比如這裡initialBytesToStrip設定為4,那麼Netty就會跳過前4位解析後面的內容
-
failFast 引數一般設定為true,當這個引數為true時,netty一旦讀到Length欄位,並判斷Length超過maxFrameLength,就立即丟擲異常。
示例:
@Override
public void channelActive(ChannelHandlerContext channelHandlerContext) throws Exception {
for (int i=0;i<100;i++){
byte[] bytes = "你好,我是客戶端".getBytes(CharsetUtil.UTF_8);
ByteBuf byteBuf = Unpooled.buffer();
byteBuf.writeInt(bytes.length);
byteBuf.writeBytes(bytes);
channelHandlerContext.writeAndFlush(byteBuf);
}
}
第2個引數和第三個參數列示:0-4個位元組是內容長度欄位,第五個引數的4代表跳過前4個位元組。
socketChannel.pipeline().addLast(new LengthFieldBasedFrameDecoder(60535,0,4,0,4));
最後輸出的內容:
4. 基於Netty和WebSocket的聊天室案例
1. WebSocket簡介
WebSocket是一種在單個TCP連線上進行全雙工通訊的協議。相比HTTP協議,WebSocket具備如下特點:
- 支援雙向通訊,實時性更強
- 更好的二進位制支援
- 較少的開銷:協議控制的資料包頭部較小
應用場景:
- 社交訂閱
- 協同編輯
- 股票基金報價
- 體育實況更新
- 多媒體聊天
- 線上教育
2. 服務端開發
- 引入依賴
基於SpringBoot環境
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--新增thymeleaf依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.72.Final</version>
</dependency>
- 核心後端程式碼
@Component
public class NettyWebSocketServer implements Runnable {
@Autowired
private NettyConfig nettyConfig;
@Autowired
private WebSocketChannelInit webSocketChannelInit;
private EventLoopGroup bossGroup = new NioEventLoopGroup(1);
private EventLoopGroup wokerGroup = new NioEventLoopGroup();
@PreDestroy
public void close(){
bossGroup.shutdownGracefully();
wokerGroup.shutdownGracefully();
}
@Override
public void run() {
try{
ServerBootstrap serverBootstrap=new ServerBootstrap();
serverBootstrap.group(bossGroup,wokerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(webSocketChannelInit);
ChannelFuture channelFuture = serverBootstrap.bind(nettyConfig.getPort()).sync();
System.out.println("Netty服務端啟動成功");
channelFuture.channel().closeFuture().sync();
}catch (Exception e){
e.printStackTrace();
}finally {
bossGroup.shutdownGracefully();
wokerGroup.shutdownGracefully();
}
}
}
@Component
public class WebSocketChannelInit extends ChannelInitializer {
@Autowired
private NettyConfig nettyConfig;
@Autowired
private WebSocketHandler webSocketHandler;
@Override
protected void initChannel(Channel channel) throws Exception {
ChannelPipeline pipeline = channel.pipeline();
//對http協議的支援
pipeline.addLast(new HttpServerCodec());
//對大資料流的支援
pipeline.addLast(new ChunkedWriteHandler());
//post請求分為3部分。request line、request header、body
//HttpObjectAggregator將多個資訊轉化為單一的request或者response物件
pipeline.addLast(new HttpObjectAggregator(8000));
//將http協議升級為ws協議,websocket的支援
pipeline.addLast(new WebSocketServerProtocolHandler(nettyConfig.getPath()));
pipeline.addLast(webSocketHandler);
}
}
@Component
@ChannelHandler.Sharable //設定通道共享
public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
private List<Channel> channels=new ArrayList<>();
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
channels.add(ctx.channel());
System.out.println("有新的連線了...");
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
channels.remove(ctx.channel());
System.out.println("連線下線了");
}
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {
String text = textWebSocketFrame.text();
Channel currentChannel = channelHandlerContext.channel();
for (Channel channel:channels){
//自己不給自己發訊息
if(!channel.equals(currentChannel)){
channel.writeAndFlush(new TextWebSocketFrame(text));
}
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
Channel channel = ctx.channel();
channels.remove(channel);
}
}
3. 前端js程式碼
$(function () {
//這裡需要注意的是,prompt有兩個引數,前面是提示的話,後面是當對話方塊出來後,在對話方塊裡的預設值
var username = "";
while (true) {
//彈出一個輸入框,輸入一段文字,可以提交
username = prompt("請輸入您的名字", ""); //將輸入的內容賦給變數 name ,
if (username.trim() === "")//如果返回的有內容
{
alert("名稱不能輸入空")
} else {
$("#username").text(username);
break;
}
}
var ws = new WebSocket("ws://localhost:8081/chatService");
ws.onopen = function () {
console.log("連線成功.")
};
ws.onmessage = function (evt) {
showMessage(evt.data);
};
ws.onclose = function (){
console.log("連線關閉")
};
ws.onerror = function (){
console.log("連線異常")
};
function showMessage(message) {
// 張三:你好
var str = message.split(":");
$("#msg_list").append('<li class="active"}>\n' +
' <div class="main">\n' +
' <img class="avatar" width="30" height="30" src="/img/user.png">\n' +
' <div>\n' +
' <div class="user_name">'+str[0]+'</div>\n' +
' <div class="text">'+str[1]+'</div>\n' +
' </div> \n' +
' </div>\n' +
' </li>');
// 置底
setBottom();
}
$('#my_test').bind({
focus: function (event) {
event.stopPropagation();
$('#my_test').val('');
$('.arrow_box').hide()
},
keydown: function (event) {
event.stopPropagation();
if (event.keyCode === 13) {
if ($('#my_test').val().trim() === '') {
this.blur();
$('.arrow_box').show();
setTimeout(this.focus(),1000);
} else {
$('.arrow_box').hide();
//傳送訊息
sendMsg();
this.blur();
setTimeout(this.focus())
}
}
}
});
$('#send').on('click', function (event) {
event.stopPropagation();
if ($('#my_test').val().trim() === '') {
$('.arrow_box').show()
} else {
sendMsg();
}
});
function sendMsg() {
var message = $("#my_test").val();
$("#msg_list").append('<li class="active"}>\n' +
' <div class="main self">\n' +
' <div class="text">'+message+'</div>\n' +
' </div>\n' +
' </li>');
$("#my_test").val('');
//傳送訊息
message = username + ":" + message;
ws.send(message);
// 置底
setBottom();
}
// 置底
function setBottom() {
// 傳送訊息後滾動到底部
var container = $('.m-message');
var scroll = $('#msg_list');
container.animate({
scrollTop: scroll[0].scrollHeight - container[0].clientHeight + container.scrollTop() + 100
});
}
});