從java的NIO版hello world看java源碼,我們能看到什麼?

Java NIO服務端代碼的hello world怎麼寫?

public class NBTimeServer {

    public static void main(String[] args) {

        try {
            Selector acceptSelector = SelectorProvider.provider().openSelector();
            //創建一個新的server socket,設置爲非阻塞模式
            ServerSocketChannel ssc = ServerSocketChannel.open();
            ssc.configureBlocking(false);

            // 綁定server sokcet到本機和對應的端口

            InetAddress lh = InetAddress.getLocalHost();
            InetSocketAddress isa = new InetSocketAddress(lh, 8900);
            ssc.socket().bind(isa);

            //通過selector註冊server socket,這裏即告訴selector,當accept發生的時候,socket會被放在reday隊列
            SelectionKey acceptKey = ssc.register(acceptSelector,
                    SelectionKey.OP_ACCEPT);

            int keysAdded = 0;

            // 當任何一個註冊事件發生的時候,select就會返回
            while ((keysAdded = acceptSelector.select()) > 0) {
                // 獲取已經準備好的selectorkey
                Set readyKeys = acceptSelector.selectedKeys();
                Iterator i = readyKeys.iterator();


                while (i.hasNext()) {
                    SelectionKey sk = (SelectionKey)i.next();
                    i.remove();
                    ServerSocketChannel nextReady =
                            (ServerSocketChannel)sk.channel();
                    Socket s = nextReady.accept().socket();
                    PrintWriter out = new PrintWriter(s.getOutputStream(), true);
                    Date now = new Date();
                    out.println(now);
                    out.close();
                }
            }
        } catch(Exception e) {
            e.printStackTrace();
        }
    }
}

1: 獲取selector。

SelectorProvider提供的所有provider都是同一個對象。如果沒有,它會通過AccessController.doPrivileged來給獲取provider的代碼最高的權限,執行邏輯是:
- java.nio.channels.spi.SelectorProvider 是否有配置,有就通過反射創建(本例沒有)
- 是不是在jar中已經實例化了 java.nio.channels.spi.SelectorProvider,並且他能夠通過getSystemClassLoader加載,就是用第一個獲取到的SelectorProvider(本例沒有)
- 最終通過sun.nio.ch.DefaultSelectorProvider類來創建,它在不同的操作系統下有着不同的實現

這裏寫圖片描述
以solaris的實現爲例,創建的provider會根據操作系統的版本和操作系統的名字分別創建不同的實例

if ("SunOS".equals(osname)) {
        return new sun.nio.ch.DevPollSelectorProvider();
}
if("Linux".equals(osname)){
     if (major > 2 || (major == 2 && minor >= 6)) {
        return new sun.nio.ch.EPollSelectorProvider();
    }
}
 return new sun.nio.ch.PollSelectorProvider(); //默認返回

代碼存在縮減,只取核心

類之間的關係如下
這裏寫圖片描述

下面只關注Epoll和Poll

拿到provider之後,開始執行openSelector,獲取真正的selector。
對於poll,返回的實例是PollSelectorImpl,對於Epoll返回的實例則是EpollSelectorImpl。

這裏寫圖片描述

file descriptor :unix設計哲學就是一切都是文件,它可能是一個網絡連接、一個終端等等。它本身就是一個數值,在系統中會維護文件描述符和它對應文件的一個指針,從而找到對應的文件操作

  • fd0的獲取主要是調用Native方法實現
long pipeFds = IOUtil.makePipe(false);
fd0 = (int) (pipeFds >>> 32); // >>> 表示無符號右移,最高位補0,這裏即獲取讀文件描述符
fd1 = (int) pipeFds; //截掉了高位,存儲的是讀文件描述符

IOUtil針對不同的操作系統有不同的實現,以solaris爲例,它的實現在IOUtil.c中,主要實現即通過Linux pipe方法和Linux fcntl方法 (代碼有刪減)

 int fd[2];
  if (pipe(fd) < 0) // 獲取讀和寫的文件符
  if ((configureBlocking(fd[0], JNI_FALSE) < 0) //標註爲非阻塞
       || (configureBlocking(fd[1], JNI_FALSE) < 0))
  return ((jlong) fd[0] << 32) | (jlong) fd[1]; //讀的文件描述符放在高位,寫的文件描述符放在低位

configureBlocking本身的實現在IOUtil.c中

static int configureBlocking(int fd, jboolean blocking) //設置爲非阻塞狀態
{
   int flags = fcntl(fd, F_GETFL); 
   int newflags = blocking ? (flags & ~O_NONBLOCK) : (flags | O_NONBLOCK);
   return (flags == newflags) ? 0 : fcntl(fd, F_SETFL, newflags);
}

pipe實際是創建了一個進程間通信的單向數據管道,參數中的fd[0]表示管道讀取端的結尾,fd[1]表示管道寫端的結尾;fcntl則主要是根據第二個參數,如源碼中的F_GETFL和F_SETFL,對第一個參數執行對應的操作;

  • 新建EPollArrayWrapper,部分字段如下
    這裏寫圖片描述
pollWrapper = new EPollArrayWrapper();
pollWrapper.initInterrupt(fd0, fd1);
  1. epfd:通過Native方法去構建,對應的實現在EPollArrayWrapper.c中,方法爲:Java_sun_nio_ch_EPollArrayWrapper_epollCreate,主要的實現邏輯是int epfd = (*epoll_create_func)(256);而epoll_create_func在Java_sun_nio_ch_EPollArrayWrapper_init執行的時候已經是執行了初始化,對應的是Linux epoll_create ,返回既是一個epoll實例,它實質也是一個文件描述符

       epoll_create_func = (epoll_create_t) dlsym(RTLD_DEFAULT, "epoll_create");
       epoll_ctl_func    = (epoll_ctl_t)    dlsym(RTLD_DEFAULT, "epoll_ctl");
       epoll_wait_func   = (epoll_wait_t)   dlsym(RTLD_DEFAULT, "epoll_wait");
  2. pollArray:一個用來存儲從epoll_wait中得到結果的數組,它的大小爲NUM_EPOLLEVENTS * SIZE_EPOLLEVENT,其中的NUM_EPOLLEVENTS則是去的文件描述符限制和8192相比的最小值Math.min(fdLimit(), 8192);詳見Linux getrlimit實質是AllocatedNativeObject

  3. initInterrupt:出了存儲對應的文件描述符之外,還執行了epollCtl(epfd, EPOLL_CTL_ADD, fd0, EPOLLIN);,即把fd0註冊到epfd上,將epfd上的EPOLLIN事件關聯到fd0上,詳見Linux epoll_ctl

  • 新建PollArrayWrapper,部分字段如下

這裏寫圖片描述

pollWrapper = new PollArrayWrapper(INIT_CAP); //初始爲10
pollWrapper.initInterrupt(fd0, fd1);

pollArray:它的大小爲(10+1)*SIZE_POLLFD(SIZE_POLLFD取值爲8),實質是AllocatedNativeObject


  • AllocatedNativeObject
    這裏寫圖片描述

NativeObject是用來操作本地內存的一個代理,所有的操作通過Unsafe來實現,它本身是一個單例

2: 開啓服務端socket的channel

它還是會去獲取系統級別的provider,由於已經在拿selector的時候初始化,不再新建。同樣會通過PollSelectorProvider或者是EPollSelectorProvider來開啓服務端的socket的channel,而二者的實現均是通過父類SelectorProviderImpl,創建一個ServerSocketChannelImpl實例

這裏寫圖片描述

channel:代表與硬件、文件、網絡socket或者是程序組件等能夠進行一些I/O操作(讀和寫)的實體的連接

Closeable:是關閉與流相關的系統資源

AutoCloseable:從1.7開始的支持的語法糖try-with-resources結構,實現自動關閉資源

SelectableChannel:支持通過selector複用的Channel,提供對channel的註冊,返回對應的SelectionKey,可以工作在阻塞(默認)和非阻塞模式下

NetworkChannel:對應網絡socket的channel,提供將socket綁定到本機地址的bind方法

fd是使用IOUtil.newFD創建,創建過程如下:

  1. 調用 Native方法 Net.socket0

    Net.scoket0 方法對應的實現爲Net.c中的Java_sun_nio_ch_Net_socket0,從頭文件的引入 #include <sys/socket.h> 可以看到,socket0的內部很多實現都依賴於操作系統本身,操作系統不一樣,就會有不同的調用結果。關鍵實現如下

    fd = socket(domain, type, 0);
    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (char*)&arg,sizeof(arg))
    • socket(family, type, protocol):其中family指的是要在解釋名稱時使用的地址格式(AF_INET6/AF_INET等),type指定的是通信的語義(SOCK_STREAM/SOCK_DGRAM等),protocol執行通信用的協議,0意味着使用默認的。它返回的就是socket file descriptor。詳見Linux socketAPI [solaris 下存在兩套實現,BSD風格socket庫-3SOCKET和 GNU/Linux軟件使用這個庫 XNET ]
    • setsockop:給文件描述符fd設置socket的選項,返回值小於0表示出了異常,詳見Linux setsocketopt
  2. 新建java對象FileDescriptor ,將1中返回值和新建對象一起交給IOUtil的Native方法setfdVal執行

    在IOUtil.c中存在方法 Java_sun_nio_ch_IOUtil_setfdVal,它就是調用JNI的方法將獲取的值存入到java對象FileDescriptor中取
    FileDescriptor的實例是用來表示1個打開的文件,或者是一個打開的socket或者類似的字節源

fdVal的賦值則是使用創建好的fd調用JNI中的(*env)->GetIntField(env, fdo, fd_fdID);實現

3:獲取socket

本質是通過ServerSocketAdaptor創建一個實例返回

這裏寫圖片描述
ServerSocket本質是一個對SocketImpl的包裝類,相關的請求處理都是由impl來處理
這裏寫圖片描述
SocksSocketImpl是按照SOCKS協議的TCP socket實現,而PlainSocketImpl則是一個‘平凡’的socket實現,它不對防火牆或者代理做任何的突破。
SocketImpl是所有實現socket的父抽象類,用來創建客戶端和服務端的socket

Socket類是兩臺機器之間通信的端點,端點(endpoint)指的是 服務IP和它的端口,它的實際操作還是由SocketImpl來實現。

SOCKS4(SOCKets縮寫)是一個網絡協議,它主要負責在防火牆上中繼TCP會話,以便應用用戶能夠透過防火牆進行訪問。它主要定義了兩個操作:CONNECT和BIND。
- 需要CONNECT時,客戶端發送一個CONNECT請求給SOCKS服務器,請求包含要連接的目的端口和目的主機等信息,SOCKS服務器會做一些服務權限的校驗,驗證成功SOCKS服務器建立與目標主機指定端口的連接(即應用服務器),然後發送反饋包給客戶端,反饋包通過CD的值來標識CONNECT請求的結果,CONNECT成功,SOCKS就可以在兩個方向上轉發流量了
- BIND必須發生在CONNECT之後,它實際包括一系列的步驟:1 獲取socket;2 拿到scoket對應的端口和ip地址;3 開始監聽,準備接收來自應用服務器的調用 4:使用主連接通知應用服務器它需要連接的IP地址和端口 5:接收一個來自應用服務器的連接

SOCKS5相對於SOCKS4做了功能擴展,支持UDP、IPV6、鑑定的支持

4:綁定服務器和它的端口

ServerSocketChannelImpl的bind方法。
1: 看看當前channel是不是已經綁定或者關閉,如果完成,拋出相關異常
2: 看看是否有分配服務器,沒有就隨便建一個

public InetSocketAddress(int port) {    
    this(InetAddress.anyLocalAddress(), port);
}

3: 獲取系統的SecurityManager,獲取成功,就去檢查線程是否有權限來操作端口等待連接到來,不行則拋出SecurityException
4: NetHooks.beforeTcpBind ,如果使用了com.sun.sdp.conf配置,那麼將會把Tcp Socket包裝成Sdp Socket(Hello world沒有啓用)
5: 執行綁定,實際執行Native方法Net.bind0,對應Net.c中的Java_sun_nio_ch_Net_bind0方法,關鍵代碼如下

//將傳入的java對象的InetAddress和端口轉換爲結構體:sockaddr_in或者sockaddr_in6
NET_InetAddressToSockaddr(env, iao, port, (struct sockaddr *)&sa, &sa_len, preferIPv6);
rv = NET_Bind(fdval(env, fdo), (struct sockaddr *)&sa, sa_len);

bind對於windows系統和linux系統有不同的實現,以Linux爲例,它實際執行的就是Linux bind,所做的操作就是把指定的地址(SocketAddress)分配給socket文件描述符,對於Hello world的實現來說就是它的字段fd

6: 監聽,實際爲Linux listen,表明這個socket將會用來接收即將到來的連接請求

5:通過selector註冊channel

這裏寫圖片描述
註冊事件在實質上就是維護新建channel的文件描述符和SelectionKey的關係,就實現上而言, Poll用的是數組,Epoll用的是HashMap

合法的操作爲SelectionKey.OP_READ、SelectionKey.OP_WRITE、SelectionKey.OP_CONNECT

6:從selector獲取任何已經註冊好併發生的事件

這裏寫圖片描述
根據是Poll還是Epoll有不同的實現。select的實質就是去獲取poll和epoll的結果,然後更新自身維護的selector結構對應的狀態

7:接收已經準備好的channel傳過來的數據

在非阻塞模式下,accept會立馬返回
這裏寫圖片描述
Linux accept 實際上就是從監聽狀態的socketfd的連接等待隊列中獲取第一個連接請求,然後新建一個socket返回。

這裏新建的SocketChannelImpl,而之前使用的是ServerSocketChannelImpl。區別在於 SocketChannelImpl支持讀寫數據,而ServerSocketChannelImpl則更多的用於等待連接的到來,充當服務端

接下來,獲取的socket方式同第3步中新建socket

8:從socket中獲取outputStream

outpusStream通過Channels.newOutputStream新建,它會持有accept處新建的SocketChannelImpl,它實際上就是新建OutputStream並重寫它的write方法

9:回寫數據

printWriter的print經過BufferWriter到OutputStreamWriter,再到它的StreamEncoder到它的方法writeBytes執行out.write(bb.array(), bb.arrayOffset() + pos, rem);即socket中重寫的write方法,它的主要實現是調用Channels.writeFully,然後調用Channel自己的SocketChannelImpl.write方法,它核心在於n = IOUtil.write(fd, buf, -1, nd, writeLock);

 static int write(FileDescriptor fd, ByteBuffer src, long position,
                     NativeDispatcher nd, Object lock)
        throws IOException
    {
        //判斷是否是直接內存
        if (src instanceof DirectBuffer)
            return writeFromNativeBuffer(fd, src, position, nd, lock);

        // Substitute a native buffer
        int pos = src.position();
        int lim = src.limit();
        assert (pos <= lim);
        int rem = (pos <= lim ? lim - pos : 0);
        //申請一個DirectBuffer,即通過ByteBuffer.allocateDirect來申請直接內存;
        ByteBuffer bb = Util.getTemporaryDirectBuffer(rem);
        try {
            bb.put(src);
            bb.flip();
            // Do not update src until we see how many bytes were written
            src.position(pos);
            //寫數據,實際上執行的是FileDispatcherImpl的Native方法writ0
            int n = writeFromNativeBuffer(fd, bb, position, nd, lock);
            if (n > 0) {
                // now update src
                src.position(pos + n);
            }
            return n;
        } finally {
            Util.offerFirstTemporaryDirectBuffer(bb);
        }
    }

可以看到這裏有一段從JVM的Buffer拷貝到NativeBuffer中,也就是說NIO的數據寫肯定是從直接內存發送出去的,如果本身不是直接內存則會經過一次內存拷貝。

JNIEXPORT jint JNICALL
Java_sun_nio_ch_FileDispatcherImpl_write0(JNIEnv *env, jclass clazz,
                              jobject fdo, jlong address, jint len)
{
    jint fd = fdval(env, fdo);
    void *buf = (void *)jlong_to_ptr(address);

    return convertReturnVal(env, write(fd, buf, len), JNI_FALSE);
}

最終的寫可以看到用的就是Linux write

Java NIO的本質是什麼?

爲什麼一個Selector管理了多個Channel?

SelectionKey會持有各自操作系統下的SelectorImpl對象,對於PollSelectorImpl的channel註冊內部實際是通過數組存儲了文件描述符和Selector的關係,EpollSelectorImpl的channel註冊則是內部用的HashMap存儲文件描述符和Selector的關係。當讀取到事件的時候,就通過輪詢的方式拿到所有準備好的事件返回,一個個的處理

這裏寫圖片描述

NIO是如何實現的?

它依賴於操作系統本身,對於windows/mac/linux均有不同的版本實現。這裏以Liunx爲例,它實際上就是個使用Linux的一系列方法,比如 read/write/accept等,操作文件描述符

socket是什麼?

socket本身只是獲取通信的服務和端口的一個實現類,對於服務的連接,是通過自身的屬性來處理。而這個屬性impl實際也就是對SOCKS協議的實現。來提供連接和綁定服務。

Java 阻塞IO服務端代碼的hello world怎麼寫?

public class TimeServer {

    private static Charset charset = Charset.forName("US-ASCII");
    private static CharsetEncoder encoder = charset.newEncoder();

    public static void main(String[] args) throws IOException {
        ServerSocketChannel ssc = ServerSocketChannel.open();
        InetSocketAddress isa = new InetSocketAddress(InetAddress.getLocalHost(), 8013);
        ssc.socket().bind(isa);
        for (;;)
        {
            SocketChannel sc = ssc.accept();
            try {
                String now = new Date().toString();
                sc.write(encoder.encode(CharBuffer.wrap(now + "\r\n")));
                System.out.println(sc.socket().getInetAddress() + " : " + now);
                sc.close();
            } finally {
                // Make sure we close the channel (and hence the socket)
                sc.close();
            }
        }
    }

}

它與NIO的區別主要區別在於在於,NIO通過configureBlocking設置爲false,會把它自身的fd設置爲非阻塞,而阻塞IO則沒有,默認阻塞。

Java客戶端的hello world怎麼寫?

public class TimeQuery {

    // Charset and decoder for US-ASCII
    private static Charset charset = Charset.forName("US-ASCII");
    private static CharsetDecoder decoder = charset.newDecoder();

    // Direct byte buffer for reading
    private static ByteBuffer dbuf = ByteBuffer.allocateDirect(1024);

    public static void main(String[] args) {
            try {
                InetSocketAddress isa = new InetSocketAddress(InetAddress.getLocalHost(), 8900);
                SocketChannel sc = null;
                try {

                    // Connect
                    sc = SocketChannel.open();
                    sc.connect(isa);

                    // Read the time from the remote host.  For simplicity we assume
                    // that the time comes back to us in a single packet, so that we
                    // only need to read once.
                    dbuf.clear();
                    sc.read(dbuf);

                    // Print the remote address and the received time
                    dbuf.flip();
                    CharBuffer cb = decoder.decode(dbuf);
                    System.out.print(isa + " : " + cb);

                } finally {
                    // Make sure we close the channel (and hence the socket)
                    if (sc != null)
                        sc.close();
                }
            } catch (IOException x) {
                System.err.println( x);
            }
    }

}

真實的執行實際上也就是Linux connectLinux read

附錄

jdk 7 源碼地址
NIO服務端 源碼地址
IO服務端 源碼地址
客戶端 源碼地址
如何讀open jdk native 源碼
java JNI簡介

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章