使用Netty可以编写一个 Http服务器, 就像tomcat那样,能接受用户发送的http请求, , 只不过没有实现Servelt规范, 但是它也能解析携带的参数, 对请求的路径进行路由导航, 从而实现将不同的请求导向不同的handler进行处理
Netty可是实现的第二件事就是Socket编程,也是它最为广泛的应用领域
进行微服务开发时不可丢弃的一个点, 服务和服务之间如果使用Http通信不是不行,但是http的底层使用的也是socket, 相对我们直接使用netty加持socket效果会更好 (比如阿里的Dubbo)
当然Netty能做的事还有很多比如自定义通信协议等等,,,
Netty对WebSocket也提供了强大的支持, netty内置的处理器会为我们做好大量的机械性麻烦性的工作, 如 WebSocketServerProtocolHandler
内置编解码处理, 心跳检验等, 可以让我们专注于实现自己的业务逻辑
Reactor线程模型, 顾名思义就像核反应堆一样, 是一种很劲爆的线程模型
最经典的两种图就像下面这样
上面两种图所描述的都是Reactor线程模型
handle本质上表示是一种资源 , 在不同的操作系统上他们的名称又不一样, 比如在windows上成它为文件句柄 , 而在linux中称它为文件描述符, 其实我这样说, handle的概念就显得比较抽象 不容易理解 , 具体一点说handle是啥呢 ?比如客户端向服务端发送一个连接请求,这个socket请求对操作系统来说, 本质上就是handle
意为同步事件分离器, 也是一看这个名字完全没有头绪它是什么, 其实, 在本质上它是一个操作系统层面的系统调用, 操作系统用它来 阻塞的等待 事件的发生, 具体一点它来说, 比如它可以是Linux系统上的IO多路复用, select(), 或者是 poll(),epoll() , 或者是Nio编程模型中的selector, 总之, 它的特点就是阻塞的等待事件的发生
事件处理器, 事件处理器是拥有Handle的, 我们可以直观将将EventHandler理解成是当系统中发生了某个事件后, 针对这个事件进行处理的回调 , 为啥说是回调, 结合netty的实现中, 我们在启动netty前, 会往他的pipeline中添加大量的handler ,这些handler的地位其实和 EventHandler的地位相当
顾明思议, 具体的事件处理器的实现, 换句话说, 这是我们根据自己的需求, 不同的业务逻辑自己去添加上去的处理器
初始分发器, 它其实就是整个编程模型的核心, 没错, 他就是Reactor, 具体怎么理解这个Reactor , 比如我们就能把他看成一个规范, 由它聚合, 支配其他的四大角色之间有条不紊的工作, 迸发出巨大的能量
首先: 我们需要将 EventHandler注册进 Reactor, 通过上图也能看到, EventHandler 拥有 操作系统底层的 Handle,
并且, Reactor 要求, 当EventHandler 注册进自己时, 务必将他关联的handle一并告诉自己, 由Reactor统一进行维护
更近一步: 当所有的EventHandler都注册进了Reactor中后, Reactor就开始了它放纵的一生, 于是它开始调与 同步事件分离器通信 ,企图等待一些事件的发生, 什么事件呢? 比如说 socket的连接事件
当同步事件分离器发现了某个handle的状态发生了改变, 比如它的状态变成了ready, 就说明这个handle中发生了感兴趣的事件, 这时, 同步事件分离器会将这个handle的情况告诉Reactor , Reactor进一步用这个handle当成key, 获取出相对应的 EventHandler 开始方法的回调...
当我们进行socket编程时, 我们得给Server端绑定上一个端口号, 客户端一般会被自动分配Server所在的机器上的一个端口号, 区间一般是1025-65535之间, 这样看上去, 即使服务器的性能再强, 即使netty再快, 并发数目都被操作系统的特性限制的死死的
像 windows中的句柄或者是linux的文件描述符 这种能打开的资源的数量是有限的, 每一个socket连接都是一个句柄或者是描述符, 换句话说, 这一个特性限制了socket连接数, 也就限制了并发数
查看linux系统中一个进程能打开的单个文件数,(它限制了单个jvm能打开的文件描述符数,每一个tcp连接对linux来说都是一个文件)
ulimit -n
修改这个限制:
# 在 /etc/security/limits.conf 追加以下内容 , 表示任何用户的链接最多都能打开100万个文件 * hard nofile 100000 * soft nofile 100000
重启机器生效:
查看当前系统中的全局文件句柄数
cat /proc/sys/fs/file-max
修改这个配置
# 在 /etc/sysctl.conf 中追加如下的内容 fs.file-max = 1000000
# 堆内存 -Xms6.5g -Xmx6.5g # 新生代的内存 -XX:NewSize=5.5g -XXMaxNewSize=5.5g # 对外内存 -XX: MaxDirectMemorySize=1g
Netty基于Reactor这种线程模型的, 进行非阻塞式编程, 一般我们在编写服务端的代码时, 都会在 往 服务端的Channel pipeline上添加大量的 入站出站处理器, 客户端的消息一般我们都是在 handler中的 ChannelRead()
或者是 ChannelRead0()
中取出来, 再和后台的业务逻辑结合
客户端的消息,会从Pipeline这个双向链表中的header中开始往后传播, 一直到tail, 这其实是个责任链
这时, 如果我们将耗时的操作放在这些处理器中, 毫无疑问, nettey会被拖垮, 系统的并发量也提升不上去
新开辟一个线程池 , 将耗时的业务逻辑放到新开辟的业务去执行
public class MyThreadPoolHandler extends SimpleChannelInboundHandler<String> { private static ExecutorService threadPool = Executors.newFixedThreadPool(20); @Override protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception { threadPool.submit(() -> { Object result = searchFromMysql(msg); ctx.writeAndFlush(ctx); }); } public Object searchFromMysql(String msg) { Object obj = null; // todo obj = return obj; } }
netty 提供的一种原生的解决方式, netty可以将我们的handler 放到一个专门的线程池中去处理
public class MyInitializer extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); // 业务线程池 NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup(20); // 使用这个重载的方法,给handler指定线程池去执行 pipeline.addLast(eventLoopGroup,new MyHandler()); } }
当时我在学netty时, 是从github上拉取的netty原生工程, 然后在本地重新编译运行, 这样我就能在源码工程中写注解, 记笔记...
在这个工程里面大概写了 1400 多行注释(所有的笔记都打上了todo 高亮标记) , 也翻译了一些类和方法上的文档和注释, 不能说百分百正确, 但是这个过程也是挺走心的, 比如netty是如何解决JDK空轮询的bug的? 这些在代码中都是有迹可循的,如下
github地址: https://github.com/zhuchangwu/netty-project