Netty 1-1 入門實例

Netty 1-1 入門實例


NettyServer AND ServerChannelHandler 

創建Netty服務端

創建服務端的都是模板代碼,
1.設置group,需要設置兩個EventLoopGroup。bossGroup用於監聽客戶端Channel連接的線程組,Selector作用。workGroup用於處理網絡IO,可以自定義線程數。
2.設置服務端的ServerSocketChannel類型,這裏使用了NioServerSocketChannel。需要注意的是,客戶端設置要對應爲NioSocketChannel
3.設置option參數,主要是TCP協議的一些參數
4.設置childHandler。指的是 NioServerSocketChannel產生的子Channel,對設置初始化的Handler處理器。
5.綁定端口,默認是異步的。調用sync方法進行阻塞
6.調用closeFuture,等待關閉(服務端永遠不會關閉)
7.回收EventLoopGroup資源

服務端可以綁定多個端口,接受請求的能力增加,處理能力不一定增加。

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

/**
 * 對於ChannelOption.SO_BACKLOG的解釋:tcp連接緩存區
 * 服務器端TCP內核模塊維護有兩個隊列。我們稱之爲A和B。
 * 客戶端想服務端connect的時候,會發送帶有SYN標誌的包(第一次握手)
 * 服務器收到客戶端的SYN時,向客戶端發送SYN ACK確認(第二次握手)。TCP內核將完成兩次握手的連接加入到隊列A,等待客戶端發來ACK。
 * 收到客戶端ACK(第三次握手)。TCP內核模塊將連接從A隊列轉移到B隊列,連接完成,應用程序的accept就會返回。
 * 也就是說accept方法從隊列B中取出完成三次握手的連接。
 * A隊列和B隊列的長度之和就是backlog。當隊列長度之和大於backlog時,新連接會被TCP內核拒絕。
 * 所以backlog的值過小,會出現accept的速度跟不上新連接加入的速度,AB隊列滿了,新的客戶端無法連接
 * 注意:backlog對程序支持的線程數並無影響,只會影響沒有被accept取出的連接數
 *
 */
public class NettyServer {
    public static void main(String[] args) {

        try {
            EventLoopGroup bossGroup = new NioEventLoopGroup();   //用於監聽客戶端Channel連接的線程組,Selector作用,默認一個線程
            EventLoopGroup workGroup = new NioEventLoopGroup(5);  //進行網絡IO讀寫的線程組,可自定義線程數
            ServerBootstrap b = new ServerBootstrap();   //服務端引導類,整合Selector線程、IO工作線程、Channel、ChaneelPipeline
            b.group(bossGroup,workGroup)                 //綁定線程組
                    .channel(NioServerSocketChannel.class)   //確定服務端的ServerSocketChannel
                    .option(ChannelOption.SO_BACKLOG,2)   //設置tcp緩衝區
                    .option(ChannelOption.SO_SNDBUF,8*1024)  //設置發送緩衝區大小
                    .option(ChannelOption.SO_RCVBUF,8*1024)  //設置接收緩衝區大小
                    .option(ChannelOption.SO_KEEPALIVE,true) //保持連接,默認爲true
                    .childHandler(new ChannelInitializer<SocketChannel>() {  //定義Channel創建 Handler處理器
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new StringToByteEncode());
                            ch.pipeline().addLast(new ServerHelloInboundHandler()); //添加入站handler
                        }
                    });

            ChannelFuture cf = b.bind(12345).sync();  //異步的綁定端口,調用sync方法阻塞等待綁定完成。
            ChannelFuture cf2 = b.bind(12346).sync(); //可以開放多個端口
            System.out.println("Netty Server start Success");
            cf.channel().closeFuture().sync(); //異步等待channel關閉,調用sync方法阻塞。也就是服務端關閉。
            cf2.channel().closeFuture().sync();

            bossGroup.shutdownGracefully();  //釋放線程組資源
            workGroup.shutdownGracefully();
        } catch (Exception e){
            e.printStackTrace();
        }

    }

}

創建服務端Handler處理器。

1.繼承ChannelInboundHandlerAdapter 父類,作爲Handler處理的入口。重寫channelRead方法。
2.接受Netty默認的ByteBuf。
因爲作爲服務端處理的第一個Handler,之前沒有解碼器Handler。所以傳入的Object msg就是ByteBuf
因爲代碼是基於TCP協議的,TCP使用字節流進行傳輸數據,所以客戶端發出的數據和服務端接受的數據 必須是ByteBuf。 如果想對Channel寫入對象,就需要添加對應的編碼器,將Java對象轉爲字節數組。(序列化)

3.對獲取到的ByteBuf進行處理。
這裏因爲客戶端傳來String(轉爲ByteBuf後的String)。所以就直接打印了。
客戶端代碼中對服務端進行了三次請求,也就是沖刷了三次數據。 可以從打印結果中看到,第一個客戶端,ServerHandler處理了三次。但是第二個客戶端,ServerHandler只處理了兩次。這是因爲Handler處理器沒有區分長連接中的不同次的請求,兩次長連接flush數據,可能會同時沖刷到同一個ByteBuf中,也就是服務端只會處理一次。需要專門的解碼器來實現 同一個長連接,不同請求的ByteBuf數據切割。
專業說法:因爲TCP是基於流的傳輸,基於流的傳輸並不是一個數據包隊列,而是一個字節隊列。即使你發送了2個獨立的數據包,操作系統也不會作爲2個消息處理而僅僅是作爲一連串的字節而言。因此這是不能保證你遠程寫入的數據就會準確地讀取。這是後就需要涉及TCP 粘包拆包。


4.Channel寫入數據,沖刷數據到客戶端。。
通過ChannelHandlerContext,向CHannel寫入數據。WriteAndFlush方法沖刷數據到客戶端Channel。這裏需要注意,需要衝刷的數據必須是ByteBuf,否則無法傳輸。 如果需要傳輸String,則需要添加自定義的 StringToByteEncode 編碼器,將String在發送之前編碼寫入到ByteBuf。

5.添加ChannelFutureListner,關閉Channel。實現長連接和短連接。
Netty的讀寫操作都是異步的,調用WriteAndFlush會返回一個ChannelFuture,可以添加監聽器,實現關閉Channel,這邊關閉的Channel是客戶端和服務端通信的CHannel。添加了監聽器後,客戶端的cf.channel().closeFuture().sync(); 同步阻塞結束,客戶端關閉。(服務端關閉Channel)
客戶端想實現Channel,就必須在Handler中添加ChannelFutureListener.CLOSE。(客戶端確定本次請求完畢,主動關閉連接)。
可以通過是否添加ChannelFutureListener.CLOSE,來實現長連接和短連接。





import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.ReferenceCountUtil;

public class ServerHelloInboundHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        try {
            //Handler接受到的Object 如果沒有進行解碼的話,默認是ByteBuf。
            if (msg instanceof ByteBuf){
                ByteBuf byteBuf = (ByteBuf) msg;
                byte[] buf = new byte[byteBuf.readableBytes()];
                byteBuf.readBytes(buf);
                String body = new String (buf,"utf-8");
                System.out.println("get request Message:" +body);

                String response = new String("hello "+body);
                ctx.writeAndFlush(Unpooled.copiedBuffer(response.getBytes()));
                ctx.writeAndFlush("return a string");

                //添加監聽器,關閉Channel。客戶端的cf.channel().closeFuture().sync()就會往下執行,關閉客戶端連接。
                //通過添加ChannelFutureListener.CLOSE 實現長連接和短連接
                //關閉Channel 可以在服務端可以在客戶端。                     //ctx.writeAndFlush(Unpooled.copiedBuffer("".getBytes())).addListener(ChannelFutureListener.CLOSE);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            ReferenceCountUtil.release(msg);
            super.channelRead(ctx, msg);
        }
    }
}


NettyClient AND ClientChannelHandler 

創建NettyClient 

1.創建過程 和Server端類似
2.添加客戶端超時時間 ChannelOption.CONNECT_TIMEOUT_MILLIS
3.啓動客戶端連接。分爲兩種同步、異步兩種方式。使用異步方式,需要使用CountDownLatch。
4.通過write和flush方法沖刷數據,這邊因爲連續沖刷了mdq 和 mdq second。可能導致服務端接受到一次ByteBuf。(TCP粘包拆包問題)
這邊一個騷操作就是:把第一次(channelActive方法中)和第二次之間 sleep了3s。 這樣從時間上實現了拆包。本質是因爲,Netty服務已經處理完數據,將ByteBuf 字節流重置刷新爲空了。
注意:這不是正常操作。這不是正常操作。這不是正常操作。

5.closeFuture().sync()阻塞主線程。通過ClientChannelHandler完成 Channel的讀寫操作。

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import java.util.concurrent.CountDownLatch;

public class NettyClient {

    public static void main(String[] args) {
        EventLoopGroup group = new NioEventLoopGroup();
        Bootstrap b = new Bootstrap();
        try {
            b.group(group)
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.CONNECT_TIMEOUT_MILLIS,3000)  //設置連接超時時間
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new StringToByteEncode());
                            ch.pipeline().addLast(new ClientHelloInboundHandler());

                        }
                    });
            final CountDownLatch connectedLatch = new CountDownLatch(1);
            //因爲使用了的異步連接方式,需要CountDownLatch進行阻塞,確保連接完成。如果使用了sync同步,則不需要使用CountDownLatch
            //如果設置了sync同步方法,出現連接錯誤時,需要在finally代碼塊中釋放EventLoopGroup的資源。不然無法釋放線程。
            ChannelFuture cf = b.connect("127.0.0.1",12345).sync();
            //監聽Channel是否建立成功
            cf.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    if (future.isSuccess()) {
                        //若Channel建立成功,保存建立成功的標記
                        System.out.println("Netty client connection Success");
                    } else {
                        //若Channel建立失敗,保存建立失敗的標記
                        System.out.println("Netty client connection Failed");
                        future.cause().printStackTrace();
                    }
                    connectedLatch.countDown();
                }
            });
            connectedLatch.await();

            Channel channel = cf.channel();
            Thread.sleep(3000);
            channel.write("mdq");
            channel.flush();
            channel.writeAndFlush(Unpooled.copiedBuffer("mdq second".getBytes()));
            cf.channel().closeFuture().sync();
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            group.shutdownGracefully();
        }

    }
}


ClientChannelHandler 


1.接受到服務的數據,默認也是ByteBuf
2.對ByteBuf 進行處理,客戶端可會出現和服務端出現一樣的情況。 服務端多次沖刷的數據,在客戶端只接收到一個ByteBuf。
3.可以通過ctx.writeAndFlush 繼續想服務端寫入數據。
4.可以添加監聽器,客戶端主動關閉連接

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.ReferenceCountUtil;

public class ClientHelloInboundHandler extends ChannelInboundHandlerAdapter {

        //連接創建的時候執行
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ctx.writeAndFlush(Unpooled.copiedBuffer("channelActive".getBytes()));
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        try {
            if (msg instanceof ByteBuf){
                ByteBuf byteBuf = (ByteBuf) msg;
                byte[] buf = new byte[byteBuf.readableBytes()];
                byteBuf.readBytes(buf);
                String response = new String (buf,"utf-8");
                System.out.println("get response Message:" +response);
//客戶端確認請求結束,主動關閉連接。
//ctx.writeAndFlush(Unpooled.copiedBuffer("".getBytes())).addListener(ChannelFutureListener.CLOSE);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            ReferenceCountUtil.release(msg);
            super.channelRead(ctx, msg);
        }
    }
}



TCP




在服務端和客戶端保持長連接的過程中,客戶端可以不斷向服務端沖刷數據。,服務端可以不斷的接收數據並向客戶端返回數據。

Netty中TCP傳輸數據的對象必須是ByteBuf,如果需要傳輸其他對象需要添加編碼器和解碼器。也就是說客戶端最後發出的必須是ByteBuf,而服務端首先接受的到也必定是ByteBuf。
在正常應用中:需要客戶端和服務端中設置編碼器和解碼器(MessageToByteEncoder ByteToMessageDecoder)。 編碼器的實現比較簡單,只需要將Java對象轉化爲byte數組(序列化、String.getBytes),並將byte數組到ByteBuf out中即可。解碼器的功能就比較重要了,需要判斷當前的ByteBuf中的數據是否能反序列化成一個完整的對象。如果ByteBuf中數據不夠怎麼處理、如果ByteBuf中包含了下一個長連接請求的數據該如何處理(這個還是粘包和解包的問題)。


創建編碼器 MessageToByteEncoder

因爲 Netty TCP需要傳輸 ByteBuf進行傳輸數據,這邊添加簡單的自定義編碼器,將String對象 編碼爲ByteBuf,然後進行傳輸。

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;

public class StringToByteEncode extends MessageToByteEncoder {

    @Override
    protected void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) throws Exception {
        if (msg instanceof String){
            String s = (String) msg;
            out.writeBytes(s.getBytes());
        } else if (msg instanceof  ByteBuf){
            ByteBuf buf = (ByteBuf) msg;
            byte[] b = new byte[buf.readableBytes()];
            buf.readBytes(b) ;
            out.writeBytes(b);
        }
    }
}




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章