IO
IO 的方式通常分为几种:同步阻塞的 BIO、同步非阻塞的 NIO、异步非阻塞的 AIO。
BIO
在 JDK1.4
之前,我们建立网络连接的时候只能采用 BIO,需要先在服务端启动一个ServerSocket
,然后在客户端启动 Socket
来对服务端进行通信,默认情况下服务端需要对每个请求建立一个线程等待请求,而客户端发送请求后,先咨询服务端是否有线程响应,如果没有则会一直等待或者遭到拒绝,如果有的话,客户端线程会等待请求结束后才继续执行,这就是阻塞式IO。
BIO服务器端程序
public class TCPServer {
public static void main(String[] args) throws Exception {
//1.创建 ServerSocket 对象
ServerSocket ss=new ServerSocket(9999);
while (true) {
//2.监听客户端
Socket s = ss.accept(); //阻塞
//3.从连接中取出输入流来接收消息
InputStream is = s.getInputStream(); //阻塞
byte[] b = new byte[10];
is.read(b);
String clientIP = s.getInetAddress().getHostAddress();
System.out.println(clientIP + "说:" + new String(b).trim());
//4.从连接中取出输出流并回话
OutputStream os = s.getOutputStream();
os.write("hello".getBytes());
//5.关闭
s.close();
}
}
}
accept 方法用来监听客户端连接,如果没有客户端连接,就一直等待,程序会阻塞。
getInputStream方法用来等待客户端返回数据,如果没有数据返回,就一直等待,程序会阻塞。
BIO客户端程序
public class TCPClient {
public static void main(String[] args) throws Exception {
while (true) {
//1.创建 Socket 对象
Socket s = new Socket("127.0.0.1", 9999);
//2.从连接中取出输出流并发消息
OutputStream os = s.getOutputStream();
System.out.println("请输入:");
Scanner sc = new Scanner(System.in);
String msg = sc.nextLine();
os.write(msg.getBytes());
//3.从连接中取出输入流并接收回话
InputStream is = s.getInputStream(); //阻塞
byte[] b = new byte[20];
is.read(b);
System.out.println("服务器发送消息:"+new String(b).trim());
//4.关闭
s.close();
}
}
}
getInputStream方法用来等待服务器端返回数据,如果数据没有返回,就一直等待,程序会阻塞。
NIO
java.nio
全称 java non-blocking IO
,是指 JDK 提供的新 API。从 JDK1.4 开始,Java 提供了一系列改进的输入/输出的新特性,被统称为 NIO(即 New IO)。新增了许多用于处理输入输出的类,这些类都被放在 java.nio
包及子包下,并且对原 java.io
包中的很多类进行改写,新增了满足 NIO 的功能。
NIO 主要有三大核心部分:Channel
(通道),Buffer
(缓冲区), Selector
(选择器)。传统的 BIO基于字节流和字符流进行操作,而 NIO 基于 Channel
(通道)和 Buffer
(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)
用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道。
NIO核心 API
- 缓冲区(
Buffer
):实际上是一个容器,是一个特殊的数组,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer。
ByteBuffer
(二进制数据),该类的主要方法如下所示:
public abstract ByteBuffer put(byte[] b); 存储字节数据到缓冲区
public abstract byte[] get(); 从缓冲区获得字节数据
public final byte[] array(); 把缓冲区数据转换成字节数组
public static ByteBuffer allocate(int capacity); 设置缓冲区的初始容量
public static ByteBuffer wrap(byte[] array); 把一个现成的数组放到缓冲区中使用
public final Buffer flip(); 翻转缓冲区,重置位置到初始位置
- 通道(
Channel
):用来建立到目标(文件,网络套接字,硬件设备等)的一个连接,常用的Channel
类有:FileChannel
、DatagramChannel
、ServerSocketChannel
和SocketChannel
。FileChannel
用于文件的数据读写,DatagramChannel
用于 UDP 的数据读写,ServerSocketChannel
和SocketChannel
用于 TCP 的数据读写。
2.1 ServerSocketChannel
:用于服务器端监听新的客户端Socket
连接
public static ServerSocketChannel open() 得到一个 ServerSocketChannel 通道
public final ServerSocketChannel bind(SocketAddress local) 设置服务器端端口号
public final SelectableChannel configureBlocking(boolean block) 设置阻塞或非阻塞模式,取值 false 表示采用非阻塞模式
public SocketChannel accept() 接受一个连接,返回代表这个连接的通道对象
public final SelectionKey register(Selector sel, int ops) 注册一个选择器并设置监听事件
2.2 SocketChannel
:网络IO通道,具体负责进行读写操作
public static SocketChannel open() 得到一个 SocketChannel 通道
public final SelectableChannel configureBlocking(boolean block) 设置阻塞或非阻塞模式,取值 false 表示采用非阻塞模式
public boolean connect(SocketAddress remote) 连接服务器
public boolean finishConnect() 如果上面的方法连接失败,接下来就要通过该方法完成连接操作
public int write(ByteBuffer src) 往通道里写数据
public int read(ByteBuffer dst) 从通道里读数据
public final SelectionKey register(Selector sel, int ops, Object att) 注册一个选择器并设置监听事件,最后一个参数可以设置共享数据
public final void close() 关闭通道
- Selector(
选择器
):够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接。这样使得只有在连接真正有读写事件发生时,才会调用函数来进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了多线程之间的上下文切换导致的开销。
3.1 Selector:
public static Selector open() 得到一个选择器对象
public int select(long timeout) 监控所有注册的通道,当其中有IO操作可以进行时,将对应的 SelectionKey 加入到内部集合中并返回
public Set<SelectionKey> selectedKeys() 从内部集合中得到所有的 SelectionKey
3.2 SelectionKey:代表了Selector 和网络通道的注册关系
int OP_ACCEPT: 有新的网络连接可以 accept,值为 16
int OP_CONNECT: 代表连接已经建立,值为 8
int OP_READ : 读操作,值为 1
int OP_WRITE: 写操作,值为 4
常用方法:
public abstract Selector selector() 得到与之关联的 Selector 对象
public abstract SelectableChannel channel() 得到与之关联的通道
public final Object attachment() 得到与之关联的共享数据
public abstract SelectionKey interestOps(int ops) 设置或改变监听事件
public final boolean isAcceptable() 是否可以 accept
public final boolean isReadable() 是否可以读
public final boolean isWritable() 是否可以写
网络服务器端程序
public class NIOServer {
public static void main(String[] args) throws Exception{
//1. 得到一个ServerSocketChannel对象
ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
//2. 得到一个Selector对象
Selector selector=Selector.open();
//3. 绑定一个端口号
serverSocketChannel.bind(new InetSocketAddress(9999));
//4. 设置非阻塞方式
serverSocketChannel.configureBlocking(false);
//5. 把 ServerSocketChannel 对象注册给 Selector 对象
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while(true){
//6.1 监控客户端
if(selector.select(2000)==0){ //nio 非阻塞式的优势
System.out.println("Server:没有客户端搭理我,我就干点别的事");
continue;
}
//6.2 得到SelectionKey,判断通道里的事件
Iterator<SelectionKey> keyIterator=selector.selectedKeys().iterator();
while(keyIterator.hasNext()){
SelectionKey key=keyIterator.next();
if(key.isAcceptable()){ //客户端连接请求事件
System.out.println("OP_ACCEPT");
SocketChannel socketChannel=serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
if(key.isReadable()){ //读取客户端数据事件
SocketChannel channel=(SocketChannel) key.channel();
ByteBuffer buffer=(ByteBuffer) key.attachment(); channel.read(buffer);
System.out.println("客户端发来数据:"+new String(buffer.array()));
}
// 6.3 手动从集合中移除当前 key,防止重复处理
keyIterator.remove();
}
}
}
}
网络客户端程序
public class NIOClient {
public static void main(String[] args) throws Exception{
//1. 得到一个网络通道
SocketChannel channel=SocketChannel.open();
//2. 设置非阻塞方式
channel.configureBlocking(false);
//3. 提供服务器端的 IP 地址和端口号
InetSocketAddress address=new InetSocketAddress("127.0.0.1",9999);
//4. 连接服务器端
if(!channel.connect(address)){
while(!channel.finishConnect()){ //nio 作为非阻塞式的优势
System.out.println("Client:连接服务器端的同时,我还可以干别的一些事情");
}
}
//5. 得到一个缓冲区并存入数据
String msg="hello,Server";
ByteBuffer writeBuf = ByteBuffer.wrap(msg.getBytes());
//6. 发送数据
channel.write(writeBuf);
System.in.read();
}
}
AIO
JDK 7 引入了 Asynchronous I/O,即 AIO。在进行 I/O 编程中,常用到两种模式:Reactor和 Proactor。Java 的 NIO 就是 Reactor,当有事件触发时,服务器端得到通知,进行相应的处理。
AIO 即 NIO2.0,叫做异步不阻塞的 IO。AIO 引入异步通道的概念,采用了 Proactor 模式,简化了程序编写,一个有效的请求才启动一个线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。