侧边栏壁纸
博主头像
DJ's Blog博主等级

行动起来,活在当下

  • 累计撰写 133 篇文章
  • 累计创建 51 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

【java】IO

Administrator
2022-04-05 / 0 评论 / 0 点赞 / 54 阅读 / 8224 字

【java】IO

传统的IO

原理

传统的I/O是阻塞的。
使用传统的I/O程序读取文件内容,并写入到另一个文件或Socket, 如下程序:

File.read(fileDesc, buf, len);
Socket.send(socket, buf, len);

会有较大的性能开销,主要表现在以下两方面:

  1. 上下文切换(context switch),此处有4次用户态和内核态的切换。
  2. buffer内存开销,一个是应用程序buffer,另一个是系统读取buffer以及socket buffer

其运行示意图如下:

  1. 先将文件内容从磁盘中拷贝到操作系统Read buffer
  2. 再从操作系统Read buffer拷贝到应用程序Application buffer
  3. 从应用程序Application buffer拷贝到Socket buffer
  4. Socket buffer拷贝到协议引擎NIC buffer

运行流程

IO模型.png
服务端通过acceptor来监听客户端请求,当有请求过来,服务端就手动开启一个线程来处理。这样主线程就不会被阻塞。
但是对于每个子线程来说都是阻塞的,这是伪异步方式。

Socket示例代码

客户端程序

public class ServiceClient {
    public static void main(String[] args) {
        InputStream inputStream = null;
        OutputStream outputStream = null;
        Socket socket = null;
        try {
            // 1.创建socket连接,向服务器发出请求
            socket = new Socket("localhost", 8899);
            // 2.从socket中获取输入输出流
            inputStream = socket.getInputStream();
            outputStream = socket.getOutputStream();
            // 3.向socket输出流中写入数据
            PrintWriter pw = new PrintWriter(outputStream);
            pw.println("hello");
            pw.flush();
            // 4.从socket输入流中读取数据
            BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
            String result = br.readLine();
            System.out.println(result);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 5.关闭资源
            try {
                inputStream.close();
                outputStream.close();
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

服务端程序

public class ServiceServer {
    public static void main(String[] args) {
        try {
            // 1.创建一个ServerSocket,绑定到本机的8899端口上
            ServerSocket server = new ServerSocket();
            server.bind(new InetSocketAddress("localhost", 8899));
            System.out.println("socket服务端已开启:");
            while (true) {
                // 2.接受客户端的连接请求。注意:accept是一个阻塞方法,会一直等待,直到有客户端请求连接才返回。
                Socket socket = server.accept();
                // 3.每次有客户端连接过来就开启一个线程来执行任务。
                Thread thread = new Thread(new ServiceServerTask(socket), "thread");
                thread.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

服务端线程处理类

public class ServiceServerTask implements Runnable {
    private Socket socket;
    public ServiceServerTask() {
    }
    public ServiceServerTask(Socket socket) {
        this.socket = socket;
    }
    @Override
    public void run() {
        InputStream inputStream = null;
        OutputStream outputStream = null;
        try {
            System.out.println("接收到来自IP" + socket.getInetAddress().getHostAddress() + "的请求");
            // 1.从socket连接中获取到与client之间的网络通信输入输出流。
            inputStream = socket.getInputStream();
            outputStream = socket.getOutputStream();
            // 2.从网络通信输入流中读取客户端发送过来的数据。注意:socketinputstream的读数据的方法都是阻塞的。
            BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
            String param = br.readLine();
            // 3.业务处理
            GetDataServiceImpl service = new GetDataServiceImpl();
            String result = service.getData(param);
            // 4.将调用结果写到sokect的输出流中,以发送给客户端
            PrintWriter pw = new PrintWriter(outputStream);
            pw.println(result);
            pw.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 5.关闭资源
            try {
                if (null != inputStream) {
                    inputStream.close();
                }
                if (null != outputStream) {
                    outputStream.close();
                }
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

业务处理类

public class GetDataServiceImpl {
    public String getData(String param) {
        return "ok-" + param;
    }
}

输出

  • 服务端
socket服务端已开启:
接收到来自IP127.0.0.1的请求
  • 客户端
ok-hello

总结

如上例中,服务端通过readLine()方法从SocketInputStream中读取数据,但是这个方法是阻塞的,也就是说服务端一直会从SocketInputStream中读取数据(即使客户端那边已经将数据写完了,但是没有告知服务端),除非客户端手动的将OutputStream关闭,即调用socket.shutdownOutput()方法,但是这样的话,对于这次Socket连接,客户端就无法再次向服务端写数据。除非再次创建Socket连接。
还有一种情况,如果客户端向OutputStream中写入数据之后未关闭流,立即希望从InputStream中读取服务端返回的数据。这时候服务端因为没有收到客户端数据写完的通知,所以他处于一直从InputStream中读取数据的状态,无法返回数据,这样客户端也会一直处于从InputStream中读取数据的状态。这样就造成两边都阻塞住了。

NIO

特性

NIONew IO的简称,在jdk1.4 里提供的新api。Sun官方标榜的特性如下:

  1. 为所有的原始类型提供(Buffer)缓存支持。
  2. 字符集编码解码解决方案。
  3. Channel :一个新的原始I/O抽象。
  4. 支持锁和内存映射文件的文件访问接口。
  5. 提供多路非阻塞式(non-bloking)的高伸缩性网络I/O

原理

NIO是非阻塞的。但是不一定比传统的I/O快,只是增加了服务器的并发量,提高服务器的响应速度。

NIO技术省去了将操作系统的Read buffer拷贝到应用程序Application buffer,以及从应用程序Application buffer拷贝到Socket buffer的步骤,直接将操作系统的Read buffer拷贝到Socket buffer
Java中的 FileChannel.transferTo()方法就是这样的实现,这个实现是依赖于操作系统底层的sendFile()实现的。

public void transferTo(long position, long count, WritableByteChannel target);

他的底层调用的是操作系统的sendFile方法。

sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

其运行示意图如下:
Alt text

  1. 先将文件内容从磁盘中拷贝到操作系统Read buffer
  2. 再从操作系统Read buffer拷贝到Socket buffer
  3. Socket buffer拷贝到协议引擎NIC buffer

运行流程

Alt text

  1. 服务端程序向操作系统内核注册一个监听器selector,等待内核回调。
  2. 内核发现有客户端请求,会发起事件回调通知selector
  3. selector接收到通知之后,会向内核注册连接建立。
  4. 内核就会和客户端通过TCP的三次握手来建立连接(channel)。
  5. 连接建立成功之后,内核会发起事件回调通知selector连接(channel)已经建立。
  6. selector接收到通知之后,会向内核注册READ监听,这个READ监听是针对某个channel
  7. 此时客户端通过channel发送数据过来,则先会进入内核的tcp缓存。
  8. 内核发现tcp缓存中有数据了之后,会把tcp缓存中的数据写入到bytebuffer中。
  9. 内核再发起事件回调通知服务端程序可以READ了。
  10. 最后服务端程序就可以通过channelbytebuffer中读取数据。
  11. netty框架已经把上面的流程全部封装好了,我们只需要写自己的handlerchannel中读取数据处理业务逻辑。

handler可以有多个,并按照一定的顺序执行。

示例代码

0

评论区