IO模型
IO模型就是说用什么样的通道进行数据的发送和接收,Java共支持3种网络编程IO模式:BIO,NIO,AIO
BIO
特点
同步阻塞模型,一个客户端连接对应一个处理线程
缺点
1、IO代码里read操作是阻塞操作,如果连接不做数据读写操作会导致线程阻塞,浪费资源
2、如果线程很多,会导致服务器线程太多,压力太大。
应用场景
BIO 方式适用于连接数目比较小且固定的架构, 这种方式对服务器资源要求比较高, 但程序简单易理解。
public class SocketServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(9000);
while (true) {
System.out.println("等待连接。。");
final Socket socket = serverSocket.accept();
//阻塞方法
System.out.println("有客户端连接了。。");
// new Thread(new Runnable() {
// @Override
// public void run() {
// try {
// handler(socket);
// } catch (IOException e) {
// e.printStackTrace();
// }
// }
// }).start();
handler(socket);
}
}
private static void handler(Socket socket) throws IOException {
System.out.println("thread id = " + Thread.currentThread().getId());
byte[] bytes = new byte[1024];
System.out.println("准备read。。");
//接收客户端的数据,阻塞方法,没有数据可读时就阻塞
int read = socket.getInputStream().read(bytes);
System.out.println("read完毕。。");
if (read != -1) {
System.out.println("接收到客户端的数据:" + new String(bytes, 0, read));
System.out.println("thread id = " + Thread.currentThread().getId());
}
socket.getOutputStream().write("HelloClient".getBytes());
socket.getOutputStream().flush();
}
}
以上代码是bio的服务端代码,我们看到他有一个accept()方法用于接收客户端连接,如果没有连接会一直阻塞到这里,有链接之后进入handle方法,然后准备接收客户端发送的数据,但是如果一直没有数据发过来的话就会阻塞到这里,即使我们在数据处理中使用线程池也是不行的,如果一直有资源卡到这里最终只会导致资源耗尽。
NIO
特点
同步非阻塞,服务器实现模式为一个线程可以处理多个请求(连接),客户端发送的连接请求都会注册到多路复用器selector上,多路复用器 轮询到连接有IO请求就进行处理。
应用场景
NIO 方式适用于连接数目比较多且连接比较短的架构, 比如聊天服务器,弹幕系统等。
NIO有三大组件:Channel(通道)、Buffer(缓冲区)、Selector(复用器)
1.channel就相当于是流,每个channel对应一个缓冲区buffer;
2.channel还会注册到selector上根据channel的读写事件的发生将其交给某个空闲的线程处理;
3.selector可以对应一个或多个线程,在业务处理的时候可以选择使用线程池;
//服务端代码
public class NIOServer {
public static void main(String[] args) throws IOException {
// 创建一个在本地端口进行监听的服务Socket通道.并设置为非阻塞方式
ServerSocketChannel ssc = ServerSocketChannel.open();
//必须配置为非阻塞才能往selector上注册,否则会报错,selector模式本身就是非阻塞模式
ssc.configureBlocking(false);
ssc.socket().bind(new InetSocketAddress(9000));
// 创建一个选择器并将serverSocketChannel注册到它上面
Selector selector = Selector.open();
// 把channel注册到selector上,并且selector对客户端accept连接操作感兴趣
ssc.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
System.out.println("等待事件发生。。");
// 轮询监听key,select是阻塞的,accept()也是阻塞的
selector.select();
System.out.println("有事件发生了。。");
// 有客户端请求,被轮询监听到
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
//删除本次已处理的key,防止下次select重复处理
it.remove();
handle(key);
}
}
}
private static void handle(SelectionKey key) throws IOException {
if (key.isAcceptable()) {
System.out.println("有客户端连接事件发生了。。");
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//此处accept方法是阻塞的,但是这里因为是发生了连接事件,所以这个方法会马上执行完
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
//通过Selector监听Channel时对读事件感兴趣
sc.register(key.selector(), SelectionKey.OP_READ);
} else if (key.isReadable()) {
System.out.println("有客户端数据可读事件发生了。。");
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.clear();
int len = sc.read(buffer);
if (len != -1) {
System.out.println("读取到客户端发送的数据:" + new String(buffer.array(), 0, len));
}
ByteBuffer bufferToWrite = ByteBuffer.wrap("HelloClient".getBytes());
sc.write(bufferToWrite);
key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
sc.close();
}
}
}
1.创建一个ServerSocketChannel和Selector,设置ServerSocketChannel为非阻塞;
2.将ServerSocketChannel注册到Selector上,监听连接事件;
3.Selector通过select()方法监听channel的连接事件,当客户端连接的时候,selector监听到连接事件,会获取到连接中的SelectionKey,select()方法相当于BIO中的accept方法,也是阻塞的。
4.当有事件发生的时候会轮询SelectionKey(一般用epoll事件通知方式),接收到通知之后,判断是哪种事件;
5.如果是连接事件,SelectionKey通过channel方法获取到连接的ServerSocketChannel,ServerSocketChannel通过accept方法获取到SocketChannel;
6.把SocketChannel注册到Selector上,监听读取事件;
7.当发生读取事件时,根据key获取SocketChannel;
8.将SocketChannel数据读取出来。
9.使用ByteBuffer写到客户端。
AIO
特点
异步非阻塞, 由操作系统完成后回调通知服务端程序启动线程去处理, 一般适用于连接数较多且连接时间较长的应用
应用场景
AIO方式适用于连接数目多且连接比较长(重操作) 的架构,JDK7 开始支持
//服务端代码
public class AIOServer {
public static void main(String[] args) throws Exception {
final AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open()
.bind(new InetSocketAddress(9000));
serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
@Override
public void completed(final AsynchronousSocketChannel socketChannel, Object attachment) {
try {
// 再此接收客户端连接,如果不写这行代码后面的客户端连接连不上服务端
serverChannel.accept(attachment, this);
System.out.println(socketChannel.getRemoteAddress());
ByteBuffer buffer = ByteBuffer.allocate(1024);
socketChannel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buffer) {
buffer.flip();
System.out.println(new String(buffer.array(), 0, result));
socketChannel.write(ByteBuffer.wrap("HelloClient".getBytes()));
}
@Override
public void failed(Throwable exc, ByteBuffer buffer) {
exc.printStackTrace();
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, Object attachment) {
exc.printStackTrace();
}
});
Thread.sleep(Integer.MAX_VALUE);
}
}
我们可以看出accept方法时,方法是阻塞的,然后再使用回调函数进行监听,这样的话就是异步操作,可以同时进行接收事件和读写事件。