【Netty4 簡單項目實踐】十二、客戶端連接池-壓測利器

【前言】

之前想寫壓測工具來着,但是沒有很好的分析工具,所以沒什麼動力寫。最近學會了用阿爾薩斯趕緊把壓測工具補上了,瘋狂的壓榨服務器性能。用Netty已有的連接池模型,可以省去自己維護連接保活的工作。本來寫了5個類,整理了一下代碼發現非常簡單,基礎類就一個。

【連接池】

忘記自己借鑑了誰的代碼,客戶端連接池採用Netty的ChannelPoolMap接口,用網絡連接地址做Key,用FixedChannelPool實例化value,即不同的連接服務地址對應不同的連接池。FixedChannelPool的理論連接數上限是Integer.MAX_VALUE,並且使用ChannelHealthChecker接口來判斷channel被取出池的時候是否是活的,如果是活的才嚮應用層吐出去。這樣一來保活問題就不用自己操心了。

public class TcpClientPool {
    final EventLoopGroup group = new NioEventLoopGroup();
    final Bootstrap bootstrap = new Bootstrap();
    private static final int thread_num = Runtime.getRuntime().availableProcessors();
    // key 是地址, value是pool,即一個地址一個pool
    private AbstractChannelPoolMap<InetSocketAddress, SimpleChannelPool> poolMap;

    public void build(ChannelPoolHandler poolHandler) throws Exception {
        bootstrap.group(group)
            .channel(NioSocketChannel.class)
            .option(ChannelOption.TCP_NODELAY, true)
            .option(ChannelOption.SO_KEEPALIVE, true);

        poolMap = new AbstractChannelPoolMap<InetSocketAddress, SimpleChannelPool>() {
            @Override
            protected SimpleChannelPool newPool(InetSocketAddress key) {
                return new FixedChannelPool(bootstrap.remoteAddress(key), poolHandler, thread_num);
            }
        };
    }
    /* 下面的代碼省略 */
}

構造方法需要傳入一個實現了ChannelPoolHandler接口處理Handler,這個Handler需要實現三個方法

void channelReleased(Channel ch) throws Exception;

void channelAcquired(Channel ch) throws Exception;

void channelCreated(Channel ch) throws Exception;

在處理類中最重要的事情是給channel加載業務協議的編解碼處理器

public abstract class BaseChannelPoolHandler implements ChannelPoolHandler {

    private HandlerConfiguratorInterface configurator;
    public BaseChannelPoolHandler(HandlerConfiguratorInterface configurator) {
        this.configurator = configurator;
    }
    /**
     * 因爲是裸的channel,所以需要給他配置上編解碼器
     * 只需要配置一次就可以,因爲channel會被還回到池裏
     */
    @Override
    public void channelCreated(Channel ch) throws Exception {
        configurator.configChannel(ch);
    }

    @Override
    public void channelReleased(Channel ch) throws Exception {}

    @Override
    public void channelAcquired(Channel ch) throws Exception {}
}

其中實現HandlerConfiguratorInterface接口(自定義的接口,只有一個方法public void configChannel(Channel channel);)的類,需要通過configChannel方法給Channel對象裝配編解碼器

對於Http的實現

public class HttpConfigurator implements HandlerConfiguratorInterface {
    private static final int HTTP_AGGREGATE_SIZE = 8192;

    @Override
    public void configChannel(Channel ch) {
        SocketChannel channel = (SocketChannel)ch;
        channel.config().setKeepAlive(true);
        channel.config().setTcpNoDelay(true);
        channel.pipeline()
            .addLast(new HttpClientCodec())
            .addLast(new HttpObjectAggregator(HTTP_AGGREGATE_SIZE))
            .addLast(new HttpResponseHandler());
    }
}
這一步和常見的Netty處理器掛載方式是一致的。最後一個HttpResponseHandler是處理應答的Handler。

【關閉連接池】

客戶端池還需要提供關閉的能力,否則程序無法正常退出

public void close() {
    poolMap.close();
    group.shutdownGracefully();
}

【發消息】

客戶端池封裝了異步和同步發消息的方法

異步方法

public void asyncWriteMessage(InetSocketAddress address, Object message) {
    SimpleChannelPool pool = getPool(address);
    Future<Channel> future = pool.acquire();
    // 獲取到實例後發消息
   future.addListener((FutureListener<Channel>)f -> {
        if (f.isSuccess()) {
            Channel ch = f.getNow();
            if (ch.isWritable()) {
                ch.writeAndFlush(message);
            }
            // 歸還實例
            pool.release(ch);
        }
    });
}

同步方法

    public boolean syncWriteMessage(InetSocketAddress address, Object message) {
        SimpleChannelPool pool = getPool(address);
        Future<Channel> future = pool.acquire();
        try {
            Channel channel = future.get();
            if (channel.isWritable()) {
                channel.writeAndFlush(message);
                pool.release(channel);
                return true;
            }
            pool.release(channel);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

【其他】

如果要發HTTP消息,需要自己封裝Http消息體,否則Netty編碼器會扔掉

public class HttpMsgComposer {
    public static Object compose(URI uri, String msg) throws Exception  {
        DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET,
            uri.toASCIIString(), Unpooled.wrappedBuffer(msg.getBytes("UTF-8")));

        // 構建http請求
        request.headers().set(HttpHeaderNames.HOST, uri.getHost());
        request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
        request.headers().set(HttpHeaderNames.CONTENT_LENGTH, request.content().readableBytes());
        request.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json");
        return request;
    }
}

【調用方式】

public class App {
    public static void main(String[] args) throws Exception {
        TcpClientPool pool = new TcpClientPool();
        pool.build(new HttpChannelPoolHandler(new HttpConfigurator()));

        String url = "http://163.com";
        URI uri = new URI(url);
        String host = uri.getHost();
        int port = uri.getPort() == -1 ? 80 : uri.getPort();
        InetSocketAddress address = new InetSocketAddress(host, port);

        for (int i = 0; i < 10; i++) {
            pool.asyncWriteMessage(address, HttpMsgComposer.compose(uri, "Hello World"));
        }

        while (true) {
            Thread.sleep(1000L);
            pool.close();
            break;
        }
    }
}

【後續計劃】

1. github上發佈代碼

2. 支持UDP的客戶端

3. 支持websocket

4. 支持Protocol Buff

5. 支持MQTT 


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