IO模型基础

IO模型就是用怎样的通道进行数据的发送和接收,Java共支持BIONIOAIO3种网络编程IO模型。

BIO

BIO同步阻塞模型,一个客户端连接对应一个处理线程ServerSocketaccept方法是一个阻塞方法,若没有客户端链接将被阻塞,且服务端在接收客户端数据从输入流中读取数据时若没有数据可读也会被阻塞。同样客户端在通过输入流接收服务端回传的数据没有数据时也会被阻塞,若连接不做数据读写操作会导致线程阻塞浪费资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 服务端
ServerSocket serverSocket = new ServerSocket(9000);
final Socket clientSocket = serverSocket.accept(); // 阻塞方法,没有连接时被阻塞
byte[] bytes = new byte[1024];
int read = socket.getInputStream().read(bytes); // 接收客户端的数据,没有数据可读时阻塞
if (read != -1) {
System.out.println("接收到客户端数据:" + new String(bytes, 0, read));
}
socket.getOutputStream().write("HelloClient".getBytes()); // 向客户端发送数据
socket.getOutputStream().flush();
socket.close(); // 关闭客户端
// 客户端
Socket socket = new Socket("localhost", 9000);
socket.getOutputStream().write("HelloServer".getBytes()); //向服务端发送数据
socket.getOutputStream().flush();
byte[] bytes = new byte[1024];
socket.getInputStream().read(bytes); // 接收服务端回传的数据,没有数据时阻塞
System.out.println("接收到服务端的数据:" + new String(bytes));
socket.close();

若想同一个连接可不断的收发数据,但该方式只能处理一个请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 服务端
ServerSocket serverSocket = new ServerSocket(9000);
final Socket clientSocket = serverSocket.accept(); // 阻塞方法,没有连接时被阻塞
while (true) {
byte[] bytes = new byte[1024];
int read = clientSocket.getInputStream().read(bytes); // 接收客户端的数据,没有数据可读时阻塞
if (read != -1) {
System.out.println("接收到客户端数据:" + new String(bytes, 0, read));
} else {
clientSocket.close();
}
}
// 客户端
Socket socket = new Socket("localhost", 9000);
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String msg = scanner.nextLine();
socket.getOutputStream().write(msg.getBytes()); // 向服务端发送数据
socket.getOutputStream().flush();
}

为了处理多个请求,可将数据的处理改为异步的,但若请求非常多会导致线程数非常多,会导致服务器线程太多,压力太大,如C10K问题。BIO方式适用于连接数目比较小固定的架构, 该方式对服务器资源要求比较高, 但程序简单易理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 服务端
ServerSocket serverSocket = new ServerSocket(9000);
while (true) {
final Socket clientSocket = serverSocket.accept();
new Thread(new Runnable() {
public void run() {
try {
while (true) {
byte[] bytes = new byte[1024];
int read = clientSocket.getInputStream().read(bytes); // 接收客户端的数据,没有数据可读时阻塞
if (read != -1) {
System.out.println("接收到客户端数据:" + new String(bytes, 0, read));
} else {
clientSocket.close();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}

NIO

同步非阻塞,服务器实现模式为一个线程可处理多个请求连接,客户端发送的连接请求都会注册到多路复用器Selector上,多路复用器轮询到连接有IO请求就进行处理。NIO适用于连接数目多且连接比较短的轻操作架构, 比聊天服务器、弹幕系统、服务器间通讯,但编程比较复杂。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
static List<SocketChannel> channelList = new ArrayList<>(); // 保存客户端连接
public static void main(String[] args) throws IOException {
// 创建NIO ServerSocketChannel,与BIO的serverSocket类似
ServerSocketChannel serverSocket = ServerSocketChannel.open();
serverSocket.socket().bind(new InetSocketAddress(9000));
serverSocket.configureBlocking(false); // 设置ServerSocketChannel为非阻塞
while (true) {
// 非阻塞模式accept方法不会阻塞,NIO非阻塞是由操作系统内部实现的,底层调用了linux内核的accept函数
SocketChannel socketChannel = serverSocket.accept();
if (socketChannel != null) { // 如果有客户端进行连接
socketChannel.configureBlocking(false); // 设置SocketChannel为非阻塞
channelList.add(socketChannel); // 保存客户端连接在List中
}
Iterator<SocketChannel> iterator = channelList.iterator();
while (iterator.hasNext()) { // 遍历连接进行数据读取
SocketChannel sc = iterator.next();
ByteBuffer byteBuffer = ByteBuffer.allocate(128);
int len = sc.read(byteBuffer); // 非阻塞模式read方法不会阻塞,否则会阻塞
if (len > 0) { // 若有数据,把数据打印出来
System.out.println("接收到消息:" + new String(byteBuffer.array()));
} else if (len == -1) { // 若客户端断开,把socket从集合中去掉
iterator.remove();
}
}
}
}

连接数太多会有大量无效遍历,且由于NIO非阻塞外层循环会一直执行,若有10000个连接,其中只有1000个连接有写数据,但由于其他9000个连接并没有断开,每次还是轮询遍历一万次,其中有十分之九的遍历都是无效的。引入多路复用器,当没有事件处理时程序将阻塞在Selectorselect()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// 服务端
ServerSocketChannel serverSocket = ServerSocketChannel.open(); // 创建NIO ServerSocketChannel
serverSocket.socket().bind(new InetSocketAddress(9000));
serverSocket.configureBlocking(false); // 设置ServerSocketChannel为非阻塞
Selector selector = Selector.open(); // 打开Selector处理Channel,即创建epoll
// 把ServerSocketChannel注册到selector上,并且selector对客户端accept连接操作感兴趣
SelectionKey selectionKey = serverSocket.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select(); // 阻塞等待需要处理的事件发生
// 获取selector中注册的全部事件的SelectionKey实例
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) { // 遍历SelectionKey对事件进行处理
SelectionKey key = iterator.next();
iterator.remove(); //从事件集合里删除本次处理的key,防止下次select重复处理
if (key.isAcceptable()) { // 如果是OP_ACCEPT事件,则进行连接获取和事件注册
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel sc = ssc.accept(); // 处理完连接请求不会继续等待客户端的数据发送
sc.configureBlocking(false);
sc.register(key.selector(), SelectionKey.OP_READ); // 通过Selector监听Channel时对读事件感兴趣
} else if (key.isReadable()) {
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
// NIO非阻塞体现: 首先read方法不会阻塞,其次这种事件响应模型,当调用到read方法时肯定是发生了客户端发送数据的事件
int len = sc.read(buffer);
if (len != -1) { // 如果有数据,把数据打印出来
System.out.println("读取到客户端发送的数据:" + new String(buffer.array(), 0, len));
} else if (len == -1) { // 如果客户端断开连接,关闭Socket
socketChannel.close();
}
ByteBuffer bufferToWrite = ByteBuffer.wrap("helloClient".getBytes());
sc.write(bufferToWrite); // 向客户端发送数据
key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE); // 通过Selector监听Channel时对读写事件感兴趣
} else if (key.isWritable()){
SocketChannel sc = (SocketChannel) key.channel();
System.out.println("write事件");
// NIO事件触发是水平触发,使用Java的NIO编程时,在没有数据可以往外写的时候要取消写事件,在有数据往外写的时候再注册写事件
key.interestOps(SelectionKey.OP_READ);
}
}
}
// 客户端
SocketChannel clientChannel = SocketChannel.open(); // 获得一个Socket通道
clientChannel.configureBlocking(false); // 设置通道为非阻塞
Selector selector = Selector.open(); // 获得一个通道管理器
// 客户端连接服务器,需要在listen方法中调用channel.finishConnect()才能完成连接
clientChannel.connect(new InetSocketAddress("127.0.0.1", 9000));
//将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_CONNECT事件。
clientChannel.register(selector, SelectionKey.OP_CONNECT);
while (true) { // 轮询访问selector
selector.select();
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) { // 遍历SelectionKey对事件进行处理
SelectionKey key = (SelectionKey) it.next();
it.remove(); // 删除已选的key,以防重复处理
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(selector, SelectionKey.OP_READ); // 获得了可读的事件
} else if (key.isReadable()) {
// 和服务端的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));
}
}
}
}

NIOChannel通道Buffer缓冲区Selector多路复用器三大核心组件:

  • Channel类似于流,每个Channel对应一个Buffer缓冲区,Buffer底层是个数组
  • Channel会注册到Selector上,由Selector根据Channel读写事件的发生将其交由某个空闲的线程处理
  • NIO的BufferChannel都是既可读也可写

NIO底层在JDK1.4版本是用Linux内核函数select()poll()来实现,Selector每次都会轮询所有SockChannel看哪个Channel有事件,有的话就处理,没有就继续遍历,JDK1.5开始引入了Linux内核函数基于事件响应机制的epoll来优化NIO。

在Linux环境中调用Selectoropen()方法时调用EPollSelectorProvider.openSelector()方法创建EPollArrayWrapper对象,最终调用native方法epollCreate()方法从而调用Linux内核函数epoll_create创建epoll实例。然后调用SelectableChannelregister方法时,将SelectableChannel添加到EPollArrayWrapper内部集合中。

当调用Selector的select()方法时调用EPollSelectorImpldoSelect方法,从EPollArrayWrapper内部集合中poll数据,然后调用epollCtl方法最终调用native方法epollCtl从而调用Linux内核函数epoll_ctl,然后调用native方法epollWait方法最终调用Linux内核方法epoll_wait等待epoll实例上的事件,当Socket收到数据后,中断程序调用回调函数epoll实例的事件就绪列表rdlist中添加该Socket引用,这是由操作系统实现的,当程序执行到epoll_wait时,若rdlist已经引用了Socket则直接返回,若rdlist为空阻塞进程

NIO整个调用流程就是Java调用操作系统的内核函数来创建Socket,获取到Socket文件描述符,再创建一个Selector对象,对应操作系统的Epoll描述符,将获取到的Socket连接的文件描述符的事件绑定到Selector对应的Epoll文件描述符上,进行事件的异步通知,就实现了使用一个线程且不需要太多的无效的遍历,将事件处理交给了操作系统内核中断程序实现,大大提高了效率。

AIO

JDK7开始支持AIO异步非阻塞, 由操作系统完成后回调通知服务端程序启动线程去处理通知, 一般适用于连接数较多且连接时间较长的应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 服务端
ExecutorService executorService = Executors.newCachedThreadPool(); // 也可不设置线程池
//initialSize代表使用几个线程池处理
AsynchronousChannelGroup threadGroup = AsynchronousChannelGroup.withCachedThreadPool(executorService, 1);
final AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open(threadGroup).bind(new InetSocketAddress(9000));
serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
@Override
public void completed(AsynchronousSocketChannel socketChannel, Object attachment) {
try {
System.out.println("2--"+Thread.currentThread().getName());
// 再此接收客户端连接,如果不写这行代码后面的客户端连接连不上服务端
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) {
System.out.println("3--"+Thread.currentThread().getName());
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();
}
});
System.out.println("1--"+Thread.currentThread().getName());
Thread.sleep(Integer.MAX_VALUE);
// 客户端
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));
}