BIO/NIO
Redis、Zookeeper、Netty,游戏服务器等其实底层就是IO通信程序
J2SE
BIO:阻塞IO(Blocking IO)
1 | public class SocketServer { |
程序启动的时候,会阻塞在Socket clientSocket = serverSocket.accept();
这一行。
只有有客户端连接进来了之后,才会跳出阻塞,建立客户端Socket连接。
Telnet
客户端:与服务端建立连接(telnet localhost 9000
)
int read = clientSocket.getInputStream().read(bytes)
这里的read也是个阻塞方法,如果客户端没有给服务端发数据就一直阻塞在那里,客户端一旦发了数据,就跳出阻塞。
很少用BIO写网络通信的东西了,不能支撑高并发。
但如果改成这样:
1 | public static void main(String[] args) throws IOException { |
是可以支持高并发。但是C10K、C10M问题。
C10K、C10M问题:线程是会占有内存空间的。如果没有几万几十万的连接进来,没有及时发数据,会把服务端的内存撑爆掉。
如果用线程池,最多500个线程,内存可能不会爆掉,但是并发量会被线程数限制住了。
所以BIO程序再怎么优化并发都是有问题的。
比如500个连接,一直不给我们发数据,线程一直会保持着,再来一个客户端连接,是进不来了,因为线程用完了。
主要问题就是这些方法是阻塞的。
后来JDK1.4一些版本中,提供了NIO一些类库。
BIO阻塞模型图
同步阻塞模型,一个客户端连接对应一个处理线程。
NIO:非阻塞IO(non block IO/ new IO)
1 | public class NioServer { |
以上是基本入门级NIO模型。
不管来多少客户端,都会处理。
redis单线程模型为什么并发量那么多
后端的模型,也是NIO非阻塞的。
问题:
while(true):会一直在那转,CPU可能百分之百。
while循环假设十万个连接已建立连接,每秒可能只有几个十个才有数据发送来。大量的CPU的浪费和空转。
优化:
- 把专门有数据收发的放到集合中,只遍历这个集合。每次循环都能读到数据。
- 如果连接没有给服务端发数据,就不转,避免空转。
多路复用器——selector
1 | public class NioSelectorServer { |
NIO线程模型图
selector、epoll
1 | Selector selector = Selector.open(); |
Linux的JDK底层实际上是EPollSelectorImpl
对象。
初始化的时候会初始一个 EPollArrayWrapper
一个数组。
Hotspot VM源码:
本地方法,native
怎么找底层的本地方法(C/C++实现)呢?
下载openjdk,全文搜索,类名_方法 (EPollArrayWrapper_epollCreate
)
EpollArrayWrapper.c
Epfd: 文件描述符。可以看做 epoll Instance的索引
epoll_create:Linux系统函数,Linux内核的源代码。
去到Linux系统:man epoll_create
创建了一个epoll的结构体。Selector是对epoll的封装。
serverSocket.register(selector, SelectionKey.OP_ACCEPT);
register底层调用了implRegister
方法(EpollSelectorImpl)
fd:serverSocketChannel、SocketChannel等 文件描述符
实际上把channel丢到了selector内部的array集合里去。
selector.select()
java程序实现通信是底层调用了大量的系统函数让他们进行通信的。
有事件发生之后,是先被操作系统感知到,中断程序会把这些事件
放到就绪事件列表。
epoll_wait:监听。(阻塞,让出CPU)
NIO其实是底层用了epoll的几个系统函数帮我们做的这一套实现(放事件,epoll_wait监听,没有事件进来就阻塞着)。
select、poll、epoll模型的区别
都是Linux的内核函数。
jdk的1.4是用select实现。
有channel的时候,select会遍历所有的channel。十万个channel也会轮询一遍。有大量的空循环。(最多不能连1024)
1.4升级了一版——poll。
逐个遍历,还是会有空转。(但是没有连接上限)
jdk1.5以上——epoll。
redis的底层线程模型
NIO、多路复用
redis源码:ae_epoll.c
、ae_kqueue.c
(不同操作系统用的不同)
通过C语言的ae_epoll.c实现的。
redis启动的时候
监听channel注册过来的事件。
Redis、Zookeeper、Netty,游戏服务器等其实底层就是IO通信程序。
redis、Netty底层用的NIO。
Zookeeper:NIO、BIO都用了,用在不同的场景。
Netty主从Reactor高并发线程模型
Netty:IO通信程序
真正关心拿到数据之后怎么开发我们的业务逻辑。
netty帮我们把一系列的事情封装好。
重复性的代码封装好放到框架里,最终它就给你数据,你来处理数据。
netty的入门程序:
1 | public class NettyServer { |
1 | /** |
我们发现Netty框架的目标就是让你的业务逻辑从网络基础应用编码中分离出来,让你可以专注业务的开 发,而不需写一大堆类似NIO的网络处理操作。
可以分多个selector(多路复用器),连接事件用一个,读事件用另一个
主从selector
Netty:底层就是对NIO程序的封装。
线程池:Boos Group主线程池1、Worker Group从线程池8
redis为什么不用netty底层来通信?
要用原生的epol函数而不用netty呢?
netty:大量的异步、大量的多线程模型。优化服务的性能。
如果用netty来实现就是多线程了。redis6.0多线程,但是核心处理服务还是单线程的,只是IO通信数据解析用的多线程。
需要控制并发安全问题了。
分布式锁、幂等控制都是基于单线程模型来实现的。
响应式编程
Reactor Pattern
把我们的IO事件分发给对应的处理方法去处理。基于事件、驱动
NIO其实也是响应式编程模型。只处理事件,关心事件发生后的处理逻辑就可以了。
真正就绪事件列表只不过是操作系统内核中断程序(epoll_wait)放的,Java程序只需要处理。
selector相当于观察者。监听着事件发生。
Netty集群
单机百万连接Netty高并发架构