网络IO编程的演进
演进:BIO--->NIO--->IO多路复用
-
BIO编程
阻塞式IO
- 阶段一:一个线程处理所有网络连接,缺陷:若线程在处理某个连接时阻塞了,则其他连接就得不到处理,服务器吞吐量极低。
- 阶段二:一个线程处理一个网络连接(经典的Connection Per Thread) ,相比于【阶段一】极大地提高了系统吞吐量,但也有缺陷:海量连接就要海量线程,非常吃系统资源,系统可能承受不住,另外线程的创建、销毁和上下文切换开销也很大,所以这种模式也很有局限性。
-
NIO编程
非阻塞式IO
一个线程可以很好地处理多个网络连接(若当前连接可读/可写,就进行读/写(读/写完处理下一个连接),不可读/不可写也不会阻塞,可以继续处理下一个连接),这能让系统用少量线程处理大量连接,解决了BIO需要海量线程的问题,大大提高了服务器吞吐量,但也有局限性:每个线程每次read/write系统调用只能判断一个连接是否可读/可写,会有大量的系统调用,也有很大开销,一定程度限制了服务器的性能和吞吐量。
-
IO多路复用
一次系统调用可以判断很多连接是否可读/可写
一个线程可以很好地处理多个网络连接,且一次系统调用能处理多个连接,大大减少了系统调用次数,减少了很大一部分开销,解决了NIO的局限性,极大地提高了服务器性能和吞吐量。
高性能网络编程之Reactor模式
两个角色:
- Reactor反应器:负责监听IO事件,并分发给Handler处理。(监听+分发)
- Handler处理器:负责处理IO事件,承担主要业务逻辑处理。(处理业务)
可以看到,Reactor模式的思想很简单:事件驱动。
优缺点:
优点:
- 少量线程就能处理海量连接
- 性能好,服务器吞吐量大
- 事件驱动,扩展性高
缺点:
- 复杂
- 需要操作系统底层支持IO多路复用。(如今大多数操作系统已经支持了)
Reactor模式3种实现方式
单Reactor单线程
Reactor和Handler处理都是在一个线程中,即一个线程既负责监听所有连接的IO事件,又负责处理所有连接的IO事件。
缺陷:
- 单线程,不能充分利用多核优势,性能差:当某个连接的Handler处理逻辑重、处理慢时,就会阻塞其他连接的处理和接受新连接,从而影响服务器整体性能和吞吐量。
单Reactor单线程瓶颈在【单线程】上。
单Reactor多线程
仍然只有一个Reactor线程监听和分发IO事件,但使用多线程执行Handler处理。
仍然有局限性:
一个Reactor线程负责监听所有连接的IO事件,在高并发场景下,就会有很多事件需要响应,可能会存在性能瓶颈:一个Reactor线程来不及响应IO事件,那就会影响连接的处理速度,且会影响新连接的接入速度。
单Reactor多线程瓶颈在【单Reactor】上。
多Reactor多线程
mainReactor(主线程) :负责连接事件。(通常只需要1个,因为通常只需要监听一个服务端套接字)
subReactor(子线程) :负责读写事件。(通常会有多个,因为一般会有大量连接)
即主从Reactor
优点:解决了单Reactor的局限性,有多个Reactor负责响应IO事件,能很好的应对高并发场景,吞吐量更大。
一个例子理解为什么会有3种实现
餐厅:网络服务器
前台:Reactor
服务员:Handler
单Reactor单线程:
适合场景:吞吐量不大时
相当于,前台和服务员是同一个人(同一个线程) ,当一下来很多客人时,会来不及接待客人(接收新连接) ,也会来不及提供服务(即handler执行业务处理) 。
单Reactor多线程:
适合场景:吞吐量比较大时
前台是一个人,服务员也有多个人
多Reactor多线程:
适合场景:吞吐量很大时
前台有多个人,服务员也有多个人
总之,单Reactor单线程--->单Reactor多线程--->多Reactor多线程演进的核心思想就是【人多力量大】,各自有不同的应用场景。
单Reactor单线程实现demo
实现EchoServer回显服务器,接收客户端发送的数据,并返回给客户端。
思考:
-
Reactor监听到IO事件后怎么知道分发给哪个handler?
感觉Reactor不应该理解分发给哪个handler,Reactor本身只是单纯的监听和分发,但具体分发给哪个handler其实是业务逻辑,不应该由Reactor负责,Reactor应该约定一个协议,比如,Reactor规定注册到我这的Channel必须实现什么协议,我按照这个协议去分发IO事件到handler,但我不理解具体的业务逻辑,我只遵守这个协议。
这就是面向接口编程的好处(接口就是协议):高内聚低耦合。
server端程序
@Slf4j
public class EchoServer {
ServerSocketChannel serverSocketChannel;
Selector selector;// 作为Reactor监听IO事件
public EchoServer(SocketAddress socketAddress) throws IOException {
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(socketAddress);
selector = Selector.open();
SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 绑定Handler
selectionKey.attach(new AcceptHandler());
}
public void run() throws IOException {
log.info("EchoServer run...");
while (true) {
int select = selector.select();
if (select <= 0) {
continue;
}
log.info("EchoServer select:{}", select);
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
// 重点:协议约定每个selectionKey的attachment都是Handler
((Handler) selectionKey.attachment()).Dispatch(selectionKey);
}
}
}
public static void main(String[] args) throws IOException {
EchoServer echoServer = new EchoServer(Const.socketAddress);
echoServer.run();
}
}
interface Handler {
// 分发并处理SelectionKey,由子Handler具体实现
void Dispatch(SelectionKey selectionKey) throws IOException;
}
@Slf4j
class AcceptHandler implements Handler {
@Override
public void Dispatch(SelectionKey selectionKey) throws IOException {
if (selectionKey.isAcceptable()) {
SocketChannel socketChannel = ((ServerSocketChannel) selectionKey.channel()).accept();
if (socketChannel == null) {
return;
}
log.info("accept success {}", socketChannel.getRemoteAddress());
socketChannel.configureBlocking(false);
SelectionKey newKey = socketChannel.register(selectionKey.selector(), SelectionKey.OP_READ);
// 绑定Handler
newKey.attach(new IOHandler());
}
}
}
@Slf4j
class IOHandler implements Handler {
ByteBuffer readBuffer;
public IOHandler() {
}
@Override
public void Dispatch(SelectionKey selectionKey) throws IOException {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
// 大致逻辑:先读取客户端发送的数据,再回写给客户端
if (selectionKey.isReadable()) {
// 读取客户端发送的数据 TODO 怎么知道客户端发完一份完整数据呢?(涉及粘包和半包问题)
// 这里假设一次读取就收到了客户端一份完整的数据(很简单的处理)
readBuffer = ByteBuffer.allocate(1024);
int read = socketChannel.read(readBuffer);
if (read == -1) {
log.info("close socketChannel [{}]", socketChannel.getRemoteAddress());
socketChannel.close();
return;
}
if (read == 0) {
log.info("read nothing");
return;
}
byte[] bufferArray = readBuffer.array();
byte[] data = new byte[read];
System.arraycopy(bufferArray, 0, data, 0, read);
log.info("read data from client[{}] read:{}, msg:{}", socketChannel.getRemoteAddress(), read, new String(data, StandardCharsets.UTF_8));
readBuffer.flip();
// 读完之后需要回写
selectionKey.interestOps(SelectionKey.OP_WRITE);
}
if (selectionKey.isWritable()) {
// 如果没写完要继续写
if (readBuffer.hasRemaining()) {
int write = socketChannel.write(readBuffer);
log.info("send client data:{}", write);
} else {
log.info("echo end to client[{}]", socketChannel.getRemoteAddress());
readBuffer = null;
// 写完了再读 TODO 能否同时读写呢?
selectionKey.interestOps(SelectionKey.OP_READ);
}
}
}
}
client端程序
@Slf4j
public class EchoClient {
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(Const.socketAddress);
Selector selector = Selector.open();
socketChannel.register(selector, SelectionKey.OP_CONNECT);
while (true) {
int select = selector.select();
if (select <= 0) {
continue;
}
log.info("EchoClient select:{}", select);
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeys.iterator();
while (iterator.hasNext()) {
SelectionKey curKey = iterator.next();
iterator.remove();
SocketChannel curChannel = (SocketChannel) curKey.channel();
if (curKey.isConnectable()) {
log.info("connect success {}", curChannel.finishConnect());
// 连接完则写数据
curKey.interestOps(SelectionKey.OP_WRITE);
}
if (curKey.isWritable()) {
String identify = curChannel.getLocalAddress().toString();
String msg = "hello, this is " + identify;
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes(StandardCharsets.UTF_8));
// 简单处理,不考虑缓冲区满的问题,认为能一次写入
int write = curChannel.write(buffer);
log.info("client send:{}", write);
// 写完后再读来自服务端响应的数据
curKey.interestOps(SelectionKey.OP_READ);
}
if (curKey.isReadable()) {
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 认为能一次读取完
int read = curChannel.read(byteBuffer);
if (read == -1) {
log.info("read close");
continue;
}
if (read == 0) {
log.info("read nothing");
continue;
}
byte[] bufferArray = byteBuffer.array();
byte[] data = new byte[read];
System.arraycopy(bufferArray, 0, data, 0, read);
log.info("receive data from server: {}", new String(data));
// 读完服务端响应直接close
curChannel.close();
}
}
}
}
}
多Reactor多线程实现demo
有时间再实现
Java NIO编程很底层,比较难
从做demo的过程中可以看出来,想要使用Java NIO去开发网络服务器其实是挺困难的,需要处理很多细节问题(因为Java NIO还是比较底层的),问题有:
-
需要自己解决粘包半包问题、定义消息格式、消息编码和解码
-
需要自己实现Reactor模式
-
......
感悟
从简单到复杂这中间是非常多的细节,将细节隐藏就简单,将细节暴露就复杂,这也是抽象和具体的关系。