netty建立聊天室服務端及單機併發測試結果

lightTrace發表於2018-10-09

前言

netty應該是Java網路通訊開發的殺手級框架,它提供非同步,事件驅動的網路應用程式框架和工具。

至於netty的概念和優點很多,在此我就不獻醜了,我純粹實在官方文件的小例子基礎上做一些測試以印證自己心中的想法。

首先搭建一個netty服務端,新建一個maven專案,引入依賴:

        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.30.Final</version>
        </dependency>

handler處理器

package cn.com.handler.server;

import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;

/**
 * @Auther: kc
 * @Date: 2018/10/8 20:28
 * @Description:
 */
public class SimpleChatServerHandler extends SimpleChannelInboundHandler<String> { // (1)

    public static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    //服務端從客戶端收到新的連線的時候,客戶端的channel存入channerGroup列表中,並通知列表中的其他客戶端
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {  // (2)
        Channel incoming = ctx.channel();

        // Broadcast a message to multiple Channels
        channels.writeAndFlush("[SERVER] - " + incoming.remoteAddress() + " 加入\n");

        channels.add(ctx.channel());
    }
    //當從服務端收到客戶端斷開時,客戶端自動從channelgroup中離開,並通知其它客戶端
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {  // (3)
        Channel incoming = ctx.channel();

        // Broadcast a message to multiple Channels
        channels.writeAndFlush("[SERVER] - " + incoming.remoteAddress() + " 離開\n");

        // A closed Channel is automatically removed from ChannelGroup,
        // so there is no need to do "channels.remove(ctx.channel());"
    }
    //每當服務端讀到客戶端寫入資訊時,將資訊轉發給其它客戶端的channel
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String s) throws Exception { // (4)
        Channel incoming = ctx.channel();
        for (Channel channel : channels) {
            if (channel != incoming){
                channel.writeAndFlush("[" + incoming.remoteAddress() + "]" + s + "\n");
            } else {
                channel.writeAndFlush("[you]" + s + "\n");
            }
        }
    }
    //服務端監聽到客戶端活動
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception { // (5)
        Channel incoming = ctx.channel();
        System.out.println("SimpleChatClient:"+incoming.remoteAddress()+"線上");
    }
    //服務端監聽到客戶端不活動
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception { // (6)
        Channel incoming = ctx.channel();
        System.out.println("SimpleChatClient:"+incoming.remoteAddress()+"掉線");
    }
    //當處理方法出現throwable物件才會被呼叫,即當netty由於IO錯誤或者處理器在處理事件時丟擲的異常
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // (7)
        Channel incoming = ctx.channel();
        System.out.println("SimpleChatClient:"+incoming.remoteAddress()+"異常");
        // 當出現異常就關閉連線
        cause.printStackTrace();
        ctx.close();
    }
}

SimpleChatServerInitializer.java

用來增加多個的處理類到 ChannelPipeline 上,包括編碼、解碼、SimpleChatServerHandler 等。

package cn.com.handler.server;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.Delimiters;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

/**
 * @Auther: kc
 * @Date: 2018/10/8 20:35
 * @Description: 用來增加多個的處理類到ChannelPipeline,包括編碼、解碼、simpleChatServerHandler
 */
public class SimpleChatServerInitializer extends
        ChannelInitializer<SocketChannel> {

    @Override
    public void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();

        pipeline.addLast("framer", new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));
        pipeline.addLast("decoder", new StringDecoder());
        pipeline.addLast("encoder", new StringEncoder());
        pipeline.addLast("handler", new SimpleChatServerHandler());

        System.out.println("SimpleChatClient:"+ch.remoteAddress() +"連線上");
    }
}

SimpleChatServer.java

編寫一個 main() 方法來啟動服務端:

package cn.com.handler.server;

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

/**
 * @Auther: kc
 * @Date: 2018/10/8 20:38
 * @Description:
 */
public class SimpleChatServer {
    private int port;

    public SimpleChatServer(int port) {
        this.port = port;
    }

    public void run() throws Exception {
        //用來處理IO操作的多執行緒事件迴圈器,boss用來接收接進來的連線,worker用來處理已經接收的連線,一旦boss收到連線就會把連線資訊註冊到worker上
        EventLoopGroup bossGroup = new NioEventLoopGroup(); // (1)
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap(); // ServerBootStrap是一個啟動NIO服務的輔助啟動類
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class) // 指定使用NioServerSocketChannel類來舉例說明一個新的channel如何接收進來的連線
                    .childHandler(new SimpleChatServerInitializer())  //呼叫SimpleChatServerInitializer
                    .option(ChannelOption.SO_BACKLOG, 128)          // (5)
                    .childOption(ChannelOption.SO_KEEPALIVE, true); // (6)

            System.out.println("SimpleChatServer 啟動了");

            // 繫結埠,開始接收進來的連線
            ChannelFuture f = b.bind(port).sync(); // (7)

            // 等待伺服器  socket 關閉 。
            // 在這個例子中,這不會發生,但你可以優雅地關閉你的伺服器。
            f.channel().closeFuture().sync();

        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();

            System.out.println("SimpleChatServer 關閉了");
        }
    }

    public static void main(String[] args) throws Exception {
        int port;
        if (args.length > 0) {
            port = Integer.parseInt(args[0]);
        } else {
            port = 8080;
        }
        new SimpleChatServer(port).run();

    }
}

啟動main方法

Jmeter測試

我是按照官網的例子寫的,在自己電腦(4核,8G記憶體/沒測試之前記憶體用了4。5G,cpu佔用15%左右)跑服務端,跑jmeter測試,沒經過任何優化,首先我用1秒鐘500次請求
在這裡插入圖片描述

可以發現請求的一瞬間cpu佔用會到80%,而記憶體卻沒有多大佔用。

然後我用100秒的時間去請求10000次,這裡截不了圖,因為會很卡,大概到6000個請求的時候,就會吃完我電腦剩下的記憶體,但是cpu應該還猶有餘力。

總結

我並不知道我這樣的測試是否合理,但是看來一臺8G,4核的伺服器host1w個請求肯定是可以得,我覺得應該還可以優化讓netty接受更多的請求,繼續學習吧!

相關文章