設(shè)置 Java Socket 選項(xiàng)
1. 前言
前面章節(jié)介紹了 Java TCP、UDP Socket 編程方法,按照文中介紹的方法去編寫 Socket 程序,是完全可以正常工作的。其實(shí),TCP/IP 協(xié)議棧允許你對 Socket 做一些定制,比如設(shè)置 Socket 的接收、發(fā)送緩沖區(qū)的大小,這就是常說的 Socket 選項(xiàng)。
本文首先會(huì)以 Linux 系統(tǒng)為例,介紹操作系統(tǒng) Socket 選項(xiàng)的基本概念,然后再介紹 Java 中如何去設(shè)置 Socket 選項(xiàng)。
2. Socket 選項(xiàng)的概念
操作系統(tǒng)協(xié)議棧支持的 Socket 選項(xiàng)參數(shù)有很多,匯總起來如下圖所示:
從圖中可以看出,Socket 選項(xiàng)按照級別進(jìn)行分類,級別有很多種,但是總結(jié)起來分兩類:
- 通用 Socket 級別的選項(xiàng)。枚舉值為 SOL_SOCKET。
- 協(xié)議相關(guān)的選項(xiàng)。協(xié)議棧為我們提供了控制所有協(xié)議的選項(xiàng),比如 IP、IPv6、TCP、UDP、ICMP 等。枚舉值的格式為 IPPROTO_XXX,XXX 代表協(xié)議。
每一種選項(xiàng)級別下面包含了很多選項(xiàng)參數(shù)。比如,通用 Socket 選項(xiàng)的級別枚舉值是 SOL_SOCKET,其下面包含 SO_RCVBUF 和 SO_SNDBUF 選項(xiàng)參數(shù);IP 協(xié)議選項(xiàng)的級別的枚舉值是 IPPROTO_IP,其下面包含 IP_TTL、IP_TOS 等選項(xiàng)參數(shù)。
在 Linux 系統(tǒng)下,所有的選項(xiàng)參數(shù)都可以在幫助手冊里面查找,具體方法如下:
通用 Socket 級別選項(xiàng)參數(shù)
man 7 socket
IP 協(xié)議級別選項(xiàng)參數(shù):
man 7 ip
IPv6 協(xié)議級別選項(xiàng)參數(shù):
man 7 ipv6
TCP 協(xié)議級別選項(xiàng)參數(shù):
man 7 tcp
UDP 協(xié)議級別選項(xiàng)參數(shù):
man 7 udp
Socket 選項(xiàng)參數(shù)最終是如何設(shè)置到協(xié)議棧的呢?協(xié)議棧提供了 getsockopt() 和 setsockopt() 兩個(gè) C 語言函數(shù),分別用于獲取和設(shè)置選項(xiàng)參數(shù)。
調(diào)用兩個(gè)函數(shù)所需要包含的頭文件,以及他們的聲明如下:
#include <sys/types.h>
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
如果你對系統(tǒng)本身的 Socket 選項(xiàng)感興趣,可以通過 man 查找相關(guān)幫助。本節(jié)重點(diǎn)介紹通用 Socket 選項(xiàng)。
3. 通用 Socket 選項(xiàng)
通用 Socket 選項(xiàng)的 level 枚舉值是 SOL_SOCKET。
表格中選項(xiàng)名稱不用多說,數(shù)據(jù)類型列表示選項(xiàng)值的類型,大多數(shù)是整形,還有一些是結(jié)構(gòu)體類型。有的選項(xiàng)是既可以設(shè)置值也可以讀取值,用 set 表示;有的選項(xiàng)只能讀取值,用 get 表示。常見選項(xiàng)參數(shù)如下:
選項(xiàng)名稱 | 數(shù)據(jù)類型 | get 或 set | 說明 |
---|---|---|---|
SO_BROADCAST | int | set | 設(shè)置 Socket 可以進(jìn)行局域網(wǎng)廣播,目標(biāo) IP 需要填網(wǎng)段的廣播地址或者是統(tǒng)一受限廣播地址 255.255.255.255。 |
SO_KEEPALIVE | int | set | 用于設(shè)置 TCP 連接的?;睿话愫苌儆?。 |
SO_LINGER | struct linger | set | 用于設(shè)置當(dāng) TCP 連接已經(jīng)關(guān)閉,但是未發(fā)送數(shù)據(jù)等待時(shí)間。通常設(shè)置 SO_LINGER 等待時(shí)間為 0,解決大量 TIME_WAIT 狀態(tài)的問題。 |
SO_OOBINLINE | int | set | 用于設(shè)置將“帶外數(shù)據(jù)”作為普通數(shù)據(jù)流來處理。 |
SO_RCVBUF | int | set | 設(shè)置 Socket 接收緩沖區(qū)大小。 |
SO_REUSEADDR | int | set | 用于設(shè)置在調(diào)用 bind() 函數(shù)時(shí),重用已經(jīng) bind 的 Socket 地址。 |
SO_SNDBUF | int | set | 設(shè)置 Socket 發(fā)送緩沖區(qū)大小。 |
4. 常用選項(xiàng)說明
下來,我們對 Socket 編程中常用的 Socket 選項(xiàng)重點(diǎn)介紹。
4.1 SO_REUSEADDR
TCP 連接關(guān)閉過程中,主動(dòng)關(guān)閉的一方會(huì)處于 TIME_WAIT 狀態(tài),要等待 2MSL 時(shí)間。而服務(wù)器在工作過程中有可能由于配置的改變而要重啟,或者是由于程序異常奔潰要重新啟動(dòng)。在這種情況下,如果服務(wù)器監(jiān)聽的 Socket 處于 TIME_WAIT 狀態(tài),那么調(diào)用 bind 方法綁定 Socket 就會(huì)失敗。如果要等待 2MSL 時(shí)間,對于服務(wù)器來說是難以接受的。要想解決此問題,需要給監(jiān)聽 Socket 設(shè)置 SO_REUSEADDR 選項(xiàng)。
Java 的 java.net.ServerSocket 類提供了 setReuseAddress 方法,可以用以設(shè)置 SO_REUSEADDR 選項(xiàng),如下:
ServerSocket ss = null;
try {
ss = new ServerSocket();
ss.setReuseAddress(true);
ss.bind(new InetSocketAddress(8022));
ss.accept();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (ss != null){
try {
ss.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 注意:
- SO_REUSEADDR 選項(xiàng)需要在 bind 方法調(diào)用前設(shè)置,所以要?jiǎng)?chuàng)建一個(gè)未綁定的 ServerSocket 對象,然后手動(dòng)執(zhí)行 bind 操作。
4.2 SO_KEEPALIVE
SO_KEEPALIVE 是協(xié)議棧提供的一種連接?;顧C(jī)制,一般是用在 TCP 協(xié)議中。主要目的是當(dāng)通信雙方長時(shí)間沒有數(shù)據(jù)交互,然而 Socket還沒有被關(guān)閉,協(xié)議棧會(huì)向?qū)Ψ桨l(fā)送一個(gè) Heartbeat 消息期望對方回復(fù)一個(gè) ACK,如果對方能回復(fù)說明連接是正常的,如果對方不能回復(fù),嘗試幾次以后就會(huì)關(guān)閉連接。系統(tǒng)?;畹臅r(shí)間一般是 2 小時(shí)。
Java 的 java.net.Socket 類提供了 setKeepAlive 方法,可以用以設(shè)置 SO_KEEPALIVE 選項(xiàng),如下:
sock.setKeepAlive(true);
4.3 SO_LINGER
SO_LINGER 是用來設(shè)置“連接關(guān)閉以后,未發(fā)送完的數(shù)據(jù)包還可以在協(xié)議棧逗留的時(shí)間”。java.net.Socket 提供了 setSoLinger 方法可以設(shè)置 SO_LINGER 選項(xiàng)。原型如下:
public void setSoLinger(boolean on, int linger) throws SocketException
-
如果設(shè)置 on 為 false,則該選項(xiàng)的值被忽略,協(xié)議棧會(huì)采用默認(rèn)行為。close 調(diào)用會(huì)立即返回給調(diào)用者,協(xié)議棧會(huì)盡可能將 Socket 發(fā)送緩沖區(qū)未發(fā)送的數(shù)據(jù)發(fā)送完成。
-
如果設(shè)置 on 為 true,但是 linger 為 0,當(dāng)你調(diào)用了 close() 方法以后,協(xié)議棧將丟棄保留在 Socket 發(fā)送緩沖區(qū)中未發(fā)送完的數(shù)據(jù),然后向?qū)Ψ桨l(fā)送一個(gè) RST。這樣連接很快會(huì)被關(guān)閉,不會(huì)進(jìn)入 TIME_WAIT 狀態(tài),這也是一個(gè)避免“由于大量 TIME_WAIT 狀態(tài)的 Socket 導(dǎo)致連接失敗“的解決辦法。
-
如果設(shè)置 on 為 true ,但是 linger 的取值大于 0,當(dāng)你調(diào)用了 close() 方法以后,如果 Socket 發(fā)送緩沖區(qū)還有未發(fā)送完的數(shù)據(jù),那么系統(tǒng)會(huì)等待一個(gè)指定的時(shí)間,close() 才返回。注意,這種情況下 close() 方法返回,并不能保證 Socket 發(fā)送緩沖區(qū)中未發(fā)送的數(shù)據(jù)被成功發(fā)送完。
- 注意:
- 參數(shù) linger 的單位是秒。
sock.setSoLinger(true, 20);
4.4 SO_RCVBUF
SO_RCVBUF 很好理解,用于設(shè)置 Socket 的接收緩沖區(qū)大小。TCP 一般不需要設(shè)置,UDP 可能需要設(shè)置。java.net.Socket 類提供了 setReceiveBufferSize 方法可以設(shè)置接收緩沖區(qū)的大小。
sock.setReceiveBufferSize(16384);
4.5 SO_SNDBUF
SO_SNDBUF 也很好理解,用于設(shè)置 Socket 的發(fā)送緩沖區(qū)大小。一般不需要設(shè)置,采用系統(tǒng)默認(rèn)大小即可。java.net.Socket 類提供了 setSendBufferSize 方法可以設(shè)置發(fā)送緩沖區(qū)的大小。
sock.setSendBufferSize(16384);
4.6 SO_OOBINLINE
SO_OOBINLINE 用于設(shè)置將“帶外數(shù)據(jù)”作為普通數(shù)據(jù)流來處理。java.net.Socket 類提供了 setOOBInline 方法可以設(shè)置 SO_OOBINLINE 選項(xiàng)。
sock.setOOBInline(true);
4.7 TCP_NODELAY
TCP_NODELAY 用于關(guān)閉 Nagle 算法,一般是用在實(shí)時(shí)性要求比較高的場景。java.net.Socket 提供了 setTcpNoDelay 方法用于設(shè)置 TCP_NODELAY 選項(xiàng)。
sock.setTcpNoDelay(true);
5 小結(jié)
本節(jié)重點(diǎn)是介紹在 java 中設(shè)置常用 Socket 選項(xiàng)的方法。當(dāng)然,我們是從 Linux 系統(tǒng)本身提供的 Socket 選項(xiàng)開始的,我們也介紹了在 linux 系統(tǒng)中如何查找 Socket 選項(xiàng)的方法。了解操作系統(tǒng)對 Socket 選項(xiàng)的支持,可以讓你形成一個(gè)完整的認(rèn)識(shí)。
文中列出了常用 Socket 選項(xiàng)的應(yīng)用場景。SO_REUSEADDR 是服務(wù)器必須要設(shè)置的一個(gè)選項(xiàng),也只有服務(wù)器才需要此功能。TCP_NODELAY 是在開發(fā)實(shí)時(shí)性要求很高的程序時(shí),必須要設(shè)置的,比如音視頻通信系統(tǒng)。
SO_LINGER 是在服務(wù)器端解決“由于 TIME_WAIT 過多,導(dǎo)致連接失敗的問題”時(shí)的一個(gè)常用方法。其他選項(xiàng),可以根據(jù)需要選擇是否開啟。