网络IO模型
阻塞式I/O
默认情况下,所有的套接字的方法都是阻塞的,如上面的accept、recv。
对应的代码如下:
package com.morris.bio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
public class BioSingleThreadServer {
public static final int PORT = 8899;
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(); // socket(AF_INET6, SOCK_STREAM, IPPROTO_IP) = 5
serverSocket.bind(new InetSocketAddress(PORT)); // bind(5, {sa_family=AF_INET6, sin6_port=htons(8899), inet_pton(AF_INET6, "::", &sin6_addr), sin6_flowinfo=htonl(0), sin6_scope_id=0}, 28) = 0
// bind()里面会调用listen方法 listen(5, 50)
System.out.println("server is start at " + PORT); //
while (true) {
try {
Socket socket = serverSocket.accept(); // accept(3, {sa_family=AF_INET6, sin6_port=htons(34270), inet_pton(AF_INET6, "::ffff:127.0.0.1", &sin6_addr), sin6_flowinfo=htonl(0), sin6_scope_id=0}, [28]) = 5
InputStream inputStream = socket.getInputStream();
System.out.println("connect success " + socket.getPort());
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
System.out.println("receive from client: " + reader.readLine()); // recv(5, "hello\r\n", 8192, 0) = 7
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
上面的代码需要在jdk1.4上面运行才会出现accept的阻塞,高版本的jdk会使用poll方法阻塞,poll方法返回后才会去调用accept获取连接。
当然使用NIO的阻塞模式也会出现上面的效果,只不过recv函数会缓存read函数。
非阻塞式I/O
NIO需要调用套接字的方法前,将套接字设置为非阻塞模式,这样调用方法时就不会因为阻塞而进入休眠,而是立即返回,如果没有数据,就会返回一个错误。
对应的代码如下:
package com.morris.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;
import java.util.Objects;
public class NioSingleThreadServer {
public static final int PORT = 8899;
public static void main(String[] args) throws IOException, InterruptedException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // socket
serverSocketChannel.bind(new InetSocketAddress(PORT)); // bind+listen
serverSocketChannel.configureBlocking(false); // 设置accept不阻塞
LinkedList<SocketChannel> socketChannels = new LinkedList<>(); // 存放所有的客户端连接
while (true) {
Thread.sleep(1000);
// 获取连接
SocketChannel socketChannel = serverSocketChannel.accept(); // 不阻塞 accept
if (Objects.isNull(socketChannel)) { // 没有连接会返回null
System.out.println("null");
} else {
System.out.println("connect success: " + socketChannel.socket().getPort());
socketChannel.configureBlocking(false); // 设置read不阻塞
socketChannels.add(socketChannel);
}
// 读取数据
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
for (SocketChannel s : socketChannels) {
if (s.isOpen()) {
int readLength = s.read(byteBuffer); // 不阻塞,没数据返回-1 read
if (readLength > 0) {
byteBuffer.flip();
byte[] bytes = new byte[byteBuffer.limit()];
byteBuffer.get(bytes);
System.out.println("receive from client: " + new String(bytes));
s.close();
byteBuffer.clear();
}
}
}
}
}
}
I/O复用(select/poll/epoll)
虽然I/O多路复用的函数也是阻塞的,但是其与以上两种还是有不同的,I/O多路复用是阻塞在select,epoll这样的系统调用之上,而没有阻塞在真正的I/O系统调用如accept之上。
对应的代码如下:
package com.morris.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
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 NioSingleSelectorServer {
public static final int PORT = 8899;
private static Selector selector;
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(PORT));
serverSocketChannel.configureBlocking(false);
selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); // 关心接受连接事件
System.out.println("server is start at " + PORT);
while (true) {
while (selector.select() > 0) { // selector.select()不带时间会一直阻塞,可以带一个超时时间
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if(key.isValid()) {
if(key.isAcceptable()) {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
// select只会返回有数据的FD,真正获取连接和读取数据还需要调用accept和read
SocketChannel socketChannel = ssc.accept();
System.out.println("connect success: " + socketChannel.socket().getPort());
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
}
if(key.isReadable()) {
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
socketChannel.read(byteBuffer);
byteBuffer.flip();
byte[] bytes = new byte[byteBuffer.limit()];
byteBuffer.get(bytes);
System.out.println("receive from client: " + new String(bytes));
socketChannel.close();
}
}
}
}
}
}
}
异步I/O
异步IO的工作机制是告知内核启动某个操作,并让内核在整个操作(包括将数据从内核拷贝到用户空间)完成后通知我们。
总结
阻塞与非阻塞:调用方法能立刻返回就是非阻塞,调用方法要等有数据了才返回就是阻塞。
同步与异步:访问数据的方式,同步需要主动读写数据,在读写数据的过程中还是会阻塞;异步只需要I/O操作完成的通知,并不主动读写数据,由操作系统内核完成数据的读写。