從零開始學Netty(二)netty爲什麼成爲了高人氣的服務器框架

網絡通信其實就是幹這麼幾件事
1.建立連接
2.客戶端發送數據
3.服務器接受到數據,然後根據數據內容進行處理,然後返回數據
4.不停重複2,3
5.關閉連接

我們爲什麼要用netty來搭建服務器呢?或者說netty爲什麼成爲了高人氣的服務器框架呢?
下面我們就來仔細研究一下吧。

在網絡編程中,我們至少得有一臺服務器,有一臺客戶端,兩者建立連接,然後進行通信。

在早期,我們的服務器都是採用bio的方式進行交流的。

BIO服務器

//服務器端代碼
public class Server {

    protected static final String ip = "127.0.0.1";
    protected static final int port = 6010;

    private static void start() {
        try {
            ServerSocket server = new ServerSocket(port);//1

            while(true) {
                Socket client = server.accept();//2
                System.out.println("accept");
                new Thread(new ServerHandler(client)).start();//3

            }


        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        start();
    }


}


//服務器處理邏輯
public class ServerHandler implements Runnable {
    private Socket client;

    public ServerHandler(Socket client) {
        this.client = client;
    }

    public void run() {
        byte[] buffer = new  byte[1024];
        while (true) {
            int read = 0;
            try {
                read = client.getInputStream().read(buffer);
                if (!(read > 0)) break;
                System.out.println(new String(buffer,0, read));
                client.getOutputStream().write(buffer,0,read);
            } catch (IOException e) {
                e.printStackTrace();
                return;
            }

        }


    }
}

//客戶端代碼
public class Client {


    public static void main(String[] args) {
        start();
    }

    private static void start() {
        try {
            Socket socket = new Socket();
            socket.connect(new InetSocketAddress(Server.ip, Server.port));

            Thread.sleep(1000);//模擬客戶端阻塞
            socket.getOutputStream().write("hello world".getBytes());


            byte[] buffer = new  byte[1024];
            while (socket.getInputStream().read(buffer) > 0) {
                System.out.println(new String(buffer));
                buffer = new  byte[1024];
            }
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

網絡通信

特點

上面這就是用BIO實現的服務器和客戶端通信,可見服務器和客戶端之間是能正常通信,處理業務的,人們在實踐中發現,大量的連接其實不是一直在傳輸消息的,而是阻塞狀態。但是以BIO的方式來做的話,每一個客戶端連接上來都需要創建一個線程來與客戶端保持連接。當有大量連接時,服務器需要浪費大量的內存來保持這些連接(分配給線程)。

這個時候出現了NIO技術,即在請求數據的時候,線程不再阻塞了。

如果數據沒有準備好,那麼當線程請求時會直接收到沒有準備好的信號。當然準備好的話,就會收到準備好了的信號。

所以我們就能用一個線程,將全部的連接保存下來,然後挨個去輪詢,當找到某個連接數據準備好後就新建線程來處理這個連個連接。

這個時候我們就不需要在新建大量線程去保持連接了,只需要一個或者幾個(當連接數太多的時候,可以適當多幾個)就可以維持大量的連接,又因爲大量的連接其實是未準備好的,所以處理的連接數據的線程數量不會太多。

在此基礎上,操作系統提供了select,poll,epoll來處理上面保持連接的問題。當這些連接準備好你註冊好的事件的時候,就會返回,你能拿到與之對應的連接,然後通過連接拿到數據進行處理。這就是java的NIO的本質。

NIO服務器

//NIO實現的服務器端代碼
public class Server {

    protected static final String ip = "127.0.0.1";
    protected static final int port = 6010;

    public static void main(String[] args) {
        start();
    }

    private static void start() {
        try {
            ServerSocketChannel ssc = ServerSocketChannel.open();

            ssc.bind(new InetSocketAddress(ip, port));

            ssc.configureBlocking(false);
            Selector selector = Selector.open();

            ssc.register(selector, SelectionKey.OP_ACCEPT);

            while(true) {
                selector.select(); //阻塞方法 若返回則說明有 感興趣的事件準備好了
                Set<SelectionKey> keys = selector.selectedKeys();
                Iterator<SelectionKey> keyIterator = keys.iterator();
                ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                ByteBuffer sendBuffer = ByteBuffer.allocate(1024);
                while (keyIterator.hasNext()) {//處理客戶端連接
                    SelectionKey key = keyIterator.next();


                    if (key.isAcceptable()) {

                        ssc = (ServerSocketChannel) key.channel();
                        SocketChannel clientChannel = ssc.accept();
                        clientChannel.configureBlocking(false);
                        clientChannel.register(selector, SelectionKey.OP_READ);
                        System.out.println("有客戶端連接!");

                    }
                    if (key.isReadable()) {
                        SocketChannel socketChannel = (SocketChannel) key.channel();
                        readBuffer.clear();//清除緩衝區
                        int numRead;
                        try{
                            numRead = socketChannel.read(readBuffer);
                            if (numRead == -1) {
                                socketChannel.close();
                                key.cancel();
                                continue;
                            }
                        }catch (IOException e){
                            key.cancel();
                            socketChannel.close();
                            continue;
                        }
                        String msg = new String(readBuffer.array(),0,numRead);
                        System.out.println("接收到消息" + msg);
                        socketChannel.register(selector,SelectionKey.OP_WRITE);


                    }
                    if (key.isWritable()) {
                        String msg = "hello";
                        SocketChannel channel = (SocketChannel) key.channel();
                        System.out.println("發送消息:" + msg);

                        sendBuffer.clear();
                        sendBuffer.put(msg.getBytes());
                        sendBuffer.flip();//由寫變爲讀,反轉
                        channel.write(sendBuffer);
                        channel.register(selector,SelectionKey.OP_READ); //註冊讀操作

                    }
                    keyIterator.remove(); //移除當前的key
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }


}


//客戶端
public class Client {
    protected static final String ip = "127.0.0.1";
    protected static final int port = 6010;

    public static void main(String[] args) {
        start();
    }

    private static void start() {

        try {
            SocketChannel sc = SocketChannel.open();

            sc.connect(new InetSocketAddress(ip, port));//建立連接

            ByteBuffer readBuffer = ByteBuffer.allocate(1024);//緩衝區
            ByteBuffer sendBuffer = ByteBuffer.allocate(1024);
            sendBuffer.put("hello".getBytes());//將要發送的數據寫入buffer

           
            sc.write(sendBuffer);//發送消息
            int length = sc.read(readBuffer);//讀取信息
            String s = new String(readBuffer.array(), 0, length);
            System.out.println(s);

            sc.close();

        } catch (IOException e) {
            e.printStackTrace();
        }


    }
}

客戶端可以用BIO的客戶端,也可以NIO的SocketChannel來實現。
運行效果如下
nio

用原生NIO來寫代碼實在是太過繁瑣,而且很容易出現一些不知名的bug(因爲知識的漏洞)。所以有人將原生NIO進行的封裝,netty這個項目就此誕生了。

Netty 服務器

用netty來實現服務器和客戶端


//服務器端
public class NettyServer {

    private static final String IP = "127.0.0.1";
    private static final int PORT = 6010;
    private static final int BOSSNUM = Runtime.getRuntime().availableProcessors() * 2;
    private static final int WKNUM = 100;
    private static final EventLoopGroup bossGroup = new NioEventLoopGroup(BOSSNUM);
    private static final EventLoopGroup workGroup = new NioEventLoopGroup(WKNUM);
    public static void start() throws InterruptedException {

        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap.group(bossGroup, workGroup).channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<Channel>() {
                    protected void initChannel(Channel ch) throws Exception {
                        ChannelPipeline pipeline = ch.pipeline();
                        pipeline.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4));
                        pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
                        pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
                        pipeline.addLast(new TCPChannelHandler());
                    }
                });

        ChannelFuture channelFuture = serverBootstrap.bind(IP, PORT).sync();

        channelFuture.channel().closeFuture().sync();

        System.out.println("start");

    }

    public static void main(String[] args) throws InterruptedException {
        start();
    }
}


//服務器端業務處理代碼
public class TCPChannelHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("Channel Active");
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("收到" + msg);
        ctx.channel().writeAndFlush("Hello Clent");

    }

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



//客戶端
public class NettyClient implements Runnable{
    public void run() {
        NioEventLoopGroup group = new NioEventLoopGroup();

        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(group)
                .channel(NioSocketChannel.class)
                .option(ChannelOption.TCP_NODELAY, true)
                .handler(new ChannelInitializer<SocketChannel>() {
                    protected void initChannel(SocketChannel ch) throws Exception {

                        ChannelPipeline pipeline = ch.pipeline();
                        pipeline.addLast(new LengthFieldPrepender(4));
                        pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
                        pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
                        pipeline.addLast(new TCPClient());
                    }
                });
        try {
        ChannelFuture sync = bootstrap.connect("127.0.0.1", 6010).sync();
        sync.channel().writeAndFlush("hello");



        Thread.sleep(100);
        group.shutdownGracefully();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    public static void main(String[] args) {
        Thread thread = new Thread(new NettyClient());
        thread.start();
    }
}



//客戶端業務邏輯處理代碼
public class TCPClient extends ChannelInboundHandlerAdapter {


    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {

        System.out.println("active");
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("receive: " + msg);

    }

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

Netty封裝了NIO,降低了NIO實現網絡編程的難度,對比代碼量就可以看出。而且在代碼的可讀性上也大大提高了。

小結

不是說NIO一定比BIO好,只是說現在網絡環境是連接數量多,傳輸數據量少(瀏覽網頁)爲主,NIO更加適合,假如你的服務器連接人數不多,又是傳輸數據量比較大的長連接的話,那麼BIO肯定比NIO更加適合你。
我說這句話的目的是希望大家不會要盲目推崇NIO,具體問題具體分析。

而Netty是一個基於多路複用io模型的NIO網絡編程框架,屏蔽了很多NIO中的陷進,能更加容易的搭建自己的服務器(不一定是http服務器,可以自己實現自己的協議。比如dubbo)。

下一篇正式進入Netty的學習。

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