一、前言
本文是学习 《Scalable IO in Java》这一文章后的一些笔记,同时也是为了更好的学习Netty做一些铺垫,《Scalable IO in Java》原文地址:https://gee.cs.oswego.edu/dl/cpjslides/nio.pdf
二、网络服务
在Web服务中,通常有着相同的基本结构分为以下几个步骤
- Read Request(读取请求)
- Decode Request(请求解码/解析)
- Process Service(处理服务)
- Encode Reply(对响应编码)
- Send Reply(发送响应)
1、传统Service设计
(对应到Java中也就是我们经常说的BIO,Block IO)
从图中可以看出每个Client在服务端都会有一个Handler与之对应(每个Handler对应一个线程)。这种模式有着很明显的优缺点:
优点:理解起来非常简单,代码实现起来也比较简单。在一些用户量不大的情况下还是可以使用的。
缺点:
- 由于服务器内一个线程对应一个连接,当连接数比较大的时候会消耗大量服务器资源;且服务器端线程数过多导致上下文切换频繁对CPU也不友好。
- 由于是阻塞模型,客户端和服务端建联连接后哪怕什么都不做,服务端也一直维护这该线程所以导致资源浪费。
总结一下:该模型的优点简单好理解;缺点并发量不大,浪费资源。
代码演示
public class BIOTest {
public static void main(String[] args) throws IOException {
System.out.println("socket tcp服务器端启动....");
ServerSocket serverSocket = new ServerSocket(8080);
// 等待客户端请求
try {
while (true) {
System.out.println("服务器等待新连接。。。。。。。。。。");
Socket accept = serverSocket.accept();
System.out.println("服务器有了新连接。。。。。。。。。。");
new Thread(() -> {
try {
InputStream inputStream = accept.getInputStream();
// 转换成string类型
byte[] buf = new byte[1024];
System.out.println("线程开始read。。。。。。。。。。");
int len = inputStream.read(buf);
System.out.println("线程结束read。。。。。。。。。。");
String str = new String(buf, 0, len);
System.out.println("服务器接受客户端内容:" + str);
} catch (Exception e) {
}
}).start();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
serverSocket.close();
}
}
}
class TcpClient {
public static void main(String[] args) throws IOException {
System.out.println("socket tcp 客户端启动....");
Socket socket = new Socket("127.0.0.1", 8080);
System.out.println("客户端线程getOutputStream。。。。。。。。。。");
OutputStream outputStream = socket.getOutputStream();
System.out.println("客户端线程write。。。。。。。。。。");
outputStream.write("我是客户端".getBytes());
socket.close();
}
}
说明:该demo出自 https://blog.youkuaiyun.com/xyjy11/article/details/115116265 博客!
代码本身也是比较简单的,结合这上面的图可以很好的理解,就是一个连接过来服务器对应开启一个新的线程去处理,这里不多赘述。
2、Reactor模型
基于传统IO模型的缺点我们应该怎么解决呢?首先是一个线程对应一个客户端连接这明显是很浪费的,那能不能用一个线程去管理多个客户端连接呢?**客户端如果阻塞住服务端是否可以做别的事情呢?**带着这两个疑问我们继续往下。
在讲述Reactor模型之前,我们可以先了解一下Java中一个相对冷门的技术ATW(界面编程),在AWT中会有按钮和点击事件
这是一个很典型的基于“事件驱动”模型,点击按钮的时候事件监听器会从事件队列中查询到当前“按钮”所绑定的事件,进入执行对应的操作。
从这个例子我们可以引申到I/O模型里,我们多个连接是否也可以像“事件”一样注册到某个地方,然后等待一个“时机”通过监听器来触发某些事件呢?这里就引出了Reactor模型
基础的Reactor模型(单线程版本)
Reactor:通过分派适当的处理程序来响应IO事件
Handler:(黄色圆圈,一行表示一个handler):处理器,用于处理业务
Acceptor:负责统一管理连接。
流程说明:当客户端想要和服务端进行连接时,Reactor会根据请求类型来分发请求;如果是链接请求则会交给acceptor去处理,acceptor会创建一个Handler并且绑定当前连接。如果不是连接请求则会交由特定的Handler处理。
代码演示
Java在JDK1.4之后也支持了NIO(Non-blocking IO:非阻塞IO),这里有几个概念需要提前说明:
Channels:管道,支持非阻塞读取的文件、套接字等的连接。一个客户端连接对应一个Channel
Buffers:缓冲区,NIO是面向缓冲区的,因为客户端和服务端的交互是通过Channel而Channel和客户端之间数据的传输则是通过Buffer缓冲区,所以说NIO是面向缓冲区的。
Selectors :选择器,告诉一组通道中的哪些具有IO事件。
SelectionKeys :维护IO事件状态和绑定
这里借用一张图:
原图出自:https://www.cnblogs.com/mikechenshare/p/16587635.html
案例代码来源于《Scalable IO in Java》可做参考
class Reactor implements Runnable {
final Selector selector;
final ServerSocketChannel serverSocket;
Reactor(int port) throws IOException { //Reactor初始化
selector = Selector.open();
serverSocket = ServerSocketChannel.open();
serverSocket.socket().bind(new InetSocketAddress(port));
serverSocket.configureBlocking(false); //非阻塞
SelectionKey sk = serverSocket.register(selector, SelectionKey.OP_ACCEPT); //分步处理,第一步,接收accept事件
sk.attach(new Acceptor()); //attach callback object, Acceptor
}
public void run() {
try {
while (!Thread.interrupted()) {
selector.select();
Set<SelectionKey> selected = selector.selectedKeys();
for (SelectionKey selectionKey : selected) {
//Reactor负责dispatch收到的事件
dispatch(selectionKey);
}
selected.clear();
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
/**
* 分发请求
*
* @param k
*/
void dispatch(SelectionKey k) {
Runnable r = (Runnable) (k.attachment()); //调用之前注册的callback对象
if (r != null)
r.run();
}
/**
* Acceptor组件
*/
class Acceptor implements Runnable { // inner
public void run() {
try {
SocketChannel c = serverSocket.accept();
if (c != null)
new Handler(selector, c);
} catch (IOException ex) { /* ... */ }
}
}
}
final class Handler implements Runnable {
final SocketChannel socket;
final SelectionKey sk;
ByteBuffer input = ByteBuffer.allocate(1024);
ByteBuffer output = ByteBuffer.allocate(1024);
static final int READING = 0, SENDING = 1;
int state = READING;
Handler(Selector sel, SocketChannel c) throws IOException {
socket = c;
c.configureBlocking(false);
// Optionally try first read now
sk = socket.register(sel, 0);
sk.attach(this); //将Handler作为callback对象
sk.interestOps(SelectionKey.OP_READ); //第二步,接收Read事件
sel.wakeup();
}
boolean inputIsComplete() {
return true;
}
boolean outputIsComplete() {
return true;
}
void process() { /* ... */ }
public void run() {
try {
if (state == READING) read();
else if (state == SENDING) send();
} catch (IOException ex) { /* ... */ }
}
void read() throws IOException {
socket.read(input);
if (inputIsComplete()) {
process();
state = SENDING;
// Normally also do first write now
sk.interestOps(SelectionKey.OP_WRITE); //第三步,接收write事件
}
}
void send() throws IOException {
socket.write(output);
if (outputIsComplete()) sk.cancel(); //write完就结束了, 关闭select key
}
}
3、多线程版本实现Reactor
如果你能明白上面的所写的单线程版本的Reactor,那么多线程版本其实也不难。在学习多线程版本之前,我们需要知道一个概念,计算机是一门“工程类学科”,我们讲究的是模块化的开发各自负责各自的模块。再回到多线程版本的Reactor模型中来,单线程的版本有什么问题呢?Reactor本身及负责处理连接又负责处理请求,那我们是不是有更好的方案来解决这一问题呢?答案是有的,思想也很简单我们添加一个 工作线程池来处理具体的业务请求,Reactor本身只负责管理分发请求。
用一个通俗的例子来说,我们去饭店吃饭,前台的服务员只负责将我们带进饭店,到了饭店内部会有其他工作人员为我们服务。模型如下
可以看到具体的业务逻辑放到了一个新的线程池去处理,通常我们会称之为WorkerThread(工人线程)
一个完整的多线程模型如下图:
主Reactor只负责接收连接,而真正干活的则是SubReactor。
三、结束语
在学习了《Scalable IO in Java》之后,我们对NIO,Reactor模型有了一些新的认识,当然我们实际开发过程中很少会使用JDK原生的NIO API,因为比较晦涩难懂,我们更多的是使用Netty来做。今天这些知识点也是为后续Netty学习系列文章做铺垫,希望对你有所帮助。