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
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万了,并且连接数会一直持续不断的上涨
5.结语
如上百万连接案例实现,仅在自己模拟的环境。要是生产环境考虑大连接场景,需要从整个数据链路进行分析,包括入网的多层路由设备,代理服务器/服务网关,目的服务器的配置/目的服务器内核等。
转载请注明:西门飞冰的博客 » Netty 的单机百万连接实现