目录
- unix io模型——理解阻塞和同步
- java bio nio aio实战
- 基于reactor模式实现nio Server
- netty解析和实战,另一文章:手把手教你学习netty源码及原理
unix io模型——理解阻塞和同步
什么是阻塞什么是非阻塞,阻塞==同步?要理解两个概念的区别我们看unix 操作系统的io模型是怎么定义的。
操作系统的io操作需要经历两个阶段,
- io就绪,就是等待数据从网卡输入,os将数据读取内核空间
- 将数据从os的内核复制到调用io操作的用户进程空间
从上图可以看到,io操作分为五种模式,
- 用户进程发起io操作完全阻塞,等到操作系统把数据读完复制到用户进程才结束阻塞,
- 用户在发起io操作后,在等待io就绪即数据到来的过程中是直接返回的不阻塞,而是轮询是否有io就绪,这种方式就是非阻塞的,但是需要一直轮询会占用较多cpu资源。但是会阻塞在复制数据的过程
- io复用就是和第二种方式是一样的过程,但是优化了轮询的过程,这种方式的轮询不是每个io连接都去轮询,而是将连接注册到一个集合里面,类似集线器的东西,通过轮询这个集合降低cpu的资源消耗。
- 信号式驱动就是,不轮询io事件也不阻塞,而是在收到io事件时主动通知到内核去读取数据,然后将数据拷贝到用户空间
- 是我们说的异步模式,用户空间发起io请求时,不必阻塞在内核等待io事件和复制数据阶段,而是直接返回,等待内核完成这两步之后,通知用户进程,用户进程只需要拿到数据去处理就可以了,一般会使用回调来实现
小结
我们看前四种io方式都是要等待io数据到来或者是等待数据从内核复制到用户进程,这种需要等待内核完成io操作的方式称为同步io,同步io中以是否需要等待io数据到来区分阻塞和非阻塞模式。
最后一种不需要等待内核完成io操作的模式为完全的异步非阻塞模式。
java bio nio aio实战
理解了操作系统的io模式,我们以java为例,看java是怎么使用这几种模式来完成io操作的。
java bio oio
这种方式就是同步阻塞的实现,代码如下可以看到代码和简单,这也是bio的一个优势,实现简单,但是性能是很低的,作为server端如果有大量链接进来那么,每个read write都是阻塞的一个连接的请求处理完之前,下一个连接就必须等待。看下代码
public class BioServer {
public BioServer() throws IOException {
}
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress("localhost",6666));
Socket client;
while (null != (client = serverSocket.accept())) {
//使用线程池处理读写操作
byte[] recBuf = new byte[128];
byte[] sendBuf = new byte[128];
InputStream inputStream = client.getInputStream();
inputStream.read(recBuf);
System.out.println(new String(recBuf));
OutputStream outputStream = client.getOutputStream();
outputStream.write("hello client".getBytes());
}
}
}
//客户端
public class BioClient {
public static void main(String[] args) throws IOException {
Socket client = new Socket();
client.connect(new InetSocketAddress("localhost",6666));
OutputStream outputStream = client.getOutputStream();
outputStream.write("hello server".getBytes());
InputStream inputStream = client.getInputStream();
byte[] recvBuf = new byte[128];
inputStream.read(recvBuf);
System.out.println(new String(recvBuf));
}
}
一个优化方式是使用线程池去处理客户端读写操作,但是每个连接都要消耗一个线程,耗费很多内存,其实也不能支撑更大数量级的连接。
java nio nonblocking/new io
java nio是基于io复用模式的java实现,底层依赖在linux上是epoll的io复用机制。
java提供了一下几个核心的类
- Channel
- 是socket连接的抽象,一个channel代表一个连接
- FileChannel/SocketChannel/DatagramChannel
- Buffer
- io缓冲区
- 读写模式 flip()
- Selector
- io复用器,用于轮询io事件是否就绪
- 执行Select()操作
话不多说看下 java nio的是怎么使用的,使用在注释中写的很详细。
java nio使用分下面的步骤:
- 创建serverchannel
- bind server端口
- 配置serverchannel为非阻塞模式
- 创建selector,并将serverchannel注册到selector,监听SelectionKey.OP_ACCEPT事件即等待客户端连接
- selector在循环中select()轮询是否有serverchannel监听的io事件就绪
- 如果有io事件就绪,则进行处理
- 连接事件
- 可读事件
- 可写事件
public class NioNonBlockingServer {
public static void main(String[] args) throws IOException {
//1.初始化
ServerSocketChannel nonBlockingServer = ServerSocketChannel.open();
//2.bind
nonBlockingServer.bind(new InetSocketAddress("localhost", 7777));
//!!配置非阻塞
nonBlockingServer.configureBlocking(false);
//3.创建selector
Selector selector = Selector.open();
//4.注册事件监听
nonBlockingServer.register(selector, SelectionKey.OP_ACCEPT);
int selectNum = 0;
while (true) {
try {
//5.阻塞select,等待io事件就绪
selectNum = selector.select();
if (selectNum == 0) {
continue;
}
//6.io已就绪的channel 集合,遍历
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey next = iterator.next();
//!!!手动remove,否则会导致select一直返回0
iterator.remove();
//6.1连接事件
if (next.isAcceptable()) {
//6.1.1 客户端新连接,channel
SocketChannel clientChannel = nonBlockingServer.accept();
if (clientChannel != null) {
clientChannel.configureBlocking(false);
System.out.println("新连接:" + clientChannel);
//6.1.2 注册read write事件监听
clientChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
}
//6.2 读事件就绪
} else if (next.isReadable()) {
//6.2.1 receiveBuffer
ByteBuffer receiveBuffer = ByteBuffer.allocate(1024);
SocketChannel channel = (SocketChannel) next.channel();
channel.read(receiveBuffer);
//6.2.1 buffer切换读写模式
receiveBuffer.flip();
System.out.println(next.channel() + "客户端发来数据:" + new java.lang.String(receiveBuffer.array()));
next.interestOps(SelectionKey.OP_WRITE);
//6.3写事件就绪
} else if (next.isWritable()) {
ByteBuffer sendBuffer = ByteBuffer.allocate(1024);
sendBuffer.put("hello world from server".getBytes());
SocketChannel channel = (SocketChannel) next.channel();
sendBuffer.flip();
System.out.println("服务端发送返回:---》" + channel);
channel.write(sendBuffer);
next.interestOps(SelectionKey.OP_READ);
}
}
} catch (Exception e) {
e.printStackTrace();
continue;
}
}
}
}
//客户端实现
/**
* 阻塞式的io
*/
public class NioBlockingClient {
public static void main(String[] args) throws IOException, InterruptedException {
// sendAndReceiveClient();
for (int i=0;i<10;i++) {
//创建多个并发请求
new Thread(new Runnable() {
@Override
public void run() {
try {
sendAndReceiveClient();
} catch (IOException e) {
System.out.println(e);
}
}
}).start();
}
Thread.sleep(100000);
}
private static void sendAndReceiveClient() throws IOException {
SocketChannel clientChannel = SocketChannel.open(new InetSocketAddress("localhost", 6666));
//阻塞模式
// clientChannel.configureBlocking(false);
ByteBuffer sendBuffer = ByteBuffer.allocate(128);
sendBuffer.put("hello world".getBytes());
sendBuffer.flip();
clientChannel.write(sendBuffer);
System.out.println(clientChannel+"发送到服务端:"+new String(sendBuffer.array()));
ByteBuffer receiveBuffer = ByteBuffer.allocate(128);
clientChannel.read(receiveBuffer);
System.out.println(clientChannel+"接受到服务端:"+new String(receiveBuffer.array()));
// System.out.println(new String(receiveBuffer.array()));
}
}
io复用的nio核心操作的server端的第5步操作,select()方法,他会轮询selector中的Selecttionkey,在操作系统中是通过select()方法的系统调用实现的。
nio使用需要注意的是ByteBuffer的使用需要进行读写切换。写入完数据之后需要执行flip()操作才能从里面读数据。
ByteBuffer使用三个指针来确定读写的位置。
- position,当前读写的位置
- limit,写模式和capacity一样,读模式为buffer内数据的长度
- capacity,buffer最大容量
执行flip()操作使position复位,limit移动到数据的长度的位置。
可以看到使用nio实现的代码比较复杂,那么就需要一种能简化的方式或者说将这种耦合度很高的代码进行解耦,将不同的同能使用不同的模块来实现,比如说下面的reactor模式。
基于多reactor模式实现的nio server
reactor模式将server 分解成几个模块
- mainReactor,创建serverchannel,selector轮询select(),将accept事件分发给acceptor
- subreactor,创建轮询读写事件的selector ,将读写事件分发给workerhandler处理
- Acceptor,处理客户端连接事件
- WorkerHandler,处理io读写和读取到数据后进行业务逻辑处理
基本的结构图如下
直接看代码实现更直观
public abstract class AbstractReactor implements Runnable {
Selector selector;
ServerSocketChannel serverSocket;
ExecutorService threadPool ;
@Override
public void run() {
while (!Thread.interrupted()) {
try {
//select 和register竞争锁会阻塞,需要有个超时时间
selector.select(100);
Set selected = selector.selectedKeys();
itertor(selected);
} catch (Exception ex) {
System.out.println(ex);
}
}
}
private void itertor(Set selected) {
Iterator it = selected.iterator();
while (it.hasNext()) {
SelectionKey next = (SelectionKey) it.next();
//线程不安全的
it.remove();
dispatch((SelectionKey) next,selected);
}
}
void dispatch(SelectionKey selectionKey, Set selected) {
try {
if (selectionKey.isAcceptable()) {
Acceptor r = (Acceptor) (selectionKey.attachment());
if (r != null) {
r.run();
}
}
if (selectionKey.isValid()&&(selectionKey.isReadable()||selectionKey.isWritable())) {
Runnable workerHandler = (Runnable) (selectionKey.attachment());
execute(workerHandler);
//线程不安全的,remove有问题
// selected.remove(selectionKey);
}
} catch (Exception e) {
e.printStackTrace();
}
}
void start() {
threadPool.execute(this);
}
void execute(Runnable runnable) {
}
}
/**
* main reactor 和sub reactor,io数据处理,在同一个线程
*轮询selector和分发客户端io连接事件给acceptor处理
*/
class MainReactor extends AbstractReactor {
MainReactor(int port) throws IOException {
selector = Selector.open();
serverSocket = ServerSocketChannel.open();
serverSocket.socket().bind( new InetSocketAddress(port));
serverSocket.configureBlocking(false);
SelectionKey sk = serverSocket.register(selector, SelectionKey.OP_ACCEPT);
//
sk.attach(new Acceptor(serverSocket,selector));
threadPool = Executors.newSingleThreadExecutor();
}
}
/**
* selectionkey和thread对应上
*/
final class SubReactor extends AbstractReactor{
//线程池模式
SubReactor(Selector sel, ServerSocketChannel c)
throws IOException {
threadPool = Executors.newFixedThreadPool(3);
selector = Selector.open();
}
public void registerInSubReactor(SocketChannel clientChannel) throws IOException {
clientChannel.configureBlocking(false);
SelectionKey selectionKey = clientChannel.register(selector, SelectionKey.OP_READ);
// selectionKey.interestOps();
selector.wakeup();
WorkerHandler workerHandler = new WorkerHandler(clientChannel,selectionKey);
//给selectionKey固定一个线程和handler去执行读写,netty eventgroup的目的?
selectionKey.attach(workerHandler);
workerHandler.registered(clientChannel);
}
/**
提交handler任务
*/
void execute(Runnable runnable) {
threadPool.execute(runnable);
}
}
/**
* 处理客户端socket连接事件,将读写事件给worker/hanlder处理
*/
class Acceptor {
ServerSocketChannel serverSocket;
private Selector selector;
SubReactor subReactor;
public Acceptor(ServerSocketChannel serverSocket,Selector selector) throws IOException {
this.serverSocket = serverSocket;
this.selector = selector;
this. subReactor = new SubReactor(selector, serverSocket);
subReactor.start();
}
public void run() {
try {
//客户端连接建立
SocketChannel client = serverSocket.accept();
if (client != null) {
System.out.println("收到连接请求");
//通过acceptor将mainreactor、subreactor连接起来
//netty handler对应
subReactor.registerInSubReactor(client);
}
} catch (IOException ex) {
System.out.println(ex);
ex.printStackTrace();
}
}
}
/**
* 一个channel一个handler
*/
public class WorkerHandler implements Runnable {
static final int READING = 0, SENDING = 1;
int state = READING;
final SocketChannel socket;
SelectionKey selectionKey;
ByteBuffer recvBuf = ByteBuffer.allocate(128);
ByteBuffer sendBuf = ByteBuffer.allocate(128);
public WorkerHandler(SocketChannel socket,SelectionKey selectionKey) {
this.selectionKey = selectionKey;
this.socket = socket;
}
/**
* 处理拆包,判断是否读完
* @return
*/
boolean readIsComplete(int l) {
//TODO
return true;
}
/**
* 处理粘包,判断是否写完
* @return
*/
boolean writeIsComplete(int l) {
//TODO
return true;
}
@Override
public void run() {
while (true) {
try {
SocketChannel clientChannel;
while ((clientChannel=taskQueue.poll() )!= null) {
clientChannel.configureBlocking(false);
SelectionKey selectionKey = clientChannel.register(subSelector, 0);
selectionKey.attach(clientChannel);
selectionKey.interestOps(SelectionKey.OP_READ );
}
int select = subSelector.select(100);
if (select <= 0) {
continue;
}
Set<SelectionKey> selectionKeys = subSelector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey next = iterator.next();
if (next.isReadable()) {
read(next, ByteBuffer.allocate(128), ByteBuffer.allocate(128));
} else if (next.isWritable()) {
byte[] bytes = ("hello client").getBytes();
send(next, ByteBuffer.wrap(bytes));
}
}
} catch (IOException ex) {
System.out.println("SubReactor");
ex.printStackTrace();
}
}
}
//读数据
void read() throws IOException {
//read是线程安全的,枷锁,避免使用多线程访问同一个channel
int read = socket.read(recvBuf);
if (readIsComplete(read)) {
handleInputData(socket,recvBuf, sendBuf);
state = SENDING;
selectionKey.interestOps(SelectionKey.OP_WRITE);
}
}
//写数据
void send() throws IOException {
System.out.println("发送给客户端:"+socket+"数据:" + new String(sendBuf.array()));
sendBuf.flip();
if (socket.isConnected() && socket.isOpen()) {
int write = socket.write(sendBuf);
if (writeIsComplete(write)) {
// System.out.println("服务端写完了");
// selectionKey.cancel();
}
//需要注册其他事件否则一直会有可写事件
selectionKey.interestOps(SelectionKey.OP_READ);
}
}
void registered(SocketChannel socketChannel) {
System.out.println("新连接完成"+socketChannel);
}
/**
* 处理io数据包
* @param socket
* @param input
* @param output
*/
void handleInputData(SocketChannel socket, ByteBuffer input, ByteBuffer output) {
//TODO 处理读取的数据
//io读写模式
input.flip();
output.put(input);
System.out.println("收到客户端"+socket+"数据:" + new String(input.array()));
}
}
public class BootStrap {
public void start(int port) throws IOException {
AbstractReactor main = new MainReactor(port);
main.start();
}
}
public class ReactorTest {
public static void main(String[] args) throws IOException {
// new Thread(new MainReactor(6666)).start();
new BootStrap().start(6666);
System.in.read();
}
}
以上各个组件的实现如上所示,注释还算详细。
主要在mainreactor和subreactor两个类中,都有创建一个selector进行轮询io事件,mainreactor负责轮询accept事件,subreactor负责轮询读写事件。然后将事件分别发给acceptor和workerhandler。
最后,在ReactorTest启动这个server即可。其实,这也是netty的实现基本原理,但是netty处理了更多的细节,
- 处理了jdk bug
- 处理了粘包和拆包
- 简化了bytebuffer使用,优化了内存拷贝
- api使用更简洁,只需要关注handler的实现即可
更详细的netty解析,请看下一篇文章:手把手教你学习netty源码及原理。
有任何问题欢迎指出,共同交流。