几种IO模式

首先我们需要弄明白几种IO模型, 关于基础知识这块我觉得已经说臭了:)。

首先说同步IO, 同步IO指的是, 你需要不断的监控资源的状态, 如果资源是可读或者可写的, 那么就会读, 如果资源不可以读或者写, 就会出现下面两种情况:

  • BIO:指的是阻塞IO, 读取不到数据, 会阻塞当前线程, 比如sleep(100)
  • NIO:指的是非阻塞IO,读取不到数据, 不会阻塞当前线程, 但是会返回一个错误编号。

第三种方式就是AIO, 这种方式的好处就是, 你只要将资源挂在线程上面, 如果可以读或者可写, 那么它会自动告诉你

区分这三个东西, 也非常简单, 那就是BIO关心的是我要读, NIO关心的就是我可以读了, AIO关心的就是读完了

对比这三种方式, 显然AIO这种方式更好, 因为BIO的阻塞会影响性能, 其次NIO的不断监听资源又会耗费很多的时间

说完了这三种IO模型, 接下来我们来说一说关于如何高效的使用线程去读取资源, 记住当我们已经引入多线程, IO多路复用这些概念的时候, 我们需要处理的资源往往都不是一个。

IO模式的线程改进

首先我们来看第一种IO模式的线程改进, 因为第一种IO模式是阻塞的, 所以一旦没有资源就会阻塞, 那么我们就要给每个连接都绑定一个线程, 这样就不会出现程序一直挂死的情况,

在Java中我们可以写出代码的原型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Server implements Runnable {
@Override
public void run() {
try {
ServerSocket ss = new ServerSocket(PORT);
while (!Thread.interrupted()) {
//每次来一个连接事件, 我们就新建一个线程
new Thread(new Handler(ss.accept())).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

但是这种做法的坏处也是显而易见的

  1. 操作系统的线程资源是十分昂贵的, 每次创建一个线程的就对应一段资源的分配,
  2. 线程直接切换导致性能急剧下降, 可能还没有单线程环境下面好。

下面我们就通过非阻塞的方式来解决上面的问题, 在Java中我们可以使用scoketchannel, 去配置非阻塞

缓冲区

当tcp三次握手成功, tcp连接成功建立后, 操作系统内核会为每一个连接创建配套的基础设施, 比如发送缓冲区域,发送缓冲区的大小可以通过套接字选项来改变, 当我们的应用程序调用write函数的时候, 实际上做的事情就是把数据从应用程序中拷贝到操作系统的内核的发送缓冲区

那么发送缓冲区的大小就是一个比较重要的因素。

第一种情况就是, 操作系统内核的发送缓冲区足够大, 可以直接容纳这份数据, 那么我们的程序就会返回对应的缓冲区大小。

第二种情况就是, 操作系统的发送缓冲区不足以容纳这份数据, 这时候应用程序被阻塞, 也就是应用程序在write函数处停留, 不直接返回, 但是操作系统会源源不断的处理发送缓冲区中的数据,

操作系统按照TCP/IP的语义, 将取出的包裹封装成为TCP的MSS包, 以及IP的MTU包, 最后走数据链路层将数据发送出去, 这样我们的缓冲区域就会不断的空出来。

1
linux内核发送缓冲区----->用户1

于是又可以继续从应用程序搬离一部分到内核的发送缓冲区

1
用户 ----> linux内核发送缓冲区

这样就会一直下去, 直到某一时刻, 应用程序的数据可以完全放置到发送缓冲区中, 这样就会一直进行下去, 到某一时刻, 应用程序的数据可以完全放置到发送缓冲区中, 这时候write阻塞调用返回

read函数, 要求从操作系统内核从套接字描述子socketfd读取最多多少字节, 并将结果存储到buffer中, 返回值告诉我们实际读取的字节数目,

  • 如果返回值为0, 表示EOF, 这在网络中表示对端发送了FIN包, 要处理断连的情况
  • 如果返回值为-1, 表示出错