【java】IO
传统的IO
原理
传统的
I/O是阻塞的。
使用传统的I/O程序读取文件内容,并写入到另一个文件或Socket, 如下程序:
File.read(fileDesc, buf, len);
Socket.send(socket, buf, len);
会有较大的性能开销,主要表现在以下两方面:
- 上下文切换
(context switch),此处有4次用户态和内核态的切换。 buffer内存开销,一个是应用程序buffer,另一个是系统读取buffer以及socket buffer。
其运行示意图如下:

- 先将文件内容从磁盘中拷贝到操作系统
Read buffer。 - 再从操作系统
Read buffer拷贝到应用程序Application buffer。 - 从应用程序
Application buffer拷贝到Socket buffer。 - 从
Socket buffer拷贝到协议引擎NIC buffer。
运行流程

服务端通过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
特性
NIO是New IO的简称,在jdk1.4 里提供的新api。Sun官方标榜的特性如下:
- 为所有的原始类型提供(Buffer)缓存支持。
- 字符集编码解码解决方案。
Channel:一个新的原始I/O抽象。- 支持锁和内存映射文件的文件访问接口。
- 提供多路非阻塞式
(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);
其运行示意图如下:

- 先将文件内容从磁盘中拷贝到操作系统
Read buffer。 - 再从操作系统
Read buffer拷贝到Socket buffer。 - 从
Socket buffer拷贝到协议引擎NIC buffer。
运行流程

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