【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
可以有多个,并按照一定的顺序执行。
评论区