关于JAVA I/O的文章已经很多,以前对这方面使用也不少,不过比较零散的认识,近来看了一下这方面的文章,梳理一下记录下来。
1. BIO
说到BIO,对于Java的程序员或多或少都有所了解,属于core java里比较基础的知识了。网络编程的基本编程模型是Client/Server模型,是两个进程间进行相互通讯。其中server端提供位置信息(IP/Port),客户端连接服务端监听的地址和端口从而发生连接请求,通过三次握手建立连接,如果连接成功,两者便可以通过Socket进行通讯。
在传统同步阻塞模型开发中,ServerSocket负责绑定IP地址,启动监听端口,Socker负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞方式通讯。
Socket服务器端流程如下:加载套接字->创建监听的套接字->绑定套接字->监听套接字->处理客户端相关请求。
Socket客户端:加载套接字,然后创建套接字,不过之后不用绑定和监听了,而是直接连接服务器,发送相关请求。
1.1 实现方式
服务端通常采用一个acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成后通过输出流返回应答给客户端,随后线程销毁。One connection one thread,无论连接是否有真实的数据请求都需要独占一个线程。
这种模式缺陷很显而易见,当客户端并发量大时,服务器线程数与客户端并发访问数成1:1正比关系,当服务端线程数过多,系统性能将急剧下降,随着并发访问量的继续增大,系统会发生堆栈溢出、创建新线程失败等问题。最终导致进程宕机或僵死,无法对外提供服务。
1.2 BIO的简单实现代码
以下是BIO的典型代码:
服务端代码:
public class Server {
public static void main(String[] args) {
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(8080);
Socket socket;
while (true) {
socket = serverSocket.accept();
new Thread(new Handler(socket)).start();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// close socket
}
}
}
服务端处理请求的线程代码:
public class Handler implements Runnable {
private Socket socket;
public Handler(Socket socket) {
this.socket = socket;
}
public void run() {
try {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter printWriter = new PrintWriter(
socket.getOutputStream(), true);
String receiverMsg = null;
while (true) {
receiverMsg = bufferedReader.readLine(); // 读取客户端发来的信息
if (receiverMsg == null) break;
}
printWriter.print("这是写给客户的响应信息");
} catch (IOException e) {
e.printStackTrace();
} finally {
// close io, socket
}
}
}
客户端代码:
public class Client {
public static void main(String[] args) {
Socket socket;
BufferedReader bufferedReader;
PrintWriter printWriter;
try {
socket = new Socket("127.0.0.1", 8080);
bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
printWriter = new PrintWriter(socket.getOutputStream(), true);
printWriter.print("这是往服务器发送的信息");
bufferedReader.readLine(); // 从服务器接收的相应信息
} catch (Exception e) {
e.printStackTrace();
} finally {
// close io, socket
}
}
}
2. 伪异步IO
为了解决同步阻塞I/O面临的一个链路需要一个线程处理的问题,有人对其进行优化:服务端通过使用线程池来处理多个客户端的请求接入。这样可以使得服务端线程连接数可以远少于客户端连接数,通过设置线程池最大线程数量,防止由于大量并发接入而导致线程的耗尽。
修改后服务端代码:
public class Server2 {
public static void main(String[] args) {
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(8080);
Socket socket;
int POOL_SIZE = 20;
Executor executor = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors() * POOL_SIZE);
while (true) {
socket = serverSocket.accept();
executor.execute(new Handler(socket)); // here is
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// close socket
}
}
}
从本质上来说伪异步IO实际上仅仅是对之前IO线程模型的简单优化,无法从根本上解决同步IO导致的通讯线程阻塞问题。为什么?原因在于上述还是采用Java输入和输出流,读和写部分的代码都是同步阻塞的,阻塞的时间取决于对方IO线程的处理速度和网络IO的传输速度。
- socket输入流进行读取操作,read方法,会一直阻塞,直到有数据可读或可用数据已读取完毕,或发生异常。
- 同样调用OutputStream的write方法会被阻塞,直到所有要发送的字节全部写入完毕或发生异常。
3. NIO
什么叫NIO?说法一:New I/O,说法二:非阻塞I/O(Non-block IO),说法二其实更能体现NIO的特征。NIO是JDK1.4中推出的,对高速地块读取、对I/O多路复用和非阻塞进行支持。需要重点关注的是NIO中有几个比较关键的概念:Channel(通道),Buffer(缓冲区),Selector(选择器)
Buffer(缓冲区)
在NIO库中,所有的数据都是采用缓冲区处理的。在读取数据时,它直接读到缓冲区中;在写入数据时,写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。缓冲区实质上是一个数组,但不仅仅是数组,缓冲区提供了对数据结构化访问以及维护读写位置(limit)等信息。Buffer是一个顶层父类,它是一个抽象类,常用的Buffer的子类有:ByteBuffer,IntBuffer,CharBuffer…
简单的使用场景:客户端发送数据时,先将数据存入Buffer中,然后将Buffer中的内容写入Channel。服务端接收数据必须通过Channel将数据读入到Buffer中,然后再从Buffer中取出数据来处理。Channel(通道)
Channel顾名思义,网络数据通过Channel读取和写入。和传统IO中的Stream主要区别为:通道是全双工的,通过一个Channel同时既可以进行读,也可以进行写;而Stream只能进行单向操作,流只是在一个方向上移动,通过一个Stream只能进行读或者写。常用的通道类有:ServerSocketChannel、SocketChanel、FileChannel、DatagramChannel。
SocketChannel,以TCP来向网络连接的两端读写数据;
ServerSocketChanel能够监听客户端发起的TCP连接,并为每个TCP连接创建一个新的SocketChannel来进行数据读写;
DatagramChannel,以UDP协议来向网络连接的两端读写数据;
FileChannel可以从文件读或者向文件写入数据。Selector(多路复用器)
Selector类是NIO的核心类,提供选择已经就绪的任务的能力。Selector能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理。这样一来,只是用一个单线程就可以管理多个通道,也就是管理多个连接。这样使得只有在连接真正有读写事件发生时,才会调用函数来进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了多线程之间的上下文切换导致的开销。具体的实现是Selector不断轮询注册其中的Channel,如果某个Channel上发生读或写事件,那么这个Channel处于就绪状态,通过SelectionKey可以获取就绪的Channel集合。以下是netty权威指南里的简单例子,很好的一个例子,这里引用下
服务器端:
public class NioServer implements Runnable {
private Selector selector;
private ServerSocketChannel serverSocketChannel;
public NioServer() {
try {
selector = Selector.open();
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress("127.0.0.1", 8080), 1024);
// 注册关注服务端接收客户端连接事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Server is start in port : 8080");
} catch (Exception e) {
e.printStackTrace();
}
}
public void run() {
while (true) {
try {
selector.select(1000L);
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
SelectionKey key;
while (iterator.hasNext()) {
key = iterator.next();
iterator.remove();
doHandleInput(key);
}
} catch (Exception e) {
e.printStackTrace();
if (selector != null) {
try {
selector.close();
} catch (IOException e1) {
e.printStackTrace();
}
}
}
}
}
private void doHandleInput(SelectionKey key) throws IOException {
if (key.isValid()) {
if (key.isAcceptable()) {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
SocketChannel channel = serverSocketChannel.accept();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ);
}
if (key.isReadable()) {
doRead(key);
}
}
}
private void doRead(SelectionKey key) throws IOException {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int readBytes = channel.read(byteBuffer);
if (readBytes > 0) { // 有读到字节
byteBuffer.flip();
byte[] bytes = new byte[byteBuffer.remaining()];
byteBuffer.get(bytes);
String receiverMsg = new String(bytes, "UTF-8");
System.out.println("NioServer receiver msg: " + receiverMsg);
doWrite(channel);
} else if (readBytes < 0) { // 链路已关闭
key.cancel();
channel.close();
}
}
private void doWrite(SocketChannel channel) throws IOException {
String respMsg = "Server has received the msg !!!";
ByteBuffer outBuffer = ByteBuffer.wrap(respMsg.getBytes());
channel.write(outBuffer);
}
public static void main(String[] args) {
NioServer server = new NioServer();
Thread thread = new Thread(server);
thread.start();
}
}
客户端:
public class NioClient implements Runnable {
private Selector selector;
private SocketChannel clientChannel;
private NioClient() {
try {
selector = Selector.open();
clientChannel = SocketChannel.open();
clientChannel.configureBlocking(false);
System.out.println("Client will start connect.");
} catch (Exception e) {
e.printStackTrace();
}
}
private void doConnect() throws Exception {
if (clientChannel.connect(new InetSocketAddress("127.0.0.1", 8080))) {
clientChannel.register(selector, SelectionKey.OP_READ);
doWrite(clientChannel);
} else {
clientChannel.register(selector, SelectionKey.OP_CONNECT);
}
}
public void run() {
try {
doConnect();
} catch (Exception e) {
e.printStackTrace();
}
while (true) {
try {
selector.select(1000L);
Iterator<SelectionKey> iterator = this.selector.selectedKeys().iterator();
SelectionKey key;
while (iterator.hasNext()) {
key = iterator.next();
iterator.remove();
doHandleInput(key);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
private void doHandleInput(SelectionKey key) throws IOException {
if (key.isValid()) {
if (key.isConnectable()) {
SocketChannel channel = (SocketChannel) key.channel();
if (channel.finishConnect()) {
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ);
doWrite(channel);
} else {
System.exit(1);
}
}
if (key.isReadable()) {
doRead(key);
}
}
}
private void doRead(SelectionKey key) throws IOException {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int readBytes = channel.read(byteBuffer);
if (readBytes > 0) { // 有读到字节
byteBuffer.flip();
byte[] bytes = new byte[byteBuffer.remaining()];
byteBuffer.get(bytes);
String receiverMsg = new String(bytes, "UTF-8");
System.out.println("NioClient receiver reponse from server which is : " + receiverMsg);
} else if (readBytes < 0) { // 链路已关闭
key.cancel();
channel.close();
}
}
private void doWrite(SocketChannel channel) throws IOException {
String sendMsg = "This msg is client send to server !!!";
ByteBuffer outBuffer = ByteBuffer.wrap(sendMsg.getBytes());
channel.write(outBuffer);
}
public static void main(String[] args) {
NioClient nioClient = new NioClient();
Thread thread = new Thread(nioClient);
thread.start();
}
}
这里收录一个网上有人给出的一个说明,比较有意思:
以前的流总是堵塞的,一个线程只要对它进行操作,其它操作就会被堵塞,也就相当于水管没有阀门,你伸手接水的时候,不管水到了没有,你就都只能耗在接水(流)上。
nio的Channel的加入,相当于增加了水龙头(有阀门),虽然一个时刻也只能接一个水管的水,但依赖轮换策略,在水量不大的时候,各个水管里流出来的水,都可以得到妥善接纳,这个关键之处就是增加了一个接水工,也就是Selector,他负责协调,也就是看哪根水管有水了的话,在当前水管的水接到一定程度的时候,就切换一下:临时关上当前水龙头,试着打开另一个水龙头(看看有没有水)。
当其他人需要用水的时候,不是直接去接水,而是事前提了一个水桶给接水工,这个水桶就是Buffer。也就是,其他人虽然也可能要等,但不会在现场等,而是回家等,可以做其它事去,水接满了,接水工会通知他们。
这其实也是非常接近当前社会分工细化的现实,也是统分利用现有资源达到并发效果一种很经济的手段,而不是动不动就来个并行处理,虽然那样是最简单的,但也是最浪费资源的方式。
4. AIO
AIO(NIO.2) : 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。JDK1.7开始支持,并提供异步文件通道和异步套接字通道的实现。异步通道提供以下两种方式获取操作结果:
1. 通过java.util.concurrent.Future类来表示异步操作的结果。
2. 在执行异步操作的时候传入一个java.nio.channels。
CompletionHandler接口的实现类作为操作完成的回调。
代码可以参考这个简单例子:
http://blog.csdn.net/xxb2008/article/details/42424105
上述参考资料来源于其他大牛的文章,推荐大家也可以看看
http://my.oschina.net/u/658658/blog/521016
http://blog.sina.com.cn/s/blog_aed82f6f010194ky.html
http://stevex.blog.51cto.com/4300375/1284437
http://my.oschina.net/fhd/blog/369136
http://www.cnblogs.com/dolphin0520/p/3919162.html
http://qindongliang.iteye.com/blog/2018539
参考资料《Netty权威指南》把这部分写的很详细,推荐大家可以看看,里面的例子代码就是上面实现的部分代码