Java面試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 的構造器比較多,可以分成兩大類:

指定代理類型(Proxy)創建套節點,一共有三種類型為:DIRECT(直連)、HTTP(HTTP、FTP 高級協議的代理)、SOCKS(SOCKS 代理),三種不同的代碼方式對應的 SocketImpl 不同,分別是:PlainSocketImpl、HttpConnectSocketImpl、SocksSocketImpl,除瞭類型之外 Proxy 還指定瞭地址和端口;

默認 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;
    }
}

從源碼中可以看出:

  • 在構造 Socket 的時候,你可以選擇 TCP 或 UDP,默認是 TCP;
  • 如果構造 Socket 時,傳入地址和端口,那麼在構造的時候,就會嘗試在此地址和端口上創建套接字;
  • Socket 的無參構造器隻會初始化 SocksSocketImpl,並不會和當前地址端口綁定,需要我們手動的調用 connect 方法,才能使用當前地址和端口;
  • 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)一定數量的輸出數據後一次提交。特別的是,隻要有已提交的數據包尚未確認,發送者會持續緩沖數據包,直到累積一定數量的數據才提交。

總結算法開啟關閉的場景:

如果 Nagle 算法關閉,對於小數據包,比如一次鼠標移動,點擊,客戶端都會立馬和服務端交互,實時響應度非常高,但頻繁的通信卻很占用不少網絡資源;如果 Nagle 算法開啟,算法會自動合並小數據包,等到達到一定大小(MSS)後,才會和服務端交互,優點是減少瞭通信次數,缺點是實時響應度會低一些。

Socket 創建時,默認是開啟 Nagle 算法的,可以根據實時性要求來選擇是否關閉 Nagle 算法。

4.2、setSoLinger

setSoLinger 方法主要用來設置 SO_LINGER 屬性值的。

註釋上大概是這個意思:在我們調用 close 方法時,默認是直接返回的,但如果給 SO_LINGER 賦值,就會阻塞 close 方法,在 SO_LINGER 時間內,等待通信雙方發送數據,如果時間過瞭,還未結束,將發送 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 屬性的。

註釋上說:用來設置阻塞操作的超時時間,阻塞操作主要有:

  • ServerSocket.accept() 服務器等待客戶端的連接;
  • SocketInputStream.read() 客戶端或服務端讀取輸入超時;
  • 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 時,需要註意:

必須在 Socket 連接客戶端之前設置緩沖值;必須在 ServerSocket 綁定本地地址之前設置緩沖值。

4.7、setKeepAlive

setKeepAlive 方法主要用來設置 SO_KEEPALIVE 屬性,主要是用來探測服務端的套接字是否還是存活狀態,默認設置是 false,不會觸發這個功能。

如果 SO_KEEPALIVE 開啟的話,TCP 自動觸發功能:如果兩小時內,客戶端和服務端的套接字之間沒有任何通信,TCP 會自動發送 keepalive 探測給對方,對方必須響應這個探測(假設是客戶端發送給服務端),預測有三種情況:

服務端使用預期的 ACK 回復,說明一切正常;服務端回復 RST,表示服務端處於死機或者重啟狀態,終止連接;沒有得到服務端的響應(會嘗試多次),表示套接字已經關閉瞭。

4.8、setReuseAddress

setReuseAddress 方法主要用來設置 SO_REUSEADDR 屬性,入參是佈爾值,默認是 false。

套接字在關閉之後,會等待一段時間之後才會真正的關閉,如果此時有新的套接字前來綁定同樣的地址和端口時,如果 setReuseAddress 為 true 的話,就可以綁定成功,否則綁定失敗。

5、總結

如果平時一直在做業務代碼,Socket 可能用到的很少,但面試問到網絡協議時,或者以後有機會做做中間件的時候,就會有大概率會接觸到 Socket,所以多學學,作為知識儲備也蠻好的。

以上就是Java編程Socket結構常用參數設置源碼及面試題的詳細內容,更多關於Java編程Socket結構常用參數面試的資料請關註WalkonNet其它相關文章!

推薦閱讀: