IO概述
软件开发的核心是数据,而数据的传输、保存、读取都是通过IO技术实现的。
Java中的IO模型主要有三种:
- BIO 同步阻塞式IO
- NIO 同步非阻塞式IO
- AIO 异步非阻塞式IO
BIO
Blocking IO 同步阻塞式IO,是比较常用的IO模型
特点是
- 编写相对简单
- 分为输入流和输出流
- 进行网络通讯时,输入流的读操作会阻塞住线程,直到有输出流执行写操作
一旦服务器的线程阻塞,服务器就不能处理其他客户端的业务,为解决这种问题,一般我们会为每个客户端单独启动线程。
这样造成了新的问题,就是如果客户端比较多,就会大量消耗服务器的线程资源,导致服务器压力过大。
所以这种模型比较适合连接数不大、数量相对固定的情况。
下面是使用BIO模型的Socket进行网络通信
/**
* BIO服务器
*/
public class SocketServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(9000);
while (true) {
System.out.println("等待连接。。");
//阻塞方法
Socket socket = serverSocket.accept();
System.out.println("有客户端连接了。。");
//启动线程处理客户端IO
new Thread(new Runnable() {
@Override
public void run() {
try {
handler(socket);
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
}
/**
* 处理客户端IO
* @param socket
* @throws IOException
*/
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);
if (read != -1) {
System.out.println("接收到客户端的数据:" + new String(bytes, 0, read));
}
//给客户端返回数据
socket.getOutputStream().write("你好,客户端".getBytes());
socket.getOutputStream().flush();
}
}
/**
* 客户端
*/
public class SocketClient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1", 9000);
//向服务端发送数据
socket.getOutputStream().write("服务器你好!!".getBytes());
socket.getOutputStream().flush();
System.out.println("向服务端发送数据结束");
byte[] bytes = new byte[1024];
//接收服务端回传的数据
socket.getInputStream().read(bytes);
System.out.println("接收到服务端的数据:" + new String(bytes));
socket.close();
}
}
NIO
jdk1.4出现了NIO(Noblocking IO 同步非阻塞式IO) ,与BIO不同的是,NIO进行读取操作时,不会阻塞线程
NIO有三大核心组件:
- Channel 通道,类似BIO的输入输出流的作用,不同IO流的是Channel同时可以完成读写操作
- Buffer 缓冲区,类似BIO中的byte数组,用于存放数据
- Selector 多路复用器,每个Channel都会注册到selector上,selector会在多个通道间轮询,一旦有读写事件出现就会立即进行处理
优点:单线程就可以处理大量的IO请求,这样就大大降低了服务器的压力。
缺点:如果每个IO请求处理时间比较长,会产生长时间排队的情况;编程也比较复杂。
比较适合处理连接数比较多并且时间较短的场景,如:聊天、发弹幕等。
服务器
public class NIOServer {
public static void main(String[] args) throws IOException {
// 创建一个在本地端口进行监听的服务Socket通道
ServerSocketChannel ssc = ServerSocketChannel.open();
//必须配置为非阻塞才能往selector上注册
ssc.configureBlocking(false);
ssc.socket().bind(new InetSocketAddress(9000));
// 创建选择器
Selector selector = Selector.open();
// 注册到选择器上,对客户端连接操作感兴趣
ssc.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// 轮询监听channel里的key,select是阻塞的
selector.select();
System.out.println("事件发生");
// 有客户端请求,被轮询监听到
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
//删除本次已处理的key,防止下次select重复处理
it.remove();
handle(key);
}
}
}
/**
* 事件处理
* @param key
* @throws IOException
*/
private static void handle(SelectionKey key) throws IOException {
//处理连接事件
if (key.isAcceptable()) {
System.out.println("处理连接事件");
//获得ServerSocketChannel
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//获得ServerSocketChannel接收的客户端
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
//注册对SocketChannel读事件感兴趣
sc.register(key.selector(), SelectionKey.OP_READ);
}
//处理读取事件
else if (key.isReadable()) {
System.out.println("处理读取事件");
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
//read方法不会阻塞,调用时肯定是发生了客户端发送数据的事件
int len = sc.read(buffer);
if (len != -1) {
System.out.println("读取到客户端发送的数据:" + new String(buffer.array(), 0, len));
}
ByteBuffer bufferToWrite = ByteBuffer.wrap("Hello 客户端".getBytes());
sc.write(bufferToWrite);
key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
}
//处理写事件
else if (key.isWritable()) {
SocketChannel sc = (SocketChannel) key.channel();
System.out.println("处理写事件");
key.interestOps(SelectionKey.OP_READ);
}
}
}
客户端
public class NIOClient {
//多路复用器
private Selector selector;
/**
* 启动客户端测试
*
* @throws IOException
*/
public static void main(String[] args) throws IOException {
NIOClient client = new NIOClient();
client.initClient("127.0.0.1", 9000);
client.connect();
}
/**
* 获得一个Socket通道,并对该通道做一些初始化的工作
*
* @param ip 连接的服务器的ip
* @param port 连接的服务器的端口号
* @throws IOException
*/
public void initClient(String ip, int port) throws IOException {
// 获得一个Socket通道
SocketChannel channel = SocketChannel.open();
// 设置通道为非阻塞
channel.configureBlocking(false);
// 获得一个通道管理器
this.selector = Selector.open();
// 客户端连接服务器,其实方法执行并没有实现连接,需要在listen()方法中调
//用channel.finishConnect() 才能完成连接
channel.connect(new InetSocketAddress(ip, port));
//将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_CONNECT事件。
channel.register(selector, SelectionKey.OP_CONNECT);
}
/**
* 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
*
* @throws IOException
*/
public void connect() throws IOException {
// 轮询访问selector
while (true) {
selector.select();
// 获得selector中选中的项的迭代器
Iterator<SelectionKey> it = this.selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey key = (SelectionKey) it.next();
// 删除已选的key,以防重复处理
it.remove();
// 连接事件发生
if (key.isConnectable()) {
SocketChannel channel = (SocketChannel) key.channel();
// 如果正在连接,则完成连接
if (channel.isConnectionPending()) {
channel.finishConnect();
}
// 设置成非阻塞
channel.configureBlocking(false);
//在这里可以给服务端发送信息哦
ByteBuffer buffer = ByteBuffer.wrap("HelloServer".getBytes());
channel.write(buffer);
//在和服务端连接成功之后,为了可以接收到服务端的信息,需要给通道设置读的权限。
channel.register(this.selector, SelectionKey.OP_READ); // 获得了可读的事件
} else if (key.isReadable()) {
read(key);
}
}
}
}
/**
* 处理读取服务端发来的信息 的事件
*
* @param key
* @throws IOException
*/
public void read(SelectionKey key) throws IOException {
//和服务端的read方法一样
// 服务器可读取消息:得到事件发生的Socket通道
SocketChannel channel = (SocketChannel) key.channel();
// 创建读取的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = channel.read(buffer);
if (len != -1) {
System.out.println("客户端收到信息:" + new String(buffer.array(), 0, len));
}
}
}
AIO
AIO(异步非阻塞),由操作系统完成后回调通知服务端程序启动线程去处理
适合于连接数多且时间较长的操作,JDK1.7开始
服务器
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(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);
}
}
客户端
public class AIOClient {
public static void main(String[] args) throws Exception {
AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1", 9000)).get();
socketChannel.write(ByteBuffer.wrap("HelloServer".getBytes()));
ByteBuffer buffer = ByteBuffer.allocate(512);
Integer len = socketChannel.read(buffer).get();
if (len != -1) {
System.out.println("客户端收到信息:" + new String(buffer.array(), 0, len));
}
}
}
BIO、NIO、AIO对比
BIO | NIO | AIO | |
---|---|---|---|
IO模型 | 同步阻塞 | 同步非阻塞 | 异步阻塞 |
编程难度 | 较低 | 较高 | 较高 |
可靠性 | 差 | 好 | 好 |
吞吐量 | 低 | 高 | 高 |
举个栗子:去餐厅吃饭等位子
BIO:拿号后在门口排队等候,什么都不做
NIO:拿号后去旁边逛街,每隔一段时间来看看有没有到自己
AIO:在手机上预订后,去旁边逛街,到自己后手机通知再过去