BIO到NIO原始碼的一些事兒之NIO 中

知秋z發表於2019-03-04

前言

此係列文章會詳細解讀NIO的功能逐步豐滿的路程,為Reactor-Netty 庫的講解鋪平道路。

關於Java程式設計方法論-Reactor與Webflux的視訊分享,已經完成了Rxjava 與 Reactor,b站地址如下:

Rxjava原始碼解讀與分享:www.bilibili.com/video/av345…

Reactor原始碼解讀與分享:www.bilibili.com/video/av353…

本系列原始碼解讀基於JDK11 api細節可能與其他版本有所差別,請自行解決jdk版本問題。

Channel解讀

接上一篇BIO到NIO原始碼的一些事兒之NIO 上

賦予Channel支援網路socket的能力

我們最初的目的就是為了增強Socket,基於這個基本需求,沒有條件創造條件,於是為了讓Channel擁有網路socket的能力,這裡定義了一個java.nio.channels.NetworkChannel介面。花不多說,我們來看這個介面的定義:

public interface NetworkChannel extends Channel
{
    NetworkChannel bind(SocketAddress local) throws IOException;

    SocketAddress getLocalAddress() throws IOException;

    <T> NetworkChannel setOption(SocketOption<T> name, T value) throws IOException;

    <T> T getOption(SocketOption<T> name) throws IOException;

    Set<SocketOption<?>> supportedOptions();
}
複製程式碼

通過bind(SocketAddress) 方法將socket繫結到本地 SocketAddress上,通過getLocalAddress()方法返回socket繫結的地址,
通過 setOption(SocketOption,Object)getOption(SocketOption)方法設定和查詢socket支援的配置選項。

bind

接下來我們來看 java.nio.channels.ServerSocketChannel抽象類及其實現類sun.nio.ch.ServerSocketChannelImpl對之實現的細節。
首先我們來看其對於bind的實現:

//sun.nio.ch.ServerSocketChannelImpl#bind
@Override
public ServerSocketChannel bind(SocketAddress local, int backlog) throws IOException {
    synchronized (stateLock) {
        ensureOpen();
        //通過localAddress判斷是否已經呼叫過bind
        if (localAddress != null)
            throw new AlreadyBoundException();
        //InetSocketAddress(0)表示繫結到本機的所有地址,由作業系統選擇合適的埠
        InetSocketAddress isa = (local == null)
                                ? new InetSocketAddress(0)
                                : Net.checkAddress(local);
        SecurityManager sm = System.getSecurityManager();
        if (sm != null)
            sm.checkListen(isa.getPort());
        NetHooks.beforeTcpBind(fd, isa.getAddress(), isa.getPort());
        Net.bind(fd, isa.getAddress(), isa.getPort());
        //開啟監聽,s如果引數backlog小於1,預設接受50個連線 
        Net.listen(fd, backlog < 1 ? 50 : backlog);
        localAddress = Net.localAddress(fd);
    }
    return this;
}
複製程式碼

下面我們來看看Net中的bind和listen方法是如何實現的。

Net.bind
//sun.nio.ch.Net#bind(java.io.FileDescriptor, java.net.InetAddress, int)
public static void bind(FileDescriptor fd, InetAddress addr, int port)
        throws IOException
    {
        bind(UNSPEC, fd, addr, port);
    }

static void bind(ProtocolFamily family, FileDescriptor fd,
                    InetAddress addr, int port) throws IOException
{
    //如果傳入的協議域不是IPV4而且支援IPV6,則使用ipv6
    boolean preferIPv6 = isIPv6Available() &&
        (family != StandardProtocolFamily.INET);
    bind0(fd, preferIPv6, exclusiveBind, addr, port);
}

private static native void bind0(FileDescriptor fd, boolean preferIPv6,
                                    boolean useExclBind, InetAddress addr,
                                    int port)
    throws IOException;
複製程式碼

bind0為native方法實現:

JNIEXPORT void JNICALL
Java_sun_nio_ch_Net_bind0(JNIEnv *env, jclass clazz, jobject fdo, jboolean preferIPv6,
                          jboolean useExclBind, jobject iao, int port)
{
    SOCKETADDRESS sa;
    int sa_len = 0;
    int rv = 0;
    //將java的InetAddress轉換為c的struct sockaddr
    if (NET_InetAddressToSockaddr(env, iao, port, &sa, &sa_len,
                                  preferIPv6) != 0) {
        return;//轉換失敗,方法返回
    }
    //呼叫bind方法:int bind(int sockfd, struct sockaddr* addr, socklen_t addrlen) 
    rv = NET_Bind(fdval(env, fdo), &sa, sa_len);
    if (rv != 0) {
        handleSocketError(env, errno);
    }
}

複製程式碼

socket是使用者程式與核心互動資訊的樞紐,它自身沒有網路協議地址和埠號等資訊,在進行網路通訊的時候,必須把一個socket與一個地址相關聯。
很多時候核心會我們自動繫結一個地址,然而有時使用者可能需要自己來完成這個繫結的過程,以滿足實際應用的需要;
最典型的情況是一個伺服器程式需要繫結一個眾所周知的地址或埠以等待客戶來連線。
對於客戶端,很多時候並不需要呼叫bind方法,而是由核心自動繫結;

這裡要注意,繫結歸繫結,在有連線過來的時候會建立一個新的Socket,然後服務端操作這個新的Socket即可。這裡就可以關注accept方法了。由sun.nio.ch.ServerSocketChannelImpl#bind最後,我們知道其通過Net.listen(fd, backlog < 1 ? 50 : backlog)開啟監聽,如果引數backlog小於1,預設接受50個連線。由此,我們來關注下Net.listen方法細節。

Net.listen
//sun.nio.ch.Net#listen
static native void listen(FileDescriptor fd, int backlog) throws IOException;
複製程式碼

可以知道,Net.listennative方法,原始碼如下:

JNIEXPORT void JNICALL
Java_sun_nio_ch_Net_listen(JNIEnv *env, jclass cl, jobject fdo, jint backlog)
{
    if (listen(fdval(env, fdo), backlog) < 0)
        handleSocketError(env, errno);
}
複製程式碼

可以看到底層是呼叫listen實現的,listen函式在一般在呼叫bind之後到呼叫accept之前呼叫,它的函式原型是:
int listen(int sockfd, int backlog)返回值:0表示成功, -1表示失敗

我們再來關注下bind操作中的其他細節,最開始時的ensureOpen()方法判斷:

//sun.nio.ch.ServerSocketChannelImpl#ensureOpen
// @throws ClosedChannelException if channel is closed
private void ensureOpen() throws ClosedChannelException {
    if (!isOpen())
        throw new ClosedChannelException();
}
//java.nio.channels.spi.AbstractInterruptibleChannel#isOpen
public final boolean isOpen() {
        return !closed;
    }
複製程式碼

如果socket關閉,則丟擲ClosedChannelException

我們再來看下Net#checkAddress

//sun.nio.ch.Net#checkAddress(java.net.SocketAddress)
public static InetSocketAddress checkAddress(SocketAddress sa) {
    if (sa == null)//地址為空  
        throw new NullPointerException();
        //非InetSocketAddress型別地址 
    if (!(sa instanceof InetSocketAddress))
        throw new UnsupportedAddressTypeException(); // ## needs arg
    InetSocketAddress isa = (InetSocketAddress)sa;
    //地址不可識別  
    if (isa.isUnresolved())
        throw new UnresolvedAddressException(); // ## needs arg
    InetAddress addr = isa.getAddress();
        //非ip4和ip6地址  
    if (!(addr instanceof Inet4Address || addr instanceof Inet6Address))
        throw new IllegalArgumentException("Invalid address type");
    return isa;
}
複製程式碼

從上面可以看出,bind首先檢查ServerSocket是否關閉,是否繫結地址, 如果既沒有繫結也沒關閉,則檢查繫結的socketaddress是否正確或合法; 然後通過Net工具類的bindlisten,完成實際的ServerSocket地址繫結和開啟監聽,如果繫結是開啟的引數小於1,則預設接受50個連線。

對照我們之前在第一篇中接觸的BIO,我們來看些accept()方法的實現:

//sun.nio.ch.ServerSocketChannelImpl#accept()
@Override
public SocketChannel accept() throws IOException {
    acceptLock.lock();
    try {
        int n = 0;
        FileDescriptor newfd = new FileDescriptor();
        InetSocketAddress[] isaa = new InetSocketAddress[1];

        boolean blocking = isBlocking();
        try {
            begin(blocking);
            do {
                n = accept(this.fd, newfd, isaa);
            } while (n == IOStatus.INTERRUPTED && isOpen());
        } finally {
            end(blocking, n > 0);
            assert IOStatus.check(n);
        }

        if (n < 1)
            return null;
        //針對接受連線的處理通道socketchannelimpl,預設為阻塞模式 
        // newly accepted socket is initially in blocking mode
        IOUtil.configureBlocking(newfd, true);

        InetSocketAddress isa = isaa[0];
        //構建SocketChannelImpl,這個具體在SocketChannelImpl再說  
        SocketChannel sc = new SocketChannelImpl(provider(), newfd, isa);

        // check permitted to accept connections from the remote address
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            try {
                //檢查地址和port許可權
                sm.checkAccept(isa.getAddress().getHostAddress(), isa.getPort());
            } catch (SecurityException x) {
                sc.close();
                throw x;
            }
        }
         //返回socketchannelimpl  
        return sc;

    } finally {
        acceptLock.unlock();
    }
}
複製程式碼

對於accept(this.fd, newfd, isaa),呼叫accept接收socket中已建立的連線,我們之前有在BIO中瞭解過,函式最終會呼叫:int accept(int sockfd,struct sockaddr *addr, socklen_t *addrlen);

  • 如果fd監聽socket的佇列中沒有等待的連線,socket也沒有被標記為Non-blocking,accept()會阻塞直到連線出現;
  • 如果socket被標記為Non-blocking,佇列中也沒有等待的連線,accept()返回錯誤EAGAIN或EWOULDBLOCK

這裡begin(blocking);end(blocking, n > 0);的合作模式我們在InterruptibleChannel 與可中斷 IO這一篇文章中已經涉及過,這裡再次提一下,讓大家看到其應用,此處專注的是等待連線這個過程,期間可以出現異常打斷,這個過程正常結束的話,就會正常往下執行邏輯,不要搞的好像這個Channel要結束了一樣,end(blocking, n > 0)的第二個引數completed也只是在判斷這個等待過程是否結束而已,不要功能範圍擴大化。

supportedOptions

我們再來看下NetworkChannel的其他方法實現,首先來看supportedOptions

//sun.nio.ch.ServerSocketChannelImpl#supportedOptions
@Override
public final Set<SocketOption<?>> supportedOptions() {
    return DefaultOptionsHolder.defaultOptions;
}
//sun.nio.ch.ServerSocketChannelImpl.DefaultOptionsHolder
private static class DefaultOptionsHolder {
    static final Set<SocketOption<?>> defaultOptions = defaultOptions();

    private static Set<SocketOption<?>> defaultOptions() {
        HashSet<SocketOption<?>> set = new HashSet<>();
        set.add(StandardSocketOptions.SO_RCVBUF);
        set.add(StandardSocketOptions.SO_REUSEADDR);
        if (Net.isReusePortAvailable()) {
            set.add(StandardSocketOptions.SO_REUSEPORT);
        }
        set.add(StandardSocketOptions.IP_TOS);
        set.addAll(ExtendedSocketOptions.options(SOCK_STREAM));
        //返回不可修改的HashSet 
        return Collections.unmodifiableSet(set);
    }
}
複製程式碼

對上述配置中的一些配置我們大致來瞅眼:

//java.net.StandardSocketOptions
//socket接受快取大小  
public static final SocketOption<Integer> SO_RCVBUF =
        new StdSocketOption<Integer>("SO_RCVBUF", Integer.class);
//是否可重用地址  
public static final SocketOption<Boolean> SO_REUSEADDR =
        new StdSocketOption<Boolean>("SO_REUSEADDR", Boolean.class);
//是否可重用port
public static final SocketOption<Boolean> SO_REUSEPORT =
        new StdSocketOption<Boolean>("SO_REUSEPORT", Boolean.class);
//Internet協議(IP)標頭(header)中的服務型別(ToS)。
public static final SocketOption<Integer> IP_TOS =
        new StdSocketOption<Integer>("IP_TOS", Integer.class);
複製程式碼

setOption實現

知道了上面的支援配置,我們來看下setOption實現細節:

//sun.nio.ch.ServerSocketChannelImpl#setOption
@Override
public <T> ServerSocketChannel setOption(SocketOption<T> name, T value)
    throws IOException
{
    Objects.requireNonNull(name);
    if (!supportedOptions().contains(name))
        throw new UnsupportedOperationException("`" + name + "` not supported");
    synchronized (stateLock) {
        ensureOpen();

        if (name == StandardSocketOptions.IP_TOS) {
            ProtocolFamily family = Net.isIPv6Available() ?
                StandardProtocolFamily.INET6 : StandardProtocolFamily.INET;
            Net.setSocketOption(fd, family, name, value);
            return this;
        }

        if (name == StandardSocketOptions.SO_REUSEADDR && Net.useExclusiveBind()) {
            // SO_REUSEADDR emulated when using exclusive bind
            isReuseAddress = (Boolean)value;
        } else {
            // no options that require special handling
            Net.setSocketOption(fd, Net.UNSPEC, name, value);
        }
        return this;
    }
}
複製程式碼

這裡,大家就能看到supportedOptions().contains(name)的作用了,首先會進行支援配置的判斷,然後進行正常的設定邏輯。裡面對於Socket配置設定主要執行了Net.setSocketOption,這裡,就只對其程式碼做中文註釋就好,整個邏輯過程沒有太複雜的。

static void setSocketOption(FileDescriptor fd, ProtocolFamily family,
                            SocketOption<?> name, Object value)
    throws IOException
{
    if (value == null)
        throw new IllegalArgumentException("Invalid option value");

    // only simple values supported by this method
    Class<?> type = name.type();

    if (extendedOptions.isOptionSupported(name)) {
        extendedOptions.setOption(fd, name, value);
        return;
    }
    //非整形和布林型,則丟擲斷言錯誤  
    if (type != Integer.class && type != Boolean.class)
        throw new AssertionError("Should not reach here");

    // special handling
    if (name == StandardSocketOptions.SO_RCVBUF ||
        name == StandardSocketOptions.SO_SNDBUF)
    {
        //判斷接受和傳送緩衝區大小  
        int i = ((Integer)value).intValue();
        if (i < 0)
            throw new IllegalArgumentException("Invalid send/receive buffer size");
    }
        //緩衝區有資料,延遲關閉socket的的時間 
    if (name == StandardSocketOptions.SO_LINGER) {
        int i = ((Integer)value).intValue();
        if (i < 0)
            value = Integer.valueOf(-1);
        if (i > 65535)
            value = Integer.valueOf(65535);
    }
    //UDP單播  
    if (name == StandardSocketOptions.IP_TOS) {
        int i = ((Integer)value).intValue();
        if (i < 0 || i > 255)
            throw new IllegalArgumentException("Invalid IP_TOS value");
    }
    //UDP多播  
    if (name == StandardSocketOptions.IP_MULTICAST_TTL) {
        int i = ((Integer)value).intValue();
        if (i < 0 || i > 255)
            throw new IllegalArgumentException("Invalid TTL/hop value");
    }

    // map option name to platform level/name
    OptionKey key = SocketOptionRegistry.findOption(name, family);
    if (key == null)
        throw new AssertionError("Option not found");

    int arg;
    //轉換配置引數值  
    if (type == Integer.class) {
        arg = ((Integer)value).intValue();
    } else {
        boolean b = ((Boolean)value).booleanValue();
        arg = (b) ? 1 : 0;
    }

    boolean mayNeedConversion = (family == UNSPEC);
    boolean isIPv6 = (family == StandardProtocolFamily.INET6);
    //設定檔案描述符的值及其他
    setIntOption0(fd, mayNeedConversion, key.level(), key.name(), arg, isIPv6);
}
複製程式碼

getOption

接下來,我們來看getOption實現,原始碼如下:

//sun.nio.ch.ServerSocketChannelImpl#getOption
@Override
@SuppressWarnings("unchecked")
public <T> T getOption(SocketOption<T> name)
    throws IOException
{
    Objects.requireNonNull(name);
    //非通道支援選項,則丟擲UnsupportedOperationException  
    if (!supportedOptions().contains(name))
        throw new UnsupportedOperationException("`" + name + "` not supported");

    synchronized (stateLock) {
        ensureOpen();
        if (name == StandardSocketOptions.SO_REUSEADDR && Net.useExclusiveBind()) {
            // SO_REUSEADDR emulated when using exclusive bind
            return (T)Boolean.valueOf(isReuseAddress);
        }
        //假如獲取的不是上面的配置,則委託給Net來處理 
        // no options that require special handling
        return (T) Net.getSocketOption(fd, Net.UNSPEC, name);
    }
}
//sun.nio.ch.Net#getSocketOption
static Object getSocketOption(FileDescriptor fd, ProtocolFamily family,
                                SocketOption<?> name)
    throws IOException
{
    Class<?> type = name.type();

    if (extendedOptions.isOptionSupported(name)) {
        return extendedOptions.getOption(fd, name);
    }
    //只支援整形和布林型,否則丟擲斷言錯誤  
    // only simple values supported by this method
    if (type != Integer.class && type != Boolean.class)
        throw new AssertionError("Should not reach here");

    // map option name to platform level/name
    OptionKey key = SocketOptionRegistry.findOption(name, family);
    if (key == null)
        throw new AssertionError("Option not found");

    boolean mayNeedConversion = (family == UNSPEC);
    //獲取檔案描述的選項配置 
    int value = getIntOption0(fd, mayNeedConversion, key.level(), key.name());

    if (type == Integer.class) {
        return Integer.valueOf(value);
    } else {
        //我們要看到前面支援配置處的原始碼其支援的型別要麼是Boolean,要麼是Integer
        //所以,返回值為Boolean.FALSE 或 Boolean.TRUE也就不足為奇了
        return (value == 0) ? Boolean.FALSE : Boolean.TRUE;
    }
}
複製程式碼

ServerSocketChannel與ServerSocket在bind處的異同

Net.bind一節中,我們最後說了一個注意點,每個連線過來的時候都會建立一個Socket來供此連線進行操作,這個在accept方法中可以看到,其在得到連線之後,就 new SocketChannelImpl(provider(), newfd, isa)這個物件。那這裡,就引出一個話題,我們在使用bind方法的時候,是不是也應該繫結到一個Socket之上呢,那之前bio是怎麼做呢,我們先來回顧一下。
我們之前在呼叫java.net.ServerSocket#ServerSocket(int, int, java.net.InetAddress)方法的時候,裡面有一個setImpl():

//java.net.ServerSocket
 public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException {
        setImpl();
        if (port < 0 || port > 0xFFFF)
            throw new IllegalArgumentException(
                       "Port value out of range: " + port);
        if (backlog < 1)
          backlog = 50;
        try {
            bind(new InetSocketAddress(bindAddr, port), backlog);
        } catch(SecurityException e) {
            close();
            throw e;
        } catch(IOException e) {
            close();
            throw e;
        }
    }
//java.net.ServerSocket#setImpl
private void setImpl() {
        if (factory != null) {
            impl = factory.createSocketImpl();
            checkOldImpl();
        } else {
            // No need to do a checkOldImpl() here, we know it`s an up to date
            // SocketImpl!
            impl = new SocksSocketImpl();
        }
        if (impl != null)
            impl.setServerSocket(this);
    }
複製程式碼

但是,我們此處的重點在bind(new InetSocketAddress(bindAddr, port), backlog);,這裡的程式碼如下:

//java.net.ServerSocket
public void bind(SocketAddress endpoint, int backlog) throws IOException {
        if (isClosed())
            throw new SocketException("Socket is closed");
        if (!oldImpl && isBound())
            throw new SocketException("Already bound");
        if (endpoint == null)
            endpoint = new InetSocketAddress(0);
        if (!(endpoint instanceof InetSocketAddress))
            throw new IllegalArgumentException("Unsupported address type");
        InetSocketAddress epoint = (InetSocketAddress) endpoint;
        if (epoint.isUnresolved())
            throw new SocketException("Unresolved address");
        if (backlog < 1)
          backlog = 50;
        try {
            SecurityManager security = System.getSecurityManager();
            if (security != null)
                security.checkListen(epoint.getPort());
                //重點!!
            getImpl().bind(epoint.getAddress(), epoint.getPort());
            getImpl().listen(backlog);
            bound = true;
        } catch(SecurityException e) {
            bound = false;
            throw e;
        } catch(IOException e) {
            bound = false;
            throw e;
        }
    }
複製程式碼

我們有看到 getImpl()我標示了重點,這裡面做了什麼,我們走進去:

//java.net.ServerSocket#getImpl
SocketImpl getImpl() throws SocketException {
    if (!created)
        createImpl();
    return impl;
}
複製程式碼

在整個過程中created還是物件剛建立時的初始值,為false,那麼,鐵定會進入createImpl()方法中:

//java.net.ServerSocket#createImpl
void createImpl() throws SocketException {
    if (impl == null)
        setImpl();
    try {
        impl.create(true);
        created = true;
    } catch (IOException e) {
        throw new SocketException(e.getMessage());
    }
}
複製程式碼

而此處,因為前面impl已經賦值,所以,會走impl.create(true),進而將created設定為true。而此刻,終於到我想講的重點了:

//java.net.AbstractPlainSocketImpl#create
protected synchronized void create(boolean stream) throws IOException {
    this.stream = stream;
    if (!stream) {
        ResourceManager.beforeUdpCreate();
        // only create the fd after we know we will be able to create the socket
        fd = new FileDescriptor();
        try {
            socketCreate(false);
            SocketCleanable.register(fd);
        } catch (IOException ioe) {
            ResourceManager.afterUdpClose();
            fd = null;
            throw ioe;
        }
    } else {
        fd = new FileDescriptor();
        socketCreate(true);
        SocketCleanable.register(fd);
    }
    if (socket != null)
        socket.setCreated();
    if (serverSocket != null)
        serverSocket.setCreated();
}

複製程式碼

可以看到,socketCreate(true);,它的實現如下:

@Override
void socketCreate(boolean stream) throws IOException {
    if (fd == null)
        throw new SocketException("Socket closed");

    int newfd = socket0(stream);

    fdAccess.set(fd, newfd);
}
複製程式碼

通過本地方法socket0(stream)得到了一個檔案描述符,由此,Socket建立了出來,然後進行相應的繫結。
我們再把眼光放回到sun.nio.ch.ServerSocketChannelImpl#accept()中,這裡new的SocketChannelImpl物件是得到連線之後做的事情,那對於伺服器來講,繫結時候用的Socket呢,這裡,我們在使用ServerSocketChannel的時候,往往要使用JDK給我們提供的對我統一的方法open,也是為了降低我們使用的複雜度,這裡是java.nio.channels.ServerSocketChannel#open:

//java.nio.channels.ServerSocketChannel#open
public static ServerSocketChannel open() throws IOException {
    return SelectorProvider.provider().openServerSocketChannel();
}
//sun.nio.ch.SelectorProviderImpl#openServerSocketChannel
public ServerSocketChannel openServerSocketChannel() throws IOException {
    return new ServerSocketChannelImpl(this);
}
//sun.nio.ch.ServerSocketChannelImpl#ServerSocketChannelImpl(SelectorProvider)
ServerSocketChannelImpl(SelectorProvider sp) throws IOException {
    super(sp);
    this.fd =  Net.serverSocket(true);
    this.fdVal = IOUtil.fdVal(fd);
}
//sun.nio.ch.Net#serverSocket
static FileDescriptor serverSocket(boolean stream) {
    return IOUtil.newFD(socket0(isIPv6Available(), stream, true, fastLoopback));
}
複製程式碼

可以看到,只要new了一個ServerSocketChannelImpl物件,就相當於拿到了一個socket然後bind也就有著落了。但是,我們要注意下細節ServerSocketChannel#open得到的是ServerSocketChannel型別。我們accept到一個客戶端來的連線後,應該在客戶端與伺服器之間建立一個Socket通道來供兩者通訊操作的,所以,sun.nio.ch.ServerSocketChannelImpl#accept()中所做的是SocketChannel sc = new SocketChannelImpl(provider(), newfd, isa);,得到的是SocketChannel型別的物件,這樣,就可以將Socket的讀寫資料的方法定義在這個類裡面。

由ServerSocketChannel的socket方法延伸的

關於ServerSocketChannel,我們還有方法需要接觸一下,如socket():

//sun.nio.ch.ServerSocketChannelImpl#socket
@Override
public ServerSocket socket() {
    synchronized (stateLock) {
        if (socket == null)
            socket = ServerSocketAdaptor.create(this);
        return socket;
    }
}
複製程式碼

我們看到了ServerSocketAdaptor,我們通過此類的註釋可知,這是一個和ServerSocket呼叫一樣,但是底層是用ServerSocketChannelImpl來實現的一個類,其適配是的目的是適配我們使用ServerSocket的方式,所以該ServerSocketAdaptor繼承ServerSocket並按順序重寫了它的方法,所以,我們在寫這塊兒程式碼的時候也就有了新的選擇。

InterruptibleChannel 與可中斷 IO這一篇文章中已經涉及過java.nio.channels.spi.AbstractInterruptibleChannel#close的實現,這裡,我們再來回顧下其中的某些細節,順帶引出我們新的話題:

//java.nio.channels.spi.AbstractInterruptibleChannel#close
public final void close() throws IOException {
    synchronized (closeLock) {
        if (closed)
            return;
        closed = true;
        implCloseChannel();
    }
}
//java.nio.channels.spi.AbstractSelectableChannel#implCloseChannel
protected final void implCloseChannel() throws IOException {
        implCloseSelectableChannel();

        // clone keys to avoid calling cancel when holding keyLock
        SelectionKey[] copyOfKeys = null;
        synchronized (keyLock) {
            if (keys != null) {
                copyOfKeys = keys.clone();
            }
        }

        if (copyOfKeys != null) {
            for (SelectionKey k : copyOfKeys) {
                if (k != null) {
                    k.cancel();   // invalidate and adds key to cancelledKey set
                }
            }
        }
    }
//sun.nio.ch.ServerSocketChannelImpl#implCloseSelectableChannel
@Override
protected void implCloseSelectableChannel() throws IOException {
    assert !isOpen();

    boolean interrupted = false;
    boolean blocking;

    // set state to ST_CLOSING
    synchronized (stateLock) {
        assert state < ST_CLOSING;
        state = ST_CLOSING;
        blocking = isBlocking();
    }

    // wait for any outstanding accept to complete
    if (blocking) {
        synchronized (stateLock) {
            assert state == ST_CLOSING;
            long th = thread;
            if (th != 0) {
                //本地執行緒不為null,則本地Socket預先關閉
                //並通知執行緒通知關閉
                nd.preClose(fd);
                NativeThread.signal(th);

                // wait for accept operation to end
                while (thread != 0) {
                    try {
                        stateLock.wait();
                    } catch (InterruptedException e) {
                        interrupted = true;
                    }
                }
            }
        }
    } else {
        // non-blocking mode: wait for accept to complete
        acceptLock.lock();
        acceptLock.unlock();
    }

    // set state to ST_KILLPENDING
    synchronized (stateLock) {
        assert state == ST_CLOSING;
        state = ST_KILLPENDING;
    }

    // close socket if not registered with Selector
    //如果未在Selector上註冊,直接kill掉
    //即關閉檔案描述  
    if (!isRegistered())
        kill();

    // restore interrupt status
    //印證了我們上一篇中在非同步打斷中若是通過執行緒的中斷方法中斷執行緒的話
    //最後要設定該執行緒狀態是interrupt
    if (interrupted)
        Thread.currentThread().interrupt();
}

@Override
public void kill() throws IOException {
    synchronized (stateLock) {
        if (state == ST_KILLPENDING) {
            state = ST_KILLED;
            nd.close(fd);
        }
    }
}
複製程式碼
channel的close()應用

也是因為close()並沒有在InterruptibleChannel 與可中斷 IO這一篇文章中進行具體的講解應用,這裡其應用的更多是在SocketChannel這裡,其更多的涉及到客戶端與服務端建立連線交換資料,所以斷開連線後,將不用的Channel關閉是很正常的。
這裡,在sun.nio.ch.ServerSocketChannelImpl#accept()中的原始碼中:

@Override
public SocketChannel accept() throws IOException {
        ...
        // newly accepted socket is initially in blocking mode
        IOUtil.configureBlocking(newfd, true);

        InetSocketAddress isa = isaa[0];
        SocketChannel sc = new SocketChannelImpl(provider(), newfd, isa);

        // check permitted to accept connections from the remote address
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            try {
                sm.checkAccept(isa.getAddress().getHostAddress(), isa.getPort());
            } catch (SecurityException x) {
                sc.close();
                throw x;
            }
        }
        return sc;

    } finally {
        acceptLock.unlock();
    }
}
複製程式碼

這裡通過對所接收的連線的遠端地址做合法性判斷,假如驗證出現異常,則關閉上面建立的SocketChannel
還有一個關於close()的實際用法,在客戶端建立連線的時候,如果連線出異常,同樣是要關閉所建立的Socket:

//java.nio.channels.SocketChannel#open(java.net.SocketAddress)
public static SocketChannel open(SocketAddress remote)
        throws IOException
    {
        SocketChannel sc = open();
        try {
            sc.connect(remote);
        } catch (Throwable x) {
            try {
                sc.close();
            } catch (Throwable suppressed) {
                x.addSuppressed(suppressed);
            }
            throw x;
        }
        assert sc.isConnected();
        return sc;
    }
複製程式碼

接著,我們在implCloseSelectableChannel中會發現nd.preClose(fd);nd.close(fd);,這個在SocketChannelImplServerSocketChannelImpl兩者對於implCloseSelectableChannel實現中都可以看到,這個nd是什麼,這裡,我們拿ServerSocketChannelImpl來講,在這個類的最後面有一段靜態程式碼塊(SocketChannelImpl同理),也就是在這個類載入的時候就會執行:

//C:/Program Files/Java/jdk-11.0.1/lib/src.zip!/java.base/sun/nio/ch/ServerSocketChannelImpl.java:550
static {
     //載入nio,net資源庫
        IOUtil.load();
        initIDs();
        nd = new SocketDispatcher();
    }
複製程式碼

也就是說,在ServerSocketChannelImpl這個類位元組碼載入的時候,就會建立SocketDispatcher物件。通過SocketDispatcher允許在不同的平臺呼叫不同的本地方法進行讀寫操作,然後基於這個類,我們就可以在sun.nio.ch.SocketChannelImpl做Socket的I/O操作。

//sun.nio.ch.SocketDispatcher
class SocketDispatcher extends NativeDispatcher
{

    static {
        IOUtil.load();
    }
    //讀操作  
    int read(FileDescriptor fd, long address, int len) throws IOException {
        return read0(fd, address, len);
    }

    long readv(FileDescriptor fd, long address, int len) throws IOException {
        return readv0(fd, address, len);
    }
    //寫操作  
    int write(FileDescriptor fd, long address, int len) throws IOException {
        return write0(fd, address, len);
    }

    long writev(FileDescriptor fd, long address, int len) throws IOException {
        return writev0(fd, address, len);
    }
    //預關閉檔案描述符
    void preClose(FileDescriptor fd) throws IOException {
        preClose0(fd);
    }
    //關閉檔案描述
    void close(FileDescriptor fd) throws IOException {
        close0(fd);
    }

    //-- Native methods
    static native int read0(FileDescriptor fd, long address, int len)
        throws IOException;

    static native long readv0(FileDescriptor fd, long address, int len)
        throws IOException;

    static native int write0(FileDescriptor fd, long address, int len)
        throws IOException;

    static native long writev0(FileDescriptor fd, long address, int len)
        throws IOException;

    static native void preClose0(FileDescriptor fd) throws IOException;

    static native void close0(FileDescriptor fd) throws IOException;
}
複製程式碼

FileDescriptor

我們有看到FileDescriptor在前面程式碼中有大量的出現,這裡,我們對它來專門介紹。通過FileDescriptor 這個類的例項來充當底層機器特定結構的不透明處理,表示開啟檔案,開啟socket或其他位元組源或接收器。
檔案描述符的主要用途是建立一個 FileInputStream或 FileOutputStream來包含它。
注意: 應用程式不應建立自己的檔案描述符。
我們來看其部分原始碼:

public final class FileDescriptor {

    private int fd;

    private long handle;

    private Closeable parent;
    private List<Closeable> otherParents;
    private boolean closed;

    /**
     * true, if file is opened for appending.
     */
    private boolean append;

    static {
        initIDs();
    }
    /**
     * 在未明確關閉FileDescriptor的情況下進行清理.
     */
    private PhantomCleanable<FileDescriptor> cleanup;

    /**
     * 構造一個無效的FileDescriptor物件,fd或handle會在之後進行設定
     */
    public FileDescriptor() {
        fd = -1;
        handle = -1;
    }

    /**
     * Used for standard input, output, and error only.
     * For Windows the corresponding handle is initialized.
     * For Unix the append mode is cached.
     * 僅用於標準輸入,輸出和錯誤。
     * 對於Windows,初始化相應的控制程式碼。
     * 對於Unix,快取附加模式。
     * @param fd the raw fd number (0, 1, 2)
     */
    private FileDescriptor(int fd) {
        this.fd = fd;
        this.handle = getHandle(fd);
        this.append = getAppend(fd);
    }
    ...
}
複製程式碼

我們平時所用的標準輸入,輸出,錯誤流的控制程式碼可以如下,通常,我們不會直接使用它們,而是使用java.lang.System.injava.lang.System#outjava.lang.System#err:

public static final FileDescriptor in = new FileDescriptor(0);
public static final FileDescriptor out = new FileDescriptor(1);
public static final FileDescriptor err = new FileDescriptor(2);
複製程式碼

測試該檔案描述符是否有效可以使用如下方法:

//java.io.FileDescriptor#valid
public boolean valid() {
        return (handle != -1) || (fd != -1);
    }
複製程式碼

返回值為true的話,那麼這個檔案描述符物件所代表的socket 檔案操作或其他活動的網路連線都是有效的,反之,false則是無效。
更多內容,讀者可以自行深入原始碼,此處就不過多解釋了。為了讓大家可以更好的理解上述內容,我們會在後面的部分還要進一步涉及一下。

NIO包下SocketChannel解讀

在前面,我們已經接觸了SocketChannel,這裡,來接觸下細節。

同樣,我們也可以通過呼叫此類的open方法來建立socket channel。這裡需要注意:

  • 無法為任意預先存在的socket建立channel
  • 新建立的socket channel已開啟但尚未連線。
  • 嘗試在未連線的channel上呼叫I/O操作將導致丟擲NotYetConnectedException
  • 可以通過呼叫connect方法連線socket channel;
  • 一旦連線後,socket channel會保持連線狀態,直到它關閉。
  • 是否有連線socket channel可以通過確定呼叫其isConnected方法。

socket channel支援 非阻塞連線:

  • 可以先建立socket channel,然後可以通過 connect 方法建立到遠端socket的連線。
  • 通過呼叫finishConnect方法來結束連線。
  • 判斷是否正在進行連線操作可以通過呼叫isConnectionPending方法來確定。

socket channel支援非同步關閉,類似於Channel類中的非同步關閉操作。

  • 如果socket的輸入端被一個執行緒關閉而另一個執行緒在此socket channel上因在進行讀操作而被阻塞,那麼被阻塞執行緒中的讀操作將不讀取任何位元組並將返回 -1
  • 如果socket的輸出端被一個執行緒關閉而另一個執行緒在socket channel上因在進行寫操作而被阻塞,則被阻塞的執行緒將收到AsynchronousCloseException

接下來,我們來看其具體實現方法。

ServerSocketChannel與SocketChannel的open()

//java.nio.channels.SocketChannel#open()
public static SocketChannel open() throws IOException {
    return SelectorProvider.provider().openSocketChannel();
}
//java.nio.channels.SocketChannel#open(java.net.SocketAddress)
//這個方法省的我們再次呼叫connect了
public static SocketChannel open(SocketAddress remote)
    throws IOException
{
    //預設是堵塞的,這個在AbstractSelectableChannel處討論過了
    SocketChannel sc = open();
    try {
        sc.connect(remote);
    } catch (Throwable x) {
        try {
            sc.close();
        } catch (Throwable suppressed) {
            x.addSuppressed(suppressed);
        }
        throw x;
    }
    assert sc.isConnected();
    return sc;
}
//sun.nio.ch.SelectorProviderImpl#openSocketChannel
public SocketChannel openSocketChannel() throws IOException {
    return new SocketChannelImpl(this);
}
//sun.nio.ch.SocketChannelImpl#SocketChannelImpl(java.nio.channels.spi.SelectorProvider)
SocketChannelImpl(SelectorProvider sp) throws IOException {
    super(sp);
     //呼叫socket函式,true表示TCP
    this.fd = Net.socket(true);
    this.fdVal = IOUtil.fdVal(fd);
}
//sun.nio.ch.Net#socket(boolean)
static FileDescriptor socket(boolean stream) throws IOException {
    return socket(UNSPEC, stream);
}
//sun.nio.ch.Net#socket(java.net.ProtocolFamily, boolean)
static FileDescriptor socket(ProtocolFamily family, boolean stream)
    throws IOException {
    boolean preferIPv6 = isIPv6Available() &&
        (family != StandardProtocolFamily.INET);
    return IOUtil.newFD(socket0(preferIPv6, stream, false, fastLoopback));
}
//sun.nio.ch.IOUtil#newFD
public static FileDescriptor newFD(int i) {
    FileDescriptor fd = new FileDescriptor();
    setfdVal(fd, i);
    return fd;
}
static native void setfdVal(FileDescriptor fd, int value);
複製程式碼

關於Net.socket(true),我們前面已經提到過了,這裡,通過其底層原始碼來再次調教下 (此處不想看可以跳過):

JNIEXPORT jint JNICALL
Java_sun_nio_ch_Net_socket0(JNIEnv *env, jclass cl, jboolean preferIPv6,
                            jboolean stream, jboolean reuse, jboolean ignored)
{
    int fd;
    //位元組流還是資料包,TCP對應SOCK_STREAM,UDP對應SOCK_DGRAM,此處傳入的stream=true;
    int type = (stream ? SOCK_STREAM : SOCK_DGRAM);
    //判斷是IPV6還是IPV4
    int domain = (ipv6_available() && preferIPv6) ? AF_INET6 : AF_INET;

    //呼叫Linux的socket函式,domain為代表協議;
    //type為套接字型別,protocol設定為0來表示使用預設的傳輸協議
    fd = socket(domain, type, 0);
    //出錯
    if (fd < 0) {
        return handleSocketError(env, errno);
    }

    /* Disable IPV6_V6ONLY to ensure dual-socket support */
    if (domain == AF_INET6) {
        int arg = 0;
        //arg=1設定ipv6的socket只接收ipv6地址的報文,arg=0表示也可接受ipv4的請求
        if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, (char*)&arg,
                       sizeof(int)) < 0) {
            JNU_ThrowByNameWithLastError(env,
                                         JNU_JAVANETPKG "SocketException",
                                         "Unable to set IPV6_V6ONLY");
            close(fd);
            return -1;
        }
    }

    //SO_REUSEADDR有四種用途:
    //1.當有一個有相同本地地址和埠的socket1處於TIME_WAIT狀態時,而你啟動的程式的socket2要佔用該地址和埠,你的程式就要用到該選項。 
    //2.SO_REUSEADDR允許同一port上啟動同一伺服器的多個例項(多個程式)。但每個例項繫結的IP地址是不能相同的。
    //3.SO_REUSEADDR允許單個程式繫結相同的埠到多個socket上,但每個socket繫結的ip地址不同。 
   //4.SO_REUSEADDR允許完全相同的地址和埠的重複繫結。但這隻用於UDP的多播,不用於TCP;
    if (reuse) {
        int arg = 1;
        if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (char*)&arg,
                       sizeof(arg)) < 0) {
            JNU_ThrowByNameWithLastError(env,
                                         JNU_JAVANETPKG "SocketException",
                                         "Unable to set SO_REUSEADDR");
            close(fd);
            return -1;
        }
    }

#if defined(__linux__)
    if (type == SOCK_DGRAM) {
        int arg = 0;
        int level = (domain == AF_INET6) ? IPPROTO_IPV6 : IPPROTO_IP;
        if ((setsockopt(fd, level, IP_MULTICAST_ALL, (char*)&arg, sizeof(arg)) < 0) &&
            (errno != ENOPROTOOPT)) {
            JNU_ThrowByNameWithLastError(env,
                                         JNU_JAVANETPKG "SocketException",
                                         "Unable to set IP_MULTICAST_ALL");
            close(fd);
            return -1;
        }
    }

     //IPV6_MULTICAST_HOPS用於控制多播的範圍,
     // 1表示只在本地網路轉發,
     //更多介紹請參考(http://www.ctt.sbras.ru/cgi-bin/www/unix_help/unix-man?ip6+4);
    /* By default, Linux uses the route default */
    if (domain == AF_INET6 && type == SOCK_DGRAM) {
        int arg = 1;
        if (setsockopt(fd, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, &arg,
                       sizeof(arg)) < 0) {
            JNU_ThrowByNameWithLastError(env,
                                         JNU_JAVANETPKG "SocketException",
                                         "Unable to set IPV6_MULTICAST_HOPS");
            close(fd);
            return -1;
        }
    }
#endif
    return fd;
}

複製程式碼

Linux 3.9之後加入了SO_REUSEPORT配置,這個配置很強大,多個socket(不管是處於監聽還是非監聽,不管是TCP還是UDP)只要在繫結之前設定了SO_REUSEPORT屬性,那麼就可以繫結到完全相同的地址和埠。
為了阻止”port 劫持”(Port hijacking)有一個特別的限制:所有希望共享源地址和埠的socket都必須擁有相同的有效使用者id(effective user ID)。這樣一個使用者就不能從另一個使用者那裡”偷取”埠。另外,核心在處理SO_REUSEPORT socket的時候使用了其它系統上沒有用到的”特殊技巧”:

  • 對於UDP socket,核心嘗試平均的轉發資料包;
  • 對於TCP監聽socket,核心嘗試將新的客戶連線請求(由accept返回)平均的交給共享同一地址和埠的socket(伺服器監聽socket)。

例如:一個簡單的伺服器程式的多個例項可以使用SO_REUSEPORT socket,這樣就實現一個簡單的負載均衡,因為核心已經把請求的分配都做了。

在前面的程式碼中可以看到,在這個socket建立成功之後,呼叫IOUtil.newFD建立了檔案描述符
。這裡,我只是想知道這個Socket是可以輸入呢,還是可以讀呢,還是有錯呢,參考FileDescriptor這一節最後那幾個標準狀態的設定,其實這裡也是一樣,因為我們要往Socket中寫和讀,其標準狀態無非就這三種:輸入,輸出,出錯。而這個Socket是繫結在SocketChannel上的,那就把FileDescriptor也繫結到上面即可,這樣我們就可以獲取到它的狀態了。由於FileDescriptor沒有提供外部設定fd的方法,setfdVal是通過本地方法實現的:

JNIEXPORT void JNICALL
Java_sun_nio_ch_IOUtil_setfdVal(JNIEnv *env, jclass clazz, jobject fdo, jint val)
{
    (*env)->SetIntField(env, fdo, fd_fdID, val);
}
複製程式碼

假如各位有對Linux下的shell程式設計或者命令有了解的話,我們知道,shell對報錯進行重定向要使用2>,也就是將錯誤資訊由2號所指向的通道寫出,這裡0和1 同樣指向一個通道。此處同樣也代表了狀態,這樣就可以對代表Socket的狀態進行操作了,也就是改變SelectionKeyinterest ops,即首先對SelectionKey按輸入輸出型別進行分類,然後我們的讀寫狀態的操作也就有著落了。此處我們打個戳,在下一篇中會對其進行細節講解。

我們迴歸到SocketChannelopen方法中。我們可以看到,SelectorProvider.provider().openSocketChannel()返回的是SocketChannelImpl物件例項。在SocketChannelImpl(SelectorProvider sp)中我們並未看到其對this.state進行值操作,也就是其預設為0,即ST_UNCONNECTED(未連線狀態),同時Socket預設是堵塞的。
所以,一般情況下,當採用非同步方式時,使用不帶引數的open方法比較常見,這樣,我們會隨之呼叫configureBlocking來設定非堵塞。

SocketChannel的connect解讀

由前面可知,我們呼叫connect方法連線到遠端伺服器,其原始碼如下:

//sun.nio.ch.SocketChannelImpl#connect
@Override
public boolean connect(SocketAddress sa) throws IOException {
    InetSocketAddress isa = Net.checkAddress(sa);
    SecurityManager sm = System.getSecurityManager();
    if (sm != null)
        sm.checkConnect(isa.getAddress().getHostAddress(), isa.getPort());

    InetAddress ia = isa.getAddress();
    if (ia.isAnyLocalAddress())
        ia = InetAddress.getLocalHost();

    try {
        readLock.lock();
        try {
            writeLock.lock();
            try {
                int n = 0;
                boolean blocking = isBlocking();
                try {
                    //支援執行緒中斷,通過設定當前執行緒的Interruptible blocker屬性實現
                    beginConnect(blocking, isa);
                    do {
                    //呼叫connect函式實現,如果採用堵塞模式,會一直等待,直到成功或出//現異常
                        n = Net.connect(fd, ia, isa.getPort());
                    } while (n == IOStatus.INTERRUPTED && isOpen());
                } finally {
                    endConnect(blocking, (n > 0));
                }
                assert IOStatus.check(n);
                //連線成功
                return n > 0;
            } finally {
                writeLock.unlock();
            }
        } finally {
            readLock.unlock();
        }
    } catch (IOException ioe) {
        // connect failed, close the channel
        close();
        throw SocketExceptions.of(ioe, isa);
    }
}
複製程式碼

關於beginConnectendConnect,是針對AbstractInterruptibleChannelbegin()end方法的一種增強。這裡我們需要知道的是,假如是非阻塞Channel的話,我們無須去關心連線過程的打斷。顧名思義,只有阻塞等待才需要去考慮打斷這一場景的出現。剩下的細節我已經在程式碼中進行了完整的註釋,讀者可自行檢視。

//sun.nio.ch.SocketChannelImpl#beginConnect
private void beginConnect(boolean blocking, InetSocketAddress isa)
    throws IOException
{   //只有阻塞的時候才會進入begin
    if (blocking) {
        // set hook for Thread.interrupt
        //支援執行緒中斷,通過設定當前執行緒的Interruptible blocker屬性實現
        begin();
    }
    synchronized (stateLock) {
        //預設為open, 除非呼叫了close方法
        ensureOpen();
        //檢查連線狀態
        int state = this.state;
        if (state == ST_CONNECTED)
            throw new AlreadyConnectedException();
        if (state == ST_CONNECTIONPENDING)
            throw new ConnectionPendingException();
        //斷言當前的狀態是否是未連線狀態,如果是,賦值表示正在連線中
        assert state == ST_UNCONNECTED;
        //表示正在連線中
        this.state = ST_CONNECTIONPENDING;
        //只有未繫結本地地址也就是說未呼叫bind方法才執行,
        //該方法在ServerSocketChannel中也見過
        if (localAddress == null)
            NetHooks.beforeTcpConnect(fd, isa.getAddress(), isa.getPort());
        remoteAddress = isa;

        if (blocking) {
            // record thread so it can be signalled if needed
            readerThread = NativeThread.current();
        }
    }
}
複製程式碼

在連線過程中,我們需要注意的就是幾個連線的狀態:ST_UNCONNECTEDST_CONNECTEDST_CONNECTIONPENDINGST_CLOSINGST_KILLPENDINGST_KILLED,也是因為其是一個公共狀態,可能會有多個執行緒對其進行連線操作的。所以,state被定義為一個volatile變數,這個變數在改變的時候需要有stateLock這個物件來作為synchronized鎖物件來控制同步操作的。

//sun.nio.ch.SocketChannelImpl#endConnect
private void endConnect(boolean blocking, boolean completed)
    throws IOException
{
    endRead(blocking, completed);
    //當上面程式碼中n>0,說明連線成功,更新狀態為ST_CONNECTED
    if (completed) {
        synchronized (stateLock) {
            if (state == ST_CONNECTIONPENDING) {
                localAddress = Net.localAddress(fd);
                state = ST_CONNECTED;
            }
        }
    }
}
//sun.nio.ch.SocketChannelImpl#endRead
private void endRead(boolean blocking, boolean completed)
    throws AsynchronousCloseException
{   //當阻塞狀態下的話,才進入
    if (blocking) {
        synchronized (stateLock) {
            readerThread = 0;
            // notify any thread waiting in implCloseSelectableChannel
            if (state == ST_CLOSING) {
                stateLock.notifyAll();
            }
        }
        //和begin成對出現,當執行緒中斷時,丟擲ClosedByInterruptException
        // remove hook for Thread.interrupt
        end(completed);
    }
}
複製程式碼

我們來關注connect中的Net.connect(fd, ia, isa.getPort())方法:

//sun.nio.ch.Net#connect
static int connect(FileDescriptor fd, InetAddress remote, int remotePort)
    throws IOException
{
    return connect(UNSPEC, fd, remote, remotePort);
}
//sun.nio.ch.Net#connect
static int connect(ProtocolFamily family, FileDescriptor fd, InetAddress remote, int remotePort)
    throws IOException
{
    boolean preferIPv6 = isIPv6Available() &&
        (family != StandardProtocolFamily.INET);
    return connect0(preferIPv6, fd, remote, remotePort);
}
複製程式碼

該方法最終會呼叫native方法,具體註釋如下:

JNIEXPORT jint JNICALL
Java_sun_nio_ch_Net_connect0(JNIEnv *env, jclass clazz, jboolean preferIPv6,
                             jobject fdo, jobject iao, jint port)
{
    SOCKETADDRESS sa;
    int sa_len = 0;
    int rv;
    //地址轉換為struct sockaddr格式
    if (NET_InetAddressToSockaddr(env, iao, port, &sa, &sa_len, preferIPv6) != 0) {
        return IOS_THROWN;
    }
    //傳入fd和sockaddr,與遠端伺服器建立連線,一般就是TCP三次握手
   //如果設定了configureBlocking(false),不會堵塞,否則會堵塞一直到超時或出現異常
    rv = connect(fdval(env, fdo), &sa.sa, sa_len);
    //0表示連線成功,失敗時通過errno獲取具體原因
    if (rv != 0) {
        //非堵塞,連線還未建立(-2)
        if (errno == EINPROGRESS) {
            return IOS_UNAVAILABLE;
        } else if (errno == EINTR) {
            //中斷(-3)
            return IOS_INTERRUPTED;
        }
        return handleSocketError(env, errno);
    }
    //連線建立,一般TCP連線連線都需要時間,因此除非是本地網路,
    //一般情況下非堵塞模式返回IOS_UNAVAILABLE比較多;
    return 1;
}
複製程式碼

從上面可以通過註釋看到,如果是非堵塞,而且連線也並未立馬建立成功,其返回的是-2,也就是連線未建立成功,由之前beginConnect部分原始碼可知,此時狀態為ST_CONNECTIONPENDING,那麼,非阻塞條件下,什麼時候會變為ST_CONNECTED?有什麼方法可以查詢狀態或者等待連線完成?
那就讓我們來關注下sun.nio.ch.SocketChannelImpl#finishConnect

SocketChannelImpl中finishConnect解讀

首先,我們回顧下,前面我們涉及了sun.nio.ch.ServerSocketAdaptor的用法,方便我們只有Socket程式設計習慣人群使用,這裡,我們也就可以看到基本的核心實現邏輯,那麼有ServerSocketAdaptor就有SocketAdaptor,這裡,在BIO的Socket程式設計中最後也是呼叫了connect(address)操作:

//java.net.Socket#Socket
private Socket(SocketAddress address, SocketAddress localAddr,
                boolean stream) throws IOException {
    setImpl();

    // backward compatibility
    if (address == null)
        throw new NullPointerException();

    try {
        createImpl(stream);
        if (localAddr != null)
            bind(localAddr);
        connect(address);
    } catch (IOException | IllegalArgumentException | SecurityException e) {
        try {
            close();
        } catch (IOException ce) {
            e.addSuppressed(ce);
        }
        throw e;
    }
}
複製程式碼

這裡,我們可以呼叫java.nio.channels.SocketChannel#open(),然後呼叫所得到的SocketChannel物件的socket()方法,就可以得到sun.nio.ch.SocketAdaptor物件例項了。我們來檢視SocketAdaptor的connect實現:

//sun.nio.ch.SocketAdaptor#connect
public void connect(SocketAddress remote) throws IOException {
    connect(remote, 0);
}

public void connect(SocketAddress remote, int timeout) throws IOException {
    if (remote == null)
        throw new IllegalArgumentException("connect: The address can`t be null");
    if (timeout < 0)
        throw new IllegalArgumentException("connect: timeout can`t be negative");

    synchronized (sc.blockingLock()) {
        if (!sc.isBlocking())
            throw new IllegalBlockingModeException();

        try {
            //未設定超時則會一直在此等待直到連線或者出現異常
            // no timeout
            if (timeout == 0) {
                sc.connect(remote);
                return;
            }
            //有超時設定,則會將Socket給設定為非阻塞
            // timed connect
            sc.configureBlocking(false);
            try {
                if (sc.connect(remote))
                    return;
            } finally {
                try {
                    sc.configureBlocking(true);
                } catch (ClosedChannelException e) { }
            }

            long timeoutNanos = NANOSECONDS.convert(timeout, MILLISECONDS);
            long to = timeout;
            for (;;) {
                //通過計算超時時間,在允許的時間範圍內無限迴圈來進行連線,
                //如果超時,則關閉這個Socket
                long startTime = System.nanoTime();
                if (sc.pollConnected(to)) {
                    boolean connected = sc.finishConnect();
                    //看下文解釋
                    assert connected;
                    break;
                }
                timeoutNanos -= System.nanoTime() - startTime;
                if (timeoutNanos <= 0) {
                    try {
                        sc.close();
                    } catch (IOException x) { }
                    throw new SocketTimeoutException();
                }
                to = MILLISECONDS.convert(timeoutNanos, NANOSECONDS);
            }

        } catch (Exception x) {
            Net.translateException(x, true);
        }
    }

}
複製程式碼

這裡先解釋下一個小注意點:在Java中,assert關鍵字是從JAVA SE 1.4 引入的,為了避免和老版本的Java程式碼中使用了assert關鍵字導致錯誤,Java在執行的時候預設是不啟動斷言檢查的(這個時候,所有的斷言語句都 將忽略!),如果要開啟斷言檢查,則需要用開關-enableassertions或-ea來開啟。
通過上面的原始碼註釋,相信大夥已經知道大致的流程了,那關於sun.nio.ch.SocketChannelImpl#finishConnect到底做了什麼,此處,我們來探索一番:

//sun.nio.ch.SocketChannelImpl#finishConnect
@Override
public boolean finishConnect() throws IOException {
    try {
        readLock.lock();
        try {
            writeLock.lock();
            try {
                // no-op if already connected
                if (isConnected())
                    return true;

                boolean blocking = isBlocking();
                boolean connected = false;
                try {
                    beginFinishConnect(blocking);
                    int n = 0;
                    if (blocking) {
                        do {
                            //阻塞情況下,第二個引數傳入true
                            n = checkConnect(fd, true);
                        } while ((n == 0 || n == IOStatus.INTERRUPTED) && isOpen());
                    } else {
                        //非阻塞情況下,第二個引數傳入false
                        n = checkConnect(fd, false);
                    }
                    connected = (n > 0);
                } finally {
                    endFinishConnect(blocking, connected);
                }
                assert (blocking && connected) ^ !blocking;
                return connected;
            } finally {
                writeLock.unlock();
            }
        } finally {
            readLock.unlock();
        }
    } catch (IOException ioe) {
        // connect failed, close the channel
        close();
        throw SocketExceptions.of(ioe, remoteAddress);
    }
}
//sun.nio.ch.SocketChannelImpl#checkConnect
private static native int checkConnect(FileDescriptor fd, boolean block)
    throws IOException;
複製程式碼

關於beginFinishConnectendFinishConnect和我們之前分析的sun.nio.ch.SocketChannelImpl#beginConnectsun.nio.ch.SocketChannelImpl#endConnect過程差不多,不懂讀者可回看。剩下的,就是我們關注的主要核心邏輯checkConnect(fd, true),它也是一個本地方法,涉及到的原始碼如下:

JNIEXPORT jint JNICALL
Java_sun_nio_ch_SocketChannelImpl_checkConnect(JNIEnv *env, jobject this,
                                               jobject fdo, jboolean block)
{
    int error = 0;
    socklen_t n = sizeof(int);
    //獲取FileDescriptor中的fd
    jint fd = fdval(env, fdo);
    int result = 0;
    struct pollfd poller;
    //檔案描述符
    poller.fd = fd;
    //請求的事件為寫事件
    poller.events = POLLOUT;
    //返回的事件
    poller.revents = 0;
    
    //-1表示阻塞,0表示立即返回,不阻塞程式
    result = poll(&poller, 1, block ? -1 : 0);
    //小於0表示呼叫失敗
    if (result < 0) {
        if (errno == EINTR) {
            return IOS_INTERRUPTED;
        } else {
            JNU_ThrowIOExceptionWithLastError(env, "poll failed");
            return IOS_THROWN;
        }
    }
    //非堵塞時,0表示沒有準備好的連線
    if (!block && (result == 0))
        return IOS_UNAVAILABLE;
    //準備好寫或出現錯誤的socket數量>0
    if (result > 0) {
        errno = 0;
        result = getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &n);
        //出錯
        if (result < 0) {
            return handleSocketError(env, errno);
        //發生錯誤,處理錯誤
        } else if (error) {
            return handleSocketError(env, error);
        } else if ((poller.revents & POLLHUP) != 0) {
            return handleSocketError(env, ENOTCONN);
        }
        //socket已經準備好,可寫,即連線已經建立好
        // connected
        return 1;
    }
    return 0;
}
複製程式碼

具體的過程如原始碼註釋所示,其中是否阻塞我們在本地方法原始碼中和之前sun.nio.ch.SocketChannelImpl#finishConnect的行為產生對應。另外,從上面的原始碼看到,底層是通過poll查詢socket的狀態,從而判斷連線是否建立成功;由於在非堵塞模式下,finishConnect方法會立即返回,根據此處sun.nio.ch.SocketAdaptor#connect的處理,其使用迴圈的方式判斷連線是否建立,在我們的nio程式設計中,這個是不建議的,屬於半成品,而是建議註冊到Selector,通過ops=OP_CONNECT獲取連線完成的SelectionKey,然後呼叫finishConnect完成連線的建立;
那麼finishConnect是否可以不呼叫呢?答案是否定的,因為只有finishConnect中才會將狀態更新為ST_CONNECTED,而在呼叫readwrite時都會對狀態進行判斷。

這裡,我們算是引出了我們即將要涉及的SelectorSelectionKey,我們會在下一篇中進行詳細講解。

相關文章