IO原理
文件IO
一切皆文件抽象-> I/O流
操作系统内核,管理应用,管理硬件
vfs:虚拟文件系统树
,根,树上的节点映射到不同的物理位置,内存
虚拟文件系统抽象了硬件到程序,因为不同的文件系统驱动都不同,虚拟文件系统是暴露给用户程序的接口
inode号:id.磁盘中一个文件打开就会有一个inode号被加载(唯一)
page cache
(4k):文件从磁盘中读到内存,开辟一个空间,内存维护数据的缓存
dirty 脏.page cache读到内存后,app对它进行修改,它就会标记为dirty,于是要flush到磁盘中去.
flush到磁盘的方式决定了io模型(什么时候刷到磁盘),是到了物理内存的10%,还是一修改直接刷磁盘.
fd:文件描述符
硬盘分区,windows下是c盘,d盘,e盘
linux下只有文件目录,虽然在/根目录下都是在一起的,但是实际上这些文件分别在不同的硬盘分区中
例子:系统启动,先加载了3分区(sda3)中的/boot、/dev目录(实际存在的),再加载了1分区中的目录(sda1)
可以卸载磁盘的1号分区,此时根节点的/boot就没有值了,然后再安装1号分区,将目录挂载到根节点的/boot系统目录下就有值了(覆盖)
文件类型:开头
-
l:链接
-
s:socket
-
p:管道
-
-:普通文件
-
d:目录
链接:ln msb.txt ooxx.txt,这两个文件就被链接起来了都有同一个INode号,指向同一个物理内存位置,其中一方修改了文件内容另一方也看得见
硬、软链接:软链接删除链接文件后会报错,硬链接不会
持久化都是内核约束,如redis mysql
fd:文件描述符.0u标准输入,1u标准输出,2u报错输出.当前进程内用来描述文件的符号.可以理解为当前黑窗口,每个黑窗口都是不一样的.相当于一个变量,用来描述文件.同一个文件可以创建多个文件描述符fd
type:reg基础文件,chr字符设备,dir目录,ipV4链接
device:设备号
size/off:偏移量,如0t16,代表当前文件描述符已经读到16了,与文件描述符联合起来看,当前文件描述符内读到哪了
Swap:交换分区,内存虽然只有1个g,但是可以跑超过1个g的app,因为其中有一部分存在了swap区,swap用来存一些还没执行到的程序
socket:链接,如exec 8<> /dev/tcp/www.baidu.com:80 就会变成一个8的文件描述符指向百度的ip.
查看当前黑窗口的所有文件描述符 /proc/$$/fd
任何程序都有0,1,2代表标准输入,标准输出,报错输出
File file = New File("/wdd/myFile.txt");
out = new OutputStream(file);
在java层面得到的是一个对象out,在操作系统层面得到的是一个fd的文件描述符,会在/proc/$$/fd(linux)
150w REG 1,4 0 26456129 /Users/mcj/mcj.txt(这是在macos中的fd)
如ls命令,它的标准输出就是屏幕,标准输入是当前目录,
ls ./ 1> ./1.out 把当前目录,1标准输出输出到某个文件上1.out文件
进程隔离,子进程无法读取父进程数据
所以环境变量要加export,让变量具备导出功能,可以被子进程读取
管道
|:管道符号,语义为“通过管道”.
比如 { a = 9; echo “mcjnb”} | cat
定义变量a=9,打印mcjnb,将前面的值输出,通过管道传给cat命令,cat命令具有输入输出的功能.于是屏幕就出现了mcjnb.但是 打印a=9吗
管道的机制是,bash是解释执行的,看到管道 | 后,会在昨天创建一个子进程,右边创建一个子进程,把两个子进程的输入输出对接起来,然后再去执行a=9,echo “mcjnb”,但是由于操作系统的进程隔离级别很高,子进程a=9执行完之后就退出了,所以父进程a=1
但是echo $$ | cat 命令打印的还是父进程的pid,因为 的 ‘ 优 先 级 ‘ 要 高 于 ∣ 解 释 执 行 先 看 见 的`优先级`要高于 | 解释执行先看见 的‘优先级‘要高于∣解释执行先看见
相反 echo $BASHPID | cat 是先看见 | 再看见 bashpid的,所以先创建了子进程,打印子进程的pid.
此时查看/proc/4512(是pid)/fd可以看见左边的1输出,指向了一个pipe,/proc/4513(是pid)/fd的0输入指向了一个输入,管道被对接起来了
##page cache(访问文件)
页缓存,内核折中方案.也可以没有.
被缓存到内核中.
由于程序在内存中以page 4k为单位存储,所以内存中会有非常多的page空间,但是又不希望每次都去磁盘上读取,所以内核中就有page cache,是一种优化策略
操作系统进程想读取数据,进程找内核,内核找驱动,驱动找硬件然后返回数据,但是io延迟大,所以加了一个page cache缓存,应用app中也有缓存
为什么使用buffer 比不使用buffer快.
应用app访问内核时要进行system call,频繁进行syscall,效率就低
system call的实现 int 0x80:128 指为10000000,int是cpu的指令,要放在cpu寄存器中的中断描述符表相对应执行对应方法.
如果没有缓冲区优化,数据流通过程为,内核调用read fd方法,cpu找到磁盘,cpu将磁盘中数据放到cpu 寄存器
中,寄存器将数据放到内存中的page cache中(4k,数量很多)
从硬盘读取数据时,cpu发出指令,硬盘将数据存放到内存的某个位置,直接内存访问 dma
那么page cache使用多大内存,是否淘汰,是否会刷新
在内核中有限定值默认百分之90,指page cache超过系统可用内存
的百分之90后才写入磁盘,所以可能会丢失很多数据.
一个page cache从内存中被申请出来就是脏的,如果页没有脏数据那么内存空间满了优先淘汰.
程序访问文件,只要内存够,文件就一直被缓存着.
物理条件为内存可用3g
new FileOutputStream();
持久化
程序开始往文件中写数据,此时内存空闲非常多,程序对文件写数据,数据都写在内核的page cache中,并没有持久化到磁盘里.需要达到某临界点才会写到磁盘.如果此时断电,那么之前写的文件数据全部丢失
new BufferedOutputStream(); //jvm里面 有8kb大小的数组,满了之后再调用一次系统调用syscall,写入内核
淘汰
此时文件大小上涨的非常快,当内核page 持续上涨,并占用超过3g的样子,内核开始向磁盘写脏数据
,并淘汰掉一些page cache,持续写持续淘汰page 释放一些内存,再继续写.此时程序结束,内核中缓存了一些页.再开启程序进行持续写入文件,内核中的page cache被全部淘汰,成为新的page cache.链表
,内存不够才会淘汰
开启程序写数据,写到一半,没达到内核持久化要求断电了,那么数据则全部丢失.所以redis,mysql的持久化要求没有写完全交给内核
DMA:协处理器,
有了dma处理器之后,就不需要cpu寄存器来做中转了,内核直接访问硬盘,不需要cpu寄存器保存数据
内存以4k为单位,page(核心)
一块一块的使用内存,当产生缺页异常时(程序运行内存不够),产生软中断,内核会申请一块空间(4k),将虚拟内存地址再指向物理内存地址.一个程序消耗的内存以页为单位
由于进程间是隔离的,一个app在自己的视角内占有整个操作系统
操作系统程序(进程)的内存空间由,
- 代码段:如果内核空间缓存了page cache,那么一份文件读取到内存中变成多个进程时的代码段就可以指向同一个page cache
- 数据段
- 栈
- 堆,正常程序堆:,jvm是一个进程,进程内部功能又分配了1g的堆(若干个4k).ByteBuffer.allocateDirect(8192);就是分配在这里,堆外分配.则ByteBuffer.allocate(8192);是jvm堆上分配
2个相同进程,对应1个硬盘文件,对应1个page cache,有各自的fd文件描述符,各自的seek偏移量(打开文件行)
为什么不推荐直接io而是使用buffer方式
cpu态之间的切换过于频繁,带buffer的有缓冲区减少了系统调用损耗
随机读写类,randomAccessFile类,文件io
有seek,修改指针偏移:每一个程序打开一个文件,得到fd,fd内有一个seek,记录读取的字符数量,相当于一个指针,可以自由的从中间插入数据.一半的IO类没有,seek可以移动到其他的page cache,因为一个文件有很多page cache组成
抽象channel,将输入输出合并了.只有文件FIlechannel有map方法,map方法通过内核系统调用,获得一个堆外的buffer,只有文件可以做内存映射,把内存的page cache与文件的物理地址映射起来,
map.put方法会将数据放到内核的page cache.
曾经使用out.write().这样的系统调用才能让程序的data 进入内核的page cache,必须有用户态内核态的切换.但是mmap的内存映射依然是内核的page cache体系所约束,不会进行系统调用,但是还是会丢数据
fileChannel
FileChannel channel = rw.getChannel();
channel.map
map方法映射到内核的page cache,不会进系统调用.是一个进程和内核共享的内存区域,这个内存区域是page cache到文件的映射
channel.read方法也会进行系统调用,将数据放入page cache
性能 on heap < off heap < mapped,只限文件系统io
kafka : mmap
netty : on heap、off heap.
os没有绝对的数据可靠性,为什么设计page cache,减少硬件的 io 调用,优先使用内存
网络IO
bio(会有阻塞的)
当开启socket服务端,监听端口时,调用netstat -natp 会出现0.0.0.0:9090 listen 7932/java代表正在监听9090端口
此时lsof -p 7932(服务端口),新增了一个name为listen的文件描述符(单纯开启了socket服务端).
虽然此时没有进行accpet方法(通常accept会永久阻塞等待客户端,但是也可以设置超时时间),但是客户端启动连接,服务端已经可以抓包了,并且客户端也可以给服务端发数据.这条socket在内核中已经有了,但是没有分配给程序.
本地 客户端 pid
192.168.1.150.11:9090 192.168.159.12:47513 (pid这里没有)
~~
此时若是执行了accept方法,lsof -p 7932进程则会新增一个文件描述符,描述socket连接,
0.0.0.0:8090 0.0.0.0:* listen
151u IPv6 0x1fee7bed41cec58f 0t0 TCP *:8090 (LISTEN)
同时netstat -natp,那条在内核中的socket连接被分配给了7932
本地 客户端 pid state
192.168.1.150.11:9090 192.168.159.12:47513 7932。 established(缓冲区开好,3次握手走完)
state
SYN_RECV状态 ,当非常多的连接进入内核时,同时又没有分配给进程,代码中配置的back_log=2(配置的连接后续队列2个,备胎,备胎之外的连接就不要再链接了,拒绝连接)
tcp是面向连接的可靠的传输协议
链接不是物理的,面向三次握手,内核要开启资源(链接),socket是一个四元组,C_IP_C_P+S_IP_S_P,内核级的,即便不调用accept,也会进行资源的接收,这个四元组记录在内核中保存.只要四元组唯一,就可以开辟十几万链接.每一个四元组都会分配一个fd文件描述符(唯一,使用流),文件描述符是在进程内分配的,进程内唯一.一个文件描述符对应一条内核中的连接条目socket.
socket下有一个缓冲区(网卡、内核等各级缓冲)buffer,client链接走传输控制层,链接到socket.这就可以通信了,buffer满了会有数据丢弃.比如满数据大小为1920,后面客户端再发送的数据,服务端就不会接受了.因为缓冲区满了
在客户端,也有内核,内核中也有socket链接条目,建立连接后,客户端进程也有文件描述符fd.双方进行数据读写在buffer中完成
- api
setSendBuffer(20).设置发送的缓冲区大小为20,但是可以超过20
tcpNodelay(true):不优化、不延迟,直接发送数据包.false的话会攒一攒数据再发送,比如qwertyuiop
这些字母发出去如果为false则发送端会存储一下再发,到服务端就成为了.“qwer”,“tyu”,“opi”,这样分开的,因为没有优化了
keepalive,tcp如果双方建立连接,如果很久都没消息,于是会发送心跳,数据包length为0
网络io变化模型
同步、异步、阻塞、非阻塞
阻塞:当服务端调用read开始读数据时,会切换到内核态进行系统调用,此时用户空间就不能继续往下执行,这就是阻塞,相反,如果可以继续往下执行,就叫非阻塞
sout,进行了系统调用的write方法
accept也进行了系统调用阻塞了accept(3),3是socket连接的fd,是监听8090.
Accept(3 , 7)这个7代表新的客户端连接fd,和java api的 accept方法很相似监听有无连接
new Socket系统调用
在代码中,new Socket(8090),在系统调用就是创建一个socket(调用socket的),用文件描述符3来表示,同时把3绑定(bind系统调用)到端口8090,然后再监听3fd.
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress(8090));
fd
mcj 152w REG 1,4 0 26456129 /Users/mcj/mcj.txt
mcj 151u IPv6 0x1fee7bed41cea0cf 0t0 TCP *:8090 (LISTEN)
然后程序代码调用*accept*
,进行系统调用accept(3,
日志在此处阻塞了,此时如果有客户端连接,那么日志继续往下执行,日志显示客户端ip,端口,在服务端内生成的连接文件描述符fd.例子中创建了一个线程(调用系统调用clone方法,生成的,系统调用日志中会显示一个进程号比如8447)
当有客户端连接服务端后,准备读取socket内的数据了,系统调用recv(fd3, -----在读数据时阻塞
所以,bio由一个线程迎接每个到来的连接,并分配一个独立的线程去接受.
随着连接数的变大
线程的增多,系统硬件无法承受
线程调度成本、内存成本
只要是网络IO,都要进行上面的系统调用
NIO(new IO jdk中的意思,系统里是 nonblock的意思)
此时有代码逻辑1:windows使用虚拟+本机两个ip地址,使用同一端口连接服务器,情况只有一个ip地址连接了另一个不见了.是因为回包时的路由出了问题
连接的速度很慢?是因为BIO在接受客户端连接,创建线程clone系统调用,内核态用户态切换,进行了两次的系统调用
bio的弊端.accept阻塞,read也阻塞,线程创建要切换内核态用户态
- 优势
通过1个或者几个线程就可以实现接收非常多的连接,并且接受数据
非阻塞如何实现.
- api
ServerSocketChannel.open();//输入输出都在一起
ss.bind(new InetSocketAddress(8090));
ss.configureBlocking(false);//核心,不要阻塞
SocketChannel socket = ss.accept;//nio不阻塞,不阻塞返回客户端,如果有连接就不为null
client.configureBlocking(false);//客户端也可以设置不阻塞,操作系统非阻塞
client.read(allocate);//bio时,如果客户端不发数据给服务端,read系统调用就会阻塞住,现在设置了非阻塞则不会阻塞,没有数据就返回0
//在非阻塞模型中,接受客户端与读取数据是分两部分
List<Client> client;//会有很多client
非阻塞的系统调用实现
原本socket监听时会调用accept,若是阻塞的话,accept(4, 非阻塞的话accept(4,0xkdkdjfdf)) = -1
再系统调用fcntl(4(socket的fd),F_SETFL,O_RDWR|O_NONBLOCK) = 0,设置非阻塞
会直接返回-1.所以每次都要死循环接收新客户端连接,同时也要读取所有老客户端传来的数据(代码逻辑)
同时需要不断的循环客户端连接进行查看哪些客户端可以读取数据read(一次系统调用recv效率低下,并且存在严重浪费)
问题:连接建立的还不够快,因为是单线程接收客户端请求,若是有请求,再3次握手,再建立连接,再三次握手,建立连接,是一个线性的过程.
系统配置文件描述符不得高于1024,那么如果高于1024呢.
为什么连接数超过了1024呢?root用户与普通用户的区别
问题,调用了多余浪费的read系统调用!程序去问有无数据,程序遍历fd
多路复用器(NIO的延伸,内核遍历fd),内核里的空间
路:每一条io
复用:一次调用,将很多条路的状态都获取到
通过一次系统调用,获取所有路的状态,然后由程序自己读写有状态的io.
同步:程序自己r/w,等待有效返回数据
异步:内核来r/w,不访问io,只访问buffer
阻塞非阻塞,系统调用accept是否是nonblock
同步阻塞:程序自己r/w,等待有效返回数据
同步非阻塞,程序自己读取,在调用读取方法一瞬间会给出是否得到数据,
第一个多路复用器 select(将fd发给内核,由内核遍历,上面的NIO则是用户态遍历).一个系统调用select
需要程序主动调用,调用时内核再遍历
根据给出的fd,查看哪些fd的buffer里有数据,然后返回.
监听多个文件描述符,先调用select()系统调用将文件描述符传入返回,得到哪些描述符已经可以读写了,再调用recv接受数据.但是限制了1024个文件描述符
poll(问题:重新传递文件描述符,重复,对这次调用触发文件描述符全量遍历的复杂度)
没有1024个文件描述符限制
无论是nio select poll,都是要遍历所有的io,询问状态,但是NIO的成本是在用户态内核态转换以及一直调用recv
select 以及 poll只进行了一次系统调用,将所有文件描述符都传给了内核.由内核进行遍历,查询状态,遍历比nio的方式快.
它们的弊端在于:
-
总是接收,拷贝数据,传参数,用户态内核态传递数据
-
每次调用都要重新传递文件描述符
-
每次内核被调用之后,针对这次调用触发文件描述符全量的复杂度,遍历
为什么需要传参数拷贝数据呢?因为内核空间和用户空间都是虚拟空间,用户空间.不可以访问内核空间,所以需要传递参数.为了不再拷贝数据,所以开辟了一个共享空间
mmap系统调用是用来创建虚拟地址与物理地址的映射关系
插入
中断向量表里会有指令的回调函数,int值
软中断:cpu读取了程序的指令,程序要调用内核方法,触发软中断 int 0x80,都可以打断cpu
硬中断:时间中断,cpu分片技术,打断cpu
IO中断:由IO请求产生中断,网卡,键盘,鼠标,打断cpu
最终:中断-回调-event
#如何知道接收到数据
网卡IO(通过软中断)
中断的回调上有无加东西,当有客户端连接,发送数据包到了网卡就一定会发生IO中断,调用中断向量表回调函数(重点)
网卡会把收到的数据传给内存,网卡向cpu发送一个中断信号,此时中断程序将接收到的数据写入socket的缓冲区内,再唤醒阻塞的读取线程
epoll(全称eventpoll,中断延伸,有缓存区,可以存储fd,不需要遍历 ,但是依然是同步模型,需要程序调用wait函数,函数告诉程序哪些fd有数据可以读 ,多路复用器只管状态能不能读,不管要不要读)高效的管理多个链接
ByteBuffer buffer1 = (ByteBuffer) selectionKey.attachment();
//这个buffer有三个值capacity表示容量指针
//position 在写时,表示当前写到哪了到位置指针。在读时,指针在头上
//limit 在写时,表示最多能写多少数据,通常与capacity在一起, 在读时,表示最多能读到多少数据
Recv-q:收到的数据在本地缓存中,但是没有被进程取走
Send-q:发送数据在本地,对方还没有收到
客户端给服务端发送数据,服务端没收取就是在send-q里面.为什么在读之后注册写事件,写事件被一直触发.人想什么时候写就是自己决定的,多路复用器判断能不能写是依赖send-q是否为空
第一个疑问
client.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(8192));
最后面的这个buffer是什么意思.这个buffer似乎可以被传递
selectorKey.cancel 是 register的 相反,将注册的事件给去除掉
,模型中的selectKey指fd4,accept(连起来看)
对于poll重复传递fds的问题,如果内核中开辟空间缓存fds就可以规避,主要是规避了遍历,让数据主动通知
epoll多路复用器会在内核中开辟fd红黑树,在callback时fd加入红黑树
,
创建socket = fd4 ,bind,listen,accept常规创建socket连接
epoll_create,epoll_ctl(add\dele),epoll_wait
调用epoll_create返回一个epfd6
,开辟一个内核空间,fd6是描述的红黑树空间),后频繁调用epoll_ctl(fd6,add,fd4,accept);表示在fd6这个空间内加入fd4,关注accept事件.红黑树中就会添加一个fd4.再调用epoll_wait,wait的是一个这个空间中的链表(链表内存储哪些fd有数据发送过来),当网卡内有fd数据发送过来时,将数据发送到fd的buffer内,同时将新的客户端连接fd放到红黑树结构内,并设置一个可以读事件(EPOLLIN,系统调用里的)
epoll_wait(7(7是epfd),{{EPOLLIN,{u32=4,u64=8737272282333}}},4096,-1)=1
如果有一个连接的fd触发了事件
则会拷贝到一个链表中供程序获取epoll_wait
内核中有许多fd,每个fd都会有网卡数据传输事件的过程.
调用epollwait,解决了不需要select(fds)传递fds的问题,因为已经有了,不触发内核遍历
netstat -natp 有读队列,写队列,写队列是会满的
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
//listen的fd,创建的socket = fd4
serverSocketChannel.bind(new InetSocketAddress(9090));
//如果是epoll模型,那么就会执行epoll_create = fd6,开辟一个多路复用器空间
//如果select,poll的情况下.jvm里创建一个数组,fd放进去
selector=Selector.open();//多路复用器,根据平台选择也可以指定
//epoll则是执行epoll_ctl(fd6,add,fd4,事件)
//serverSocketChannel是listen的fd4(创建的socket),将它注册到多路复用器上,并且关注accept这个行为
//register并不是调用就马上执行,而是等下一次selector在执行,有懒加载
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
public void start() throws IOException {
//调用多路复用器
//java中的select()方法是什么意思,
try {
//查看所有注册的fd
Set<SelectionKey> keys = selector.keys();
//调用多路复用器
/*
如果是select、poll是在jvm开辟数组调用内核的select方法并把fd4放进去
如果是epoll的话就调用内核的epoll_wait方法,得到触发事件的fd
在java中,调用select()方法是什么意思
1。select模型下,调用内核select(fd4),这个fd4中没有数据可读
2。在epoll下,是调用epoll_wait(),查看可以读写的fd链表
3.在poll模型下,会调用poll()系统调用
例子poll([{fd=5,events=POLLIN},{fd=4,events=POLLIN}],3,-1) = ’1‘
如果poll返回了1代表一个fd有事件,如果是-1非阻塞下,没有事件
*/
while (selector.select() > 0) {
System.out.println("有事件发生");
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
System.out.println(key);
if (key.isAcceptable()) {
acceptHandler(key);
//key是有很多状态的
} else if (key.isReadable()) {
System.out.println("可读");
read(key);
} else if (key.isWritable()) {
//只要send-q为空就一定可以返回能写的事件,就会回调
System.out.println("可写");
writeHandler(key);
}
}
}
}
}
//read事件只有在recv没有取出时才会触发,也就是fd.read(buffer)没调用时触发,fd的recv-q里有数据,没被进程取走
public void read(SelectionKey key){
//获取fd的通道
SocketChannel client =(SocketChannel) key.channel();
//获取跟随着fd的缓冲区?
ByteBuffer buffer = (ByteBuffer)key.attachment();
//缓冲区的数据需要手动清空
buffer.clear();
//自己创建的缓冲区,每次都是新的
ByteBuffer allocate = ByteBuffer.allocate(1024);
try {
//client读取数据放到缓冲区内,recv-q里的数据读取出来
int read = client.read(buffer);
if(read>0){
//关于buffer的操作,指在写的时候会不会重复写回去的问题,因为只想写一次,所以要操作一下
buffer.flip();
//写回客户端
client.write(buffer);
//注册了一个写事件,同时将,数据buffer,注册
client.register(key.selector(), SelectionKey.OP_WRITE, allocate);
buffer.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
}
//写事件,什么时候发生-》send-q有关,netstat -natp,只要send-q是空的就一定会返回可以写的事件,就会回调我们写的方法
//客户端给服务端发数据时,表示链接的fd会有send-q,表示对方没有收到数据,send-q表示服务端发给客户端的数据对方没收到
//recv-q如果有数据就表示没被服务端进程拿走,客户端发送给服务端的数据,存在服务端本地
//写事件以来读事件
//但是什么时候写?不是依赖send-q队列是否为空
//1。准备好写什么数据
//2。关心send-q是否有空间
//3。读,read一开始就可以注册读事件,但是write,需要读到东西之后,再注册写事件fd,然后才可以写,write依赖以上读关系,什么时候用什么时候注册
//如果一开始就注册write,就会进入死循环状态一直调用
//原本accept得到新客户端后注册read事件,当有fd可以读时就wait得到fd
//注册关心write事件就是关注send-q是否有空间
public void writeHandler(SelectionKey selectionKey) throws IOException {
SocketChannel client = (SocketChannel) selectionKey.channel();
//fd的buffer?
System.out.println(selectionKey);
ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
client.write(buffer);
buffer.clear();
//原本做的功能是不断开连接客户端一直发送,也一直可以收到回复,当然是加了以下这个代码实现
//去掉以下代码,则一直重复可写,推测是重新注册了fd的事件,将fd的recv-q给清空了
//client.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(8192));
//client.cancel();//功能似乎是将client的fd从内核中去除
}
/*
首先虽然开启了socket连接服务端,但是并没有进行accept,但是内核中已经开启了监听listen
,客户端已经可以给服务端发送数据了。连接已经在内核中了,只是没有分配进程。没有分配fd
*/
public void acceptHandler(SelectionKey selectionKey) throws IOException {
ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel();
SocketChannel client = channel.accept();
client.configureBlocking(false);
System.out.println(client.getRemoteAddress());
client.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(8192));
}
引出问题,异步处理 与 IO解藕
在readHandler中多线程调用时,会引发调用重复读,因为readHandler不阻塞了,会直接返回,但是线程中执行完代码调用key.cancel还有一段时间所以会引发问题.cancel是进行系统调用epoll_ctl(m,del,n)在epfd空间踢掉了内核中的文件描述符
为什么提出多线程模型呢?因为考虑到资源利用,如果有一个fd执行非常耗时,那么会阻塞后面的fd执行.
当有多个fd需要r/w时,可以讲fd分组,一组一个selector,将一个selector放到一个线程上,最好的线程数量是cpu的核数或者X2的数量.单个线程内是一部分fd,里面是线性处理的.“分治”,如果有100万链接,里面有4个线程,每个线程里有selector,那么每个线程处理25万.是否可以分一个线程只关注accept,然后将客户端fd分配给其他线程?
多个Selector(新思路,分治)
坑:在多线程情况下,当初始化
SelectorThreadGroup(int num){
selectorThreads=new SelectorThread[num];
for (int i = 0; i < num; i++) {
selectorThreads[i]=new SelectorThread();
//创建了一个多路复用器
new Thread(selectorThreads[i]).start();
}
}
多路复用器先调用了select();此时被阻塞住了,因为没有超时事件,然后在执行绑定端口时
public void bind(int port) {
try {
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(port));
//server得注册到哪个selector呢.开局绑定一个端口,后面不会调用了,
SelectorThread selectorThread = nextSelector(serverSocketChannel);
} catch (IOException e) {
e.printStackTrace();
}
}
//无论是server还是普通socket都用这个方法选择一个.
public SelectorThread nextSelector(Channel fd) {
//其中一个线程,其中一个多路复用器
SelectorThread next = next();
System.out.println("调用next"+Thread.currentThread().getName()+next);
// ServerSocketChannel selector1 = (ServerSocketChannel) fd;
// try {
// //所以需要马上让它返回,效果与select(50)一样
// //但是是放在注册上面还是下面呢
// next.selector.wakeup();
// //因为初始化线程时已经调用select()方法,系统调用已经阻塞,所以注册事件也会阻塞
// ((ServerSocketChannel) fd).register(next.selector, SelectionKey.OP_ACCEPT);
// } catch (ClosedChannelException e) {
// e.printStackTrace();
// }
//通过队列传递数据
next.linkedBlockingQueue.add(fd);
//通过打断阻塞,让对应线程去自己在打断后完成注册selector
next.selector.wakeup();
return next;
}
此时register就不会执行,因为已经被阻塞了,系统调用里面还在epoll_wait
@Override
public void run() {
//loop
while (true){
try {
int nums = selector.select();
if(nums>0){
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
iterator.remove();
if(key.isAcceptable()){
acceptHandler(key);
}else if(key.isReadable()){
readHandler(key);
}else if(key.isWritable()){
}
}
}
//此时是selector里面没有注册东西,所以在此处可以写从队列拿fd注册的逻辑.此时分治的多路复用器是空的.可以往里面注册东西,同时用一个linkedblockqueue,来保存要注册的fd
//next选择方法,可以理解为选择一个多路复用器
if(!linkedBlockingQueue.isEmpty()){
Channel channel = linkedBlockingQueue.take();
if(channel instanceof ServerSocketChannel){
ServerSocketChannel server = (ServerSocketChannel) channel;
server.register(selector,SelectionKey.OP_ACCEPT);
}else if(channel instanceof SocketChannel){
SocketChannel channel1 = (SocketChannel) channel;
ByteBuffer bf = ByteBuffer.allocate(1024);
channel1.register(selector,SelectionKey.OP_READ,bf);
}
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
此时有两个多路复用器,这里设定当有新客户端链接时,随机挑选一个多路复用器注册读取事件,否则fd都是在这个多路复用器里,另一个
复用器就浪费了
*/
private void acceptHandler(SelectionKey key) {
ServerSocketChannel channel = (ServerSocketChannel)key.channel();
try {
SocketChannel client = channel.accept();
client.configureBlocking(false);
SelectorThread next = selectorThreadGroup.nextSelector(channel);
client.register(next.selector,SelectionKey.OP_READ);
} catch (IOException e) {
e.printStackTrace();
}
}
修改建议,分配算法建议修改为均匀一些的,
新版本 boss、worker
此时SelectorThreadGroup为一个线程组,里面的线程数组用来处理listen监听,将客户端都放到worker组里面去,所以在选择nextSelector的时候需要选到workerGroup里面的线程.
最终模型变为可以listen多个端口,工作线程都放在group的group里面
public class SelectorThreadGroup {
ServerSocketChannel serverSocketChannel;
SelectorThread[] selectorThreads;
AtomicInteger atomicInteger = new AtomicInteger(0);
//核心是持有一个work group的对象
SelectorThreadGroup worker;
public void nextSelectorV2(Channel channel) {
if (channel instanceof ServerSocketChannel) {
SelectorThread selectorThread = nextV2();
System.out.println("本地注册一个listen"+selectorThread);
try {
selectorThread.linkedBlockingQueue.put(channel);
selectorThread.selector.wakeup();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
SelectorThread work = next();
System.out.println(work+"的selector注册一个客户端");
//通过队列传递数据
work.linkedBlockingQueue.add(channel);
//通过打断阻塞,让对应线程去自己在打断后完成注册selector
work.selector.wakeup();
}
}
private SelectorThread next() {
int index = atomicInteger.incrementAndGet() % worker.selectorThreads.length;
return worker.selectorThreads[index];
}
private SelectorThread nextV2() {
int index = atomicInteger.incrementAndGet() % selectorThreads.length;
return selectorThreads[index];
}
netty
抽象出NioEventLoop来表示一个不断循环执行处理任务的线程,每个NioEventLoop有一个selector,用于监听绑定在其上的socket链路。
一个NioEventLoopGroup下包含多个NioEventLoop
每个NioEventLoop中包含有一个Selector,一个taskQueue,一个delayedTaskQueue
每个NioEventLoop的Selector上可以注册监听多个AbstractNioChannel
每个AbstractNioChannel只会绑定在唯一的NioEventLoop上
每个AbstractNioChannel都绑定有一个自己的DefaultChannelPipeline
//客户端
public static void main(String[] args) throws IOException, InterruptedException {
//多路复用器?group,线程池
//这个thread数量可以理解为有多少个工作线程
//可以理解为selectorGroup,多路复用器
NioEventLoopGroup selector = new NioEventLoopGroup(2);
//创建一个链接fd,链接的抽象
NioSocketChannel client = new NioSocketChannel();
//将客户端注册到一个多路复用器上
selector.register(client);
//netty的Handler,注册事件.这些handler最终会放到pipeline里面
//响应式
//当自身为客户端时,关注的handler关注的register就是客户端向服务端链接成功时触发
//服务端的epfd空间内只有0.0.0.0:9090 listen 关注,每一条链接在内核中都是一个fd,单纯的一个fd,
//读写事件只是recv-q 或者 send-q 是否为空所决定触发的而已
ChannelPipeline pipeline = client.pipeline();
pipeline.addLast(new MyInHandler());
//io中,事件是第一步,读取是第二步
//reactor 异步的特征
ChannelFuture connect = client.connect(new InetSocketAddress("127.0.0.1", 9999));
//链接阻塞一下
ChannelFuture sync = connect.sync();
ByteBuf buf = Unpooled.copiedBuffer("hello server".getBytes());
//客户端给服务端发送数据
ChannelFuture channelFuture = client.writeAndFlush(buf);
//发送阻塞一下,等待发送成功
channelFuture.sync();
//等着关闭链接事件,是否关闭了链接,
sync.channel().closeFuture().sync();
client.close();
}
public class MyInHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
System.out.println("注册成功");
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("active");
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//可读事件
ByteBuf buf = (ByteBuf) msg;
//读多少,读这个buf有多大的字节数
// CharSequence str = buf.readCharSequence(buf.readableBytes(), CharsetUtil.UTF_8);
CharSequence charSequence = buf.getCharSequence(0, buf.readableBytes(), CharsetUtil.UTF_8);
// System.out.println(str);
System.out.println(charSequence);
//往回写
ctx.writeAndFlush(buf);
}
}
nio 服务端
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup thread = new NioEventLoopGroup(1);
NioServerSocketChannel server = new NioServerSocketChannel();
thread.register(server);
ChannelFuture bind = server.bind(new InetSocketAddress("127.0.0.1", 9090));
//accept接收客户端 并注册客户端
server.pipeline().addLast(new AcceptHandler(thread, new MyInHandler()));
//关注关闭事件,close未来会发生,等到他发生后再往下执行
bind.channel().closeFuture().sync();
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
SocketChannel client = (SocketChannel) msg;
//1。注册
nioEventLoopGroup.register(client);
//2。响应,理解为客户端关注了某些事件,这些事件就是handler可重写的事件
//原本channel1.register(selector,SelectionKey.OP_READ,bf);
//某个客户端注册进epfd空间,并关注这个客户端的read事件
//这里代码是直接关注client的所有事件,并选择性重写方法
client.pipeline().addLast(channelHandler);
}
但是现在没办法接收更多的客户端.于是添加了一个@ChannelHandler.Sharable
那么如果,只要有客户端链接我就给它添加一个通用handler,这样它就可以被添加进别的客户端的pipeline里了,但是它就得设计成单例的.
所以设计了一个通用的handler
public class ChannelInit extends ChannelInboundHandlerAdapter {
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
//只要有客户端链接就给他添加一个myinhandler
Channel client = ctx.channel();
//在此处添加了一个handler
client.pipeline().addLast(new MyInHandler());
}
}
acceptHandler
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//accept拿到了一个客户端
System.out.println("拿到了一个客户端");
SocketChannel client = (SocketChannel) msg;
//2。响应,理解为客户端关注了某些事件,这些事件就是handler可重写的事件
//为这个客户端关注事件,然后就会调用
//此时这个handler是ChannelInit
client.pipeline().addLast(channelHandler);
//1。注册进selector
nioEventLoopGroup.register(client);
}
myinhandler
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//可读事件
ByteBuf buf = (ByteBuf) msg;
//读多少,读这个buf有多大的字节数
// CharSequence str = buf.readCharSequence(buf.readableBytes(), CharsetUtil.UTF_8);
CharSequence charSequence = buf.getCharSequence(0, buf.readableBytes(), CharsetUtil.UTF_8);
// System.out.println(str);
System.out.println(charSequence);
//往回写
ctx.writeAndFlush(buf);
}
-bash-3.2$ nc 127.0.0.1 9090
gg
gg
-bash-3.2$
中间绕了一下,为了共享handler,简称过桥.功能是为了预埋一个handler.
首先加入了一个handler,这个handler关注register事件,并在register里放入了一个会回写的handler,当有客户端链接时,会触发register事件,然后触发逻辑放了一个会回写的handler
nio框架已经写好了预埋handler的逻辑.只要调用childHandler(new ChannelInitializer就可以了
Bootstrap快速构建客户端
public static void main(String[] args) throws InterruptedException {
//selector
NioEventLoopGroup eventExecutors = new NioEventLoopGroup(1);
//nio的封装
Bootstrap bootstrap = new Bootstrap();
//启动类
Bootstrap bs = bootstrap.group(eventExecutors)
//和server建立连接时要自己new,现在让启动类处理
.channel(NioSocketChannel.class)
//建立链接后,客户端放什么,放了一个会回写的handler
.handler(new ChannelInit());
ChannelFuture connect = bs.connect(new InetSocketAddress("127.0.0.1", 9999));
connect.sync();
//关注关闭同步,close未来会发生,等到他发生后再往下执行
connect.channel().closeFuture().sync();
}
改良
public static void main(String[] args) throws InterruptedException {
//selector
NioEventLoopGroup eventExecutors = new NioEventLoopGroup(1);
//nio的封装
Bootstrap bootstrap = new Bootstrap();
//启动类
Bootstrap bs = bootstrap.group(eventExecutors)
//和server建立连接时要自己new,现在让启动类处理
.channel(NioSocketChannel.class)
//建立链接后,客户端放什么
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ChannelInit());
}
});
ChannelFuture connect = bs.connect(new InetSocketAddress("127.0.0.1", 9999));
connect.sync();
//关注关闭同步,close未来会发生,等到他发生后再往下执行
connect.channel().closeFuture().sync();
}
nettyServer
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup eventExecutors = new NioEventLoopGroup(1);
ServerBootstrap serverBootstrap = new ServerBootstrap();
ChannelFuture bind = serverBootstrap.group(eventExecutors, eventExecutors)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new MyInHandler());
}
})
.bind(new InetSocketAddress("127.0.0.1", 9090));
bind.sync().channel().closeFuture().sync();
}
对象池
对象池类似于线程池,避免大规模创建同一个类型的对象,对象会缓存好一些已经有的对象
基于threadlocal实现的轻量级对象池,每个线程都有一个对象池,如果从某个线程中取出对象传递给了其他线程,其他线程用完要归还对象
Recycle,对象池的管理者
public final T get() {
if (maxCapacityPerThread == 0) {
return newObject((Handle<T>) NOOP_HANDLE);
}
//每个线程从这个stack中拿到对象
Stack<T> stack = threadLocal.get();
DefaultHandle<T> handle = stack.pop();
if (handle == null) {
handle = stack.newHandle();
handle.value = newObject(handle);
}
return (T) handle.value;
}
stack,对象池容器
static final class Stack<T> {
// we keep a queue of per-thread queues, which is appended to once only, each time a new thread other
// than the stack owner recycles: when we run out of items in our stack we iterate this collection
// to scavenge those that can be reused. this permits us to incur minimal thread synchronisation whilst
// still recycling all items.
final Recycler<T> parent;
// We store the Thread in a WeakReference as otherwise we may be the only ones that still hold a strong
// Reference to the Thread itself after it died because DefaultHandle will hold a reference to the Stack.
//
// The biggest issue is if we do not use a WeakReference the Thread may not be able to be collected at all if
// the user will store a reference to the DefaultHandle somewhere and never clear this reference (or not clear
// it in a timely manner).
final WeakReference<Thread> threadRef;
final AtomicInteger availableSharedCapacity;
final int maxDelayedQueues;
private final int maxCapacity;
private final int ratioMask;
private DefaultHandle<?>[] elements;
private int size;
private int handleRecycleCount = -1; // Start with -1 so the first one will be recycled.
private WeakOrderQueue cursor, prev;
private volatile WeakOrderQueue head;
WeakOrderQueue
private static final class WeakOrderQueue {
static final WeakOrderQueue DUMMY = new WeakOrderQueue();
// Let Link extend AtomicInteger for intrinsics. The Link itself will be used as writerIndex.
@SuppressWarnings("serial")
static final class Link extends AtomicInteger {
private final DefaultHandle<?>[] elements = new DefaultHandle[LINK_CAPACITY];
private int readIndex;
Link next;
}
netty的堆外内存,内存池
堆外内存可以扩展更大的空间,减少jvm的对象复制,减少io时的内存复制.zero copy
当某个channel可以读数据了,直接复制到堆外内存,无需复制到属于用户空间的程序内存(如果创建堆内内存的话,读写需要复制一份到堆内内存中)中
申请内存
directByteBuffer底层使用unsafe对象申请,native方法
nio中对零拷贝应用有directByteBuffer,堆外内存,提高传输效率
rpc框架
rpc是通过本地去调用远程方法,面向java就是interface开发,比如dubbo.通过动态代理加入,
粘包拆包
粘包和半包:指的都不是一次正常的bytebuf接收
粘包:接收数据时多个bytebuf粘在了一起,读取一次bytebuf,结果读到了许多bytebuf
半包:接收端将发送端的bytebuf拆开来了,需要读取很多次
Bytebuf.Readable 可读取的数据 有多少字节 大于 设定的空包存放数据的大小.
原因:当多个线程共同往一个链接里发数据时,数据会被发送到内核缓冲区中,其中多个包会连在一起.netty在read的时候不能保证数据的完整性,而且一次read可能处理多个message.同时多个massage时,最后一个message可能就只有一半数据需要下次读取才会补全.可能也有数据会丢失,内核中只有一半数据
bytebuf.getbyte();读取指定大小数据,但是指针不变
bytebuf.readbyte();读取指定大小数据,指针改变
public class NettyDecode extends ByteToMessageDecoder {
//父类里有channel read
//收纳in里没读取完的数据,用来解决粘包半包问题,将留存的与新接的拼在一起
//这个实现在 channel。read后执行,channel.read会拼接buf
//在handler前执行,out就是每一个消息体的封装
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
//往out里加数据后,对象会给到后面addlast(handler的msg变量里),原本的msg变量就不是bytebuf了而是out里的对象
}
}
由于IO是双向的,发送与接收端都要解决这样的问题
原理
底层buffer存不了这么多数据
服务端为什么会报端口被占用
唯一性,未来有数据包发送到服务端时,没办法区分到底给谁
为什么netty使用nio而不是aio
查阅资料说是,linux上的aio底层还是epoll 与 nio效率相差不大
aio是接收数据需要先分配缓存,nio是要接收了才分配缓存,空间的利用率上