Java IO -- 笔记

感觉这一部分,面试问不太多,快速看一下。同样还是看cyc大佬笔记

1、Java IO分类:

1、磁盘操作:File
2、字节操作:InputStream、OutputStream
3、字符操作:Reader、Writer
4、对象操作:Serializable
5、网络操作:Socket
6、新的输入输出:NIO

2、磁盘操作:File类

File类可以用于表示文件和目录的信息,但是不表示文件内容。
Java7开始,可以使用Paths和Files代替File

//使用File类递归地列出一个目录下所有文件:
public static void listAllFiles(File dir) {
    if (dir == null || !dir.exists()) {
        return;
    }
    if (dir.isFile()) {
        System.out.println(dir.getName());
        return;
    }
    for (File file : dir.listFiles()) {
        listAllFiles(file);
    }
}

3、字节操作:InputStream及OutputStream

//实现文件复制
public static void copyFile(String src, String dist) throws IOException {
    FileInputStream in = new FileInputStream(src);
    FileOutputStream out = new FileOutputStream(dist);
    byte[] buffer = new byte[20 * 1024];
    int cnt;
    // read() 最多读取 buffer.length 个字节
    // 返回的是实际读取的个数
    // 返回 -1 的时候表示读到 eof,即文件尾
    while ((cnt = in.read(buffer, 0, buffer.length)) != -1) {
        out.write(buffer, 0, cnt);
    }
    in.close();
    out.close();
}

Java IO使用了装饰者模式来实现。

InputStream是抽象组件。
FileInputStream是InputStream的子类,属于具体组件,提供字节流的输入操作。
FilterInputStream属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能。
		---例如BufferedInputStream为FileInputStream提供缓存的功能。

在这里插入图片描述
实例化一个具有缓存功能的字节流对象,只需要在FileInputStream对象上套一层BufferedInputStream对象

FileInputStream fileInputStream = new FileInputStream(filePath);
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);

DataInputStream 装饰者提供了对更多数据类型进行输入的操作,比如 int、double 等基本类型。

4、字符操作:writer、reader

编码&解码

编码是把字符转换为字节,解码是把字节重新组合成字符。
如果编码解码的编码方式不同就会出现乱码。

GBK 编码中,中文字符占 2 个字节,英文字符占 1 个字节;
UTF-8 编码中,中文字符占 3 个字节,英文字符占 1 个字节;
UTF-16be 编码中,中文字符和英文字符都占 2 个字节。
---- be 指的是 Big Endian,也就是大端。UTF-16le,le 指的是 Little Endian,也就是小端。

Java 的内存编码使用双字节编码 UTF-16be,这不是指 Java 只支持这一种编码方式,而是说 char 这种类型使用 UTF-16be 进行编码。char 类型占 16 位,也就是两个字节,Java 使用这种双字节编码是为了让一个中文或者一个英文都能使用一个 char 来存储

String的编码方式

String 可以看成一个字符序列,可以指定一个编码方式将它编码为字节序列,也可以指定一个编码方式将一个字节序列解码为 String。

String str1 = "中文";
byte[] bytes = str1.getBytes("UTF-8");
String str2 = new String(bytes, "UTF-8");
System.out.println(str2);

//getBytes() 的默认编码方式与平台有关,一般为 UTF-8。
byte[] bytes = str1.getBytes();

InputStreamReader & OutputStreamWriter:

InputStreamReader 实现从字节流解码成字符流
OutputStreamWriter 实现字符流编码成为字节流

实现逐行输出文本文件的内容:

public static void readFileContent(String filePath) throws IOException {

    FileReader fileReader = new FileReader(filePath);
    BufferedReader bufferedReader = new BufferedReader(fileReader);

    String line;
    while ((line = bufferedReader.readLine()) != null) {
        System.out.println(line);
    }

    // 装饰者模式使得 BufferedReader 组合了一个 Reader 对象
    // 在调用 BufferedReader 的 close() 方法时会去调用 Reader 的 close() 方法
    // 因此只要一个 close() 调用即可
    bufferedReader.close();
}

5、对象操作:Serializable

序列化就是讲一个对象转换成字节序列,方便存储和传输。

序列化:ObjectOutputStream.writeObject()
反序列化:ObjectInputStream.readObject()
不会对静态变量进行序列化,因为序列化只是保存对象的状态,静态变量属于类的状态。

Serializable
序列化的类需要实现 Serializable 接口,它只是一个标准,没有任何方法需要实现,但是如果不去实现它的话而进行序列化,会抛出异常。

transient
transient 关键字可以使一些属性不会被序列化。
transient 关键字的作⽤是:阻⽌实例中那些⽤此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。 transient 只能修饰变量,不能修饰类和⽅法。

例:
ArrayList 中存储数据的数组elementData 是用 transient 修饰的,因为这个数组是动态扩展的,并不是所有的空间都被使用
因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据。

6、网络操作

Java中的网络支持:

1、InetAddress:用于表示网络上的硬件资源,即IP地址
		--没有公有的构造函数,只能通过静态方法创建实例。
		
2、URL:统一资源定位符
		--可以直接从URL中读取字节流数据。
		
3、Sockets:使用TCP协议实现网络通信
		--ServerSocket:服务器端类
		--Socket:客户端类

4、Datagram:使用UDP协议实现网络通信
		--DatagramSocket:通信类
		--DatagramPacket:数据包类

Sockets的服务端和客户端通过InputStream 和 OutputStream进行输入输出。
在这里插入图片描述

7、NIO

JDK1.4引入了新的输入/输出库(NIO),提供了高速的、面向块的I/O。

1、NIO与普通IO的区别:

1、NIO是非阻塞的
2、NIO面向块,IO面向流。

2、流与块

数据打包和传输的方式:IO以流的方式处理数据,NIO以块的方式处理数据。

面向流的IO一次性处理一个字节数据:一个输入流产生一个字节数据,一个输出流消费一个字节数据。
为流式数据创建过滤器非常容易,链接几个过滤器,以便每个过滤器只负责复杂处理机制的一部分
不利的一面是,面向流的 I/O 通常相当

面向块的 I/O 一次处理一个数据块,按块处理数据比按流处理数据要快得多。
但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。

java.io.* 包中的一些类包含以块的形式读写数据的方法,这使得即使在面向流的系统中,处理速度也会更快。

3、通道与缓冲区

1、通道 Channel

是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。

通道与流的不同之处在于:
流只能在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),
而通道是双向的,可以用于读、写或者同时用于读写。

通道的类型:

1、FileChannel:从文件中读写数据;
2、DatagramChannel:通过 UDP 读写网络中数据;
3、SocketChannel:通过 TCP 读写网络中数据;
4、ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。

2、缓冲区

发送给一个通道的所有数据都必须首先放到缓冲区中,同样地,从通道中读取的任何数据都要先读到缓冲区中。也就是说,不会直接对通道进行读写数据,而是要先经过缓冲区。

缓冲区实质上是一个数组,但它不仅仅是一个数组。
缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。

缓冲区包括以下类型:

1、ByteBuffer
2、CharBuffer
3、ShortBuffer
4、IntBuffer
5、LongBuffer
6、FloatBuffer
7、DoubleBuffer

4、缓冲区状态变量:

capacity:最大容量
position:当前已经读写的字节数
limit:还可以读写的字节数

状态变量的改变过程举例:

① 新建一个大小为 8 个字节的缓冲区,此时 position 为 0,而 limit = capacity = 8。capacity 变量不会改变,下面的讨论会忽略它。
② 从输入通道中读取 5 个字节数据写入缓冲区中,此时 position 为 5,limit 保持不变。
③ 在将缓冲区的数据写到输出通道之前,需要先调用 flip() 方法,这个方法将 limit 设置为当前 position,并将 position 设置为 0。
④ 从缓冲区中取 4 个字节到输出缓冲中,此时 position 设为 4。
⑤ 最后需要调用 clear() 方法来清空缓冲区,此时 position 和 limit 都被设置为最初位置。

NIO快速复制文件:

public static void fastCopy(String src, String dist) throws IOException {
    /* 获得源文件的输入字节流 */
    FileInputStream fin = new FileInputStream(src);
    /* 获取输入字节流的文件通道 */
    FileChannel fcin = fin.getChannel();
    /* 获取目标文件的输出字节流 */
    FileOutputStream fout = new FileOutputStream(dist);
    /* 获取输出字节流的文件通道 */
    FileChannel fcout = fout.getChannel();
    /* 为缓冲区分配 1024 个字节 */
    ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
    while (true) {
        /* 从输入通道中读取数据到缓冲区中 */
        int r = fcin.read(buffer);
        /* read() 返回 -1 表示 EOF */
        if (r == -1) {
            break;
        }
        /* 切换读写 */
        buffer.flip();
        /* 把缓冲区的内容写入输出文件中 */
        fcout.write(buffer);
        /* 清空缓冲区 */
        buffer.clear();
    }
}

5、选择器:

NIO被叫作非阻塞IO,在网络通信中具有非阻塞特性

NIO 实现了 IO 多路复用中的 Reactor 模型,一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去监听多个通道 Channel 上的事件,从而让一个线程就可以处理多个事件

在这里插入图片描述

通过配置监听的通道 Channel 为非阻塞,那么当 Channel 上的 IO 事件还未到达时,就不会进入阻塞状态一直等待,而是继续轮询其它 Channel找到 IO 事件已经到达的 Channel 执行

因为创建和切换线程的开销很大,因此使用一个线程来处理多个事件而不是一个线程处理一个事件,对于 IO 密集型的应用具有很好地性能。

应该注意的是,只有套接字 Channel 才能配置为非阻塞,而 FileChannel 不能,为 FileChannel 配置非阻塞也没有意义。

//创建选择器
Selector selector = Selector.open();

//将通道注册到选择器上
ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.configureBlocking(false);//通道必须配置为非阻塞模式
ssChannel.register(selector, SelectionKey.OP_ACCEPT);

//监听事件
int num = selector.select();//使用 select() 来监听到达的事件,它会一直阻塞直到有至少一个事件到达。

//获取到达的事件
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    if (key.isAcceptable()) {
        // ...
    } else if (key.isReadable()) {
        // ...
    }
    keyIterator.remove();
}

//事件循环
//因为一次 select() 调用不能处理完所有的事件,并且服务器端有可能需要一直监听事件,因此服务器端处理事件的代码一般会放在一个死循环内。

while (true) {
    int num = selector.select();
    Set<SelectionKey> keys = selector.selectedKeys();
    Iterator<SelectionKey> keyIterator = keys.iterator();
    while (keyIterator.hasNext()) {
        SelectionKey key = keyIterator.next();
        if (key.isAcceptable()) {
            // ...
        } else if (key.isReadable()) {
            // ...
        }
        keyIterator.remove();
    }
}

在将通道注册到选择器上时,还需要指定要注册的具体事件,主要有以下几类:

SelectionKey.OP_CONNECT
SelectionKey.OP_ACCEPT
SelectionKey.OP_READ
SelectionKey.OP_WRITE

它们在 SelectionKey 的定义如下:

public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;

可以看出每个事件可以被当成一个位域,从而组成事件集整数。例如:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

套接字NIO实例:

public class NIOServer {

    public static void main(String[] args) throws IOException {

        Selector selector = Selector.open();

        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        ssChannel.configureBlocking(false);
        ssChannel.register(selector, SelectionKey.OP_ACCEPT);

        ServerSocket serverSocket = ssChannel.socket();
        InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
        serverSocket.bind(address);

        while (true) {

            selector.select();
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = keys.iterator();

            while (keyIterator.hasNext()) {

                SelectionKey key = keyIterator.next();

                if (key.isAcceptable()) {

                    ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel();

                    // 服务器会为每个新连接创建一个 SocketChannel
                    SocketChannel sChannel = ssChannel1.accept();
                    sChannel.configureBlocking(false);

                    // 这个新连接主要用于从客户端读取数据
                    sChannel.register(selector, SelectionKey.OP_READ);

                } else if (key.isReadable()) {

                    SocketChannel sChannel = (SocketChannel) key.channel();
                    System.out.println(readDataFromSocketChannel(sChannel));
                    sChannel.close();
                }

                keyIterator.remove();
            }
        }
    }

    private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {

        ByteBuffer buffer = ByteBuffer.allocate(1024);
        StringBuilder data = new StringBuilder();

        while (true) {

            buffer.clear();
            int n = sChannel.read(buffer);
            if (n == -1) {
                break;
            }
            buffer.flip();
            int limit = buffer.limit();
            char[] dst = new char[limit];
            for (int i = 0; i < limit; i++) {
                dst[i] = (char) buffer.get(i);
            }
            data.append(dst);
            buffer.clear();
        }
        return data.toString();
    }
}
public class NIOClient {

    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("127.0.0.1", 8888);
        OutputStream out = socket.getOutputStream();
        String s = "hello world";
        out.write(s.getBytes());
        out.close();
    }
}

6、内存映射文件

内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。

向内存映射文件写入可能是危险的,只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。

下面代码行将文件的前 1024 个字节映射到内存中,map() 方法返回一个 MappedByteBuffer,它是 ByteBuffer 的子类。因此,可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行映射。

MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);

Socket

网络中进程之间如何通信?

本地的进程间通信(IPC)可分为四类:
1、消息传递(管道、FIFO、消息队列)
2、同步(互斥量、条件变量、读写锁、文件和写记录锁、信号量)
3、共享内存(匿名的和具名的)
4、远程过程调用(Solaris门和Sun RPC)

在本地,可以通过进程PID来唯一标识一个进程。
在网络中,网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程。

什么是Socket?

网络中的进程是通过Socket来通信的,源于Unix,socket是“open—write/read—close”模式的一种实现。
在这里插入图片描述

四元组:源IP地址、目的IP地址、源端口、目的端口
五元组:源IP地址、目的IP地址、协议号、源端口、目的端口
七元组:源IP地址、目的IP地址、协议号、源端口、目的端口,服务类型以及接口索引

在这里插入图片描述

在这里插入图片描述
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

在这里插入图片描述
服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。
这时如果有个客户端初始化了一个Socket,然后连接服务器(connect),如果成功连接,这时客户端与服务器端的连接建立。
客户端发送数据请求,服务端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,完成一次交互。

1、 socket() 函数

对应普通文件的打开操作,socket()用于创建一个socket描述符,唯一标识一个socket

int socket(int domain, int type, int protocol);
domain:协议域,又称协议簇。
type:指定socket类型。
protocol:指定协议。

当调用socket创建一个socket时,返回的socket描述符存在于协议簇空间中,但没有一个具体的地址。
如果想给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统自动随机分配一个端口。

2、bind()函数

用于把一个地址簇中的特定地址赋给socket。

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:即socket描述符,通过socket() 函数创建得到,唯一标识一个socket。bind()函数给这个描述符绑定一个名字。
addr:指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,
addrlen:对应地址的长度

通常服务器在启动时会绑定一个地址(IP地址 + 端口号),客户通过它连接服务器。
而客户端不用指定,系统自动分配一个端口号和自身ip地址组合。

这就是为什么通常服务器端在listen之前会调用bind(),客户端则不会,而是在connect()时由系统随机生成一个。

3、listen()函数

服务器在调用socket()、bind()之后会调用listen()监听这个socket。

int listen(int sockfd, int backlog);

listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。
socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。

4、connect()函数

客户端通过调用connect函数来建立与TCP服务器的连接。

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。

5、accept()函数

TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。
TCP客户端依次调用socket()、connect()之后就向TCP服务器发送了一个连接请求。
TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。
之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

第一个参数是服务器的socket描述符,第二个参数是指向struct sockaddr *的指针,用来返回客户端的协议地址,第三个参数是协议地址的长度。
如果accept成功,那么返回值是由内核自动生成的一个全新的描述符,代表与返回客户的TCP连接。

第一个参数是服务器的socket描述符,是服务器最初调用socke()函数生成的,成为监听socket描述符;返回的是已连接的socket描述符。
一个服务器通常只创建一个监听socket描述符,在该服务器的生命周期内一直存在。
内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述符,当服务器完成了对某个客户的服务,响应地已连接的socke描述符就被关闭。

6、read() 、write()、close()函数

网络I/O操作:
read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()

close一个TCP socket的缺省行为是把该socket标记为已关闭,然后立即返回到调用进程。
该描述符不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。

注意:close操作只是使相应socket描述符的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。

函数的具体调用:

在这里插入图片描述
当客户端调用connect时,触发了连接请求,向服务器发送了SYN J包,这时connect进入阻塞状态;
服务器监听到连接请求,即收到SYN J包,调用accept函数接收请求向客户端发送SYN K ,ACK J+1,这时accept进入阻塞状态;
客户端收到服务器的SYN K ,ACK J+1之后,这时connect返回,并对SYN K进行确认;
服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立。

客户端的connect在第二次握手时返回,而服务器端的accept在第三次握手时返回。

在这里插入图片描述

一、五种I/O模型

一个输入操作通常包括两个阶段:
1、等待数据准备好
2、从内核向进程复制数据

对于一个套接字上的输入操作
第一步:等待数据从网络中到达,然后数据被复制到内核中某个缓冲区
第二步:把数据从内核缓冲区复制到应用进程缓冲区

Unix有五种I/O模型:

1、阻塞式I/O
2、非阻塞式I/O
3、I/O复用(select 和 poll)
4、信号驱动式I/O(SIGIO)
5、异步I/O(AIO)

1、阻塞式I/O

应用进程被阻塞,直到数据从内核缓冲区复制到应用进程缓冲区中才返回。
在阻塞的过程中其它应用进程还可以执行,因此阻塞不意味着整个操作系统都被阻塞

因为其它应用进程还可以执行,所以不消耗 CPU 时间,这种模型的 CPU 利用率会比较高。

2、非阻塞式I/O

应用进程执行系统调用后,内核返回一个错误码。

应用进程可以继续执行,但需要不断的执行系统调用来获知I/O是否完成,这种方式称为轮询(polling)。

由于 CPU 要处理更多的系统调用,因此这种模型的 CPU 利用率比较低。

3、I/O复用

使用 select 或者 poll 等待数据,并且可以等待多个套接字中的任何一个变为可读。

这一过程会被阻塞,当某一个套接字可读时返回,之后再使用 recvfrom 把数据从内核复制到进程中。
它可以让单个进程具有处理多个 I/O 事件的能力。又被称为 Event Driven I/O,即事件驱动 I/O

如果一个 Web 服务器没有 I/O 复用,那么每一个 Socket 连接都需要创建一个线程去处理。如果同时有几万个连接,那么就需要创建相同数量的线程。相比于多进程和多线程技术,I/O 复用不需要进程线程创建和切换的开销,系统开销更小。

4、信号驱动I/O

应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行,也就是说等待数据阶段应用进程是非阻塞的
内核数据到达时向应用进程发送 SIGIO 信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中

相比于非阻塞式 I/O 的轮询方式,信号驱动 I/O 的 CPU 利用率更高。

5、异步 I/O

应用进程执行 aio_read 系统调用会立即返回,应用进程可以继续执行,不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。

异步 I/O 与信号驱动 I/O 的区别在于,异步 I/O 的信号是通知应用进程 I/O 完成,而信号驱动 I/O 的信号是通知应用进程可以开始 I/O。

五大I/O模型比较:

同步I/O:将数据从内核缓冲区复制到应用进程缓冲区的阶段(第二阶段),应用进程会阻塞。
–包括阻塞式 I/O、非阻塞式 I/O、I/O 复用和信号驱动 I/O ,它们的主要区别在第一个阶段。
异步I/O:第二阶段应用进程不会阻塞。

非阻塞式 I/O 、信号驱动 I/O 和异步 I/O 在第一阶段不会阻塞。

二、I/O复用

select/poll/epoll 都是 I/O 多路复用的具体实现,select 出现的最早,之后是 poll,再是 epoll。

1、select

select 允许应用程序监视一组文件描述符等待一个或者多个描述符成为就绪状态,从而完成 I/O 操作。

int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

fd_set使用数组实现,数组大小使用FD_SETSIZE 定义,所以只能监听少于 FD_SETSIZE 数量的描述符。
有三种类型的描述符类型:readset、writeset、exceptset,分别对应读、写、异常条件的描述符集合。
timeout 为超时参数,调用 select 会一直阻塞直到有描述符的事件到达或者等待的时间超过 timeout。
成功调用返回结果大于 0,出错返回结果为 -1,超时返回结果为 0。

2、poll

poll 的功能与 select 类似,也是等待一组描述符中的一个成为就绪状态
poll 中的描述符是 pollfd 类型的数组,pollfd 的定义如下:

struct pollfd {
               int   fd;         /* file descriptor */
               short events;     /* requested events */
               short revents;    /* returned events */
           };

3、select和poll的比较

**1、功能上:**功能基本相同,在一些实现细节上有所不同。

1、select会修改描述符,poll不会;
2、select的描述符类型使用数组实现,FD_SETSIZE 大小默认为 1024,因此默认只能监听少于 1024 个描述符。
	如果要监听更多描述符的话,需要修改 FD_SETSIZE 之后重新编译;而 poll 没有描述符数量的限制;
3、poll提供了更多的事件类型,并且对描述符的重复利用上比select高;
4、如果一个线程对某个描述符调用了 select 或者 poll,另一个线程关闭了该描述符,会导致调用结果不确定。

**2、速度上:**二者速度都比较慢,每次调用都需要将全部描述符从应用进程缓冲区复制到内核缓冲区。
**3、可移植性:**几乎所有系统都支持select,但只有比较新的系统支持poll。

4、epoll

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

epoll_ctl() 用于向内核注册新的描述符或是改变某个文件描述符的状态
已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理,进程调用 epoll_wait() 便可以得到事件完成的描述符–准备就绪链表中的数据。

epoll 只需要将描述符从进程缓冲区向内核缓冲区拷贝一次,并且进程不需要通过轮询来获得事件完成的描述符。

epoll 仅适用于 Linux OS。

epoll 比 select 和 poll 更加灵活而且没有描述符数量限制。

epoll 对多线程编程更友好,一个线程调用了 epoll_wait() 另一个线程关闭了同一个描述符也不会产生像 select 和 poll 的不确定情况。

5、epoll的两种工作模式

epoll 的描述符事件有两种触发模式:**LT(level trigger,水平触发)**和 ET(edge trigger,边缘触发)

1、LT模式

默认情况下,epoll采用LT模式工作,可以处理阻塞和非阻塞套接字。

当epoll_wait()检测到描述符事件到达时,将此事件通知进程,进程可以不立即处理该事件,下次调用 epoll_wait() 会再次通知进程。只要一个事件对应的套接字缓冲区还有数据,就总能从 epoll_wait中获取这个事件。

2、ET模式

通知之后进程必须立即处理事件,下次再调用 epoll_wait() 时不会再得到事件到达的通知。
在 ET模式下事件发生时,如果没有彻底地将缓冲区数据处理完,则会导致缓冲区中的用户请求得不到响应。

很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。
只支持非阻塞套接字,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

默认情况下,Nginx是通过 ET模式使用 epoll的。

6、应用场景

1、select 应用场景

select 的 timeout 参数精度为微秒,而 poll 和 epoll 为毫秒,因此 select 更加适用于实时性要求比较高的场景,比如核反应堆的控制。

select 可移植性更好,几乎被所有主流平台所支持。

2、poll 应用场景

poll 没有最大描述符数量的限制,如果平台支持并且对实时性要求不高,应该使用 poll 而不是 select。

3、epoll 应用场景

只需要运行在 Linux 平台上,有大量的描述符需要同时轮询,并且这些连接最好是长连接

需要同时监控小于 1000 个描述符,就没有必要使用 epoll,因为这个应用场景下并不能体现 epoll 的优势。

需要监控的描述符状态变化多,而且都是非常短暂的,也没有必要使用 epoll。因为 epoll 中的所有描述符都存储在内核中,造成每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用,频繁系统调用降低效率。并且 epoll 的描述符存储在内核,不容易调试。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值