本文已被收录至 GitHub: https://github.com/JavaLiuTongXue/JavaCoding ,想要获取更多的干货文章,可以关注公众号:不会说话的刘同学
我们平时说的I/O操作有两种,一种是本地磁盘I/O,也就是我们经常使用的 InputStream 和 OutputStream (这两个类是用于本地磁盘I/O操作),另外一种是网络I/O操作,也就是我平时通过网络进行读取或发送数据,比如通过 http 请求一个接口,接口里所涉及到的请求数据和响应数据
Java 中对于网络I/O操作主要是通过 Socket 进行的,而 Sokcet 又会调用底层操作系统的相关接口,我们想要弄清楚 Java 的网络I/O操作就需要先搞清楚底层操作系统提供的I/O模型
我们的程序一般都是运行在linux系统上,linux系统对于网络I/O操作提供了五种I/O模型–同步阻塞 I/O、同步非阻塞 I/O、I/O 多路复用、信号驱动 I/O 和异步I/O
信号驱动I/O主要是运用于硬件层面,就不作详细的讲解,Java也很少会使用到这种I/O模型,本篇文章会详细的讲解除信号驱动I/O之外的其中的四种I/O模型并给出对应的代码示例
在弄清楚I/O模型之前,我们先来看看程序对于网络数据的读取会经过哪些阶段
我们现在有A、B两个系统,A系统去调用B系统的一个程序接口,这个程序接口除了一些请求数据之外,还会响应一些数据,那么从A系统发起请求调用到响应数据会经过以下几个步骤:
1、A系统调用底层操作系统的网络相关接口,底层操作系统会通过 TCP 三次握手的方式尝试与B系统建立连接
2、连接建立完成之后,也就意味着TCP通道建立完成了,A系统会这个通道向B系统发送请求的数据
3、A系统发送数据时并不是直接就发送出去了,这些数据会以二进制的形式经过系统用户空间、系统内核空间、系统的网卡,网卡再将数据发送给B系统
4、B系统接收数据的时候,数据会经过B系统的网卡、系统内核空间、系统用户空间,程序再从系统的用户空间里读取数据
这里需要注意下,操作系统是分为用户空间和内核空间的,我们的应用程序就是运行在内核空间里,因此A系统在发送数据的时候,数据会从用户空间出发,最后通过网卡发送出去
系统具体的数据发送和接收过程如下所示:
数据传输到网卡一共也分为两步:
1、应用程序调用操作系统的接口将发送的数据拷贝到内核缓冲区(这里要注意应用内程序是无法直接操作内核缓冲区的,只能通过接口来操作)
2、内核缓冲区再将数据拷贝到网卡
数据从网卡传输到应用程序步骤相反
同步阻塞I/O也就是我们所说的BIO,在数据拷贝的过程中就涉及到数据的读和写,同步阻塞I/O就是用户程序在读取数据的时候用户线程会阻塞,让出 CPU,内核等待网卡数据到来,把数据从网卡拷贝到内核空间,接着把数据拷贝到用户空间,再把用户线程叫醒
对应的时序图如下:
Java 提供了两个网络操作类 ServerSocket 和 Sokcet ,分别对应服务端和客户端,ServerSocket 提供了 accept() 方法,accept() 会阻塞线程等待客户端的连接,连接完成之后,会唤醒线程从连接通道里读取数据
Java 服务端代码如下:
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket();
SocketAddress socketAddress = new InetSocketAddress(8080);
serverSocket.bind(socketAddress);
byte[] bytes = new byte[1024];
while (true){
// 获取到客户端连接
Socket accept = serverSocket.accept();
InputStream inputStream = accept.getInputStream();
// 从客户端连接里读取数据
int read = inputStream.read(bytes);
if(read != -1){
System.out.println(new String(bytes));
}
}
}
这里要注意一下,当没有客户端来连接的时候,accept() 方法会阻塞住,有客户端连接,但是客户端如果没有发送数据过来,inputStream.read() 也会阻塞住
而这里的 read() 方法也就是上面时序图里所说的调用 read 方法读取数据
Java 客户端代码如下:
public static void main(String[] args) throws IOException, InterruptedException {
Socket socket = new Socket("127.0.0.1",8080);
OutputStream outputStream = socket.getOutputStream();
outputStream.write("Hello World".getBytes());
Thread.sleep(60000*10); //睡眠10分钟
}
这里Java客户端的也比较的简单,我这里就不做过多的讲述了
这种I/O模型也是我们所说的NIO,同步非阻塞I/O与同步阻塞I/O最大的区别就在于当用户线程发起 read 调用的时候,用户线程在某一时间段内不会被阻塞住
为啥是在某一时间内用户线程不会被阻塞住呢
这是因为如果数据没有到达内核空间时,每次 read 调用都会返回失败,不会被阻塞住,直到数据到了内核空间后,再一次 read 调用时,此时用户线程就会被阻塞住,等待数据从内核空间拷贝到用户空间,然后再唤醒用户线程
因此这里的非阻塞只是针对数据还未到达内核空间时,数据到达内核空间后用户线程还是会处于一个阻塞状态,这里需要注意下
为了方便理解,可以查看下图:
Java 中提供了 ServerSocketChannel 和 SocketChannel,分别对标BIO的 ServerSocket 和 Socket ,ServerSocketChannel 提供的 accept 方法与 SocketChannel 提供的 read 方法可以通过 configureBlocking 设置为不阻塞
Java服务端代码 :
static List<SocketChannel> socketChannelList = new ArrayList<>();
public static void main(String[] args) throws IOException {
// 创建 ServerSocketChannel 对象
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 绑定端口
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
// 将客户连接设置为不阻塞
serverSocketChannel.configureBlocking(false);
ByteBuffer allocate = ByteBuffer.allocate(128);
while (true){
SocketChannel socketChannel = serverSocketChannel.accept();
if(socketChannel != null){
// 将读写数据设置为不阻塞
socketChannel.configureBlocking(false);
socketChannelList.add(socketChannel);
}
Iterator<SocketChannel> iterator = socketChannelList.iterator();
while (iterator.hasNext()){
SocketChannel channel = iterator.next();
// 读取数据
int read = channel.read(allocate);
if(read > 0){
System.out.println(new String(allocate.array()));
allocate.clear();
}
}
}
}
Java客户端代码 :
public static void main(String[] args) throws IOException, InterruptedException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost",8080));
ByteBuffer byteBuffer = ByteBuffer.allocate(128);
byteBuffer.put("123".getBytes());
byteBuffer.flip();
while (byteBuffer.hasRemaining()) {
socketChannel.write(byteBuffer);
}
Thread.sleep(100000);
}
I/O多路复用其实就是对同步非阻塞I/O的一个增强,也是NIO的一种,这种I/O模型将用户线程的数据读取操作分了两步,首先线程发起 select 调用,目的就是查询内核空间里的数据是否准备好,如果准备好了,用户线程再次发起 read 调用,在等待数据从内核空间拷贝到用户空间这段时间里,用户线程仍然还是阻塞的
那为啥要叫I/O多路复用呢,这是因为一次 select 调用可以向内核查询多个数据通道的状态,所以叫I/O多路复用
调用顺序如下:
在Java API使用上在服务端比同步非阻塞I/O多了个 Selector 注册,其他的基本上都差不多
Java 服务端代码:
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
serverSocketChannel.configureBlocking(false);
ByteBuffer allocate = ByteBuffer.allocate(128);
Selector selector = Selector.open();
// 注册 Selector
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true){
// 阻塞等待需要处理的事件发生
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator1 = selectionKeys.iterator();
while (iterator1.hasNext()){
SelectionKey selectionKey = iterator1.next();
if(selectionKey.isAcceptable()){
ServerSocketChannel serverSocketChannel1 = (ServerSocketChannel) selectionKey.channel();
SocketChannel accept = serverSocketChannel1.accept();
accept.configureBlocking(false);
// 这里只注册了读事件,如果还需要往客户端写的话需要注册写事件
accept.register(selector,SelectionKey.OP_READ);
System.out.println("客户端连接成功....");
}else if (selectionKey.isReadable()){
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
int read = socketChannel.read(allocate);
if(read > 0){
System.out.println("收到消息:"+ new String(allocate.array()));
}else if(read == -1){
System.out.println("客户端断开连接");
socketChannel.close();
}
}
iterator1.remove();
}
}
}
异步I/O也就是我们所说的AIO,上面的几种I/O模型都需要同步的去完成客户端的连接和数据的读取
而异步I/O则会在用户发起 read 调用的同时注册一个回调函数,read 立即返回,等内核将数据准备好,再调用指定的回调函数完成处理,在这个过程中,用户线程一直没有阻塞
Java 提供了 AsynchronousServerSocketChannel 和 AsynchronousSocketChannel,
AsynchronousServerSocketChannel 提供了两个 accept 方法,其中一个需要去注册一个回调函数
Java 服务端代码:
public static void main(String[] args) throws IOException, InterruptedException {
AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
// 创建一个回调函数
CompletionHandler<AsynchronousSocketChannel, Object> handler = new CompletionHandler<AsynchronousSocketChannel,
Object>() {
@Override
public void completed(final AsynchronousSocketChannel result, final Object attachment) {
// 继续监听下一个连接请求
serverSocketChannel.accept(attachment, this);
try {
System.out.println("接受了一个连接:" + result.getRemoteAddress()
.toString());
// 给客户端发送数据并等待发送完成
result.write(ByteBuffer.wrap("From Server:Hello i am server".getBytes()))
.get();
ByteBuffer readBuffer = ByteBuffer.allocate(128);
// 阻塞等待客户端接收数据
result.read(readBuffer)
.get();
System.out.println(new String(readBuffer.array()));
} catch (IOException | InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
@Override
public void failed(final Throwable exc, final Object attachment) {
System.out.println("出错了:" + exc.getMessage());
}
};
// 注册回调函数
serverSocketChannel.accept(null, handler);
// 由于serverSocketChannel.accept(null, handler);是一个异步方法,调用会直接返回,
// 为了让子线程能够有时间处理监听客户端的连接会话,
// 这里通过让主线程休眠一段时间(当然实际开发一般不会这么做)以确保应用程序不会立即退出。
TimeUnit.MINUTES.sleep(Integer.MAX_VALUE);
}
Java 客户端代码:
public static void main(String[] args) throws IOException, InterruptedException {
// 打开一个SocketChannel通道并获取AsynchronousSocketChannel实例
AsynchronousSocketChannel client = AsynchronousSocketChannel.open();
// 连接到服务器并处理连接结果
client.connect(new InetSocketAddress("127.0.0.1", 8080), null, new CompletionHandler<Void, Void>() {
@Override
public void completed(final Void result, final Void attachment) {
System.out.println("成功连接到服务器!");
try {
// 给服务器发送信息并等待发送完成
client.write(ByteBuffer.wrap("From client:Hello i am client".getBytes())).get();
ByteBuffer readBuffer = ByteBuffer.allocate(128);
// 阻塞等待接收服务端数据
client.read(readBuffer).get();
System.out.println(new String(readBuffer.array()));
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
@Override
public void failed(final Throwable exc, final Void attachment) {
exc.printStackTrace();
}
});
TimeUnit.MINUTES.sleep(Integer.MAX_VALUE);
}
码字不易,还希望多多点赞、收藏支持下