• JNI即是Java Native Interface,主要用来Java和其他语言之间的交互,多半是和平台依赖的调用、其他高级语言的库甚至是低级语言交互。相信大家平时在JDK中看到很多native的方法,但是自己写JNI应用的会确很少。这次因为在进行开发中,遇到需要Java调用一个现成的成熟的功能完善的C语言应用库,因此 JNI便成了Java和C库之间的桥的功能。既然是初探,那么下面我们就从HelloWorld开始吧:虽然Java是一个跨平台的语言,但是C语言是一个和平台密切相关的语言;所以针对Unix、Linux、Windows等不同的操作系统,C语言的预编译,动态库的创建等都有一些差别,也需要我们分别进行处理之。下面仅基于Sun的Unix平台Sun Solaris开发为例。
    1.首先声明第一个Java native程序:HelloWorld.java。
    public class HelloWorld {
        static {
            init();
        }

        private native int hello();

        public int sayHello() {
            return hello();
        }

        /**
         * @param args
         */
        public static void main(String[] args) {
            HelloWorld hw = new HelloWorld();
            System.out.println("Say in Java:" + hw.sayHello());
        }

        private static void init() {
            System.out.println("Starting to load HelloWorld lib.");
            try {
                System.loadLibrary("HelloWorld");
            } catch (Throwable t) {
                System.out.println("Load unsuccessfully.");
                t.printStackTrace();
                System.exit(-1);
            }
            System.out.println("Load successfully.");
        }
    }
    2.编译该Java文件。
    javac HelloWorld.java
    3.生成相应的C语言的HelloWorld.h头文件,javah HelloWorld,头文件内容如下:(其中,static Java方法和非static Java方法经过javah -jni生成的C头文件是有区别的,差异在于参数,static Java方法生成的相应的C方法的第二个参数是jclass类型,非static Java方法生成的相应的C方法的第二个参数是jobject类型。)
    more HelloWorld.h
    /* DO NOT EDIT THIS FILE - it is machine generated */
    #include <jni.h>
    /* Header for class HelloWorld */

    #ifndef _Included_HelloWorld
    #define _Included_HelloWorld
    #ifdef __cplusplus
    extern "C" {
    #endif
    /*
     * Class:     HelloWorld
     * Method:    hello
     * Signature: ()I
     */
    JNIEXPORT jint JNICALL Java_HelloWorld_hello
      (JNIEnv *, jobject);

    #ifdef __cplusplus
    }
    #endif
    #endif
    4.开始编写第一个c语言的JNI的本地(native)方法:
    vi HelloWorld.c
    #include <jni.h>
    #include "HelloWorld.h"
    #include <stdio.h>

    JNIEXPORT jint JNICALL Java_HelloWorld_hello
    (JNIEnv* env, jobject target)
    {
      printf("Hello World in C program!!!\n");
      return 2;
    }
    5.生成Unix下的动态库.so文件:
    cc -G -I. -I$JAVA_HOME/include -I$JAVA_HOME/include/solaris HelloWorld.c -o libHelloWorld.so
    在当前目录下看到新生成的libHelloWorld.so文件。也许你会注意到我们在Java程序中System.loadLibrary()加载的是库HelloWorld,那么这其实是一个命名的规范而已。以lib开头的.so文件即是Unix下的动态库,而JNI加载器通过HelloWorld这个名字实际上希望加载的动态库是libHelloWorld.so。
    6.设置环境变量,查看LD_LIBRARY_PATH并设置:
    vi .cshrc
    setenv LD_LIBRARY_PATH '.:/usr/local/lib:$LD_LIBRARY_PATH'
    source .cshrc
    这个环境变量设置的是Java的动态库加载器需要加载的C动态库所在的路径。因此,我们生成的libHelloWorld.so一定要在这个环境变量中设置,在这个例子中我就把当前目录.加到环境变量LD_LIBRARY_PATH了:)
    7.执行HelloWorld文件,java HelloWorld,在标准输出上看到:
    Starting to load HelloWorld lib.
    Load successfully.
    Hello World in C program!!!
    Say in Java:2
    一切Okey,如此的easy,一阵狂喜!接下来就是将这个例子运用于JNI的开发之中了。

    Resource:
    Java Native Interface: Programmer's Guide and Specification
    http://java.sun.com/docs/books/jni/html/start.html#27008
    http://linuxmafia.com/faq/Admin/ld-lib-path.html
    https://www6.software.ibm.com/developerworks/cn/education/java/j-jni/tutorial/j-jni-2-15.html
  • 在Java1.4以前,Java的网络编程是只有阻塞方式的,在Java1.4以及之后,Java提供了非阻塞的网络编程API.从Java的发展来看,由于Java的快速发展,JVM性能的提升,涉足到服务端应用程序开发也越来越多,要求高性能的网络应用越来越多,这是Java推出非阻塞网络编程的最主要原因吧。
    对我而言,以前的大部分服务端应用主要是搭建在应用服务器之上,所以通讯这部分工作都是有应用服务器来实现和管理的。这次由于通讯和协议,我们必须自己实现一个能处理大量并发客户端的高性能并行处理的Java服务端程序。因此,选择非阻塞的处理方式也是必然的。我们首先来看看阻塞的处理方式:
    在阻塞的网络编程方式中,针对于每一个单独的网络连接,都必须有一个线程对应的绑定该网络连接,进行网络字节流的处理。下面是一段代码:
        public static void main(String[] args) {
            try {
                ServerSocket ssc = new ServerSocket(23456);
                while (true) {
                    System.out.println("Enter Accept:");
                    Socket s = ssc.accept();
                    try {
                        (new Thread(new Worker(s))).start();
                    } catch (Exception e) {
                        // TODO
                        e.printStackTrace();
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

        }

        public static class Worker implements Runnable {
            private Socket s;

            private boolean running = true;;

            public Worker(Socket s) {
                this.s = s;
            }

            public void run() {
                try {
                    InputStream is = s.getInputStream();
                    OutputStream os = s.getOutputStream();
                    while (running) {
                        byte[] b = this.readByLength(is, 1024);
                        this.process(b);
                    }
                } catch (Throwable t) {
                    // TODO
                    t.printStackTrace();
                }
            }

            private byte[] readByLength(InputStream is, int contLen) throws IOException {
                byte[] b = new byte[contLen];
                int off = 0;
                int length = 0;
                while ((length = is.read(b, off, contLen - off)) >= 0) {
                    off = +length;
                    if (off >= contLen) {
                        break;
                    }
                }
                return b;
            }

            private void process(byte[] b) {

            }
        }
    在这段代码中,我们看到有两个阻塞的方法,是ServerSocket的accept()方法;和InputStream的read()方式。因此我们需要两类型的线程分别进行处理。而且每一个阻塞方法所绑定的线程的生命周期和网络连接的生命周期是一致的。基于以上的原因,NIO应运而生,一方面,为每一个网络连接建立一个线程对应,同时每一个线程有大量的线程处于读写以外的空闲状态,因此希望降低线程的数量,降低每个空闲状态,提高单个线程的运行执行效率,实际上是在更加充分运用CPU的计算、运行能力(因为,如果有大量的链路存在,就存在大量的线程,而大量的线程都阻塞在read()或者write()方法,同时CPU又需要来回频繁的在这些线程中间调度和切换,必然带来大量的系统调用和资源竞争.);另外一方面希望提高网络IO和硬盘IO操作的性能。在NIO主要出现了三个新特性:
    1.数据缓冲处理(ByteBuffer):由于操作系统和应用程序数据通信的原始类型是byte,也是IO数据操作的基本单元,在NIO中,每一个基本的原生类型(boolean除外)都有Buffer的实现:CharBuffer、IntBuffer、DoubleBuffer、ShortBuffer、LongBuffer、FloatBuffer和ByteBuffer,数据缓冲使得在IO操作中能够连续的处理数据流。当前有两种ByteBuffer,一种是Direct ByteBuffer,另外一种是NonDirect ByteBuffer;ByteBuffer是普通的Java对象,遵循Java堆中对象存在的规则;而Direct ByteBuffer是native代码,它内存的分配不在Java的堆栈中,不受Java内存回收的影响,每一个Direct ByteBuffer都是直接分配的一块连续的内存空间,也是NIO提高性能的重要办法之一。另外数据缓冲有一个很重要的特点是,基于一个数据缓冲可以建立一个或者多个逻辑的视图缓冲(View Buffer).比方说,通过View Buffer,可以将一个Byte类型的Buffer换作Int类型的缓冲;或者一个大的缓冲转作很多小的Buffer。之所以称为View Buffer是因为这个转换仅仅是逻辑上,在物理上并没有创建新的Buffer。这为我们操作Buffer带来诸多方便。
    2.异步通道(Channel):Channel是一个与操作系统紧密结合的本地代码较多的对象。通过Channel来实现网络编程的非阻塞操作,同时也是其与ByteBuffer、Socket有效结合充分利用非阻塞、ByteBuffer的特性的。在后面我们会看到具体的SocketChannel的用法。
    3.有条件的选择(Readiness Selection):大多数操作系统都有支持有条件选择准备就绪IO通道的API,即能够保证一个线程同时有效管理多个IO通道。在NIO中,由Selector(维护注册进来的Channel和这些Channel的状态)、SelectableChannel(能被Selector管理的Channel)和SelectionKey(SelectionKey标识Selector和SelectableChannel之间的映射关系,一旦一个Channel注册到Selector中,就会返回一个SelectionKey对象。SelectionKey保存了两类状态:对应的Channel注册了哪些操作;对应的Channel的那些操作已经准备好了,可以进行相应的数据操作了)结合来实现这个功能的。
    NIO非阻塞的典型编程模型如下:
        private Selector selector = null;

        private static final int BUF_LENGTH = 1024;

        public void start() throws IOException {
            if (selector != null) {
                selector = Selector.open();
            }
            ServerSocketChannel ssc = ServerSocketChannel.open();
            ServerSocket serverSocket = ssc.socket();
            serverSocket.bind(new InetSocketAddress(80));
            ssc.configureBlocking(false);
            ssc.register(selector, SelectionKey.OP_ACCEPT);

            try {
                while (true) {
                    int nKeys = UnblockServer.this.selector.select();

                    if (nKeys > 0) {
                        Iterator it = selector.selectedKeys().iterator();
                        while (it.hasNext()) {
                            SelectionKey key = (SelectionKey) it.next();
                            if (key.isAcceptable()) {
                                ServerSocketChannel server = (ServerSocketChannel) key.channel();
                                SocketChannel channel = server.accept();
                                if (channel == null) {
                                    continue;
                                }
                                channel.configureBlocking(false);
                                channel.register(selector, SelectionKey.OP_READ);
                            }

                            if (key.isReadable()) {
                                readDataFromSocket(key);
                            }

                            it.remove();
                        }
                    }
                }
            } catch (IOException ioe) {
                ioe.printStackTrace();
            }
        }

        /**
         * @param key
         * @throws IOException
         */
        private void readDataFromSocket(SelectionKey key) throws IOException {
            ByteBuffer buf = ByteBuffer.allocate(BUF_LENGTH);
            SocketChannel sc = (SocketChannel) key.channel();

            int readBytes = 0;
            int ret;

            try {
                while ((ret = sc.read(buf.buf())) > 0) {
                    readBytes += ret;
                }
            } finally {
                buf.flip();
            }

            // process buffer
            // buf.clear();
        }

    从这段程序,我们基本可以了解到NIO网络编程的一些特点,创建一个SocketServer的方式已经发生了变化,需要指定非阻塞模式,需要创建一个Channel然后注册到Selector中去,同样,建立一个网络连接过程也是一样的模式,然后就是有条件的选择(Readiness Selection).这样,我们的每一个线程只需要处理一类型的网络选择。在代码上,我们发现处理的方式和阻塞完全不一样了,我们需要完全重新考虑如何编写网络通信的模块了:
    1.持久连接的超时问题(Timeout),因为API没有直接的支持timeout的参数设置功能,因此需要我们自己实现一个这样功能。
    2.如何使用Selector,由于每一个Selector的处理能力是有限的,因此在大量链接和消息处理过程中,需要考虑如何使用多个Selector.
    3.在非阻塞情况下,read和write都不在是阻塞的,因此需要考虑如何完整的读取到确定的消息;如何在确保在网络环境不是很好的情况下,一定将数据写进IO中。
    4.如何应用ByteBuffer,本身大量创建ByteBuffer就是很耗资源的;如何有效的使用ByteBuffer?同时ByteBuffer的操作需要仔细考虑,因为有position()、mark()、limit()、capacity等方法。
    5.由于每一个线程在处理网络连接的时候,面对的都是一系列的网络连接,需要考虑如何更好的使用、调度多线程。在对消息的处理上,也需要保证一定的顺序,比方说,登录消息最先到达,只有登录消息处理之后,才有可能去处理同一个链路上的其他类型的消息。
    6.在网络编程中可能出现的内存泄漏问题。
    在NIO的接入处理框架上,大约有两种并发线程:
    1.Selector线程,每一个Selector单独占用一个线程,由于每一个Selector的处理能力是有限的,因此需要多个Selector并行工作。
    2.对于每一条处于Ready状态的链路,需要线程对于相应的消息进行处理;对于这一类型的消息,需要并发线程共同工作进行处理。在这个过程中,不断可能需要消息的完整性;还要涉及到,每个链路上的消息可能有时序,因此在处理上,也可能要求相应的时序性。
    当前社区的开源NIO框架实现有MINA、Grizzly、NIO framework、QuickServer、xSocket等,其中MINA和Grizzly最为活跃,而且代码的质量也很高。他们俩在实现的方法上也完全大不一样。(大部分Java的开源服务器都已经用NIO重写了网络部分。 )
    不管是我们自己实现NIO的网络编程框架,还是基于MINA、Grizzly等这样的开源框架进行开发,都需要理解确定的了解NIO带来的益处和NIO编程需要解决的众多类型的问题。充足、有效的单元测试,是我们写好NIO代码的好助手:)

    Resource:
    http://www.cis.temple.edu/~ingargio/cis307/readings/unix4.html#states

    Java NIO
    GlassFish--开源的Java EE应用服务器

  • James Gosling在他May 08,2004的Blog的帖子:《The world needs more crazy people》谈到了一些做事的原则,可以作为思考的引子;更要警醒、反省、鼓励自己。

    "Most real innovation is done by crazy people doing crazy things. The keys are:
        * Learn all you can before you go adventuring.
        * Don't be afraid to make mistakes.
        * Only make new mistakes.
        * Keep your eyes open.
        * Don't just look straight ahead: develop your peripheral vision.
        * It's the things that go in unexpected directions are the most important.
    "

  • look to the master,
    follow the master,
    walk with the master,
    see through the master,
    become the master.
  • 2004-10-28

    程序思考

    Tag:opensource
    1.程序run起来。
    2.程序能够被单元测试。
    3.考虑引起变化的因素。
    4.面对引起变化的因素,重构。
    5.如何对变化进行单元测试,即面对了变化,如何进行单元测试。
    6.如何做准确性的单元测试,如何做边界的单元测试。
    7.如何保证代码在引起变化的时候仍然具有很好的可维护性(维护中需要好的单元测试做为支持的)。