由于blog各种垃圾评论太多,而且本人审核评论周期较长,所以懒得管理评论了,就把评论功能关闭,有问题可以直接qq骚扰我

Netty 的单机百万连接实现

JAVA 西门飞冰 3461℃
[隐藏]

1.介绍

做个小案例,使用Netty实现单机百万连接。

说明:此案例仅供娱乐。生产不建议单机连接太多,生产环境单机有个一两万连接就了不得了,因为一旦服务器故障,这么多的连接分摊到其他服务器处理不当可能会雪崩,就算其他服务器可以接收这么多连接,那么用户的断线重连,也挺闹心的。

2.明确瓶颈和解决方案

在实现Netty单机百万连接之前,需要先考虑一下,我们创建百万连接可能存在的瓶颈点,根据木桶原理,一个木桶盛水多少,取决于最短的那块木板。

2.1.代码层面

因为代码层面选择的是Netty来实现单机百万连接,所以代码层面不会成为瓶颈。

因为Netty使用的是非阻塞IO模型(NIO、AIO都算),可以使用单线程来实现大量Socket连接,不像BIO那样为每一个连接创建一个线程。

2.2.TCP连接

TCP 连接是实现单机百万并发的限制因素之一。

我们知道一个唯一的TCP连接,需要满足TCP四元组(源IP地址、源端口、目的ip、目的端口)其中之一唯一。也就是说,服务器端的同一个IP和port,可以和同一个客户端的多个不同端口成功建立多个TCP链接(与多个不同的客户端当然也可以),只要保证【Server IP + Server Port + Client IP + Client Port】这个组合唯一不重复即可。

单个服务器最多65535个端口,这个是TCP协议规定死的,没有办法修改。而且服务器的其他应用和系统会占用一些端口,并且系统默认1024以下端口是不被使用,也就是说Netty仅绑定一个IP和端口的情况下,被一个客户端连接最多可以创建6万个连接,无法满足单机百万连接的测试。

针对这个问题有两个解决方案:

方案一:使用多个客户端。既然一个客户端连接Netty最多可以创建6万个TCP连接,那么我们想要实现Netty百万连接,理论上只需要启动个17个客户端每个客户端创建6万个连接就可以了。

方案二:服务端使用多个端口。我们只需要让Netty监听的端口达到17个,让一个客户端分别和17个Netty端口进行连接,理论上也可以达到单机百万连接。

选择:因为我们是测试环境,资源有限不能启动那么多客户端,所以选择方案二,让Netty单IP监听50个端口来解决TCP四元祖限制。

2.3.Linux 内核层面

Linux内核是实现单机百万并发的限制因素之一。

我们知道一个TCP连接,对应到Linux服务器就是一个文件句柄,然而Linux内核默认的文件句柄数量最大是1024。这个是必须调大的。

Linux服务器内核参数调整方式如下:(客户端和服务器都需要调整)

(1)局部文件句柄限制,单个进程最大文件打开数

~]# vim /etc/security/limits.conf 
root soft nofile 1800000
root hard nofile 1800000
* soft nofile 1800000
* hard nofile 1800000

*表示当前用户,修改后要重启

(2)全局文件句柄限制,所有进程最大打开的文件数

~]# vim /etc/sysctl.conf 
fs.file-max = 1800000

修改后生效 sysctl -p

2.4.服务器层面

服务器层面没啥好说的,就是需要保证足够的CPU和内存已经网络带宽,不要让其中一项成为瓶颈即可。

3.代码实现

配置类:设置服务器端监听的端口范围

public class Config {
    public static final int BEGIN_PORT = 8000;
    public static final int END_PORT = 8050;
}

服务端Handler 用于统计服务端TCP连接

// @ChannelHandler.Sharable 多线程共用这个Handler需要添加这个注解,不然只能一个线程使用这个Handler
@ChannelHandler.Sharable
public class TcpCountHandler extends ChannelInboundHandlerAdapter {

    // 统计TCP的连接数:AtomicInteger 多线程同时使用的原子计数器
    private AtomicInteger atomicInteger = new AtomicInteger();

    // 设置一个定时任务,每隔3秒统计一次当前连接数
    public TcpCountHandler(){
        Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(()->{

            System.out.println("当前连接数为 = "+ atomicInteger.get());

        }, 0, 3, TimeUnit.SECONDS);

    }

    // 有连接建立,TCP连接数加1
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        atomicInteger.incrementAndGet();
    }

    // 有连接端口,TCP连接数减1
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        atomicInteger.decrementAndGet();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.out.println("TcpCountHandler exceptionCaught");
        cause.printStackTrace();
    }
}

服务端启动类:

public class NettyServer {

    public static void main(String [] args){
            // 启动Netty,并传入要监听的端口范围
            new NettyServer().run(Config.BEGIN_PORT, Config.END_PORT);
    }


    public void run(int beginPort, int endPort){

        System.out.println("服务端启动中");

        //配置服务端线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workGroup = new NioEventLoopGroup();

        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap.group(bossGroup, workGroup)
                .channel(NioServerSocketChannel.class)
                //快速复用端口
                .childOption(ChannelOption.SO_REUSEADDR, true);

        serverBootstrap.childHandler( new TcpCountHandler());

        // 循环绑定传入的端口范围
        for(; beginPort < endPort; beginPort++){ int port = beginPort; serverBootstrap.bind(port).addListener((ChannelFutureListener) future->{
                System.out.println("服务端成功绑定端口 port = "+port);
            });
        }

    }
}

客户端启动类:

public class NettyClient {
    //设置服务器端连接地址
    private static final String SERVER = "172.16.247.3";

    public static void main(String [] args){
        new NettyClient().run(Config.BEGIN_PORT, Config.END_PORT);
    }

    public void run(int beginPort, int endPort){

        System.out.println("客户端启动中");

        EventLoopGroup group = new NioEventLoopGroup();

        Bootstrap bootstrap = new Bootstrap();

        bootstrap.group(group)
                .channel(NioSocketChannel.class)
                .option(ChannelOption.SO_REUSEADDR, true)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        // 客户端因为没有对应的IO处理,所以可以不用配置Handler
                    }

                });

        int index = 0 ;
        int finalPort ;
        // 循环连接服务器端口
        while (true){

            finalPort = beginPort + index;

            try {
                bootstrap.connect(SERVER, finalPort).addListener((ChannelFutureListener)future ->{
                    if(!future.isSuccess()){
                        System.out.println("创建连接失败 " );
                    }
                }).get();

            } catch (Exception e) {
                //e.printStackTrace();
            }
            // 开始新一轮的端口连接
            ++index;
            if(index == (endPort - beginPort)){
                index = 0 ;
            }

        }
    }
}

4.编译部署

4.1.编译说明

因为代码中有客户端和服务器端两个main方法,所以打包的时候需要指定启动类,指定标签为mainClass,分别打包出客户端应用包和服务器端应用包。

    <build>
        <plugins>
            <!--将依赖的jar包打包到当前jar包,常规打包是不会将所依赖jar包打进来的-->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>1.2.1</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <transformers>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>com.fblinux.TcpMillionServer.NettyServer</mainClass>
<!--                                    <mainClass>com.fblinux.TcpMillionServer.NettyClient</mainClass>-->
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

4.2.部署说明

本案例,仅使用两台服务器,一台启动客户端,一台启动服务器端

4.3.测试验证

启动验证:先启动服务端在启动客户端,可以看到服务端监听在了50个端口上面,并且一会连接数就突破20万了,并且连接数会一直持续不断的上涨

image-20221108120409317

5.结语

如上百万连接案例实现,仅在自己模拟的环境。要是生产环境考虑大连接场景,需要从整个数据链路进行分析,包括入网的多层路由设备,代理服务器/服务网关,目的服务器的配置/目的服务器内核等。

转载请注明:西门飞冰的博客 » Netty 的单机百万连接实现

喜欢 (0)or分享 (0)