目錄
SO_REUSEADDR
一般來說,一個端口釋放后會等待兩分鍾之后才能再被使用,SO_REUSEADDR是讓端口釋放后立即就可以被再次使用
SO_REUSEADDR
用於對TCP套接字處於TIME_WAIT狀態下的socket,才可以重復綁定使用
server程序總是應該在調用bind()之前設置SO_REUSEADDR套接字選項
TCP,先調用close()的一方會進入TIME_WAIT狀態
SO_REUSEADDR提供如下四個功能:
- 允許啟動一個監聽服務器並捆綁其眾所周知端口,即使以前建立的將此端口用做他們的本地端口的連接仍存在。這通常是重啟監聽服務器時出現,若不設置此選項,則bind時將出錯
- 允許在同一端口上啟動同一服務器的多個實例,只要每個實例捆綁一個不同的本地IP地址即可。對於TCP,我們根本不可能啟動捆綁相同IP地址和相同端口號的多個服務器。
- 允許單個進程捆綁同一端口到多個套接口上,只要每個捆綁指定不同的本地IP地址即可。這一般不用於TCP服務器。
- SO_REUSEADDR允許完全重復的捆綁:
當一個IP地址和端口綁定到某個套接口上時,還允許此IP地址和端口捆綁到另一個套接口上。一般來說,這個特性僅在支持多播的系統上才有,而且只對UDP套接口而言(TCP不支持多播)。
SO_REUSEPORT選項有如下語義:
此選項允許完全重復捆綁,但僅在想捆綁相同IP地址和端口的套接口都指定了此套接口選項才行。
如果被捆綁的IP地址是一個多播地址,則SO_REUSEADDR和SO_REUSEPORT等效。
使用這兩個套接口選項的建議:
- 在所有TCP服務器中,在調用bind之前設置SO_REUSEADDR套接口選項;
- 當編寫一個同一時刻在同一主機上可運行多次的多播應用程序時,設置SO_REUSEADDR選項,並將本組的多播地址作為本地IP地址捆綁
time-wait
TIME_WAIT狀態有兩個存在的理由:
- (1)可靠地實現TCP全雙工連接的終止
- (2)允許老的重復分節在網絡中消逝
如圖所示,在主機A的4次揮手過程中,如果最后的數據丟失,則主機B會認為A未能收到自己發送的FIN消息,因此重傳。這時,收到FIN消息的主機A將重啟time-wait計時器。因此,如果網絡狀態不佳,time-wait狀態將持續
(1)如果服務器最后發送的ACK因為某種原因丟失了,那么客戶一定會重新發送FIN,這樣因為有TIME_WAIT的存在,服務器會重新發送ACK給客戶,如果沒有TIME_WAIT,那么無論客戶有沒有收到ACK,服務器都已經關掉連接了,此時客戶重新發送FIN,服務器將不會發送ACK,而是RST,從而使客戶端報錯。也就是說,TIME_WAIT有助於可靠地實現TCP全雙工連接的終止。
(2)如果沒有TIME_WAIT,我們可以在最后一個ACK還未到達客戶的時候,就建立一個新的連接。那么此時,如果客戶收到了這個ACK的話,就亂套了,必須保證這個ACK完全死掉之后,才能建立新的連接。也就是說,TIME_WAIT允許老的重復分節在網絡中消逝。
回到我們的問題,由於我並不是正常地經過四次斷開的方式中斷連接,所以並不會存在最后一個ACK的問題。所以,這樣是安全的。不過,最終的服務器版本,還是不要設置為端口可復用的
<?php
$address = '0.0.0.0';
$port = $argv[1] ?? 8071;
$listen = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if (false === $listen) errhandle(__LINE__);
//ctrl+c重啟時 能立馬重啟,要在bind,listen之前
if (true !== socket_set_option($listen, SOL_SOCKET, SO_REUSEADDR, 1)) errhandle(__LINE__);;
if (true !== socket_bind($listen, '0.0.0.0', $port)) errhandle(__LINE__);;
if (true !== socket_listen($listen, 5)) errhandle(__LINE__);; //待連接隊列長度
//socket_set_nonblock($listen);
echo "Server linsten on:{$address}:$port" . PHP_EOL;
while (true) {
//連接socket,處理連接的接入
$sock_client = socket_accept($listen);
if (false === $sock_client) {
errhandle(__LINE__,false);
continue;
}
processClientConn($sock_client);
}
//處理已經連入的連接
function processClientConn($sock_client)
{
if (socket_getpeername($sock_client, $clinet_addr, $client_port)) {
echo "New client " . intval($sock_client) . " come from {$clinet_addr}:$client_port" . PHP_EOL;
sayWelcome($sock_client);
}
while (true) {
//接收到不少於len
$len = socket_recv($sock_client, $buf, 2048, 0);
if ($len === false) {
echo "no data" . PHP_EOL;
continue;
} elseif ($len === 0) {
errhandle(__LINE__,false);
socket_shutdown($sock_client);
break;
} else {
echo "recv:{" . $buf . "}len=" . $len . PHP_EOL;
if ($buf == 'quit') {
socket_shutdown($sock_client);
break;
}
}
}
}
function errhandle($line_num,$exit=true)
{
echo $line_num.":".socket_last_error() . ":" . socket_strerror(socket_last_error()) . PHP_EOL;
if($exit){
exit();
}
}
function sayWelcome($client)
{
$buf = date("H:i:s") . " welcome to server! you id:" . intval($client) . PHP_EOL;
socket_write($client, $buf, strlen($buf));
}
SO_REUSEPORT
目前常見的網絡編程模型就是多進程或多線程,根據accpet的位置,分為如下場景
2種場景
- (1)單進程或線程創建socket,並進行listen和accept,接收到連接后創建進程和線程處理連接
- (2)單進程或線程創建socket,並進行listen,預先創建好多個工作進程或線程accept()在同一個服務器套接字
這兩種模型解充分發揮了多核CPU的優勢,雖然可以做到線程和CPU核綁定,但都會存在:
- 單一listener工作進程或線程在高速的連接接入處理時會成為瓶頸
- 多個線程之間競爭獲取服務套接字
- 緩存行跳躍
- 很難做到CPU之間的負載均衡
- 隨着核數的擴展,性能並沒有隨着提升
SO_REUSEPORT解決了什么問題
SO_REUSEPORT支持多個進程或者線程綁定到同一端口,提高服務器程序的性能,解決的問題:
- 允許多個套接字 bind()/listen() 同一個TCP/UDP端口
- 每一個線程擁有自己的服務器套接字
- 在服務器套接字上沒有了鎖的競爭
- 內核層面實現負載均衡
- 安全層面,監聽同一個端口的套接字只能位於同一個用戶下面
其核心的實現主要有三點:
- 擴展 socket option,增加 SO_REUSEPORT 選項,用來設置 reuseport
- 修改 bind 系統調用實現,以便支持可以綁定到相同的 IP 和端口
- 修改處理新建連接的實現,查找 listener 的時候,能夠支持在監聽相同 IP 和端口的多個 sock 之間均衡選擇。
有了SO_RESUEPORT后,每個進程可以自己創建socket、bind、listen、accept相同的地址和端口,各自是獨立平等的
讓多進程監聽同一個端口,各個進程中accept socket fd不一樣,有新連接建立時,內核只會喚醒一個進程來accept,並且保證喚醒的均衡性。
https://www.cnblogs.com/Anker/p/7076537.html
http://www.blogjava.net/yongboy/archive/2015/02/12/422893.html
沒用reuseport的
socket_fork_no_reuseport.php