模型对比
bio 线程模型
伪异步 bio 线程模型,相对上面,只是每次有连接进来,交由线程池处理,减少了线程创建的开销。但 io 本质上还是阻塞的,所以把能够处理的请求有限。
nio 是基于 selector 或者 epoll 这样的系统调用函数,一个函数管理很多连接,每当有读写、建立连接事件,才执行。
BIO NIO代码实现
BIO
java 1.4 之前 只有 bio, api 基于 stream,byte,socket,serverSocket;
bio Socket Server demo:
public class BioTest {
private static final Executor executor = Executors.newFixedThreadPool(100);
public static void main(String[] args) {
startServer();
}
static void startServer() {
try {
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress(8080));
while (true) {
// 监听 op_accept
Socket socket = serverSocket.accept();
executor.execute(new IoHandler(socket));
}
} catch (Exception ex) {
//ignore
}
}
static class IoHandler implements Runnable {
IoHandler(Socket socket) {
this.socket = socket;
}
private Socket socket;
@Override
public void run() {
while (!socket.isClosed()) {
try {
// 阻塞 read
byte[] buffer = new byte[1024];
InputStream ips = socket.getInputStream();
int length;
while ((length = ips.read(buffer)) != -1) {
System.out.println(new String(buffer, 0, length));
}
// 阻塞 write
socket.getOutputStream().write("hello server".getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
因为 socket.read(); socket.write()方法是同步阻塞的。所以 一个连接在服务端必须对应一个 thread 去处理;
bio 模型存在的问题:
- 该模型严重依赖与线程,java 线程创建、销毁会耗费大量的系统资源;
- 线程本身占用大量的系统资源,java的线程栈,一般至少分配512K~1M的空间,如果系统中的线程数过千,恐怕整个JVM的内存都会被吃掉一半。
- 每个线程中的 socket 的读写是阻塞的,意味着频繁的线程切换。操作系统发生线程切换的时候,需要保留线程的上下文,然后执行系统调用。如果线程数过高,可能执行线程切换的时间甚至会大于线程执行的时间,这时候带来的表现往往是系统load偏高、CPU sy使用率特别高(超过20%以上),导致系统几乎陷入不可用的状态;
- 可伸缩性差。上面的线程模型 单机连接 < 1000 的情况下 ok 的。 面对十万甚至百万级连接的时候,上面的BIO模型是无能为力的;
NIO
jdk 1.4 提供了一套新的api ,使 nio 编程成为可能。bio api基于 Stream,nio 基于 Bytebuffer;数据写入 bytebuffer ,基于 channel 进行传输;
byteBuffer 指针;
public abstract class Buffer {
//标记读取or写入位置
private int mark = -1;
//已读已写的位置
private int position = 0;
//最大极限
private int limit;
//容器容量
private int capacity;
//重设位置
public final Buffer position(int newPosition) {
if ((newPosition > limit) || (newPosition < 0))
throw new IllegalArgumentException();
position = newPosition;
//标记位超过新位置,重置为-1
if (mark > position) mark = -1;
return this;
}
//与position(int)方法同理
public final Buffer limit(int newLimit) {
if ((newLimit > capacity) || (newLimit < 0))
throw new IllegalArgumentException();
limit = newLimit;
//如果位置超出新限制,则重合pos和limit
if (position > limit) position = limit;
if (mark > limit) mark = -1;
return this;
}
}
byteBuffer 的分配可以在堆,也可以在直接内存,对比:
| | <br /><br />DirectByteBuffer<br />DirectByteBufferdirect
| <br /><br />HeapByteBuffer
|
| :—-: | :—-: | :—-: | |
创建开销
|
大
| <br /><br /><br />小
|
|
存储位置
|
Native heap
|
Java heap
|
|
数据拷贝
|
无需临时缓冲区做拷贝
| <br /><br /><br />拷贝到临时
DirectByteBuffer,但临 时缓冲区使用缓存。 聚集写/发散读时 没有缓存临时缓冲区。
|
|
GC影响
| <br /><br /><br />每次创建或者释放的时候
都调用一次System.gc()
|
|
nio socket server demo:
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress inetSocketAddress = new InetSocketAddress("localhost", 1111);
serverSocketChannel.bind(inetSocketAddress);
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT, null);
while (true) {
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey myKey = iterator.next();
if (myKey.isAcceptable()) {
// 连接建立(可以单独线程做)
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (myKey.isReadable()) {
// 处理read 事件,数据从 kernel->拷贝到用户空间
// 单纯的io 读写,可以在单独的一个线程池里处理
SocketChannel socketChannel = (SocketChannel) myKey.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(256);
socketChannel.read(byteBuffer);
// 业务处理,可以在单独的线程池处理
doBusiness(byteBuffer);
}
iterator.remove();
}
}
}
基于 selector ,一个selector 可以管理 n 个socket连接。select 是事件驱动的,理论上一个线程可以完成所有io操作(accept,read,write)。相对于 bio ,节省了线程数量。 java 可以做的根据不同业务需求实现不同的线程模型。
select 的存在相当于 一个同步器,对感兴趣的事件进行下发,是 reactor 事件驱动模型的一种实现;
The reactor design pattern) is an event handling pattern for handling service requests delivered concurrently) to a service handler by one or more inputs. The service handler then demultiplexes the incoming requests and dispatches them synchronously to the associated request handlers.[1]
Boss thread + worker thread
Boss 处理 OP_ACCEPT、OP_CONNECT,处理连接接入
Worker 处理 OP_READ、OP_WRITE,处理IO读写
Reactor 线程数目 Netty 1+2*cpu
NIO 带来了什么?
事件驱动模型
避免多线程
单线程处理多任务
非阻塞IO,IO读写不再阻塞,而是返回0
基于block的传输,通常比基于流的传输更高效
更高级的IO函数,zero-copy
IO多路复用大大提高了java网络应用的可伸缩性和实用性
nio,bio 底层调用不同的 linux 系统函数;
nio 基于 selector 函数