1. 概念
网络IO的过程,就是操作系统接收到网卡的数据,缓存到一个buffer中,然后应用程序调用操作系统的函数,从对应的buffer中取出数据。
2. 常见IO模型
模拟客户端连接:
public static void main(String[] args) {
try {
Socket socket = new Socket("127.0.0.1", 8082);
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
dos.writeUTF("helloServer!");
dos.flush();
dos.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
2.1 BIO
无论是获取新的连接还是读取指定连接的数据,调用操作系统的函数都是阻塞的,如果要实现服务多个连接,就必须每个连接建立一个线程异步处理,否则,当建立起一个连接,但是客户端不发送数据,服务端就会被这个客户端占用,无法接受新的连接。
2.1.1模拟服务端连接
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(8082);
while(true){
Socket socket = serverSocket.accept();//阻塞
//服务端与客户端每建立一次连接就会开启一个线程处理
new Thread(new Runnable() {
@Override
public void run() {
try {
DataInputStream dis = new DataInputStream(socket.getInputStream());//阻塞
System.out.println(dis.readUTF());
dis.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
2.1.2 优势
1.多线程,多连接,可以接收很多连接
2.1.3 劣势
2.2 NIO
解决了阻塞的问题,程序调用操作系统的函数,如果没有连接或数据,会立即返回,不会阻塞,避免了资源无效浪费。但是,它的问题在于,如果我有1万个连接,每次我需要挨个询问1万次,这个复杂度是O(n)的。每次询问都是一次系统调用,涉及到CPU的用户态内核态切换,成本很高。
2.2.1模拟服务端连接
public static void main(String[] args) throws Exception {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8082));
//OS false-NONBLOCKING,true-BLOCKING
serverSocketChannel.configureBlocking(false);
//客户端连接socket集合
LinkedList<SocketChannel> clients = new LinkedList<>();
while(true){
Thread.sleep(1000);
//serverSocketChannel.accept()不阻塞,linux底层没有客户端连接时返回-1,有客户端连接返回客户端的fd
SocketChannel socketChannel = serverSocketChannel.accept();
if(socketChannel == null){
System.out.println("null...");
}else{
//OS false-NONBLOCKING,true-BLOCKING
socketChannel.configureBlocking(false);
System.out.println("clientport:" + socketChannel.socket().getPort());
clients.add(socketChannel);
}
//遍历连接上的客户端socket读写数据
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(4096);
clients.forEach(client->{
try {
int num = client.read(byteBuffer);
if(num > 0){
byteBuffer.flip();
byte[] bytes = new byte[byteBuffer.limit()];
byteBuffer.get(bytes);
String result = new String(bytes);
System.out.println(result);
byteBuffer.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
});
}
}
2.2.2优势
1.规避了C10K问题;
2.2.3劣势
1.存在很多无意义的系统调用,用户态和内核态切换消耗时间和资源;
2.3多路复用器
2.3.1 select
这是最初级的多路复用器,从NIO到多路复用器,其实就是一个从多次到批量的演进,多路复用器支持一次询问多个文件描述符(fd)(linux中,一切皆为文件,连接也是文件,有对应的文件描述符)。从多次到批量,就能节省大量的运行态切换成本。但是select的问题在于,批量有上限,是有限的批量。
2.3.2 poll
解决了select的上限问题,一次可以询问任意个数的fd,真正做到了批量。但是,即使减少了运行态切换的成本,针对每次传来的fd,操作系统依然需要逐个遍历,复杂度依然是O(n),只是每次操作的损耗降低了。
2.3.3 epoll
解决了POLL和Select存在的遍历问题,将复杂度降为O(1),操作系统提前维护好用户程序对应的fd,每次有数据到达,就把对应的fd放到一个数据结构中存起来,当用户程序需要读取数据时,直接把这些有状态的fd返回,用户程序一次性获取fd,逐个读取即可。用户只要调用一次,操作系统也不需要遍历。这是目前大部分场景下,最高效的模型。
2.3.4模拟服务端调用
//select poll epoll
public static void main(String[] args) throws Exception {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8082));
//OS false-NONBLOCKING,true-BLOCKING
serverSocketChannel.configureBlocking(false);
/*
多路复用器 select poll epoll 优先选择:epoll 但是可以-D修正
epoll: epoll_create -> fd7
*/
Selector selector = Selector.open();
/*
将channel注册到selector上
select,poll:jvm里开辟一个数组fd3放进去
epoll:epoll_ctl(fd7,ADD,fd3,EPOLLIN
*/
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while(true){
/*
调用多路复用器
select,poll:内核的select(fd3) poll(fd3)
epoll:内核的epoll_wait()
阻塞,有时间设置一个超时
*/
while(selector.select(500) > 0){
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while(iterator.hasNext()){
SelectionKey key = iterator.next();
iterator.remove();//set 不移除会重复循环处理
if(key.isAcceptable()){
/*
accept接受连接且返回新连接的fd,那新的fd怎么办?
select,poll:因为他们内核没有空间,那么jvm中保存和前边的fd3那个listen的一起
epoll:我们希望通过epoll_ctl把新的客户端fd注册到内核空间
*/
System.out.println("a connection was accepted by a ServerSocketChannel.");
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel client = ssc.accept();//目的是调用accept接受客户端fd8
client.configureBlocking(false);
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(8192);
/*
select,poll:jvm开辟一个数组fd8放进去
epoll:epoll_ctl(fd7,ADD,fd8,EPOLLIN
*/
client.register(selector, SelectionKey.OP_READ, byteBuffer);
System.out.println("新客户端:" + client.getRemoteAddress());
}else if(key.isConnectable()){
System.out.println("a connection was established with a remote server.");
}else if(key.isReadable()){
System.out.println("a channel is ready for reading");
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = (ByteBuffer) key.attachment();
byteBuffer.clear();
int read = 0;
while(true){
//...
}
}else if(key.isWritable()){
System.out.println("a channel is ready for writing");
}
}
}
}
}
2.4 AIO
以上由于IO还是需要应用程序自己读取,所以都属于同步IO模型,AIO是由操作系统内核来读取IO,再通知应用系统;