## 45 Socket 源碼及面試題
## 引導語
Socket 中文翻譯叫套接字,可能很多工作四五年的同學都沒有用過這個 API,但只要用到這個 API 時,必然是在重要的工程的核心代碼處。
大家平時基本都在用開源的各種 rpc 框架,比如說 Dubbo、gRPC、Spring Cloud 等等,很少需要手寫網絡調用,以下三小節可以幫助大家補充這塊的內容,當你真正需要的時候,可以作為手冊示例。
本文和《ServerSocket 源碼及面試題》一文主要說 Socket 和 ServerSocket 的源碼,《工作實戰:Socket 結合線程池的使用》這章主要說兩個 API 在實際工作中如何落地。
### 1 Socket 整體結構
Socket 的結構非常簡單,Socket 就像一個殼一樣,將套接字初始化、創建連接等各種操作包裝了一下,其底層實現都是 SocketImpl 實現的,Socket 本身的業務邏輯非常簡單。
Socket 的屬性不多,有套接字的狀態,SocketImpl,讀寫的狀態等等,源碼如下圖:

套接字的狀態變更都是有對應操作方法的,比如套接字新建(createImpl 方法)后,狀態就會更改成 created = true,連接(connect)之后,狀態更改成 connected = true 等等。
### 2 初始化
Socket 的構造器比較多,可以分成兩大類:
1. 指定代理類型(Proxy)創建套節點,一共有三種類型為:DIRECT(直連)、HTTP(HTTP、FTP 高級協議的代理)、SOCKS(SOCKS 代理),三種不同的代碼方式對應的 SocketImpl 不同,分別是:PlainSocketImpl、HttpConnectSocketImpl、SocksSocketImpl,除了類型之外 Proxy 還指定了地址和端口;
2. 默認 SocksSocketImpl 創建,并且需要在構造器中傳入地址和端口,源碼如下:
```
// address 代表IP地址,port 表示套接字的端口 // address 我們一般使用 InetSocketAddress,InetSocketAddress 有 ip+port、域名+port、InetAddress 等初始化方式 public Socket(InetAddress address, int port) throws IOException { this(address != null ? new InetSocketAddress(address, port) : null, (SocketAddress) null, true); }
```
這里的 address 可以是 ip 地址或者域名,比如說 127.0.0.1 或者 www.wenhe.com。
我們一起看一下這個構造器調用的 this 底層構造器的源碼:
```
// stream 為 true 時,表示為stream socket 流套接字,使用 TCP 協議,比較穩定可靠,但占用資源多 // stream 為 false 時,表示為datagram socket 數據報套接字,使用 UDP 協議,不穩定,但占用資源少 private Socket(SocketAddress address, SocketAddress localAddr, boolean stream) throws IOException { setImpl(); // backward compatibility if (address == null) throw new NullPointerException();
try { // 創建 socket createImpl(stream); // 如果 ip 地址不為空,綁定地址 if (localAddr != null) // create、bind、connect 也是 native 方法 bind(localAddr); connect(address); } catch (IOException | IllegalArgumentException | SecurityException e) { try { close(); } catch (IOException ce) { e.addSuppressed(ce); } throw e; } }
```
從源碼中可以看出:
1. 在構造 Socket 的時候,你可以選擇 TCP 或 UDP,默認是 TCP;
2. 如果構造 Socket 時,傳入地址和端口,那么在構造的時候,就會嘗試在此地址和端口上創建套接字;
3. Socket 的無參構造器只會初始化 SocksSocketImpl,并不會和當前地址端口綁定,需要我們手動的調用 connect 方法,才能使用當前地址和端口;
4. Socket 我們可以理解成網絡溝通的語言層次的抽象,底層網絡創建、連接和關閉,仍然是 TCP 或 UDP 本身網絡協議指定的標準,Socket 只是使用 Java 語言做了一層封裝,從而讓我們更方便地使用。
### 3 connect 連接服務端
connect 方法主要用于 Socket 客戶端連接上服務端,如果底層是 TCP 層協議的話,就是通過三次握手和服務端建立連接,為客戶端和服務端之間的通信做好準備,底層源碼如下:
```
public void connect(SocketAddress endpoint, int timeout) throws IOException { }
```
connect 方法要求有兩個入參,第一個入參是 SocketAddress,表示服務端的地址,我們可以使用 InetSocketAddress 進行初始化,比如:new InetSocketAddress(“www.wenhe.com”, 2000)。
第二入參是超時時間的意思(單位毫秒),表示客戶端連接服務端的最大等待時間,如果超過當前等待時間,仍然沒有成功建立連接,拋 SocketTimeoutException 異常,如果是 0 的話,表示無限等待。
### 4 Socket 常用設置參數
Socket 的常用設置參數在 SocketOptions 類中都可以找到,接下來我們來一一分析下,以下理解大多來自類注釋和網絡。
#### 4.1 setTcpNoDelay
此方法是用來設置 TCP_NODELAY 屬性的,屬性的注釋是這樣的:此設置僅僅對 TCP 生效,主要為了禁止使用 Nagle 算法,true 表示禁止使用,false 表示使用,默認是 false。
對于 Nagle 算法,我們引用維基百科上的解釋:
納格算法是以減少數據包發送量來增進 [TCP/IP] 網絡的性能,它由約翰·納格任職于Ford Aerospace時命名。
納格的文件[注 1]描述了他所謂的“小數據包問題”-某個應用程序不斷地提交小單位的數據,且某些常只占1字節大小。因為TCP數據包具有40字節的標頭信息(TCP與
IPv4
各占20字節),這導致了41字節大小的數據包只有1字節的可用信息,造成龐大的浪費。這種狀況常常發生于Telnet工作階段-大部分的鍵盤操作會產生1字節的數據并馬上提交。更糟的是,在慢速的網絡連線下,這類的數據包會大量地在同一時點傳輸,造成壅塞碰撞。
納格算法的工作方式是合并(coalescing)一定數量的輸出數據后一次提交。特別的是,只要有已提交的數據包尚未確認,發送者會持續緩沖數據包,直到累積一定數量的數據才提交。
總結算法開啟關閉的場景:
1. 如果 Nagle 算法關閉,對于小數據包,比如一次鼠標移動,點擊,客戶端都會立馬和服務端交互,實時響應度非常高,但頻繁的通信卻很占用不少網絡資源;
2. 如果 Nagle 算法開啟,算法會自動合并小數據包,等到達到一定大小(MSS)后,才會和服務端交互,優點是減少了通信次數,缺點是實時響應度會低一些。
Socket 創建時,默認是開啟 Nagle 算法的,可以根據實時性要求來選擇是否關閉 Nagle 算法。
#### 4.2 setSoLinger
setSoLinger 方法主要用來設置 SO_LINGER 屬性值的。
注釋上大概是這個意思:在我們調用 close 方法時,默認是直接返回的,但如果給 SOLINGER 賦值,就會阻塞 close 方法,在 SOLINGER 時間內,等待通信雙方發送數據,如果時間過了,還未結束,將發送 TCP RST 強制關閉 TCP 。
我們看一下 setSoLinger 源碼:
```
// on 為 false,表示不啟用延時關閉,true 的話表示啟用延時關閉 // linger 為延時的時間,單位秒 public void setSoLinger(boolean on, int linger) throws SocketException { // 檢查是否已經關閉
if (isClosed()) throw new SocketException("Socket is closed"); // 不啟用延時關閉 if (!on) { getImpl().setOption(SocketOptions.SO_LINGER, new Boolean(on)); // 啟用延時關閉,如果 linger 為 0,那么會立即關閉 // linger 最大為 65535 秒,約 18 小時 } else { if (linger < 0) { throw new IllegalArgumentException("invalid value for SO_LINGER"); } if (linger > 65535) linger = 65535; getImpl().setOption(SocketOptions.SO_LINGER, new Integer(linger)); } }
```
#### 4.3 setOOBInline
setOOBInline 方法主要使用設置 SO_OOBINLINE 屬性。
注釋上說:如果希望接受 TCP urgent data(TCP 緊急數據)的話,可以開啟該選項,默認該選項是關閉的,我們可以通過 Socket#sendUrgentData 方法來發送緊急數據。
查詢了很多資料,都建議盡可能的去避免設置該值,禁止使用 TCP 緊急數據。
#### 4.4 setSoTimeout
setSoTimeout 方法主要是用來設置 SO_TIMEOUT 屬性的。
注釋上說:用來設置阻塞操作的超時時間,阻塞操作主要有:
1. ServerSocket.accept() 服務器等待客戶端的連接;
2. SocketInputStream.read() 客戶端或服務端讀取輸入超時;
3. DatagramSocket.receive()。
我們必須在必須在阻塞操作之前設置該選項, 如果時間到了,操作仍然在阻塞,會拋出 InterruptedIOException 異常(Socket 會拋出 SocketTimeoutException 異常,不同的套接字拋出的異常可能不同)。
對于 Socket 來說,超時時間如果設置成 0,表示沒有超時時間,阻塞時會無限等待。
#### 4.5 setSendBufferSize
setSendBufferSize 方法主要用于設置 SO_SNDBUF 屬性的,入參是 int 類型,表示設置發送端(輸出端)的緩沖區的大小,單位是字節。
入參 size 必須大于 0,否則會拋出 IllegalArgumentException 異常。
一般我們都是采取默認的,如果值設置太小,很有可能導致網絡交互過于頻繁,如果值設置太大,那么交互變少,實時性就會變低。
#### 4.6 setReceiveBufferSize
setReceiveBufferSize 方法主要用來設置 SO_RCVBUF 屬性的,入參是 int 類型,表示設置接收端的緩沖區的大小,單位是字節。
入參 size 必須大于 0,否則會拋出 IllegalArgumentException 異常。
一般來說,在套接字建立連接之后,我們可以隨意修改窗口大小,但是當窗口大小大于 64k 時,需要注意:
1. 必須在 Socket 連接客戶端之前設置緩沖值;
2. 必須在 ServerSocket 綁定本地地址之前設置緩沖值。
#### 4.7 setKeepAlive
setKeepAlive 方法主要用來設置 SO_KEEPALIVE 屬性,主要是用來探測服務端的套接字是否還是存活狀態,默認設置是 false,不會觸發這個功能。
如果 SO_KEEPALIVE 開啟的話,TCP 自動觸發功能:如果兩小時內,客戶端和服務端的套接字之間沒有任何通信,TCP 會自動發送 keepalive 探測給對方,對方必須響應這個探測(假設是客戶端發送給服務端),預測有三種情況:
1. 服務端使用預期的 ACK 回復,說明一切正常;
2. 服務端回復 RST,表示服務端處于死機或者重啟狀態,終止連接;
3. 沒有得到服務端的響應(會嘗試多次),表示套接字已經關閉了。
#### 4.8 setReuseAddress
setReuseAddress 方法主要用來設置 SO_REUSEADDR 屬性,入參是布爾值,默認是 false。
套接字在關閉之后,會等待一段時間之后才會真正的關閉,如果此時有新的套接字前來綁定同樣的地址和端口時,如果 setReuseAddress 為 true 的話,就可以綁定成功,否則綁定失敗。
- 前言
- 第1章 基礎
- 01 開篇詞:為什么學習本專欄
- 02 String、Long 源碼解析和面試題
- 03 Java 常用關鍵字理解
- 04 Arrays、Collections、Objects 常用方法源碼解析
- 第2章 集合
- 05 ArrayList 源碼解析和設計思路
- 06 LinkedList 源碼解析
- 07 List 源碼會問哪些面試題
- 08 HashMap 源碼解析
- 09 TreeMap 和 LinkedHashMap 核心源碼解析
- 10 Map源碼會問哪些面試題
- 11 HashSet、TreeSet 源碼解析
- 12 彰顯細節:看集合源碼對我們實際工作的幫助和應用
- 13 差異對比:集合在 Java 7 和 8 有何不同和改進
- 14 簡化工作:Guava Lists Maps 實際工作運用和源碼
- 第3章 并發集合類
- 15 CopyOnWriteArrayList 源碼解析和設計思路
- 16 ConcurrentHashMap 源碼解析和設計思路
- 17 并發 List、Map源碼面試題
- 18 場景集合:并發 List、Map的應用場景
- 第4章 隊列
- 19 LinkedBlockingQueue 源碼解析
- 20 SynchronousQueue 源碼解析
- 21 DelayQueue 源碼解析
- 22 ArrayBlockingQueue 源碼解析
- 23 隊列在源碼方面的面試題
- 24 舉一反三:隊列在 Java 其它源碼中的應用
- 25 整體設計:隊列設計思想、工作中使用場景
- 26 驚嘆面試官:由淺入深手寫隊列
- 第5章 線程
- 27 Thread 源碼解析
- 28 Future、ExecutorService 源碼解析
- 29 押寶線程源碼面試題
- 第6章 鎖
- 30 AbstractQueuedSynchronizer 源碼解析(上)
- 31 AbstractQueuedSynchronizer 源碼解析(下)
- 32 ReentrantLock 源碼解析
- 33 CountDownLatch、Atomic 等其它源碼解析
- 34 只求問倒:連環相扣系列鎖面試題
- 35 經驗總結:各種鎖在工作中使用場景和細節
- 36 從容不迫:重寫鎖的設計結構和細節
- 第7章 線程池
- 37 ThreadPoolExecutor 源碼解析
- 38 線程池源碼面試題
- 39 經驗總結:不同場景,如何使用線程池
- 40 打動面試官:線程池流程編排中的運用實戰
- 第8章 Lambda 流
- 41 突破難點:如何看 Lambda 源碼
- 42 常用的 Lambda 表達式使用場景解析和應用
- 第9章 其他
- 43 ThreadLocal 源碼解析
- 44 場景實戰:ThreadLocal 在上下文傳值場景下的實踐
- 45 Socket 源碼及面試題
- 46 ServerSocket 源碼及面試題
- 47 工作實戰:Socket 結合線程池的使用
- 第10章 專欄總結
- 48 一起看過的 Java 源碼和面試真題