1,socket類之間繼承關係圖java
2,服務端socket鏈接維護linux
管理客戶鏈接請求的任務是由操做系統來完成的。操做系統把這些鏈接請求存儲在一個先進先出的隊列中。許多操做系統限定了隊列的最大長度,通常爲50。當隊列中的鏈接請求達到了隊列的最大容量時,服務器進程所在的主機會拒絕新的鏈接請求。只有當服務器進程經過ServerSocket的accept()方法從隊列中取出鏈接請求,使隊列騰出空位時,隊列才能繼續加入新的鏈接請求。
對於客戶進程,若是它發出的鏈接請求被加入到服務器的隊列中,就意味着客戶與服務器的鏈接創建成功,客戶進程從Socket構造方法中正常返回。若是客戶進程發出的鏈接請求被服務器拒絕,Socket構造方法就會拋出ConnectionException。web
3,服務端socket建立過程源碼分析面試
類定義
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 是否是使用舊的實現。網絡
主要方法併發
構造函數框架
有五類構造函數,能夠什麼參數都不傳,也能夠傳入 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;
}
}
setImpl方法
設置套接字實現對象,這裏提供了工廠模式能夠方便的對接其餘的實現,而默認是沒有工廠對象的,因此模式的實現爲 SocksSocketImpl 對象。
private void setImpl() {
if (factory != null) {
impl = factory.createSocketImpl();
checkOldImpl();
} else {
impl = new SocksSocketImpl();
}
if (impl != null)
impl.setServerSocket(this);
}
createImpl方法
該方法用於建立套接字實現對象,若是實現對象爲空則先調用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 是非鏈接的非流的。
兩類鏈接是經過 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的實現,邏輯爲:
1.經過調用NET_Socket函數建立套接字句柄,其中經過 Winsock 庫的 socket函數建立句柄,而且經過SetHandleInformation函數設置句柄的繼承標誌。這裏能夠看到根據 stream 標識對應的類別爲SOCK_STREAM和 SOCK_DGRAM。若是句柄是無效的則拋出 create 異常。
2.而後經過setsockopt函數設置套接字的選項值,若是發生錯誤則拋出 create 異常。
3.最後再次經過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;
}
bind方法
該方法用於將套接字綁定到指定的地址和端口上,若是 SocketAddress 爲空,即表明地址和端口都不指定,此時系統會將套接字綁定到全部有效的本地地址,且動態生成一個端口。邏輯以下:
1.判斷是否已關閉,關閉則拋SocketException("Socket is closed")。
2.判斷是否已綁定,綁定則拋SocketException("Already bound")。
3.判斷地址是否爲空,爲空則建立一個 InetSocketAddress,默認是全部有效的本地地址,對應的爲0.0.0.0,而端口默認爲0,由操做系統動態生成。
4.判斷對象是否爲 InetSocketAddress 類型,不是則拋IllegalArgumentException("Unsupported address type")。
5.判斷地址是否已經有值了,沒有則拋SocketException("Unresolved address")。
6.backlog 若是小於1則設爲50。
7.經過安全管理器檢查端口。
8.經過套接字實現對象調用bind和listen方法。
9.bound 標識設爲 true。
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方法,邏輯以下:
1.獲取本地文件描述符 nativefd。
2.判斷地址是否爲空。
3.調用bind0本地方法。
4.若是端口爲0還會調用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本地方法邏輯以下,
1.經過NET_InetAddressToSockaddr函數將 Java 層的 InetAddress 對象的屬性值填充到 SOCKETADDRESS 聯合體中,對應的都是 Winsock 庫的結構體,目的便是爲了填充好它們。
typedef union {
struct sockaddr sa;
struct sockaddr_in sa4;
struct sockaddr_in6 sa6;
} SOCKETADDRESS;
2.NET_WinBind函數的邏輯是先根據 exclBind 標識看是否須要獨佔端口,若是須要則經過 Winsock 庫的setsockopt函數設置SO_EXCLUSIVEADDRUSE選型,在 Java 層中決定獨不獨佔端口能夠經過sun.net.useExclusiveBind參數來配置,默認狀況下是獨佔的。接着,經過操做系統的bind函數完成綁定操做。
3.若是綁定失敗則拋異常。
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");
}
}
socket系統調用listen只被tcp 服務器使用,他作兩件事:
1. 將未連接的套接口轉換爲被動套接口,指示內核接受向此套接口的鏈接請求,調用此係統調用後tcp 狀態機有close轉換到listen.
2.第二個參數制定了內核爲此套接口排隊的最大鏈接個數。
關於第二個參數,對於給定的監聽套接口,內核要維護兩個隊列,未連接隊列和已鏈接隊列,根據tcp 三路握手過程當中三個分節來分隔這兩個隊列。
服務器處於listen狀態時收到客戶端syn 分節(connect)時在未完成隊列中建立一個新的條目,而後用三路握手的第二個分節即服務器的syn 響應及對客戶端syn的ack,此條目在第三個分節到達前(客戶端對服務器syn的ack)一直保留在未完成鏈接隊列中,若是三路握手完成,該條目將從未完成鏈接隊列搬到已完成鏈接隊列尾部。當進程調用accept時,從已完成隊列中的頭部取出一個條目給進程,當已完成隊列爲空時進程將睡眠,直到有條目在已完成鏈接隊列中才喚醒。
backlog被規定爲兩個隊列總和的最大值,大多數實現默認值爲5,但在高併發web服務器中此值顯然不夠,lighttpd中此值達到128*8.須要設置此值更大一些的緣由是未完成鏈接隊列的長度可能由於客戶端SYN的到達及等待三路握手第三個分節的到達延時而增大。
當客戶端發起connect而致使發送syn分節給服務器端握手,若是這時兩個隊列都是滿的,tcp就忽略此分節,而且不發RST,這將致使客戶端TCP重發SYN(超時),服務器端忽略syn而不發RST響應的緣由是若是發RST ,客戶端connect將當即返回錯誤,強制客戶端進程處理這種狀況,而不是讓tcp的正常重傳機制來處理。實際上全部源自Berkeley的實現都是忽略新的SYN分節。
還有,backlog爲0 時在linux上代表潤許不受限制的鏈接數,這是一個缺陷,由於它可能會致使SYN Flooding(拒絕服務型攻擊), 下一篇文章會簡單解釋。
cat /proc/sys/net/ipv4/tcp_max_syn_backlog
1024
accept方法
該方法用於接收套接字鏈接,套接字開啓監聽後會阻塞等待套接字鏈接,一旦有鏈接可接收了則經過該方法進行接收操做。邏輯爲,
1.判斷套接字是否已經關閉。
2.判斷套接字是否已經綁定。
3.建立 Socket 對象,並調用implAccept方法,
4.返回 Socket 對象。
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;
}
在這裏給你們提供一個java進階的學習交流平臺
◾具備1-5工做經驗的,面對目前流行的技術不知從何下手,須要突破技術瓶頸的能夠加羣。
◾在公司待久了,過得很安逸,但跳槽時面試碰壁。須要在短期內進修、跳槽拿高薪的能夠加羣。
◾若是沒有工做經驗,但基礎很是紮實,對java工做機制,經常使用設計思想,經常使用java開發框架掌握熟練的能夠加羣。
◾731661047
implAccept方法邏輯爲,
1.傳入的 Socket 對象裏面的套接字實現若是爲空,則經過setImpl方法設置套接字實現,若是非空就執行reset操做。
2.調用套接字實現對象的accept方法完成接收操做,作這一步是由於咱們的 Socket 對象裏面的 SocketImpl 對象還差操做系統底層的套接字對應的文件描述符。
3.調用安全管理器檢查權限。
4.獲得完整的 SocketImpl 對象,賦值給 Socket 對象,而且調用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方法,邏輯爲,
1.獲取操做系統的文件描述符。
2.SocketImpl 對象爲空則拋出NullPointerException("socket is null")。
3.若是 timeout 小於等於0則直接調用本地accept0方法,一直阻塞。
4.反之,若是 timeout 大於0,即設置了超時,那麼會先調用configureBlocking本地方法,該方法用於將指定套接字設置爲非阻塞模式。接着調用waitForNewConnection本地方法,若是在超時時間內能獲取到新的套接字,則調用accept0方法獲取新套接字的句柄,獲取成功後再次調用configureBlocking本地方法將新套接字設置爲阻塞模式。最後,若是非阻塞模式失敗了,則將原來的套接字設置會紫塞模式,這裏使用了 finally,因此能保證就算髮生異常也能被執行。
5.最後將獲取到的新文件描述符賦給 SocketImpl 對象,同時也將遠程端口、遠程地址、本地端口等都賦給它相關變量。
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本地方法實現邏輯爲,
1.經過C語言的memset函數將 SOCKETADDRESS 聯合體對應的結構體內的值設置爲0。
2.經過 Winsock 庫的accept函數獲取套接字地址。
3.判斷接收的套接字描述符是否無效,分別可能拋 InterruptedIOException 或 SocketException 異常。
4.經過SetHandleInformation函數設置句柄的繼承標誌。
5.NET_SockaddrToInetAddress函數用於將獲得的套接字轉換成 Java 層的 InetAddress 對象。
6.將生成的 InetAddress 對象用於生成 Java 層的 InetSocketAddress 對象。
7.賦值給 Java 層的 InetSocketAddress 數組對象。
8.返回新接收的套接字的文件描述符。
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;
原文連接:https://juejin.im/post/5abae0895188255c566878d2?utm_source=tuicool&utm_medium=referral