netty初步使用

1、簡介

Java1.4提供了NIO使開發者可以使用Java編寫高性能的服務端程序,但使用原生的NIO API就像Linux C中網絡編程一樣,還是需要做IO處理、協議處理等低層次工作。所以,就像C服務端程序大量使用libevent作爲網絡應用框架一樣,Java社區也不斷涌現出基於NIO的網絡應用框架。在這其中,Jboss出品的Netty就是個中翹楚。Netty是個異步的事件驅動網絡應用框架,具有高性能、高擴展性等特性。Netty提供了統一的底層協議接口,使得開發者從底層的網絡協議(比如TCP/IP、UDP)中解脫出來。就使用來說,開發者只要參考 Netty提供的若干例子和它的指南文檔,就可以放手開發基於Netty的服務端程序了。

在Java社區,最知名的開源Java NIO框架要屬Mina和Netty,而且兩者淵源頗多,對兩者的比較自然不少。實際上,Netty的作者原來就是Mina作者之一,所以可以想到,Netty和Mina在設計理念上會有很多共同點。我對Mina沒什麼研究,但其作者介紹,Netty的設計對開發者有更友好的擴展性,並且性能方面要優於Mina,而Netty完善的文檔也很吸引人。所以,如果你在尋找Java NIO框架,Netty是個很不錯的選擇。本文的內容就是圍繞一個demo介紹使用Netty的點點滴滴。

2、服務端程序

2.1、ChannelHandler

服務端程序通常的處理過程是:解碼請求數據、業務邏輯處理、編碼響應。從框架角度來說,可以提供3個接口來控制並調度該處理過程;從更通用的角度來說,並不特化處理其中的每一步,而把每一步當做過濾器鏈中的一環,這也是Netty的做法。Netty對請求處理過程實現了過濾器鏈模式(ChannelPipeline),每個過濾器實現了ChannelHandler接口。Netty中有兩種請求事件流類型也做了細分:

1)downstream event:其對應的ChannelHandler子接口是ChannelDownstreamHandler。downstream event是說從頭到尾執行ChannelPipeline中的ChannelDownstreamHandler,這一過程相當於向外發送數據的過程。 downstream event有:”write”、”bind”、”unbind”、 “connect”、 “disconnect”、”close”。

2)upstream event:其對應的ChannelHandler子接口是ChannelUpstreamHandler。upstream event處理的事件方向和downstream event相反,這一過程相當於接收處理外來請求的過程。upstream event有:”messageReceived”、 “exceptionCaught”、”channelOpen”、”channelClosed”、 “channelBound”、”channelUnbound”、 “channelConnected”、”writeComplete”、”channelDisconnected”、”channelInterestChanged”。

Netty中有個註釋@interface ChannelPipelineCoverage,它表示被註釋的ChannelHandler是否能添加到多個ChannelPipeline中,其可選的值是”all”和”one”。”all”表示ChannelHandler是無狀態的,可被多個ChannelPipeline共享,而”one”表示ChannelHandler只作用於單個ChannelPipeline中。但ChannelPipelineCoverage只是個註釋而已,並沒有實際的檢查作用。對於ChannelHandler是”all”還是”one”,還是根據邏輯需要而定。比如,像解碼請求handler,因爲可能解碼的數據不完整,需要等待下一次讀事件來了之後再繼續解析,所以解碼請求handler就需要是”one”的(否則多個Channel共享數據就亂了)。而像業務邏輯處理hanlder通常是”all”的。

下面以一個簡單的例子說明如何編寫“解碼請求數據、業務邏輯處理、編碼響應”這一過程中涉及的ChannelHandler。該例子實現的協議格式很簡單,請求和響應流中頭4個字節表示後面跟的內容長度,根據該長度可得到內容體。

首先看下解碼器的實現:

public class MessageDecoder extends FrameDecoder {
 
    @Override
    protected Object decode(
            ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer) throws Exception {
        if (buffer.readableBytes() < 4) {
            return null;//(1)
        }
        int dataLength = buffer.getInt(buffer.readerIndex());
        if (buffer.readableBytes() < dataLength + 4) {
            return null;//(2)
        }
 
        buffer.skipBytes(4);//(3)
        byte[] decoded = new byte[dataLength];
        buffer.readBytes(decoded);
        String msg = new String(decoded);//(4)
        return msg;
    }
}


MessageDecoder繼承自FrameDecoder,FrameDecoder是Netty codec包中的輔助類,它是個ChannelUpstreamHandler,decode方法是FrameDecoder子類需要實現的。在上面的代碼中,有:

(1)檢查ChannelBuffer中的字節數,如果ChannelBuffer可讀的字節數少於4,則返回null等待下次讀事件。
(2)繼續檢查ChannelBuffer中的字節數,如果ChannelBuffer可讀的字節數少於dataLength + 4,則返回null等待下次讀事件。
(3)越過dataLength的字節。
(4)構造解碼的字符串返回。

@ChannelPipelineCoverage("all")
public class MessageServerHandler extends SimpleChannelUpstreamHandler {
 
    private static final Logger logger = Logger.getLogger(
            MessageServerHandler.class.getName());
 
    @Override
    public void messageReceived(
            ChannelHandlerContext ctx, MessageEvent e) {
        if (!(e.getMessage() instanceof String)) {
            return;//(1)
        }
        String msg = (String) e.getMessage();
        System.err.println("got msg:"+msg);
        e.getChannel().write(msg);//(2)
    }
 
    @Override
    public void exceptionCaught(
            ChannelHandlerContext ctx, ExceptionEvent e) {
        logger.log(
                Level.WARNING,
                "Unexpected exception from downstream.",
                e.getCause());
        e.getChannel().close();
    }
}


MessageServerHandler是服務端業務處理handler,其繼承自SimpleChannelUpstreamHandler,並主要實現messageReceived事件。關於該類,有如下註解:

(1)該upstream事件流中,首先經過MessageDecoder,其會將decode返回的解碼後的數據構造成 MessageEvent.getMessage(),所以在handler上下文關係中,MessageEvent.getMessage()並不一定都返回ChannelBuffer類型的數據。
(2)MessageServerHandler只是簡單的將得到的msg再寫回給客戶端。e.getChannel().write(msg);操作將觸發DownstreamMessageEvent事件,也就是調用下面的MessageEncoder將編碼的數據返回給客戶端。

@ChannelPipelineCoverage("all")
public class MessageEncoder extends OneToOneEncoder {
 
    @Override
    protected Object encode(
            ChannelHandlerContext ctx, Channel channel, Object msg) throws Exception {
        if (!(msg instanceof String)) {
            return msg;//(1)
        }
 
        String res = (String)msg;
        byte[] data = res.getBytes();
        int dataLength = data.length;
        ChannelBuffer buf = ChannelBuffers.dynamicBuffer();//(2)
        buf.writeInt(dataLength);
        buf.writeBytes(data);
        return buf;//(3)
    }
}


MessageEncoder是個ChannelDownstreamHandler。對該類的註解如下:

(1)如果編碼的msg不是合法類型,就直接返回該msg,之後OneToOneEncoder會調用 ctx.sendDownstream(evt);來調用下一個ChannelDownstreamHandler。對於該例子來說,這種情況是不應該出現的。
(2)開發者創建ChannelBuffer的用武之地就是這兒了,通常使用dynamicBuffer即可,表示得到的ChannelBuffer可動態增加大小。
(3)返回編碼後的ChannelBuffer之後,OneToOneEncoder會調用Channels.write將數據寫回客戶端。

2.2、MessageServerPipelineFactory

創建了3個ChannelHandler,需要將他們註冊到ChannelPipeline,而ChannelPipeline又是和Channel對應的(是全局單例還是每個Channel對應一個ChannelPipeline實例依賴於實現)。可以實現ChannelPipeline的工廠接口 ChannelPipelineFactory實現該目的。MessageServerPipelineFactory的代碼如下:

public class MessageServerPipelineFactory implements
        ChannelPipelineFactory {
 
    public ChannelPipeline getPipeline() throws Exception {
        ChannelPipeline pipeline = pipeline();
 
        pipeline.addLast("decoder", new MessageDecoder());
        pipeline.addLast("encoder", new MessageEncoder());
        pipeline.addLast("handler", new MessageServerHandler());
 
        return pipeline;
    }
}


2.3、MessageServer

服務端程序就剩下啓動代碼了,使用Netty的ServerBootstrap三下五除二完成之。

public class MessageServer {
 
    public static void main(String[] args) throws Exception {
        // Configure the server.
        ServerBootstrap bootstrap = new ServerBootstrap(
                new NioServerSocketChannelFactory(
                        Executors.newCachedThreadPool(),
                        Executors.newCachedThreadPool()));
 
        // Set up the default event pipeline.
        bootstrap.setPipelineFactory(new MessageServerPipelineFactory());
 
        // Bind and start to accept incoming connections.
        bootstrap.bind(new InetSocketAddress(8080));
 
    }
}


稍加補充的是,該Server程序並不完整,它沒有處理關閉時的資源釋放,儘管暴力的來看並不一定需要做這樣的善後工作。

3、客戶端程序

客戶端程序和服務端程序處理模型上是很相似的,這裏還是付上代碼並作簡要說明。

3.1、 ChannelHandler

客戶端是先發送數據到服務端(downstream事件流),然後是處理從服務端接收的數據(upstream事件流)。這裏有個問題是,怎麼把需要發送的數據送到downstream事件流裏呢?這就用到了ChannelUpstreamHandler的channelConnected事件了。實現的 MessageClientHandler代碼如下:

@ChannelPipelineCoverage("all")
public class MessageClientHandler extends SimpleChannelUpstreamHandler {
 
    private static final Logger logger = Logger.getLogger(
            MessageClientHandler.class.getName());
 
 
    @Override
    public void channelConnected(
            ChannelHandlerContext ctx, ChannelStateEvent e) {
        String message = "hello world";
        e.getChannel().write(message);
    }
 
    @Override
    public void messageReceived(
            ChannelHandlerContext ctx, MessageEvent e) {
        // Send back the received message to the remote peer.
        System.err.println("messageReceived send message "+e.getMessage());
        try {
            Thread.sleep(1000*3);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        e.getChannel().write(e.getMessage());
    }
 
    @Override
    public void exceptionCaught(
            ChannelHandlerContext ctx, ExceptionEvent e) {
        // Close the connection when an exception is raised.
        logger.log(
                Level.WARNING,
                "Unexpected exception from downstream.",
                e.getCause());
        e.getChannel().close();
    }
}


對於編碼和解碼Handler,複用MessageEncoder和MessageDecoder即可。

3.2、 MessageClientPipelineFactory

  public class MessageClientPipelineFactory implements
        ChannelPipelineFactory {
 
    public ChannelPipeline getPipeline() throws Exception {
        ChannelPipeline pipeline = pipeline();
 
        pipeline.addLast("decoder", new MessageDecoder());
        pipeline.addLast("encoder", new MessageEncoder());
        pipeline.addLast("handler", new MessageClientHandler());
 
        return pipeline;
    }
}


3.3、MessageClient

public class MessageClient {
 
    public static void main(String[] args) throws Exception {
        // Parse options.
        String host = "127.0.0.1";
        int port = 8080;
        // Configure the client.
        ClientBootstrap bootstrap = new ClientBootstrap(
                new NioClientSocketChannelFactory(
                        Executors.newCachedThreadPool(),
                        Executors.newCachedThreadPool()));
        // Set up the event pipeline factory.
        bootstrap.setPipelineFactory(new MessageClientPipelineFactory());
        // Start the connection attempt.
        ChannelFuture future = bootstrap.connect(new InetSocketAddress(host, port));
        // Wait until the connection is closed or the connection attempt fails.
        future.getChannel().getCloseFuture().awaitUninterruptibly();
        // Shut down thread pools to exit.
        bootstrap.releaseExternalResources();
    }
}


在寫客戶端例子時,我想像的代碼並不是這樣的,對客戶端的代碼我也沒做過多的研究,所以也可能沒有找到更好的解決方案。在上面的例子中,bootstrap.connect方法中會觸發實際的連接操作,接着觸發 MessageClientHandler.channelConnected,使整個過程運轉起來。但是,我想要的是一個連接池,並且如何寫數據也不應該在channelConnected中,這樣對於動態的數據,只能在構造函數中傳遞需要寫的數據了。但到現在,我還不清楚如何將連接池和 ChannelPipeline有效的結合起來。或許,這樣的需求可以跨過Netty來實現。

4、總結

關於Netty的初步使用,尚且總結到這裏。關於這篇文章,寫得斷斷續續,以至於到後來我都沒興趣把內容都整理出來。當然,這多少也是因爲我是先整理 Netty原理方面的東西所致。我也只能卑微的期望,該文對Netty入門者會有些許幫助。

發佈了75 篇原創文章 · 獲贊 6 · 訪問量 33萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章