NIO 源碼分析(02-1) BIO 源碼分析java
Netty 系列目錄(http://www.javashuo.com/article/p-hskusway-em.html)linux
(1) JDK BIO 啓動服務典型場景編程
// 1. 綁定端口 ServerSocket serverSocket = new ServerSocket(); serverSocket.bind(new InetSocketAddress((InetAddress) null, PROT), BACKLOG); while (true) { // 2. 獲取客戶端請求的Socket,沒有請求就阻塞 Socket socket = serverSocket.accept(); // 3. 開啓一個線程執行客戶端的任務 new Thread(new ServerHandler(socket)).start(); } // 綁定端口,開啓服務 public void bind(SocketAddress endpoint, int backlog) throws IOException { getImpl().bind(epoint.getAddress(), epoint.getPort()); getImpl().listen(backlog); }
ok,代碼已經完成!!!下面咱們和 Linux 下的網絡編程進行對比。windows
(2) Linux BIO 啓動服務典型場景數組
int listenfd = socket(AF_INET, SOCK_STREAM, 0); bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); listen(listenfd, BACKLOG); socklen_t cliaddr_len = sizeof(client_addr); int clientfd = accept(listenfd, (struct sockaddr*)&client_addr, &cliaddr_len);
對比 Linux 上網絡編程,咱們會發現 JDK Socket 的編程邏輯是如出一轍的。實時上也是這樣,JDK 網絡編程也沒有作不少事,主要仍是調用了 Linux 相關的函數。惟一的不一樣是 Linux 是面向過程程序,socket 函數返回的是一個句柄,bind 和 listen 都是對這個句柄的操做;而 JDK 是面向對象編程,new ServerSocket() 返回了一個對象,咱們能夠調用這個 serverSocket 對象的各類方法。安全
JDK 爲咱們提供了 ServerSocket 類做爲服務端套接字的實現,經過它可讓主機監聽某個端口而接收其餘端的請求,處理完後還能夠對請求端作出響應。它的內部真正實現是經過 SocketImpl 類來實現的,它提供了工廠模式,因此若是本身想要其餘的實現也能夠經過工廠模式來改變的。默認的實現類是 SocksSocketImpl,ServerSocket 和 Socket 只是一個門面模式。jvm
前面說到 ServerSocket 類真正的實現是經過 SocketImpl 類,因而能夠看到它使用了 SocketImpl 類,但因爲 windows 和 unix-like 系統有差別,而 windows 不一樣的版本也須要作不一樣的處理,因此兩類系統的類不盡相同。socket
說明:
SocketImpl 類實現了 SocketOptions 接口,接着還派生出了一系列的子類,其中 AbstractPlainSocketImpl 是原始套接字的實現的一些抽象,而 PlainSocketImpl 類是一個代理類。
windows 下 PlainSocketImpl 代理 TwoStacksPlainSocketImpl 和 DualStackPlainSocketImpl 兩種不一樣實現。存在兩種實現的緣由是一個用於處理 Windows Vista 如下的版本,另外一個用於處理 Windows Vista 及以上的版本。
unix-like 不存在版本的問題,因此它直接由 PlainSocketImpl 類實現。
這兩類操做系統都還存在一個 SocksSocketImpl 類,它其實主要是實現了防火牆安全會話轉換協議,包括 SOCKS V4 和 V5 。
根據上面能夠看到其實對於不一樣系統就是須要作差別處理,基本都是大同小異,下面涉及到套接字實現均以 Windows Vista 及以上的版本爲例進行分析,即 DualStackPlainSocketImpl。
private boolean created = false; private boolean bound = false; private boolean closed = false; private Object closeLock = new Object(); private SocketImpl impl; private boolean oldImpl = false;
下面咱們看一下 ServerSocket 的主要方法。
有五類構造函數,能夠什麼參數都不傳,也能夠傳入 SocketImpl、端口、backlog 和地址等。主要看一下最後一個構造函數,setImpl 方法用於設置實現對象,而後檢查端口大小是否正確,檢查 backlog 小於 0 就讓它等於 50,最後進行端口和地址綁定操做。
public ServerSocket() throws IOException { setImpl(); }
總結: 在 new ServerSocket() 時會經過 setImpl 方法建立一個 SocketImpl 的實現類,以 window 下 DualStackPlainSocketImpl 爲例。時序圖以下:
設置套接字實現對象,這裏提供了工廠模式能夠方便的對接其餘的實現,而默認是沒有工廠對象的,因此模式的實現爲 SocksSocketImpl 對象。
private void setImpl() { if (factory != null) { impl = factory.createSocketImpl(); checkOldImpl(); } else { impl = new SocksSocketImpl(); } if (impl != null) impl.setServerSocket(this); }
總結: 從上面的方法能夠看出構造方法只是建立了 SocksSocketImpl 對象,這些都只是 JDK 層面的東西,並沒直接建立網絡鏈接,bind 方法則會直接建立網絡鏈接。
該方法用於將套接字綁定到指定的地址和端口上,若是 SocketAddress 爲空,即表明地址和端口都不指定,此時系統會將套接字綁定到全部有效的本地地址,且動態生成一個端口。邏輯以下:
// 綁定端口,開啓服務 public void bind(SocketAddress endpoint, int backlog) throws IOException { // 省略... if (endpoint == null) endpoint = new InetSocketAddress(0); if (backlog < 1) backlog = 50; getImpl().bind(epoint.getAddress(), epoint.getPort()); getImpl().listen(backlog); }
總結: ServerSocket.bind 方法調用 impl.bind 和 impl.listen 建立網絡鏈接,下面咱們就重點分析這兩個方法都作了些什麼事。
注意: 地址是否爲空,爲空則建立一個 InetSocketAddress,默認是全部有效的本地地址,對應的爲
0.0.0.0,而端口默認爲0,由操做系統動態生成,backlog 若是小於 1 則設爲 50。
先看一下 createImpl 方法。
// ServerSocket void createImpl() throws SocketException { if (impl == null) setImpl(); try { impl.create(true); created = true; } catch (IOException e) { throw new SocketException(e.getMessage()); } }
總結: createImpl 將建立 socket 網絡套接字的任務直接委任給了對應的 impl 實現類,在這裏也就是 DualStackPlainSocketImpl。
// AbstractPlainSocketImpl protected synchronized void create(boolean stream) throws IOException { this.stream = stream; if (!stream) { // UDP ResourceManager.beforeUdpCreate(); // only create the fd after we know we will be able to create the socket fd = new FileDescriptor(); try { socketCreate(false); } catch (IOException ioe) { ResourceManager.afterUdpClose(); fd = null; throw ioe; } } else { // TCP fd = new FileDescriptor(); socketCreate(true); } if (socket != null) socket.setCreated(); if (serverSocket != null) serverSocket.setCreated(); }
總結: 在 AbstractPlainSocketImpl 抽象類中 create 方法對 UDP 和 TCP 協議分別作了處理,建立 socket 套接字的代碼就一句 socketCreate(true)
,由具體的實現類完成。下面咱們再看一下 DualStackPlainSocketImpl 是如何進行網絡鏈接的。
// DualStackPlainSocketImpl void socketCreate(boolean stream) throws IOException { if (fd == null) throw new SocketException("Socket closed"); int newfd = socket0(stream, false /*v6 Only*/); fdAccess.set(fd, newfd); }
總結: socket0 是一個 native 方法,也就是上面 Linux 的 Socket 函數完成的。而後將返回的 Socket 句柄設置到 fd 對象中(FileDescriptor 是 JDK 對句柄的抽象)。
// windows/native/java/net/DualStackPlainSocketImpl.c JNIEXPORT jint JNICALL Java_java_net_DualStackPlainSocketImpl_socket0 (JNIEnv *env, jclass clazz, jboolean stream, jboolean v6Only /*unused*/) { int fd, rv, opt=0; // 最關鍵的一句代碼,怎麼樣,是否是和 Linux 網絡編程的 socket 函數如出一轍的 fd = NET_Socket(AF_INET6, (stream ? SOCK_STREAM : SOCK_DGRAM), 0); if (fd == INVALID_SOCKET) { NET_ThrowNew(env, WSAGetLastError(), "create"); return -1; } rv = setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, (char *) &opt, sizeof(opt)); if (rv == SOCKET_ERROR) { NET_ThrowNew(env, WSAGetLastError(), "create"); } SetHandleInformation((HANDLE)(UINT_PTR)fd, HANDLE_FLAG_INHERIT, FALSE); return fd; }
// AbstractPlainSocketImpl protected synchronized void bind(InetAddress address, int lport) throws IOException { synchronized (fdLock) { if (!closePending && (socket == null || !socket.isBound())) { NetHooks.beforeTcpBind(fd, address, lport); } } socketBind(address, lport); // 綁定端口 if (socket != null) socket.setBound(); if (serverSocket != null) serverSocket.setBound(); }
總結: 和 create 方法同樣 AbstractPlainSocketImpl 也把綁定端口 socketBind 交給子類 DualStackPlainSocketImpl 實現。
// DualStackPlainSocketImpl void socketBind(InetAddress address, int port) throws IOException { int nativefd = checkAndReturnNativeFD(); if (address == null) throw new NullPointerException("inet address argument is null."); bind0(nativefd, address, port, exclusiveBind); if (port == 0) { localport = localPort0(nativefd); } else { localport = port; } this.address = address; }
總結: bind0 也是一個 native 方法,下面看一下 Winidow 上的實現。
JNIEXPORT void JNICALL Java_java_net_DualStackPlainSocketImpl_bind0 (JNIEnv *env, jclass clazz, jint fd, jobject iaObj, jint port, jboolean exclBind) { SOCKETADDRESS sa; int rv; int sa_len = sizeof(sa); if (NET_InetAddressToSockaddr(env, iaObj, port, (struct sockaddr *)&sa, &sa_len, JNI_TRUE) != 0) { return; } // 最關鍵的一句代碼,綁定端口 rv = NET_WinBind(fd, (struct sockaddr *)&sa, sa_len, exclBind); if (rv == SOCKET_ERROR) NET_ThrowNew(env, WSAGetLastError(), "JVM_Bind"); }
// DualStackPlainSocketImpl void socketListen(int backlog) throws IOException { int nativefd = checkAndReturnNativeFD(); listen0(nativefd, backlog); }
JNIEXPORT void JNICALL Java_java_net_DualStackPlainSocketImpl_listen0 (JNIEnv *env, jclass clazz, jint fd, jint backlog) { // 關鍵的一句代碼 listen 啓動服務 if (listen(fd, backlog) == SOCKET_ERROR) { NET_ThrowNew(env, WSAGetLastError(), "listen failed"); } }
該方法用於接收套接字鏈接,套接字開啓監聽後會阻塞等待套接字鏈接,一旦有鏈接可接收了則經過該方法進行接收操做。
public Socket accept() throws IOException { if (isClosed()) throw new SocketException("Socket is closed"); if (!isBound()) throw new SocketException("Socket is not bound yet"); Socket s = new Socket((SocketImpl) null); implAccept(s); return s; }
總結: accept 作了三件事:一是判斷套接字是否已經關閉。 二是判斷套接字是否已經綁定。三是建立 Socket 對象,並調用 implAccept 接收鏈接。
protected final void implAccept(Socket s) throws IOException { // 1. 建立一個空的 Socket 對象,用於接收 Socket 鏈接 SocketImpl si = null; try { if (s.impl == null) s.setImpl(); else { s.impl.reset(); } si = s.impl; s.impl = null; si.address = new InetAddress(); si.fd = new FileDescriptor(); // 2. 最關鍵的一步,接收請求 getImpl().accept(si); SecurityManager security = System.getSecurityManager(); if (security != null) { security.checkAccept(si.getInetAddress().getHostAddress(), si.getPort()); } } catch (IOException e) { if (si != null) si.reset(); s.impl = si; throw e; } catch (SecurityException e) { if (si != null) si.reset(); s.impl = si; throw e; } // 3. 調用 Socket.postAccept 通知鏈接已經完成 s.impl = si; s.postAccept(); }
總結: ServerSocket.implAccept 調用邏輯以下:
傳入的 Socket 對象裏面的套接字實現若是爲空,則經過 setImpl 方法設置套接字實現,若是非空就執行 reset 操做。
調用套接字實現對象的 accept 方法完成接收操做,作這一步是由於咱們的 Socket 對象裏面的 SocketImpl 對象還差操做系統底層的套接字對應的文件描述符。
獲得完整的 SocketImpl 對象,賦值給 Socket 對象,而且調用 postAccept 方法將 Socket 對象設置爲已建立、已鏈接、已綁定。
// AbstractPlainSocketImpl protected void accept(SocketImpl s) throws IOException { acquireFD(); // fdUseCount++ try { socketAccept(s);// 接收請求 } finally { releaseFD(); // fdUseCount-- } }
總結: accept 直接委託給 socketAccept 方法。
void socketAccept(SocketImpl s) throws IOException { // 1. 獲取操做系統的文件描述符。 int nativefd = checkAndReturnNativeFD(); if (s == null) throw new NullPointerException("socket is null"); int newfd = -1; InetSocketAddress[] isaa = new InetSocketAddress[1]; // 2. timeout <= 0 則表示阻塞式接收鏈接 if (timeout <= 0) { newfd = accept0(nativefd, isaa); // 3. 若是 timeout 大於0,即設置了超時,那麼會先非阻塞式接收鏈接 } else { // 3.1 serverSocket 設置成非阻塞模式 configureBlocking(nativefd, false); try { // 3.2 本地方法,阻塞 timeout 時長用於獲取新的 socket waitForNewConnection(nativefd, timeout); // 3.3 由於如今是非阻塞的,無論有沒有鏈接都會立刻返回 newfd = accept0(nativefd, isaa); if (newfd != -1) { // 3.4 新創建的 Socket 設置爲阻塞模式 configureBlocking(newfd, true); } } finally { // 3.5 serverSocket 恢復爲阻塞模式 configureBlocking(nativefd, true); } } // 4. 將獲取到的新文件描述符賦給 SocketImpl 對象, // 同時也將遠程端口、遠程地址、本地端口等都賦給它相關變量。 fdAccess.set(s.fd, newfd); InetSocketAddress isa = isaa[0]; s.port = isa.getPort(); s.address = isa.getAddress(); s.localport = localport; }
總結: socketAccept 調用本地方法 accept0 接收鏈接。具體邏輯以下:
本地方法邏輯很簡單,以下,核心就是經過調用 Winsock 庫的 ioctlsocket 函數來設置套接字爲阻塞仍是非阻塞,根據 blocking 標識。
JNIEXPORT void JNICALL Java_java_net_DualStackPlainSocketImpl_configureBlocking (JNIEnv *env, jclass clazz, jint fd, jboolean blocking) { u_long arg; int result; if (blocking == JNI_TRUE) { arg = SET_BLOCKING; // 0 } else { arg = SET_NONBLOCKING; // 1 } result = ioctlsocket(fd, FIONBIO, &arg); if (result == SOCKET_ERROR) { NET_ThrowNew(env, WSAGetLastError(), "configureBlocking"); } }
經過 Winsock 庫的 select 函數來實現超時的功能,它會等待 timeout 時間看指定的文件描述符是否有活動,超時了的話則會返回 0,此時向 Java 層拋出 SocketTimeoutException 異常。而若是返回了 -1 則表示套接字已經關閉了,拋出 SocketException 異常。若是返回-2則拋出 InterruptedIOException。
JNIEXPORT void JNICALL Java_java_net_DualStackPlainSocketImpl_waitForConnect (JNIEnv *env, jclass clazz, jint fd, jint timeout) { int rv, retry; int optlen = sizeof(rv); fd_set wr, ex; struct timeval t; FD_ZERO(&wr); FD_ZERO(&ex); FD_SET(fd, &wr); FD_SET(fd, &ex); t.tv_sec = timeout / 1000; t.tv_usec = (timeout % 1000) * 1000; rv = select(fd+1, 0, &wr, &ex, &t); if (rv == 0) { JNU_ThrowByName(env, JNU_JAVANETPKG "SocketTimeoutException", "connect timed out"); shutdown( fd, SD_BOTH ); return; } if (!FD_ISSET(fd, &ex)) { return; /* connection established */ } for (retry=0; retry<3; retry++) { NET_GetSockOpt(fd, SOL_SOCKET, SO_ERROR, (char*)&rv, &optlen); if (rv) { break; } Sleep(0); } if (rv == 0) { JNU_ThrowByName(env, JNU_JAVANETPKG "SocketException", "Unable to establish connection"); } else { NET_ThrowNew(env, rv, "connect"); } }
JNIEXPORT jint JNICALL Java_java_net_DualStackPlainSocketImpl_accept0 (JNIEnv *env, jclass clazz, jint fd, jobjectArray isaa) { int newfd, port=0; jobject isa; jobject ia; SOCKETADDRESS sa; int len = sizeof(sa); memset((char *)&sa, 0, len); newfd = accept(fd, (struct sockaddr *)&sa, &len); if (newfd == INVALID_SOCKET) { if (WSAGetLastError() == -2) { JNU_ThrowByName(env, JNU_JAVAIOPKG "InterruptedIOException", "operation interrupted"); } else { JNU_ThrowByName(env, JNU_JAVANETPKG "SocketException", "socket closed"); } return -1; } ia = NET_SockaddrToInetAddress(env, (struct sockaddr *)&sa, &port); isa = (*env)->NewObject(env, isa_class, isa_ctorID, ia, port); (*env)->SetObjectArrayElement(env, isaa, 0, isa); return newfd; }
總結: 本地方法 accept0 實現邏輯:
能夠看到 ServerSocket 的核心方法都是 native 方法,是由 JVM 調用 linux 的內核函數完成的。想要對 Socket 網絡編程有更詳細的瞭解就必須進一步瞭解 Linux 網絡編程。
參考:
天天用心記錄一點點。內容也許不重要,但習慣很重要!