NIO
IO Hello world 对比
传统IO
package org.io;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author fpp
* @version 1.0
* @date 2020/7/28 18:37
*/
public class OldIO {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket=new ServerSocket(101);
//此行代码是阻塞形代码 当有客户端连接进来时,才会继续执行下面的代码
Socket socket=serverSocket.accept();
BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(socket.getInputStream()));
String a;
//这个 InputStream的实现是SocketInputStream 此行代码readLine是同步阻塞形代码 当有客户端输入数据进来时,才会继续执行下面的代码
while((a=bufferedReader.readLine())!=null){
System.out.println(a);
}
}
}
并且当前这个小程序不能连多个客户端只能连一个客户端
根据控制台打印的结果?可知当客户端不输入时,是没有数据过来的,那么提问
疑问
- 是socket 主动请求的呢?还是系统底层自动推送过来的呢
- 为什么只能连一个客户端呢
通过查看源码 发现 SocketInputStream 的read 方法最终调用的是
private native int socketRead0(FileDescriptor fd,
byte b[], int off, int len,
int timeout)
该方法是同步阻塞形代码,
如果TCP RecvBuffer里没有数据,函数会一直阻塞,直到收到数据,返回读到的数据。
多线程BIO
package org.io;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @author fpp
* @version 1.0
* @date 2020/7/28 18:45
*/
public class MultiThreadOldIO {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(101);
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(2, 3, 2000, TimeUnit.MILLISECONDS, new ArrayBlockingQueue(3));
while (true) {
Socket socket = serverSocket.accept();
poolExecutor.execute(() -> {
try {
System.out.println(Thread.currentThread().getName() + "获得链接");
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String a;
while ((a = bufferedReader.readLine()) != null) {
System.out.println(a);
}
} catch (Exception e) {
e.printStackTrace();
}
});
}
}
}
通过使用while 循环,每连接上一个客户端,就为这个客户端开启一个线程 为其读取客户端数据。
NIO 非租塞式 IO
package org.io;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
/**
* @author fpp
* @version 1.0
* @date 2020/7/28 19:31
*/
public class NIOTest {
private Selector selector;
public void initServer() throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress(101));
this.selector = Selector.open();
serverSocketChannel.register(this.selector, SelectionKey.OP_ACCEPT);
}
public void initListen() throws IOException {
while (true) {
//死循环 阻塞等待新事件的到来 这个阻塞不会导致cpu的空转
this.selector.select();
Iterator<SelectionKey> selectionKeyIterator = this.selector.selectedKeys().iterator();
while (selectionKeyIterator.hasNext()) {
SelectionKey selectionKey = selectionKeyIterator.next();
//删除 避免重复处理
selectionKeyIterator.remove();
handle(selectionKey);
}
}
}
public void handle(SelectionKey key) throws IOException {
if (key.isAcceptable()) {
doHandleAccept(key);
} else if (key.isReadable()) {
doHandleRead(key);
}
}
private void doHandleRead(SelectionKey key) throws IOException {
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
socketChannel.read(byteBuffer);
String string = new String(byteBuffer.array());
System.out.println(string);
ByteBuffer byteBufferWrite = ByteBuffer.wrap("good job".getBytes("UTF-8"));
socketChannel.write(byteBufferWrite);
}
private void doHandleAccept(SelectionKey key) throws IOException {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(this.selector, SelectionKey.OP_READ);
}
public static void main(String[] args) throws IOException {
NIOTest nioTest=new NIOTest();
nioTest.initServer();
nioTest.initListen();
}
}
BIO 与NIO 对比
BIO | NIO
Socket | SocketChannel
ServerSocket | ServerSocketChannel
无 | Selector
int epoll_ctl(int epfd, intop, int fd, struct epoll_event*event);
/**函数声明:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
该函数用于控制某个epoll文件描述符上的事件,可以注册事件,修改事件,删除事件。
参数:
epfd:由 epoll_create 生成的epoll专用的文件描述符;
op:要进行的操作例如注册事件,可能的取值EPOLL_CTL_ADD 注册、EPOLL_CTL_MOD 修 改、EPOLL_CTL_DEL 删除
fd:关联的文件描述符;
event:指向epoll_event的指针;
如果调用成功返回0,不成功返回-1
第一个参数是epoll_create()的返回值,
第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD: 注册新的fd到epfd中;
EPOLL_CTL_MOD: 修改已经注册的fd的监听事件;
EPOLL_CTL_DEL: 从epfd中删除一个fd;
第三个参数是需要监听的fd,
第四个参数是告诉内核需要监听什么事件,structepoll_event结构如下:
events可以是以下几个宏的集合:
EPOLLIN: 触发该事件,表示对应的文件描述符上有可读数据。(包括对端SOCKET正常关闭);
EPOLLOUT: 触发该事件,表示对应的文件描述符上可以写数据;
EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR: 表示对应的文件描述符发生错误;
EPOLLHUP: 表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT: 只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。
**/
int epoll_wait(int epfd, struct epoll_event * events, intmaxevents, int timeout);
//等待epfd上的就绪队列 socket df事件发生
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-newwa8Xa-1596183095941)(C:\Users\Administrator\Desktop\学习\博客\Netty\linux.png)]
1.水平触发的时机
对于读操作,只要缓冲内容不为空,LT模式返回读就绪。对于写操作,只要缓冲区还不满,LT模式会返回写就绪。当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它还会通知你在上没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会一直通知你。如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率。
2.边缘触发的时机
对于读操作当缓冲区由不可读变为可读的时候,即缓冲区由空变为不空的时候。当有新数据到达时,即缓冲区中的待读数据变多的时候。当缓冲区有数据可读,且应用进程对相应的描述符进行EPOLL_CTL_MOD 修改EPOLLIN事件时。对于写操作当缓冲区由不可写变为可写时。当有旧数据被发送走,即缓冲区中的内容变少的时候。当缓冲区有空间可写,且应用进程对相应的描述符进行EPOLL_CTL_MOD 修改EPOLLOUT事件时。当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你。这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符。
在ET模式下, 缓冲区从不可读变成可读,会唤醒应用进程,缓冲区数据变少的情况,则不会再唤醒应用进程。
举例1:
读缓冲区刚开始是空的读缓冲区写入2KB数据水平触发和边缘触发模式此时都会发出可读信号收到信号通知后,读取了1KB的数据,读缓冲区还剩余1KB数据水平触发会再次进行通知,而边缘触发不会再进行通知举例2:(以脉冲的高低电平为例)
水平触发:0为无数据,1为有数据。缓冲区有数据则一直为1,则一直触发。边缘触发发:0为无数据,1为有数据,只要在0变到1的上升沿才触发。JDK并没有实现边缘触发,Netty重新实现了epoll机制,采用边缘触发方式;另外像Nginx也采用边缘触发。
JDK在Linux已经默认使用epoll方式,但是JDK的epoll采用的是水平触发,而Netty重新实现了epoll机制,采用边缘触发方式,netty epoll transport 暴露了更多的nio没有的配置参数,如 TCP_CORK, SO_REUSEADDR等等;另外像Nginx也采用边缘触发。
总结
epoll是一种I/O事件通知机制,是linux 内核实现IO多路复用的一个实现。
IO多路复用是指,在一个操作里同时监听多个输入输出源,在其中一个或多个输入输出源可用的时候返回,然后对其的进行读写操作。
当创建一个socket时,
1.内核会为这个创建一个由文件系统管理的对象socket 里面有读写缓冲区和等待队列,并且调用系统调用 epoll_create() 实例化一个epoll内核中的对象并且返回文件描述符(我的理解应该是java里面的引用)
2.系统调用epoll_ctl 将 创建好的socket的引用添加进 epoll对象中的就绪队列中,并且注册事件(回调函数)。
3.调用系统调用epll_wait 等待就绪队列中的socket事件的发生
水平触发:就是只要读缓冲区里还有数据,内核会一直通知socket有数据可以读了
边缘触发: 内核只会通知你一次,读缓冲区中有数据,当你再次调用epoll_wait epollin 事件时,是不会再次通知你有数据了,可能会造成数据的丢失。
象并且返回文件描述符(我的理解应该是java里面的引用)
2.系统调用epoll_ctl 将 创建好的socket的引用添加进 epoll对象中的就绪队列中,并且注册事件(回调函数)。
3.调用系统调用epll_wait 等待就绪队列中的socket事件的发生
水平触发:就是只要读缓冲区里还有数据,内核会一直通知socket有数据可以读了
边缘触发: 内核只会通知你一次,读缓冲区中有数据,当你再次调用epoll_wait epollin 事件时,是不会再次通知你有数据了,可能会造成数据的丢失。