首頁 > 軟體

Java面試Socket程式設計常用引數設定原始碼問題分析

2022-03-10 19:24:14

引導語

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結構常用引數面試的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com