I/O 模型简单理解为:就是使用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能。
Java 支持 3 种网络编程模型:BIO、NIO、AIO。
Java BIO:同步并阻塞
(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不作任何事情会造成不必要的线程开销。
Java NIO:同步非阻塞
,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求会被注册到多路复用器上,多路复用器轮询到有 I/O 请求就会进行处理。
Java AIO:异步非阻塞
,AIO 引入了异步通道的概念,采用了 Proactor 模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。
同步阻塞
:你到饭馆点餐,然后在那等着,还要一边喊:好了没啊!同步非阻塞
:在饭馆点完餐,就去遛狗了。不过溜一会儿,就回饭馆喊一声:好了没啊!异步阻塞
:遛狗的时候,接到饭馆电话,说饭做好了,让您亲自去拿。异步非阻塞
:饭馆打电话说,我们知道您的位置,一会给你送过来,安心遛狗就可以了。连接数比较小且固定
的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4 之前唯一的选择,程序较为简单容易理解。连接数目多且连接比较短
的架构,比如聊天服务器,弹幕系统,服务器间通讯等,编程比较复杂,JDK1.4 开始支持。连接数目多且连接比较长
的架构,比如相册服务器,充分调用 OS 参与并发操作,变成比较复杂,JDK7 开始支持。同步阻塞
,服务器实现模式为一个连接一个线程,即客户端有连接请求时,服务器就会需要启动一个线程来进行处理。如果这个连接不作任何事情就会造成不必要的开销,可以通过线程池机制改善。public class Server {
public static void main(String[] args) throws IOException {
//创建线程池
ExecutorService executorService = Executors.newCachedThreadPool();
//创建serverSocket
ServerSocket serverSocket = new ServerSocket(6666);
for (; ; ) {
System.out.println("等待连接中...");
//监听,等待客户端连接
Socket socket = serverSocket.accept();
System.out.println("连接到一个客户端");
executorService.execute(() -> handler(socket));
}
}
//编写一个handler方法,和客户端通讯
public static void handler(Socket socket) {
byte[] bytes = new byte[1024];
System.out.println("当前线程信息: " + Thread.currentThread().getName());
try {
//通过socket获取输入流
InputStream inputStream = socket.getInputStream();
//循环读取客户端发送的数据
while (inputStream.read(bytes) != -1) {
System.out.println(Thread.currentThread().getName()+ " : 发送信息为 :"+ new String(bytes, 0, bytes.length));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
System.out.println("关闭连接");
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
使用终端命令
telnet 127.0.0.1 6666
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DNOvs15E-1656935549536)(https://segmentfault.com/img/remote/1460000037714811)]
创建大量线程来处理连接
,系统资源占用较大。同步非阻塞
的。Channel(管道)
、Buffer(缓冲区)
、Selector(选择器)
。缓冲区
编程的。数据读取到了一个它稍微处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞的高伸缩性网络。public class BufferTest {
public static void main(String[] args) {
//同理对应的还有:ByteBuffer,IntBuffer,FloatBuffer,CharBuffer,ShortBuffer,DoubleBuffer,LongBuffer
//创建一个Buffer,大小为5
IntBuffer buffer = IntBuffer.allocate(5);
//存放数据
for (int i = 0; i < buffer.capacity(); i++) {
buffer.put(i);
}
//切换成读模式. 读写切换
buffer.flip();
while (buffer.hasRemaining()) {
System.out.println(buffer.get()); // 0 1 2 3 4
}
}
}
使用单个线程就可以监听多个客户端通道
。说明:
事件
决定的(Event)。flip()
切换读写模式。而 BIO 是单向的,要么输入流要么输出流。Buffer(缓冲区)基本介绍
缓冲区本质上是一个可以读写数据的内存块,可以理解为是一个容器对象(含数组)
,该对象提供了一组方法
,可以更轻松地使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。
Channel 提供从文件、网络读取数据的渠道,但是读取或者都必须经过 Buffer。
在 Buffer 子类中维护着一个对应类型的数组,用来存放数据:
public abstract class IntBuffer
extends Buffer
implements Comparable<IntBuffer>
{
// These fields are declared here rather than in Heap-X-Buffer in order to
// reduce the number of virtual method invocations needed to access these
// values, which is especially costly when coding small buffers.
//
final int[] hb; // Non-null only for heap buffers
final int offset;
boolean isReadOnly; // Valid only for heap buffers
// Creates a new buffer with the given mark, position, limit, capacity,
// backing array, and array offset
//
IntBuffer(int mark, int pos, int lim, int cap, // package-private
int[] hb, int offset)
{
super(mark, pos, lim, cap);
this.hb = hb;
this.offset = offset;
}
// Creates a new buffer with the given mark, position, limit, and capacity
//
IntBuffer(int mark, int pos, int lim, int cap) { // package-private
this(mark, pos, lim, cap, null, 0);
}
Buffer 常用子类 | 描述 |
---|---|
ByteBuffer | 存储字节数据到缓冲区 |
ShortBuffer | 存储字符串数据到缓冲区 |
CharBuffer | 存储字符数据到缓冲区 |
IntBuffer | 存储整数数据据到缓冲区 |
LongBuffer | 存储长整型数据到缓冲区 |
DoubleBuffer | 存储浮点型数据到缓冲区 |
FloatBuffer | 存储浮点型数据到缓冲区 |
Buffer 中定义了四个属性来提供所其包含的数据元素。
// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
属性 | 描述 |
---|---|
capacity | 容量,即可以容纳的最大数据量;在缓冲区被创建时候就被指定,无法修改 |
limit | 表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写操作,但极限是可以修改的 |
position | 当前位置,下一个要被读或者写的索引,每次读写缓冲区数据都会改变该值,为下次读写做准备 |
Mark | 标记当前 position 位置,当 reset 后回到标记位置。 |
NIO 的通道类似于流,但有如下区别:
常用的 Channel 有:FileChannel、DatagramChannel、SocketChannel、SocketServerChannel。
FileChannel 类
FileChannel 主要用来对本地文件进行 IO 操作,常见的方法有:
public class NIOFileChannel {
public static void main(String[] args) throws IOException {
String str = "Hello,Java菜鸟程序员";
//创建一个输出流
FileOutputStream fileOutputStream = new FileOutputStream("hello.txt");
//获取通道
FileChannel channel = fileOutputStream.getChannel();
//创建缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(100);
//写入byteBuffer
byteBuffer.put(str.getBytes());
//切换模式
byteBuffer.flip();
//写入通道
channel.write(byteBuffer);
//关闭
channel.close();
fileOutputStream.close();
}
}
public class NIOFileChannel {
public static void main(String[] args) throws IOException {
FileInputStream fileInputStream = new FileInputStream("hello.txt");
FileChannel channel = fileInputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(100);
channel.read(byteBuffer);
System.out.println(new String(byteBuffer.array(), 0, byteBuffer.limit())); //Hello,Java菜鸟程序员
channel.close();
fileInputStream.close();
}
}
public class NIOFileChannel03 {
public static void main(String[] args) throws IOException {
FileInputStream fileInputStream = new FileInputStream("hello.txt");
FileOutputStream fileOutputStream = new FileOutputStream("world.txt");
FileChannel inChannel = fileInputStream.getChannel();
FileChannel outChannel = fileOutputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1);
while (inChannel.read(byteBuffer) != -1) {
byteBuffer.flip();
outChannel.write(byteBuffer);
//清空重置
byteBuffer.clear();
}
fileOutputStream.close();
fileInputStream.close();
}
}
public class NIOFileChannel04 {
public static void main(String[] args) throws IOException {
FileInputStream fileInputStream = new FileInputStream("hello.txt");
FileOutputStream fileOutputStream = new FileOutputStream("world.txt");
FileChannel inChannel = fileInputStream.getChannel();
FileChannel outChannel = fileOutputStream.getChannel();
//从哪拷贝,从几开始到几结束 对应的还有transferTo()方法.
outChannel.transferFrom(inChannel, 0, inChannel.size());
outChannel.close();
inChannel.close();
fileOutputStream.close();
fileInputStream.close();
}
}
Scattering(分散)
:在向缓冲区写入数据时,可以使用 Buffer 数组依次写入,一个 Buffer 数组写满后,继续写入下一个 Buffer 数组。Gathering(聚集)
:从缓冲区读取数据时,可以依次读取,读完一个 Buffer 再按顺序读取下一个。Netty 的 I/O 线程 NioEventLoop 聚合了 Selector(选择器 / 多路复用器),可以并发处理成百上千个客户端连接。
当线程从某客户端 Socket 通道进行读写时,若没有数据可用,该线程可以进行其他任务。
线程通常将非阻塞 I/O 的空闲时间用于其他通道上执行 I/O 操作,所以单独的线程可以管理多个输入输出通道。
由于读写操作都是非阻塞的,就可以充分提高 I/O 线程的运行效率,避免由于频繁 I/O 阻塞导致的线程挂起。
一个 I/O 线程可以并发处理 N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞 I/O 一连接一线程模型,架构性能、弹性伸缩能力和可靠性都得到极大地提升。
public abstract class Selector implement Closeable{
public static Selector open(); //得到一个选择器对象
public int select(long timeout); //监控所有注册的通道,当其中的IO操作可以进行时,将对应的selectionkey加入内部集合并返回,参数设置超时时间
public Set<SelectionKey> selectionKeys(); //从内部集合中得到所有的SelectionKey
}
selector.select()
://若未监听到注册管道中有事件,则持续阻塞selector.select(1000)
://阻塞 1000 毫秒,1000 毫秒后返回selector.wakeup()
://唤醒 selectorselector.selectNow()
: //不阻塞,立即返回SelectionKey 中定义了四个操作标志位:
OP_READ
表示通道中发生读事件;OP_WRITE
—表示通道中发生写事件;OP_CONNECT
—表示建立连接;OP_ACCEPT
—请求新连接。
public class Server {
public static void main(String[] args) throws IOException {
//创建serverSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//绑定端口
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
//得到Selector对象
try (Selector selector = Selector.open()) {
//把ServerSocketChannel注册到selector,事件为OP_ACCEPT
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//如果返回的>0,表示已经获取到关注的事件
while (selector.select() > 0) {
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
//获得到一个事件
SelectionKey next = iterator.next();
//如果是OP_ACCEPT,表示有新的客户端连接
if (next.isAcceptable()) {
//给该客户端生成一个SocketChannel
SocketChannel accept = serverSocketChannel.accept();
accept.configureBlocking(false);
//将当前的socketChannel注册到selector,关注事件为读事件,同时给socket Channel关联一个buffer
accept.register(selector, SelectionKey.OP_READ,ByteBuffer.allocate(1024));
System.out.println("获取到一个客户端连接");
//如果是读事件
} else if (next.isReadable()) {
//通过key 反向获取到对应的channel
SocketChannel channel = (SocketChannel) next.channel();
//获取到该channel关联的buffer
ByteBuffer buffer = (ByteBuffer) next.attachment();
while (channel.read(buffer) != -1) {
buffer.flip();
System.out.println(new String(buffer.array(), 0, buffer.limit()));
buffer.clear();
}
}
iterator.remove();
}
}
}
}
}
public class Client {
public static void main(String[] args) throws IOException {
//得到一个网络通道
SocketChannel socketChannel = SocketChannel.open();
//设置为非阻塞
socketChannel.configureBlocking(false);
//提供服务器端的IP和端口
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
//连接服务器
if (!socketChannel.connect(inetSocketAddress)) {
while (!socketChannel.finishConnect()) {
System.out.println("连接需要时间,客户端不会阻塞...先去吃个宵夜");
}
}
//连接成功,发送数据
String str = "hello,Java菜鸟程序员";
ByteBuffer byteBuffer = ByteBuffer.wrap(str.getBytes());
socketChannel.write(byteBuffer);
socketChannel.close();
System.out.println("客户端退出");
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CCQo3ZO7-1656935549538)(https://segmentfault.com/img/remote/1460000037714812)]
方法 | 描述 |
---|---|
public abstract Selector selector(); | 得到与之关联的 Selector 对象 |
public abstract SelectableChannel channel(); | 得到与之关联的通道 |
public final Object attachment() | 得到与之关联的共享数据 |
public abstract SelectionKey interestOps(int ops); | 设置或改变监听的事件类型 |
public final boolean isReadable(); | 通道是否可读 |
public final boolean isWritable(); | 通道是否可写 |
public final boolean isAcceptable(); | 是否可以建立连接 ACCEPT |
public class GroupChatClient {
private static final String HOST = "127.0.0.1";
private static final int PORT = 6667;
private Selector selector;
private SocketChannel socketChannel;
private String username;
public GroupChatClient() {
try {
selector = Selector.open();
//连接服务器
socketChannel = SocketChannel.open(new InetSocketAddress(HOST, PORT));
//设置非阻塞
socketChannel.configureBlocking(false);
//注册
socketChannel.register(selector, SelectionKey.OP_READ);
username = socketChannel.getLocalAddress().toString().substring(1);
System.out.println("客户端: " + username + ",准备就绪...");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 向服务器发送数据
*
* @param info
*/
public void sendInfo(String info) {
info = username + "说: " + info;
try {
socketChannel.write(ByteBuffer.wrap(info.getBytes()));
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 读取服务端回复的消息
*/
public void readInfo() {
try {
//有可用通道
if (selector.select() > 0) {
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isReadable()) {
//得到相关的通道
SocketChannel sc = (SocketChannel) key.channel();
//得到一个buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读取
sc.read(buffer);
//把读取到的缓冲区数据转成字符串
String msg = new String(buffer.array());
System.out.println(msg.trim());
}
iterator.remove(); //删除当前的selectionKey,防止重复操作
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
//启动客户端
GroupChatClient chatClient = new GroupChatClient();
//启动一个线程,每隔3秒,读取从服务器端发送的数据
new Thread(() -> {
while (true) {
chatClient.readInfo();
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
//发送数据给服务器
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
chatClient.sendInfo(scanner.nextLine());
}
}
}
public class GroupChatServer {
//定义属性
private Selector selector;
private ServerSocketChannel listenChannel;
private static final int PORT = 6667;
public GroupChatServer() {
try {
//获得选择器
selector = Selector.open();
//listenChannel
listenChannel = ServerSocketChannel.open();
//绑定端口
listenChannel.socket().bind(new InetSocketAddress(PORT));
//设置非阻塞模式
listenChannel.configureBlocking(false);
//将该listenChannel注册到Selector
listenChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
//创建一个服务器对象
GroupChatServer groupChatServer = new GroupChatServer();
//监听
groupChatServer.listen();
}
/**
* 监听
*/
public void listen() {
try {
//如果返回的>0,表示已经获取到关注的事件
while (selector.select() > 0) {
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
//判断是否有事件
while (iterator.hasNext()) {
//获得事件
SelectionKey key = iterator.next();
//如果是OP_ACCEPT,表示有新的客户端连接
if (key.isAcceptable()) {
SocketChannel socketChannel = listenChannel.accept();
//设置为非阻塞
socketChannel.configureBlocking(false);
//注册到Selector
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("获取到一个客户端连接 : " + socketChannel.getRemoteAddress() + " 上线!");
} else if (key.isReadable()) {
//如果是读事件,就读取数据
readData(key);
}
iterator.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
}
}
/**
* 读取客户端消息
*/
private void readData(SelectionKey key) {
SocketChannel channel = null;
try {
//得到channel
channel = (SocketChannel) key.channel();
//创建buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
if (channel.read(buffer) != -1) {
String msg = new String(buffer.array());
System.out.println(msg);
// 转发消息给其它客户端(排除自己)
sendInfoOtherClients(msg, channel);
}
} catch (Exception e) {
try {
System.out.println(channel.getRemoteAddress() + " 下线了!");
// 关闭通道
key.cancel();
channel.close();
} catch (IOException ioException) {
ioException.printStackTrace();
}
} finally {
}
}
/**
* 转发消息给其它客户端(排除自己)
*/
private void sendInfoOtherClients(String msg, SocketChannel self) throws IOException {
//服务器转发消息
System.out.println("服务器转发消息中...");
//遍历所有注册到selector的socketChannel并排除自身
for (SelectionKey key : selector.keys()) {
//反向获取通道
Channel targetChannel = key.channel();
//排除自身
if (targetChannel instanceof SocketChannel && targetChannel != self) {
//转型
SocketChannel dest = (SocketChannel) targetChannel;
//将msg存储到buffer中
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
//将buffer中的数据写入通道
dest.write(buffer);
}
}
}
}
JDK 7 引入了 Asynchronous I/O,即 AIO。在进行 I/O 编程中,通常用到两种模式:Reactor 和 Proactor 。Java 的 NIO 就是 Reactor,当有事件触发时,服务器端得到通知,进行相应的处理。
AIO 叫做异步非阻塞
的 I/O,引入了异步通道的概念,采用了 Proactor 模式,简化了程序编写,有效的请求才会启动线程,特点就是先由操作系统完成后才通知服务端程序启动线程去处理,一般用于连接数较多且连接时长较长的应用。
Reactor 与 Proactor
由于 AIO 目前应用并不广泛,所以本文只是讲述 AIO 基本介绍。