概況
JDK 為我們提供了 ServerSocket 類作為服務端套接字的實現,通過它可以讓主機監聽某個埠而接收其他端的請求,處理完後還可以對請求端做出響應。它的內部真正實現是通過 SocketImpl 類來實現的,它提供了工廠模式,所以如果自己想要其他的實現也可以通過工廠模式來改變的。
繼承結構
--java.lang.Object
--java.net.ServerSocket
複製程式碼
相關類圖
前面說到 ServerSocket 類真正的實現是通過 SocketImpl 類,於是可以看到它使用了 SocketImpl 類,但由於 windows 和 unix-like 系統有差異,而 windows 不同的版本也需要做不同的處理,所以兩類系統的類不盡相同。
下圖是 windows 的類圖關係, SocketImpl 類實現了 SocketOptions 介面,接著還派生出了一系列的子類,其中 AbstractPlainSocketImpl 是原始套接字的實現的一些抽象,而 PlainSocketImpl 類是一個代理類,由它代理 TwoStacksPlainSocketImpl 和 DualStackPlainSocketImpl 兩種不同實現。存在兩種實現的原因是一個用於處理 Windows Vista 以下的版本,另一個用於處理 Windows Vista 及以上的版本。
比起 windows 的實現,unix-like 的實現則不會這麼繁瑣,它不存在版本的問題,所以它直接由 PlainSocketImpl 類實現,此外,可以看到兩類作業系統都還存在一個 SocksSocketImpl 類,它其實主要是實現了防火牆安全會話轉換協議,包括 SOCKS V4 和 V5 。
根據上面可以看到其實對於不同系統就是需要做差異處理,基本都是大同小異,下面涉及到套接字實現均以 Windows Vista 及以上的版本為例進行分析。
類定義
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;
複製程式碼
- created 表示是否已經建立了 SocketImpl 物件,ServerSocket 需要依賴該物件實現套接字操作。
- bound 是否已繫結地址和埠。
- closed 是否已經關閉套接字。
- closeLock 關閉套接字時用的鎖。
- impl 真正的套接字實現物件。
- oldImpl 是不是使用舊的實現。
主要方法
close方法
該方法用於關閉套接字,邏輯如下,
- 加鎖。
- 判斷如果已關閉則返回。
- 如果已建立則呼叫套接字實現物件的
close
方法。 - 將已關閉標識設為 true。
public void close() throws IOException {
synchronized(closeLock) {
if (isClosed())
return;
if (created)
impl.close();
closed = true;
}
}
複製程式碼
套接字實現物件的close
方法邏輯為,
- 加鎖。
- 判斷檔案描述符是否為空。
- 是否為 UDP 協議,如果是的話通過
ResourceManager.afterUdpClose()
操作將 UDP 套接字計數器減一,前面說過 Java 是有控制 UDP 套接字數量的。 - 判斷是否有執行緒在使用檔案描述符,fdUseCount 用於記錄多少執行緒在使用檔案描述符,為0時則沒有執行緒使用,此時判斷 closePending 是否為 true,它表示是否已經在關閉了,如果已經在關閉則直接返回,沒有的話則將 closePending 設為 true,標明該套接字已經在關閉了。接著再調
socketPreClose
和socketClose
兩個方法完成關閉操作,並且將檔案描述符設為 null,最後返回。 - 如果有執行緒在使用該檔案描述符,則將 closePending 設為 true,fdUseCount 減一,再呼叫
socketPreClose
方法。
protected void close() throws IOException {
synchronized(fdLock) {
if (fd != null) {
if (!stream) {
ResourceManager.afterUdpClose();
}
if (fdUseCount == 0) {
if (closePending) {
return;
}
closePending = true;
try {
socketPreClose();
} finally {
socketClose();
}
fd = null;
return;
} else {
if (!closePending) {
closePending = true;
fdUseCount--;
socketPreClose();
}
}
}
}
}
複製程式碼
socketPreClose
方法呼叫了socketClose0
方法,它的邏輯很簡單,判斷檔案描述符為空則丟擲SocketException("Socket closed")
異常,判斷檔案描述符無效則直接返回,接著獲取本地檔案描述符,通過呼叫close0
本地方法完成關閉操作。
private void socketPreClose() throws IOException {
socketClose0(true);
}
void socketClose0(boolean useDeferredClose/*unused*/) throws IOException {
if (fd == null)
throw new SocketException("Socket closed");
if (!fd.valid())
return;
final int nativefd = fdAccess.get(fd);
fdAccess.set(fd, -1);
close0(nativefd);
}
複製程式碼
close0
本地方法如下,邏輯為,
- 通過 Winsock 庫的
getsockopt
函式獲取 SO_LINGER 選項的值。 - 如果 linger 結構體的 l_onoff 為0,則呼叫 Winsock 庫的
WSASendDisconnect
來啟動關閉連線操作,達到優雅關閉。 - 最後呼叫 Winsock 庫的
closesocket
函式進行關閉操作,這裡額外說下 SO_LINGER 選項,如果 linger 結構體的第一個元素為0,此時表示關閉操作立即返回,作業系統接管套接字並且保證將所有資料傳送給對端。
JNIEXPORT void JNICALL Java_java_net_DualStackPlainSocketImpl_close0
(JNIEnv *env, jclass clazz, jint fd) {
NET_SocketClose(fd);
}
JNIEXPORT int JNICALL
NET_SocketClose(int fd) {
struct linger l = {0, 0};
int ret = 0;
int len = sizeof (l);
if (getsockopt(fd, SOL_SOCKET, SO_LINGER, (char *)&l, &len) == 0) {
if (l.l_onoff == 0) {
WSASendDisconnect(fd, NULL);
}
}
ret = closesocket (fd);
return ret;
}
複製程式碼
setOption方法
該方法用於設定套接字的選項,它通過套接字實現物件的setOption
方法來設定,
public <T> ServerSocket setOption(SocketOption<T> name, T value)
throws IOException
{
getImpl().setOption(name, value);
return this;
}
複製程式碼
套接字實現物件的setOption
方法實現如下,對不同的選項的合法性判斷,只有SO_KEEPALIVE
SO_SNDBUF
SO_RCVBUF
SO_REUSEADDR
SO_REUSEPORT
SO_LINGER
IP_TOS
TCP_NODELAY
這些選項屬於 Java 支援的選項,而其他選項則丟擲不支援異常。最後會再調另外一個setOption
方法,其中選項引數值由 SocketOptions 介面定義。
protected <T> void setOption(SocketOption<T> name, T value) throws IOException {
if (name == StandardSocketOptions.SO_KEEPALIVE &&
(getSocket() != null)) {
setOption(SocketOptions.SO_KEEPALIVE, value);
} else if (name == StandardSocketOptions.SO_SNDBUF &&
(getSocket() != null)) {
setOption(SocketOptions.SO_SNDBUF, value);
} else if (name == StandardSocketOptions.SO_RCVBUF) {
setOption(SocketOptions.SO_RCVBUF, value);
} else if (name == StandardSocketOptions.SO_REUSEADDR) {
setOption(SocketOptions.SO_REUSEADDR, value);
} else if (name == StandardSocketOptions.SO_REUSEPORT &&
supportedOptions().contains(name)) {
setOption(SocketOptions.SO_REUSEPORT, value);
} else if (name == StandardSocketOptions.SO_LINGER &&
(getSocket() != null)) {
setOption(SocketOptions.SO_LINGER, value);
} else if (name == StandardSocketOptions.IP_TOS) {
setOption(SocketOptions.IP_TOS, value);
} else if (name == StandardSocketOptions.TCP_NODELAY &&
(getSocket() != null)) {
setOption(SocketOptions.TCP_NODELAY, value);
} else {
throw new UnsupportedOperationException("unsupported option");
}
}
複製程式碼
setOption
方法邏輯如下,
- 判斷是否正在關閉,是的話拋
SocketException("Socket Closed")
異常。 - 如果是 SO_LINGER 則判斷該選項的值得合法性,並且如果是布林型別則認為關閉,因為要開啟就必須設定一個整型數字。
- 如果是 SO_TIMEOUT 判斷其合法性並將其轉換為整型。
- 其他選項也做類似處理。
- 最後調
socketSetOption
方法。
public void setOption(int opt, Object val) throws SocketException {
if (isClosedOrPending()) {
throw new SocketException("Socket Closed");
}
boolean on = true;
switch (opt) {
case SO_LINGER:
if (val == null || (!(val instanceof Integer) && !(val instanceof Boolean)))
throw new SocketException("Bad parameter for option");
if (val instanceof Boolean) {
on = false;
}
break;
case SO_TIMEOUT:
if (val == null || (!(val instanceof Integer)))
throw new SocketException("Bad parameter for SO_TIMEOUT");
int tmp = ((Integer) val).intValue();
if (tmp < 0)
throw new IllegalArgumentException("timeout < 0");
timeout = tmp;
break;
case IP_TOS:
if (val == null || !(val instanceof Integer)) {
throw new SocketException("bad argument for IP_TOS");
}
trafficClass = ((Integer)val).intValue();
break;
case SO_BINDADDR:
throw new SocketException("Cannot re-bind socket");
case TCP_NODELAY:
if (val == null || !(val instanceof Boolean))
throw new SocketException("bad parameter for TCP_NODELAY");
on = ((Boolean)val).booleanValue();
break;
case SO_SNDBUF:
case SO_RCVBUF:
if (val == null || !(val instanceof Integer) ||
!(((Integer)val).intValue() > 0)) {
throw new SocketException("bad parameter for SO_SNDBUF " +
"or SO_RCVBUF");
}
break;
case SO_KEEPALIVE:
if (val == null || !(val instanceof Boolean))
throw new SocketException("bad parameter for SO_KEEPALIVE");
on = ((Boolean)val).booleanValue();
break;
case SO_OOBINLINE:
if (val == null || !(val instanceof Boolean))
throw new SocketException("bad parameter for SO_OOBINLINE");
on = ((Boolean)val).booleanValue();
break;
case SO_REUSEADDR:
if (val == null || !(val instanceof Boolean))
throw new SocketException("bad parameter for SO_REUSEADDR");
on = ((Boolean)val).booleanValue();
break;
case SO_REUSEPORT:
if (val == null || !(val instanceof Boolean))
throw new SocketException("bad parameter for SO_REUSEPORT");
if (!supportedOptions().contains(StandardSocketOptions.SO_REUSEPORT))
throw new UnsupportedOperationException("unsupported option");
on = ((Boolean)val).booleanValue();
break;
default:
throw new SocketException("unrecognized TCP option: " + opt);
}
socketSetOption(opt, on, val);
}
複製程式碼
繼續看socketSetOption
方法,邏輯如下,
- 獲取本地的檔案描述符。
- 如果是
SO_TIMEOUT
選項則直接返回,因為SO_TIMEOUT
選項屬於 Java 層自己定義出來的,並不需要傳遞到作業系統中,所以只要在 Java 層進行維護即可。 - 如果是
SO_REUSEPORT
選項則直接丟擲UnsupportedOperationException("unsupported option")
異常,因為 windows 並沒有該選項。 - 如果是其他的選項則將其值轉換成對應的型別,最後呼叫
setIntOption
本地方法。
void socketSetOption(int opt, boolean on, Object value)
throws SocketException {
int nativefd = checkAndReturnNativeFD();
if (opt == SO_TIMEOUT) {
return;
}
if (opt == SO_REUSEPORT) {
throw new UnsupportedOperationException("unsupported option");
}
int optionValue = 0;
switch(opt) {
case SO_REUSEADDR :
if (exclusiveBind) {
isReuseAddress = on;
return;
}
case TCP_NODELAY :
case SO_OOBINLINE :
case SO_KEEPALIVE :
optionValue = on ? 1 : 0;
break;
case SO_SNDBUF :
case SO_RCVBUF :
case IP_TOS :
optionValue = ((Integer)value).intValue();
break;
case SO_LINGER :
if (on) {
optionValue = ((Integer)value).intValue();
} else {
optionValue = -1;
}
break;
default :/* shouldn't get here */
throw new SocketException("Option not supported");
}
setIntOption(nativefd, opt, optionValue);
}
複製程式碼
setIntOption
方法的邏輯主要是組裝好 Winsock 庫介面需要的資料結構,根據 Java 層對應的選項對映成本地對應的選項,接著通過NET_SetSockOpt
函式設定該選項的值。
JNIEXPORT void JNICALL
Java_java_net_DualStackPlainSocketImpl_setIntOption
(JNIEnv *env, jclass clazz, jint fd, jint cmd, jint value)
{
int level = 0, opt = 0;
struct linger linger = {0, 0};
char *parg;
int arglen;
if (NET_MapSocketOption(cmd, &level, &opt) < 0) {
JNU_ThrowByName(env, "java/net/SocketException", "Invalid option");
return;
}
if (opt == java_net_SocketOptions_SO_LINGER) {
parg = (char *)&linger;
arglen = sizeof(linger);
if (value >= 0) {
linger.l_onoff = 1;
linger.l_linger = (unsigned short)value;
} else {
linger.l_onoff = 0;
linger.l_linger = 0;
}
} else {
parg = (char *)&value;
arglen = sizeof(value);
}
if (NET_SetSockOpt(fd, level, opt, parg, arglen) < 0) {
NET_ThrowNew(env, WSAGetLastError(), "setsockopt");
}
}
複製程式碼
NET_SetSockOpt
函式核心邏輯是呼叫 Winsock 庫的setsockopt
函式對選項進行設定,另外,對於一些選項會做額外處理,比如當SO_REUSEADDR
選項時,會先查詢作業系統的SO_EXCLUSIVEADDRUSE
選項的值是否為1,即是否開啟了獨佔地址功能,如果開啟了則不用進一步呼叫setsockopt
函式而直接返回。
JNIEXPORT int JNICALL
NET_SetSockOpt(int s, int level, int optname, const void *optval,
int optlen)
{
int rv = 0;
int parg = 0;
int plen = sizeof(parg);
if (level == IPPROTO_IP && optname == IP_TOS) {
int *tos = (int *)optval;
*tos &= (IPTOS_TOS_MASK | IPTOS_PREC_MASK);
}
if (optname == SO_REUSEADDR) {
rv = NET_GetSockOpt(s, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, (char *)&parg, &plen);
if (rv == 0 && parg == 1) {
return rv;
}
}
rv = setsockopt(s, level, optname, optval, optlen);
if (rv == SOCKET_ERROR) {
...
}
return rv;
}
複製程式碼
SocketOptions 介面在 Java 層定義了以下的選項,並不是每個選項名都在 Winsock 庫中有選項與之對應,但能對應上的選項的值都相同。
public interface SocketOptions {
@Native public static final int TCP_NODELAY = 0x0001;
@Native public static final int SO_BINDADDR = 0x000F;
@Native public static final int SO_REUSEADDR = 0x04;
@Native public static final int SO_REUSEPORT = 0x0E;
@Native public static final int SO_BROADCAST = 0x0020;
@Native public static final int IP_MULTICAST_IF = 0x10;
@Native public static final int IP_MULTICAST_IF2 = 0x1f;
@Native public static final int IP_TOS = 0x3;
@Native public static final int SO_LINGER = 0x0080;
@Native public static final int SO_TIMEOUT = 0x1006;
@Native public static final int SO_SNDBUF = 0x1001;
@Native public static final int SO_RCVBUF = 0x1002;
@Native public static final int SO_KEEPALIVE = 0x0008;
@Native public static final int SO_OOBINLINE = 0x1003;
}
複製程式碼
Winsock 庫的相關的大部分選項的定義如下,比如TCP_NODELAY
選項在 Java 層和 C/C++ 層的值是相同的。其他選項也類似,在 Java 層能找到對應的選項則在本地也能找到與之對應的選項。
#define SO_DEBUG 0x0001 /* turn on debugging info recording */
#define SO_ACCEPTCONN 0x0002 /* socket has had listen() */
#define SO_REUSEADDR 0x0004 /* allow local address reuse */
#define SO_KEEPALIVE 0x0008 /* keep connections alive */
#define SO_DONTROUTE 0x0010 /* just use interface addresses */
#define SO_BROADCAST 0x0020 /* permit sending of broadcast msgs */
#define SO_USELOOPBACK 0x0040 /* bypass hardware when possible */
#define SO_LINGER 0x0080 /* linger on close if data present */
#define SO_OOBINLINE 0x0100 /* leave received OOB data in line */
#define SO_SNDBUF 0x1001 /* send buffer size */
#define SO_RCVBUF 0x1002 /* receive buffer size */
#define SO_SNDLOWAT 0x1003 /* send low-water mark */
#define SO_RCVLOWAT 0x1004 /* receive low-water mark */
#define SO_SNDTIMEO 0x1005 /* send timeout */
#define SO_RCVTIMEO 0x1006 /* receive timeout */
#define SO_ERROR 0x1007 /* get error status and clear */
#define SO_TYPE 0x1008 /* get socket type */
#define SO_BSP_STATE 0x1009 /* get socket 5-tuple state*/
#define SO_GROUP_ID 0x2001 /* ID of a socket group*/
#define SO_GROUP_PRIORITY 0x2002 /* the relative priority within a group*/
#define SO_MAX_MSG_SIZE 0x2003 /* maximum message size*/
#define SO_CONDITIONAL_ACCEPT 0x3002 /* enable true conditional accept: connection is not ack-ed to the other side until conditional function returns CF_ACCEPT*/
#define SO_PAUSE_ACCEPT 0x3003 /* pause accepting new connections*/
#define SO_COMPARTMENT_ID 0x3004 /* get/set the compartment for a socket*/
#define WSK_SO_BASE 0x4000 /* */
#define TCP_NODELAY 0x0001 /* Options to use with [gs]etsockopt at the IPPROTO_TCP level.*/
複製程式碼
setSoTimeout方法
該方法主要用於設定 ServerSocket 的 accept
方法,也就是接收套接字連線的等待超時時間,與之對應的為 SO_TIMEOUT 選項,它的單位是毫秒,一旦達到該超時時間則會丟擲 SocketTimeoutException 異常,但該 ServerSocket 物件仍然是有效,也就是說如果捕獲到以上丟擲的異常的話還是可以繼續使用它的。
public synchronized void setSoTimeout(int timeout) throws SocketException {
if (isClosed())
throw new SocketException("Socket is closed");
getImpl().setOption(SocketOptions.SO_TIMEOUT, timeout);
}
複製程式碼
套接字實現物件的setOption
方法上面有詳細的講解,注意看到SO_TIMEOUT
的情況,判斷設定的值必須為整型,且將其轉換成整型並賦給 timeout 變數,因為SO_TIMEOUT
選項屬於 Java 層自己定義出來的,並不需要傳遞到作業系統中,所以只要在 Java 層進行維護即可。最後呼叫的socketSetOption
方法也是直接返回並不做什麼操作。
setReuseAddress方法
該方法可以允許多次繫結同個地址埠,它的作用主要是某個地址埠關閉後會有一段時間處於 TIME_WAIT 狀態,該狀態下可能不在允許套接字繫結該埠,必須要等到完全關閉才允許再次繫結,通過設定該方法可以讓其重複繫結。另外,該方法實際與sun.net.useExclusiveBind
系統引數有緊密聯絡,預設情況下該引數值為 true,所以作業系統預設是使用了排他繫結的,這種情況下,呼叫setReuseAddress
方法不會真正去改變作業系統。
public void setReuseAddress(boolean on) throws SocketException {
if (isClosed())
throw new SocketException("Socket is closed");
getImpl().setOption(SocketOptions.SO_REUSEADDR, Boolean.valueOf(on));
}
複製程式碼
setReuseAddress
方法呼叫套接字實現物件的setOption
方法,該方法前面有詳細的講解,其中可以看到case SO_REUSEADDR
時將其值轉換成 boolean 值然後呼叫socketSetOption
方法。
socketSetOption
方法中,當case SO_REUSEADDR
時可以看到 exclusiveBind 為 true 時則直接設定完標識就返回了,不會繼續做其他操作,而這裡的 exclusiveBind 預設為 true,可以通過sun.net.useExclusiveBind
引數來改變。
如果sun.net.useExclusiveBind
引數設定為 false,則會呼叫setIntOption
本地方法,該函式會間接呼叫NET_SetSockOpt
函式,主要邏輯是先判斷是不是已經設定了SO_EXCLUSIVEADDRUSE
選項,如果設定了則無需再做操作了,直接返回。否則通過 Winsock 庫的setsockopt
函式來設定SO_REUSEADDR
選項。
toString方法
返回 ServerSocket 物件字串,如果還沒繫結則返回ServerSocket[unbound]
,如果繫結了則根據安全管理器為不為空分別獲取回送地址或IP地址,最後返回形如ServerSocket[addr=xxx,localport=xxx]
的字串。
public String toString() {
if (!isBound())
return "ServerSocket[unbound]";
InetAddress in;
if (System.getSecurityManager() != null)
in = InetAddress.getLoopbackAddress();
else
in = impl.getInetAddress();
return "ServerSocket[addr=" + in +
",localport=" + impl.getLocalPort() + "]";
}
複製程式碼
setReceiveBufferSize方法
該方法用於設定接收的緩衝區大小,設定後作為 ServerSocket 接收到的套接字的接收緩衝區的預設值,預設值為64K,在 ServerSocket 繫結之前設定才能生效。該方法主要邏輯是先判斷大小必須大於0且套接字不處於關閉狀態,然後呼叫套接字實現物件的setOption
方法。
public synchronized void setReceiveBufferSize (int size) throws SocketException {
if (!(size > 0)) {
throw new IllegalArgumentException("negative receive size");
}
if (isClosed())
throw new SocketException("Socket is closed");
getImpl().setOption(SocketOptions.SO_RCVBUF, size);
}
複製程式碼
setOption
方法前面有詳細講解,這裡不再贅述。
-------------推薦閱讀------------
------------------廣告時間----------------
知識星球:遠洋號
公眾號的選單已分為“分散式”、“機器學習”、“深度學習”、“NLP”、“Java深度”、“Java併發核心”、“JDK原始碼”、“Tomcat核心”等,可能有一款適合你的胃口。
歡迎關注: