Netty这个框架相信大家都很熟悉,Java世界中网络IO处理的集大成者,很多我们用到的框架的底层通信都是Netty实现的,例如Dubbo、Hadoop等等,但凡是Java体系的即时通信系统,都无法绕开Netty,我自己也还在不断的学习,今天先看一下Netty的基本使用,后面会将Netty的各个组件深入的去分析,个人水平有限,如有不正确的,希望大家可以指正。
使用SpringBoot集成Netty的方式,创建一个Netty服务端程度:
package com.example.nettysourcedemo;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.concurrent.DefaultThreadFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class ServerRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
/**
* NioEventLoopGroup本身是一个eventLoopExecutor,里面包含一个childGroup=EventExecutor[]
* 数组中是NioEventLoop,每一个NioEventLoop中都包含一个selector,还包含一个executor
* 这个executor可以用来执行任务,在使用executor.execute执行任务时,会使用ThreadPerTaskExecutor.execute()方法执行
* 这个方法又会调用DefaultThreadFactory创建一个线程来启动任务的执行
* 重点关注NioEventLoopGroup构造器中执行的父类构造器
*/
NioEventLoopGroup boss = new NioEventLoopGroup(new DefaultThreadFactory("boss"));
NioEventLoopGroup worker = new NioEventLoopGroup(new DefaultThreadFactory("worker"));
NioEventLoopGroup business = new NioEventLoopGroup(new DefaultThreadFactory("business"));
ServerBootstrap b = new ServerBootstrap();
b.group(boss,worker)
//调用ReflectiveChannelFactory的构造器,创建NioServerSocketChannel
.channel(NioServerSocketChannel.class)
//设置boss线程的属性
.option(ChannelOption.SO_BACKLOG,1024)
//设置worker线程的属性,参考Linux tcp参数设置
.childOption(ChannelOption.TCP_NODELAY,true)
//ChannelInitializer也是一个ChannelInboundHandler,主要作用是往pipeline中添加handler,添加完handler之后就会从pipeline中移除
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//使用单独的业务线程池执行业务handler
pipeline.addLast(business,"stringDecoder",new StringDecoder());
pipeline.addLast(business,"stringEncoder",new StringEncoder());
pipeline.addLast(business,"simpleHandler",new SimpleHandler());
pipeline.addLast(business,"realHandler",new RealHandler());
}
});
ChannelFuture future = b.bind(9080).sync();
log.info("netty server started");
future.channel().closeFuture().sync();
boss.shutdownGracefully();
worker.shutdownGracefully();
business.shutdownGracefully();
}
}
ServerRunner类实现CommandLineRunner接口,显示的告知Spring容器,需要运行这个类。当然@Component注解也是必不可少的,需要告知Spring容器,这也是个JavaBean,需要容器去管理。
一个比较简单的Netty服务端程序主要包括设置parentGroup线程池(boss)、childGroup线程池(worker),设置使用的channel类型(NioServerSocketChannel),设置parentGroup线程的属性(#option()方法),设置childGroup线程的属性(#childOption()方法),设置流水线上的处理器(#childHandler()方法),绑定端口(#bind()方法)。
我写的这个程序比上面介绍的多了一个business的线程池,这个线程池主要用来处理比较耗时的业务流程。
Netty很贴心的提供了ServerBootstrap这个启动类,只要简单的几行代码就能启动一个功能强大的NettyServer。
为什么要设置一个parentGroup线程池,一个childGroup线程池呢?这就不得不提Netty大名鼎鼎的Reactor(反应堆)模型了,两个线程池的参数不同,Reactor的模式也有所不同。
-
只设置parentGroup(1)
NioEventLoopGroup boss = new NioEventLoopGroup(1);
-
只设置parentGroup(默认值)
NioEventLoopGroup boss = new NioEventLoopGroup();
-
设置parentGroup和childGroup
NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();
1.只设置parentGroup且参数为1
这种模式,整个反应堆中只有一个线程,这个线程既当爹又当妈,这个线程既要轮询客户端的连接状态,又要处理连接的IO操作,除此之外,非IO的操作,也要在这个线程上处理,这可能会大大延迟IO请求的响应,可以说这个线程是非常累了。这种模式只适用于连接数非常少的系统,像互联网这种动辄千万甚至过亿的并发,完全吃不消。
如下图,应用启动时,只有一个boss线程,当有多个客户端连接时,依然只有一个boss线程。
2.只设置parentGroup且参数大于1(默认值为当前服务器逻辑核心数的2倍)
当只设置parentGroup时,一个线程专门用来轮询客户端的连接事件,其他线程用来处理客户端连接后的IO事件的处理。
parentGroup设置为默认值时,应用启动,只有一个boss线程。
有一个客户端连接时,有两个boss线程。
有两个客户端连接时,有三个boss线程。
3.parentGroup和childGroup同时设置
当parentGroup和childGroup都设置为默认值时,parentGroup中只有一个boss线程用于轮询客户端连接事件,当有客户端连接上来后,所有的IO处理都会交给childGroup中的线程来处理。
应用启动时,只有一个boss线程
有一个客户端连接时,有一个boss线程,一个worker线程
有两个客户端连接时,有一个boss线程,两个worker线程
可以看出,当parentGroup和childGroup都设置时,parentGroup只有一个线程在运行中,但是,这时候服务端只监听了一个端口,让我们看一下服务端启动多个监听端口的情况,这时候程序运行后,会有两个boss线程。
有两个客户端分别连接不同端口的时候,两个boss,两个worker线程
因此,parentGroup中的线程,只会用于监听端口的连接事件,IO操作都会交给childGroup线程来处理。
以上的几种情况,都是在没有启用business线程的情况下测试的结果,代码需做如下修改:
pipeline.addLast("stringDecoder",new StringDecoder());
pipeline.addLast("stringEncoder",new StringEncoder());
pipeline.addLast("simpleHandler",new SimpleHandler());
pipeline.addLast("realHandler",new RealHandler());
那如果启用了business线程会是什么情况呢?将代码恢复之后测试看一下结果:
除了boss线程外,worker线程和business线程都出现了。这种情况,parentGroup中的线程负责监听端口的连接事件,childGroup中的线程负责read()/write()等IO操作,业务流程处理的handler,交给business线程池处理。
上一个10年,计算机界还在为C10K(单机1万连接)问题而抓脑袋,现在C100K(单机10万连接)已经能够实现,人们已经开始展望C10M(单机千万连接)的问题,虽然现在分布式架构、集群架构都已经非常成熟了,能够足以支撑千万甚至上亿的连接数,但是计算机科学界还是在不遗余力的想要提高单机并发连接数,我认为,谁能最快的实现C10M,谁就能成就超越腾讯的伟大公司。
要想做好即时通信产品,除了Java端的知识之外,还要对Linux服务器网络、IO、文件等有非常深的理解。
这一篇文章主要介绍了NettyServer中boss线程和worker线程的配置引发的Reactor模型的差异,下一篇介绍一下NioEventLoopGroup的创建过程(非常烧脑)。