Java NIO(10): 异步模型之Callback

本节课是小密圈《进击的Java新人》第十八周第二课。

Java NIO(3): IO模型,这节课中,我们提到了5种IO模型。第四种,SIGIO一般都是在进程间使用信号通讯的时候的手段,在Java中不是很适用,我就不深入去讲了。第五种,linux 服务器上的典型代表是 aio,但在Java中也没有对应的内容。不过,有一种非常通用的模型,也应该归到第五种的纯异步模型中去,这就是Callback模型。

回调函数

Callback函数,翻译成回调函数。假如我们是A,在通知B去做一件事情的时候,告诉他,你做完的时候通过XXX方式通知我你做完了。比如说,这个XXX可能是打电话,可能是微信,可能是短信,可能是喊一嗓子。这个都是A向B注册的回调函数。

回调函数还可以做更多。我再举个例子,老板告诉员工说去查一下某个客户的联系方式。员工问,我查到以后呢?老板说你查到了就给客户打电话通知系统升级。好,这个过程中,员工问,我查到以后做什么,这其实就是请求注册一个回调函数。老板说你查到了再干某某事,这个某某事就是真正注册的回调函数。员工做完了第一件事情之后,就可以使用第一件事情的结果(电话号码)去执行第二件事了。这个过程老板甚至都不需要关心第一件和第二件事之间的切换,老板只需要通过注册回调告诉员工就行了。这可就是真正的异步模型了。

Callback的例子非常多,最出名,可能还是node.js了。例如:

//异步示例
var fs = require('fs');
fs.unlink('/tmp/hello', function(err){ // this function is callback
  if (err) throw err;
  console.log('successfully deleted /tmp/hello');
});
console.log("hello world");

这是nodejs 删除一个文件所用的代码,如果运行它的话可以看到hello world,会在 successfully 之前。这是因为fs.unlink是一个非阻塞的函数,一调用就立即返回。然后就会去打印hello world,而等到操作系统真的把文件删除的操作完成了,就会调用后面的那个匿名方法。javascript中,函数是可以做为参数传递给另一个函数的。这个注册的匿名方法就是callback。

实际上,主线程根本不知道,操作系统完成删除一个文件的操作需要多长时间。这个回调函数在什么时刻会被执行也是完全不能预定的。这是一个纯异步的模型。

Java中的实现

其实回调函数的概念出现的很早,比Java的历史都长。C语言时代就已经有了,C是用函数指针去实现。我们先不去管它,我们思考一下Java怎么实现。

C可以传递函数指针,javascript 和 python 干脆就直接传递 function 做为参数。Java都不支持啊,怎么办?不怕,我们可以使用对象封装嘛。还是上节课所说的除法的例子,我把它改成Callback形式的:

public class CallbackServer
{
    public void run() throws IOException {
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.configureBlocking(false);
        serverChannel.socket().bind(new InetSocketAddress("127.0.0.1", 8000));

        Selector selector = Selector.open();
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);

        while (true) {
            selector.select();
            Iterator ite = selector.selectedKeys().iterator();

            while (ite.hasNext()) {
                SelectionKey key = (SelectionKey)ite.next();

                if (key.isAcceptable()) {
                    ServerSocketChannel s = (ServerSocketChannel)key.channel();
                    SocketChannel clientSocket = s.accept();
                    System.out.println("Got a new Connection");

                    clientSocket.configureBlocking(false);

                    SelectionKey newKey = clientSocket.register(selector, SelectionKey.OP_WRITE);

                    CommonClient client = new CommonClient(clientSocket, newKey);
                    newKey.attach(client);

                    System.out.println("client waiting");
                }
                else if (key.isReadable()) {
                    CommonClient client = (CommonClient) key.attachment();
                    client.onRead();
                }
                else if (key.isWritable()) {
                    CommonClient client = (CommonClient) key.attachment();
                    client.onWrite();
                }

                ite.remove();
            }
        }
    }

    public static void main( String[] args ) throws Exception
    {
        CallbackServer server = new CallbackServer();
        server.run();
    }
}

class CommonClient {
    private SocketChannel clientSocket;
    private ByteBuffer recvBuffer;
    private SelectionKey key;
    private Callback callback;

    private String msg;


    public CommonClient(SocketChannel clientSocket, SelectionKey key) {
        this.clientSocket = clientSocket;
        this.key = key;
        recvBuffer = ByteBuffer.allocate(8);

        try {
            this.clientSocket.configureBlocking(false);
            key.interestOps(SelectionKey.OP_WRITE);
        } catch (IOException e) {
        }
    }

    public void close() {
        try {
            clientSocket.close();
            key.cancel();
        }
        catch (IOException e){};
    }

    // an rpc to notify client to send a number
    public void sendMessage(String msg, Callback cback)  {
        this.callback = cback;

        try {
            try {
                recvBuffer.clear();
                recvBuffer.put(msg.getBytes());
                recvBuffer.flip();
                clientSocket.write(recvBuffer);

                key.interestOps(SelectionKey.OP_READ);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        catch (Exception e) {
        }
    }

    // when key is writable, resume the fiber to continue
    // to write.
    public void onWrite() {
        sendMessage("divident", new Callback() {
            @Override
            public void onSucceed(int data) {
                int a = data;
                sendMessage("divisor", new Callback() {
                    @Override
                    public void onSucceed(int data) {
                        int b = data;

                        sendMessage(String.valueOf(a / b), null);
                    }
                });
            }
        });
    }

    public void onRead() {
        int res = 0;
        try {
            try {
                recvBuffer.clear();

                // read may fail even SelectionKey is readable
                // when read fails, the fiber should suspend, waiting for next
                // time the key is ready.
                int n = clientSocket.read(recvBuffer);
                while (n == 0) {
                    n = clientSocket.read(recvBuffer);
                }

                if (n == -1) {
                    close();
                    return;
                }

                System.out.println("received " + n + " bytes from client");
            } catch (IOException e) {
                e.printStackTrace();
            }

            recvBuffer.flip();
            res = getInt(recvBuffer);

            // when read ends, we are no longer interested in reading,
            // but in writing.
            key.interestOps(SelectionKey.OP_WRITE);
        } catch (Exception e) {
        }

        this.callback.onSucceed(res);
    }

    public int getInt(ByteBuffer buf) {
        int r = 0;
        while (buf.hasRemaining()) {
            r *= 10;
            r += buf.get() - '0';
        }

        return r;
    }
}

interface Callback {
    public void onSucceed(int data);
}

服务端的核心逻辑就在于那个sendMessage,注意看,这个方法的定义是不是很像javascript的文件操作函数了?向客户端发送一个字符串,并且注册一个Callback方法。

callback的调用是在数据从客户端发送到服务端的时候,我们通过onRead方法从网络上取到数据,然后调用callback.succeed,在第一次的callback里,我们又调了一次sendMessage。第二次send得到了b,然后计算 a / b。

这里有一点小技巧。注意看,a 和 b 都不是成员变量,而是局部变量。b 做为内嵌匿名类能够访问到定义它的匿名类的那个方法里的局部变量。这种东西叫闭包。当然在Java语言中,我们不这样去讲它,我们讲,b 在 a 的词法作用域中,所以在定义 b 的地方还是可以访问到 a。如果是js,python,我们通常会叫它闭包了。

本质上,这个程序仍然是selector在驱动,但经过了一层封装以后,我们已经把状态机模型转成了Callback模型。我们已经不再手动去维护服务端的状态迁移了,而是通过Callback来定义一件事情完成以后要做什么。组织Callback的负担要比状态机的负担轻。

相应地,把客户端程序也做了一些修改:
public class CallbackClient {
    public static void main(String[] args) {
        try {
            SocketChannel socketChannel = SocketChannel.open();
            socketChannel.connect(new InetSocketAddress("127.0.0.1", 8000));

            ByteBuffer writeBuffer = ByteBuffer.allocate(32);
            ByteBuffer readBuffer = ByteBuffer.allocate(32);

            getMessage(readBuffer, socketChannel);
            sendRandomInt(writeBuffer, socketChannel, 1000);
            getMessage(readBuffer, socketChannel);

            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            sendRandomInt(writeBuffer, socketChannel, 10);
            getMessage(readBuffer, socketChannel);

            socketChannel.close();
        } catch (IOException e) {
        }
    }

    public static void sendRandomInt(ByteBuffer writeBuffer, SocketChannel socketChannel, int bound) {
        Random r = new Random();
        int d = 0;

        d = r.nextInt(bound);
        if (d == 0)
            d = 1;
        System.out.println(d);
        writeBuffer.clear();
        writeBuffer.put(String.valueOf(d).getBytes());
        writeBuffer.flip();
        try {
            socketChannel.write(writeBuffer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void getMessage(ByteBuffer readBuffer, SocketChannel socketChannel) {
        readBuffer.clear();
        byte[] buf = new byte[16];
        try {
            socketChannel.read(readBuffer);
        } catch (IOException e) {
        }
        readBuffer.flip();
        readBuffer.get(buf, 0, readBuffer.remaining());
        System.out.println(new String(buf));
    }
}

好了,今天就到这里了,我已经把Java中的IO模型都讲完了。大家一定要多动手练习,想通它,异步编程非常复杂,也非常重要。一定要掌握它。

今天的作业:

CallbackServer的实现里,sendMessage中有真正发送的动作,这其实不合理,应该等待writable的时候再去发送,sendMessage其实应该只是准备发送而已。你改进一下吧。

上一节课:Java NIO(9) : 状态机

下一节课:Direct Buffer

目录:课程目录

编辑于 2017-06-30

文章被以下专栏收录