java的IO机制
9/100
发布文章
qq_33556348
未选择文件
new
BIO\NIO\AIO
BIO
每一个客户都创建一个单独的线程进行IO交换
BIO: java线程等待操作系统返回数据
好处: 代码简单,直接
坏处: 效率不行,存在瓶颈
有多个客户端线程时,主线程进行阻塞等待其中的一个,效率很低
public void serve(int port) throws IOException {
//将ServerSocket绑定到指定的端口里
final ServerSocket socket = new ServerSocket(port);
while (true) {
//阻塞直到收到新的客户端连接
final Socket clientSocket = socket.accept();
System.out.println("Accepted connection from " + clientSocket);
//创建一个子线程去处理客户端的请求
new Thread(new Runnable() {
@Override
public void run() {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()))) {
PrintWriter writer = new PrintWriter(clientSocket.getOutputStream(), true);
//从客户端读取数据并原封不动回写回去
while (true) {
writer.println(reader.readLine());
writer.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
}
NIO
文件描述符目录:
0 ,1,2:输入,输出,报错
3,4:监听对应的socket接口,ipv4,ipv4
accept连接之后,返回对应的文件描述符
阻塞的方式,对面线程建立连接之后,本方获得文件描述符,对面不发消息则本方阻塞。
解决方式1:
每一个连接都抛出一个线程,把文件描述符扔出去,等待接收数据
解决方式2:
使用内核提供的非阻塞模式,JAVA也提供了相应的API
没有进入阻塞状态
不抛出线程,使用内核特性进行轮回询问
再有问题:
- C10K文件描述符很多,每循环一次都要调一次系统调用revc,成本很高,O(n),n为客户端数目,但只有一个是有用的
注意,C10K 和 C1000K 的首字母 C 是 Client 的缩写。C10K 就是单机同时处理 1 万个请求(并发连接 1 万)的问题,而 C1000K 也就是单机支持处理 100 万个请求(并发连接 100 万)的问题。
多条路复用一次调用,直接告诉java哪个rev来了
使用内核的提供的Select,由内核进行查询,然后将结果返回
监控多个并发文件描述符,直到一个达到可读写的状态
多路复用器是用来判定状态的,recv还得自己读数据
这个可以理解为找了个人替咱们轮询,但咱这个线程还是要轮询的(同步),而不同于AIO那种直接由别人进行通知的方式
再再有问题:
select的固有问题,改成epoll()
适用于短连接,比如聊天服务器,问一问就好了,避免太长时间的等待
在等待数据的时候进行自旋检查,不断地主动询问内核是否已经准备好
与BIO的区别是使用一个单独的Selector线程对几个返回数据的客户端进行轮询,获取数据,主机不休息,适用于客户端信息发送较快的情况。
允许单线程处理多个Channel,例如,聊天服务器
底层使用了操作系统的多路复用:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class NIOPlainEchoServer {
public void serve(int port) throws IOException {
System.out.println("Listening for connections on port " + port);
ServerSocketChannel serverChannel = ServerSocketChannel.open();
ServerSocket ss = serverChannel.socket();
InetSocketAddress address = new InetSocketAddress(port);
//将ServerSocket绑定到指定的端口里
ss.bind(address);
serverChannel.configureBlocking(false);
Selector selector = Selector.open();
//将channel注册到Selector里,并说明让Selector关注的点,这里是关注建立连接这个事件
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
try {
//阻塞等待就绪的Channel,即没有与客户端建立连接前就一直轮询
selector.select();
} catch (IOException ex) {
ex.printStackTrace();
//代码省略的部分是结合业务,正确处理异常的逻辑
break;
}
//获取到Selector里所有就绪的SelectedKey实例,每将一个channel注册到一个selector就会产生一个SelectedKey
Set<SelectionKey> readyKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = readyKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = (SelectionKey) iterator.next();
//将就绪的SelectedKey从Selector中移除,因为马上就要去处理它,防止重复执行
iterator.remove();
try {
//若SelectedKey处于Acceptable状态
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
//接受客户端的连接
SocketChannel client = server.accept();
System.out.println("Accepted connection from " + client);
client.configureBlocking(false);
//像selector注册socketchannel,主要关注读写,并传入一个ByteBuffer实例供读写缓存
client.register(selector, SelectionKey.OP_WRITE | SelectionKey.OP_READ, ByteBuffer.allocate(100));
}
//若SelectedKey处于可读状态
if (key.isReadable()) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer output = (ByteBuffer) key.attachment();
//从channel里读取数据存入到ByteBuffer里面
client.read(output);
}
//若SelectedKey处于可写状态
if (key.isWritable()) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer output = (ByteBuffer) key.attachment();
output.flip();
//将ByteBuffer里的数据写入到channel里
client.write(output);
output.compact();
}
} catch (IOException ex) {
key.cancel();
try {
key.channel().close();
} catch (IOException cex) {
}
}
}
}
}
}
AIO
- Future 方式:即提交一个 I/O 操作请求(accept/read/write),返回一个 Future。然后您可以对 Future 进行检查(调用get(timeout)),确定它是否完成,或者阻塞 IO 操作直到操作正常完成或者超时异常。使用 Future 方式很简单,需要注意的是,因为Future.get()是同步的,所以如果不仔细考虑使用场合,使用 Future 方式可能很容易进入完全同步的编程模式,从而使得异步操作成为一个摆设。如果这样,那么原来旧版本的 Socket API 便可以完全胜任,大可不必使用异步 I/O.
- Callback 方式:即提交一个 I/O 操作请求,并且指定一个 CompletionHandler。当异步 I/O 操作完成时,便发送一个通知,此时这个 CompletionHandler 对象的 completed 或者 failed 方法将会被调用。
写完程序后,主动进行通知:
public void serve(int port) throws IOException {
System.out.println("Listening for connections on port " + port);
final AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open();
InetSocketAddress address = new InetSocketAddress(port);
// 将ServerSocket绑定到指定的端口里
serverChannel.bind(address);
final CountDownLatch latch = new CountDownLatch(1);
// 开始接收新的客户端请求. 一旦一个客户端请求被接收, CompletionHandler 就会被调用.
serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
@Override
public void completed(final AsynchronousSocketChannel channel, Object attachment) {
// 一旦完成处理,再次接收新的客户端请求
serverChannel.accept(null, this);
ByteBuffer buffer = ByteBuffer.allocate(100);
// 在channel里植入一个读操作EchoCompletionHandler,一旦buffer有数据写入,EchoCompletionHandler 便会被唤醒
channel.read(buffer, buffer, new EchoCompletionHandler(channel));
}
@Override
public void failed(Throwable throwable, Object attachment) {
try {
// 若遇到异常,关闭channel
serverChannel.close();
} catch (IOException e) {
// ingnore on close
} finally {
latch.countDown();
}
}
});
try {
latch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
三者对比
-
BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
-
NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
-
AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。