JDK 爲咱們提供了 ServerSocket 類做爲服務端套接字的實現,經過它可讓主機監聽某個端口而接收其餘端的請求,處理完後還能夠對請求端作出響應。它的內部真正實現是經過 SocketImpl 類來實現的,它提供了工廠模式,因此若是本身想要其餘的實現也能夠經過工廠模式來改變的。java
--java.lang.Object
--java.net.ServerSocket
複製代碼
前面說到 ServerSocket 類真正的實現是經過 SocketImpl 類,因而能夠看到它使用了 SocketImpl 類,但因爲 windows 和 unix-like 系統有差別,而 windows 不一樣的版本也須要作不一樣的處理,因此兩類系統的類不盡相同。windows
下圖是 windows 的類圖關係, SocketImpl 類實現了 SocketOptions 接口,接着還派生出了一系列的子類,其中 AbstractPlainSocketImpl 是原始套接字的實現的一些抽象,而 PlainSocketImpl 類是一個代理類,由它代理 TwoStacksPlainSocketImpl 和 DualStackPlainSocketImpl 兩種不一樣實現。存在兩種實現的緣由是一個用於處理 Windows Vista 如下的版本,另外一個用於處理 Windows Vista 及以上的版本。 數組
比起 windows 的實現,unix-like 的實現則不會這麼繁瑣,它不存在版本的問題,因此它直接由 PlainSocketImpl 類實現,此外,能夠看到兩類操做系統都還存在一個 SocksSocketImpl 類,它其實主要是實現了防火牆安全會話轉換協議,包括 SOCKS V4 和 V5 。安全
根據上面能夠看到其實對於不一樣系統就是須要作差別處理,基本都是大同小異,下面涉及到套接字實現均以 Windows Vista 及以上的版本爲例進行分析。bash
public class ServerSocket implements java.io.Closeable
複製代碼
ServerSocket 類的聲明很簡單,實現了 Closeable 接口,該接口只有一個close
方法。網絡
private boolean created = false;
private boolean bound = false;
private boolean closed = false;
private Object closeLock = new Object();
private SocketImpl impl;
private boolean oldImpl = false;
複製代碼
有五類構造函數,能夠什麼參數都不傳,也能夠傳入 SocketImpl、端口、backlog和地址等。主要看一下最後一個構造函數,setImpl 方法用於設置實現對象,而後檢查端口大小是否正確,檢查 backlog 小於0就讓它等於50,最後進行端口和地址綁定操做。併發
ServerSocket(SocketImpl impl) {
this.impl = impl;
impl.setServerSocket(this);
}
public ServerSocket() throws IOException {
setImpl();
}
public ServerSocket(int port) throws IOException {
this(port, 50, null);
}
public ServerSocket(int port, int backlog) throws IOException {
this(port, backlog, null);
}
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;
}
}
複製代碼
設置套接字實現對象,這裏提供了工廠模式能夠方便的對接其餘的實現,而默認是沒有工廠對象的,因此模式的實現爲 SocksSocketImpl 對象。dom
private void setImpl() {
if (factory != null) {
impl = factory.createSocketImpl();
checkOldImpl();
} else {
impl = new SocksSocketImpl();
}
if (impl != null)
impl.setServerSocket(this);
}
複製代碼
該方法用於建立套接字實現對象,若是實現對象爲空則先調用setImpl
方法設置一下,接着調用套接字實現對象的create
方法建立套接字。機器學習
void createImpl() throws SocketException {
if (impl == null)
setImpl();
try {
impl.create(true);
created = true;
} catch (IOException e) {
throw new SocketException(e.getMessage());
}
}
複製代碼
create
方法幹了些啥?它的實現邏輯在 AbstractPlainSocketImpl 類中,這裏會傳入一個 boolean 類型的 stream 變量,這裏其實用來標識是 udp 仍是 tcp 協議,stream 便是流,tcp是基於鏈接的,天然存在流的抽象。而 udp 是非鏈接的非流的。socket
兩類鏈接是經過 boolean 類型來標識的,true 爲 tcp,false 爲 udp,再經過 socketCreate 方法傳入到本地實現中,在此以前二者都會建立 FileDescriptor 對象做爲套接字的引用,FileDescriptor 爲文件描述符,能夠用來描述文件、套接字和資源等。另外,udp 協議時還會經過 ResourceManager.beforeUdpCreate()
來統計虛擬機 udp 套接字數量,超過指定最大值則會拋出異常,默認值爲25。最後將套接字的 created 標識設爲 true,對應 Java 中抽象的客戶端套接字 Socket 對象和服務端套接字 ServerSocket 對象。
protected synchronized void create(boolean stream) throws IOException {
this.stream = stream;
if (!stream) {
ResourceManager.beforeUdpCreate();
fd = new FileDescriptor();
try {
socketCreate(false);
} catch (IOException ioe) {
ResourceManager.afterUdpClose();
fd = null;
throw ioe;
}
} else {
fd = new FileDescriptor();
socketCreate(true);
}
if (socket != null)
socket.setCreated();
if (serverSocket != null)
serverSocket.setCreated();
}
複製代碼
往下看上面調用的socketCreate
方法的邏輯,判斷文件描述符不能爲空,再調用本地socket0
方法,最後將獲得的句柄關聯到文件描述符對象上。
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);
}
static native int socket0(boolean stream, boolean v6Only) throws IOException;
複製代碼
接着看本地方法socket0
的實現,邏輯爲:
NET_Socket
函數建立套接字句柄,其中經過 Winsock 庫的 socket
函數建立句柄,而且經過SetHandleInformation
函數設置句柄的繼承標誌。這裏能夠看到根據 stream 標識對應的類別爲SOCK_STREAM
和 SOCK_DGRAM
。若是句柄是無效的則拋出 create 異常。setsockopt
函數設置套接字的選項值,若是發生錯誤則拋出 create 異常。SetHandleInformation
設置句柄的繼承標誌,返回句柄。JNIEXPORT jint JNICALL Java_java_net_DualStackPlainSocketImpl_socket0
(JNIEnv *env, jclass clazz, jboolean stream, jboolean v6Only /*unused*/) {
int fd, rv, opt=0;
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;
}
int NET_Socket (int domain, int type, int protocol) {
SOCKET sock;
sock = socket (domain, type, protocol);
if (sock != INVALID_SOCKET) {
SetHandleInformation((HANDLE)(uintptr_t)sock, HANDLE_FLAG_INHERIT, FALSE);
}
return (int)sock;
}
複製代碼
該方法用於將套接字綁定到指定的地址和端口上,若是 SocketAddress 爲空,即表明地址和端口都不指定,此時系統會將套接字綁定到全部有效的本地地址,且動態生成一個端口。邏輯以下:
SocketException("Socket is closed")
。SocketException("Already bound")
。0.0.0.0
,而端口默認爲0,由操做系統動態生成。IllegalArgumentException("Unsupported address type")
。SocketException("Unresolved address")
。bind
和listen
方法。public void bind(SocketAddress endpoint) throws IOException {
bind(endpoint, 50);
}
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;
}
}
複製代碼
套接字實現對象的bind
方法會間接調用socketBind
方法,邏輯以下:
bind0
本地方法。localPort0
本地方法獲取本地端口賦值給套接字實現對象的 localport 屬性上,目的是獲取操做系統動態生成的端口。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;
}
static native void bind0(int fd, InetAddress localAddress, int localport, boolean exclBind)
static native int localPort0(int fd) throws IOException;
複製代碼
bind0
本地方法邏輯以下,
NET_InetAddressToSockaddr
函數將 Java 層的 InetAddress 對象的屬性值填充到 SOCKETADDRESS 聯合體中,對應的都是 Winsock 庫的結構體,目的便是爲了填充好它們。typedef union {
struct sockaddr sa;
struct sockaddr_in sa4;
struct sockaddr_in6 sa6;
} SOCKETADDRESS;
複製代碼
NET_WinBind
函數的邏輯是先根據 exclBind 標識看是否須要獨佔端口,若是須要則經過 Winsock 庫的setsockopt
函數設置SO_EXCLUSIVEADDRUSE
選型,在 Java 層中決定獨不獨佔端口能夠經過sun.net.useExclusiveBind
參數來配置,默認狀況下是獨佔的。接着,經過操做系統的bind
函數完成綁定操做。JNIEXPORT void JNICALL Java_java_net_DualStackPlainSocketImpl_bind0
(JNIEnv *env, jclass clazz, jint fd, jobject iaObj, jint port,
jboolean exclBind)
{
SOCKETADDRESS sa;
int rv, sa_len = 0;
if (NET_InetAddressToSockaddr(env, iaObj, port, &sa,
&sa_len, JNI_TRUE) != 0) {
return;
}
rv = NET_WinBind(fd, &sa, sa_len, exclBind);
if (rv == SOCKET_ERROR)
NET_ThrowNew(env, WSAGetLastError(), "NET_Bind");
}
複製代碼
localPort0
本地方法的實現主要是先經過 Winsock 庫的getsockname
函數獲取套接字地址,而後經過ntohs
函數將網絡字節轉成主機字節並轉爲 int 型。
JNIEXPORT jint JNICALL Java_java_net_DualStackPlainSocketImpl_localPort0
(JNIEnv *env, jclass clazz, jint fd) {
SOCKETADDRESS sa;
int len = sizeof(sa);
if (getsockname(fd, &sa.sa, &len) == SOCKET_ERROR) {
if (WSAGetLastError() == WSAENOTSOCK) {
JNU_ThrowByName(env, JNU_JAVANETPKG "SocketException",
"Socket closed");
} else {
NET_ThrowNew(env, WSAGetLastError(), "getsockname failed");
}
return -1;
}
return (int) ntohs((u_short)GET_PORT(&sa));
}
複製代碼
套接字實現對象的listen
方法會間接調用socketListen
方法,邏輯比較簡單,獲取本地的文件描述符而後調用listen0
本地方法。能夠看到本地方法很簡單,僅僅是調用了 Winsock 庫的listen
函數來完成監聽操做。
void socketListen(int backlog) throws IOException {
int nativefd = checkAndReturnNativeFD();
listen0(nativefd, backlog);
}
static native void listen0(int fd, int backlog) throws IOException;
JNIEXPORT void JNICALL Java_java_net_DualStackPlainSocketImpl_listen0
(JNIEnv *env, jclass clazz, jint fd, jint backlog) {
if (listen(fd, backlog) == SOCKET_ERROR) {
NET_ThrowNew(env, WSAGetLastError(), "listen failed");
}
}
複製代碼
該方法用於接收套接字鏈接,套接字開啓監聽後會阻塞等待套接字鏈接,一旦有鏈接可接收了則經過該方法進行接收操做。邏輯爲,
implAccept
方法,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;
}
複製代碼
implAccept
方法邏輯爲,
setImpl
方法設置套接字實現,若是非空就執行reset
操做。accept
方法完成接收操做,作這一步是由於咱們的 Socket 對象裏面的 SocketImpl 對象還差操做系統底層的套接字對應的文件描述符。postAccept
方法將 Socket 對象設置爲已建立、已鏈接、已綁定。protected final void implAccept(Socket s) throws IOException {
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();
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;
}
s.impl = si;
s.postAccept();
}
複製代碼
套接字實現對象的accept
方法主要調用以下的socketAccept
方法,邏輯爲,
NullPointerException("socket is null")
。accept0
方法,一直阻塞。configureBlocking
本地方法,該方法用於將指定套接字設置爲非阻塞模式。接着調用waitForNewConnection
本地方法,若是在超時時間內能獲取到新的套接字,則調用accept0
方法獲取新套接字的句柄,獲取成功後再次調用configureBlocking
本地方法將新套接字設置爲阻塞模式。最後,若是非阻塞模式失敗了,則將原來的套接字設置會紫塞模式,這裏使用了 finally,因此能保證就算髮生異常也能被執行。void socketAccept(SocketImpl s) throws IOException {
int nativefd = checkAndReturnNativeFD();
if (s == null)
throw new NullPointerException("socket is null");
int newfd = -1;
InetSocketAddress[] isaa = new InetSocketAddress[1];
if (timeout <= 0) {
newfd = accept0(nativefd, isaa);
} else {
configureBlocking(nativefd, false);
try {
waitForNewConnection(nativefd, timeout);
newfd = accept0(nativefd, isaa);
if (newfd != -1) {
configureBlocking(newfd, true);
}
} finally {
configureBlocking(nativefd, true);
}
}
fdAccess.set(s.fd, newfd);
InetSocketAddress isa = isaa[0];
s.port = isa.getPort();
s.address = isa.getAddress();
s.localport = localport;
}
複製代碼
configureBlocking
本地方法邏輯很簡單,以下,核心就是經過調用 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");
}
}
複製代碼
waitForNewConnection
本地方法邏輯以下,核心是經過 Winsock 庫的select
函數來實現超時的功能,它會等待 timeout 時間看指定的文件描述符是否有活動,超時了的話則會返回0,此時向 Java 層拋出 SocketTimeoutException 異常。而若是返回了-1則表示套接字已經關閉了,拋出 SocketException 異常。若是返回-2則拋出 InterruptedIOException。
JNIEXPORT void JNICALL Java_java_net_DualStackPlainSocketImpl_waitForNewConnection
(JNIEnv *env, jclass clazz, jint fd, jint timeout) {
int rv;
rv = NET_Timeout(fd, timeout);
if (rv == 0) {
JNU_ThrowByName(env, JNU_JAVANETPKG "SocketTimeoutException",
"Accept timed out");
} else if (rv == -1) {
JNU_ThrowByName(env, JNU_JAVANETPKG "SocketException", "socket closed");
} else if (rv == -2) {
JNU_ThrowByName(env, JNU_JAVAIOPKG "InterruptedIOException",
"operation interrupted");
}
}
JNIEXPORT int JNICALL
NET_Timeout(int fd, long timeout) {
int ret;
fd_set tbl;
struct timeval t;
t.tv_sec = timeout / 1000;
t.tv_usec = (timeout % 1000) * 1000;
FD_ZERO(&tbl);
FD_SET(fd, &tbl);
ret = select (fd + 1, &tbl, 0, 0, &t);
return ret;
}
複製代碼
accept0
本地方法實現邏輯爲,
memset
函數將 SOCKETADDRESS 聯合體對應的結構體內的值設置爲0。accept
函數獲取套接字地址。SetHandleInformation
函數設置句柄的繼承標誌。NET_SockaddrToInetAddress
函數用於將獲得的套接字轉換成 Java 層的 InetAddress 對象。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, &sa.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;
}
SetHandleInformation((HANDLE)(UINT_PTR)newfd, HANDLE_FLAG_INHERIT, 0);
ia = NET_SockaddrToInetAddress(env, &sa, &port);
isa = (*env)->NewObject(env, isa_class, isa_ctorID, ia, port);
(*env)->SetObjectArrayElement(env, isaa, 0, isa);
return newfd;
}
複製代碼
-------------推薦閱讀------------
------------------廣告時間----------------
知識星球:遠洋號
公衆號的菜單已分爲「分佈式」、「機器學習」、「深度學習」、「NLP」、「Java深度」、「Java併發核心」、「JDK源碼」、「Tomcat內核」等,可能有一款適合你的胃口。
歡迎關注: