天天看點

java.net.ServerSocket詳解先介紹介紹網絡知識ServerSocket重要屬性保持socket長連接配接KeepAlive詳細介紹

先介紹介紹網絡知識

TCP/IP協定

TCP/IP是個協定組,可分為三個層次:網絡層、傳輸層和應用層。

在網絡層有IP協定、ICMP協定、ARP協定、RARP協定和BOOTP協定。

在傳輸層中有TCP協定與UDP協定。

在應用層有FTP、HTTP、TELNET、SMTP、DNS等協定。

是以,HTTP本身就是一個協定,是從Web伺服器傳輸超文本到本地浏覽器的傳送協定。

TCP有一個連接配接檢測機制,就是如果在指定的時間内(一般為2個小時)沒有資料傳送,會給對端發送一個Keep-Alive資料報,使用的序列号是曾經發出的最後一個封包的最後一個位元組的序列号,對端如果收到這個資料,回送一個TCP的ACK,确認這個位元組已經收到,這樣就知道此連接配接沒有被斷開。如果一段時間沒有收到對方的響應,會進行重試,重試幾次後,向對端發一個reset,然後将連接配接斷掉。

在Windows中,第一次探測是在最後一次資料發送的兩個小時,然後每隔1秒探測一次,一共探測5次,如果5次都沒有收到回應的話,就會斷開這個連接配接。

HTTP協定

HTTP協定是建立在請求/響應模型上的。首先由客戶建立一條與伺服器的TCP連結,并發送一個請求到伺服器,請求中包含請求方法、URI、協定版本以及相關的MIME樣式的消息。伺服器響應一個狀态行,包含消息的協定版本、一個成功和失敗碼以及相關的MIME式樣的消息。

HTTP/1.0為每一次HTTP的請求/響應建立一條新的TCP連結,是以一個包含HTML内容和圖檔的頁面将需要建立多次的短期的TCP連結。一次TCP連結的建立将需要3次握手。

另外,為了獲得适當的傳輸速度,則需要TCP花費額外的回路連結時間(RTT)。每一次連結的建立需要這種經常性的開銷,而其并不帶有實際有用的資料,隻是保證連結的可靠性,是以HTTP/1.1提出了可持續連結的實作方法。HTTP/1.1将隻建立一次TCP的連結而重複地使用它傳輸一系列的請求/響應消息,是以減少了連結建立的次數和經常性的連結開銷。

Socket

Socket是應用層與TCP/IP協定族通信的中間軟體抽象層,它是一組接口。在設計模式中,Socket其實就是一個門面模式,它把複雜的TCP/IP協定族隐藏在Socket接口後面,對使用者來說,一組簡單的接口就是全部,讓Socket去組織資料,以符合指定的協定。

java.net.ServerSocket詳解先介紹介紹網絡知識ServerSocket重要屬性保持socket長連接配接KeepAlive詳細介紹

ServerSocket重要屬性

backlog

用于ServerSocket,配置ServerSocket的最大用戶端等待隊列。等待隊列的意思。

下面這個伺服器最多可以同時連接配接3個用戶端,其中2個等待隊列:

int backlog = ;
ServerSocket serverSocket = new ServerSocket(port, backlog);
           

這個參數設定為-1表示無限制,預設是50個最大等待隊列,如果設定無限制,那麼你要小心了,如果你伺服器無法處理那麼多連接配接,那麼當很多用戶端連到你的伺服器時,每一個TCP連接配接都會占用伺服器的記憶體,最後會讓伺服器崩潰的。

另外,就算你設定了backlog為10,如果你的代碼中是一直Socket clientSock = serverSocket.accept(),假設我們的機器最多可以同時處理100個請求,總共有100個線程在運作,然後你把在100個線程的線程池處理clientSock,不能處理的clientSock就排隊,最後clientSock越來越多,也意味着TCP連接配接越來越多,也意味着我們的伺服器的記憶體使用越來越高(用戶端連接配接程序,肯定會發送資料過來,資料會儲存到伺服器端的TCP接收緩存區),最後伺服器就當機了。是以如果你不能處理那麼多請求,請不要循環無限制地調用serverSocket.accept(),否則backlog也無法生效。如果真的請求過多,隻會讓你的伺服器當機(相信很多人都是這麼寫,要注意點)

TcpNoDelay

public boolean getTcpNoDelay() throws SocketException
public void setTcpNoDelay(boolean on) throws SocketException
           

在預設情況下,用戶端向伺服器發送資料時,會根據資料包的大小決定是否立即發送。當資料包中的資料很少時,如隻有1個位元組,而資料包的頭卻有幾十個位元組(IP頭+TCP頭)時,系統會在發送之前先将較小的包合并到軟大的包後,一起将資料發送出去。在發送下一個資料包時,系統會等待伺服器對前一個資料包的響應,當收到伺服器的響應後,再發送下一個資料包,這就是所謂的Nagle算法;在預設情況下,Nagle算法是開啟的。

這種算法雖然可以有效地改善網絡傳輸的效率,但對于網絡速度比較慢,而且對實作性的要求比較高的情況下(如遊戲、Telnet等),使用這種方式傳輸資料會使得用戶端有明顯的停頓現象。是以,最好的解決方案就是需要Nagle算法時就使用它,不需要時就關閉它。而使用setTcpToDelay正好可以滿足這個需求。當使用setTcpNoDelay(true)将Nagle算法關閉後,用戶端每發送一次資料,無論資料包的大小都會将這些資料發送出去。

當我們調用下面代碼,如:

Socket socket = new Socket();  
socket.connect(new InetSocketAddress(host, ));  
InputStream in = socket.getInputStream();  
OutputStream out = socket.getOutputStream();  
String head = "hello ";  
String body = "world\r\n";  
out.write(head.getBytes());  
out.write(body.getBytes());
           

我們發送了hello,當hello沒有收到ack确認(TCP是可靠連接配接,發送的每一個資料都要收到對方的一個ack确認,否則就要重發)的時候,根據納格算法,world不會立馬發送,會等待,要麼等到ack确認(最多等100ms對方會發過來的),要麼等到TCP緩沖區内容>=MSS,很明顯這裡沒有機會,我們寫了world後再也沒有寫資料了,是以隻能等到hello的ack我們才會發送world,除非我們禁用納格算法,資料就會立即發送了。

納格算法參考:http://zh.wikipedia.org/wiki/%E7%B4%8D%E6%A0%BC%E7%AE%97%E6%B3%95

另外有一篇講解納格算法和delay ack的文章(挺不錯的):http://blog.csdn.net/frankggyy/article/details/6624401

對于互動型的應用(譬如telnet),經常存在的情況是用戶端和服務端之間需要頻繁地進行一些小資料交換,譬如telnet可能每敲一個鍵盤都需要将資料發送到服務端。為了避免這種情況會産生大量小資料包,提出了Nagle算法。Nagle算法要求每次在發送端最後隻有一個未被确認的包,是以上一個包發送出去還沒有接收到響應之前,要求發送的包回先放在緩沖區,接收到響應之後,會将緩沖區中的包合并成一個包發送出去(可以看到,響應回地越快,發送出去的資料也會越快)。

需要注意的是,由Nagle算法要求隻能有一個未被确認的包,是以視窗參數會失效,在大資料量傳送的情況下會使網絡吞吐量下降,是以對于大資料量的互動,應該關閉Nagle算法,Nagle算法比較适合小資料量頻繁交換的情景。我們可以使用TCP_NODELAY關閉Nagle算法。

SoLinger

public int getSoLinger() throws SocketException
public void setSoLinger(boolean on, int linger) throws SocketException
           

這個Socket選項可以影響close方法的行為。在預設情況下,當調用close方法後,将立即傳回;如果這時仍然有未被送出的資料包,那麼這些資料包将被丢棄。如果将linger參數設為一個正整數n時(n的值最大是65,535),在調用close方法後,将最多被阻塞n秒。在這n秒内,系統将盡量将未送出的資料包發送出去;如果超過了n秒,如果還有未發送的資料包,這些資料包将全部被丢棄;而close方法會立即傳回。如果将linger設為0,和關閉SO_LINGER選項的作用是一樣的。

如果底層的Socket實作不支援SO_LINGER都會抛出SocketException例外。當給linger參數傳遞負數值時,setSoLinger還會抛出一個IllegalArgumentException例外。可以通過getSoLinger方法得到延遲關閉的時間,如果傳回-1,則表明SO_LINGER是關閉的。例如,下面的代碼将延遲關閉的時間設為1分鐘:

if(socket.getSoLinger() == -) socket.setSoLinger(true, );
           

當我們調用socket.close()傳回時,socket已經write的資料未必已經發送到對方了,例如

Socket socket = new Socket();  
socket.connect(new InetSocketAddress(host, ));  
InputStream in = socket.getInputStream();  
OutputStream out = socket.getOutputStream();  
String head = "hello ";  
String body = "world\r\n";  
out.write(head.getBytes());  
out.write(body.getBytes()); 
socket.close();
           

這裡調用了socket.close()傳回時,hello和world未必已經成功發送到對方了,如果我們設定了linger而不小于0,如:

bool on = true;
int linger = 100;
....
socket.setSoLinger(boolean on, int linger)
......
socket.close();
           

那麼close會等到發送的資料已經确認了才傳回。但是如果對方當機,逾時,那麼會根據linger設定的時間傳回。

UrgentData和OOBInline

TCP的緊急指針,一般都不建議使用,而且不同的TCP/IP實作,也不同,一般說如果你有緊急資料甯願再建立一個新的TCP/IP連接配接發送資料,讓對方緊急處理。

是以這兩個參數,你們可以忽略吧,想知道更多的,自己查下資料。

SoTimeout

public int getSoTimeout() throws SocketException
public void setSoTimeout(int timeout) throws SocketException
           

這個Socket選項在前面已經讨論過。可以通過這個選項來設定讀取資料逾時。當輸入流的read方法被阻塞時,如果設定timeout(timeout的機關是毫秒),那麼系統在等待了timeout毫秒後會抛出一個InterruptedIOException例外。在抛出例外後,輸入流并未關閉,你可以繼續通過read方法讀取資料。

如果将timeout設為0,就意味着read将會無限等待下去,直到服務端程式關閉這個Socket.這也是timeout的預設值。如下面的語句将讀取資料逾時設為30秒:

當底層的Socket實作不支援SO_TIMEOUT選項時,這兩個方法将抛出SocketException例外。不能将timeout設為負數,否則setSoTimeout方法将抛出IllegalArgumentException例外。

設定socket調用InputStream讀資料的逾時時間,以毫秒為機關,如果超過這個時候,會抛出java.net.SocketTimeoutException。

KeepAlive

public boolean getKeepAlive() throws SocketException
public void setKeepAlive(boolean on) throws SocketException
           

如果将這個Socket選項打開,用戶端Socket每隔段的時間(大約兩個小時)就會利用空閑的連接配接向伺服器發送一個資料包。這個資料包并沒有其它的作用,隻是為了檢測一下伺服器是否仍處于活動狀态。如果伺服器未響應這個資料包,在大約11分鐘後,用戶端Socket再發送一個資料包,如果在12分鐘内,伺服器還沒響應,那麼用戶端Socket将關閉。如果将Socket選項關閉,用戶端Socket在伺服器無效的情況下可能會長時間不會關閉。SO_KEEPALIVE選項在預設情況下是關閉的,可以使用如下的語句将這個SO_KEEPALIVE選項打開:

socket1.setKeepAlive(true);
           

keepalive不是說TCP的常連接配接,當我們作為服務端,一個用戶端連接配接上來,如果設定了keeplive為true,當對方沒有發送任何資料過來,超過一個時間(看系統核心參數配置),那麼我們這邊會發送一個ack探測包發到對方,探測雙方的TCP/IP連接配接是否有效(對方可能斷點,斷網),在Linux好像這個時間是75秒。如果不設定,那麼用戶端當機時,伺服器永遠也不知道用戶端當機了,仍然儲存這個失效的連接配接。

SO_KEEPALIVE 保持連接配接檢測對方主機是否崩潰,避免(伺服器)永遠阻塞于TCP連接配接的輸入。

設定該選項後,如果2小時内在此套接口的任一方向都沒有資料交換,TCP就自動給對方 發一個保持存活探測分節(keepalive probe)。這是一個對方必須響應的TCP分節.它會導緻以下三種情況:

1、對方接收一切正常:以期望的ACK響應,2小時後,TCP将發出另一個探測分節。

2、對方已崩潰且已重新啟動:以RST響應。套接口的待處理錯誤被置為ECONNRESET,套接 口本身則被關閉。

3、對方無任何響應:源自berkeley的TCP發送另外8個探測分節,相隔75秒一個,試圖得到一個響應。在發出第一個探測分節11分鐘15秒後若仍無響應就放棄。套接口的待處理錯誤被置為ETIMEOUT,套接口本身則被關閉。如ICMP錯誤是“host unreachable(主機不可達)”,說明對方主機并沒有崩潰,但是不可達,這種情況下待處理錯誤被置為 EHOSTUNREACH。

有關SO_KEEPALIVE的三個參數詳細解釋如下:

(16)tcp_keepalive_intvl,保活探測消息的發送頻率。預設值為75s。

發送頻率tcp_keepalive_intvl乘以發送次數tcp_keepalive_probes,就得到了從開始探測直到放棄探測确定連接配接斷開的時間,大約為11min。

(17)tcp_keepalive_probes,TCP發送保活探測消息以确定連接配接是否已斷開的次數。預設值為9(次)。

注意:隻有設定了SO_KEEPALIVE套接口選項後才會發送保活探測消息。

(18)tcp_keepalive_time,在TCP保活打開的情況下,最後一次資料交換到TCP發送第一個保活探測消息的時間,即允許的持續空閑時間。預設值為7200s(2h)。

在一個TCP連接配接建立之後,我們會很奇怪地發現,預設情況下,如果一端異常退出(譬如網絡中斷後一端退出,使地關閉請求另一端無法接收到),TCP的另一端并不能獲得這種情況,仍然會保持一個半關閉的連接配接,對于服務端,大量半關閉的連接配接将會是非常緻命的。SO_KEEPALIVE提供了一種手段讓TCP的一端(通常服務提供者端)可以檢測到這種情況。如果我們設定了SO_KEEPALIVE,TCP在距離上一次TCP包互動2個小時(取決于作業系統和TCP實作,規範建議不低于2小時)後,會發送一個探測包給另一端,如果接收不到響應,則在75秒後重新發送,連續10次仍然沒有響應,則認為對方已經關閉,系統會将該連接配接關閉。一般情況下,如果對方已經關閉,則對方的TCP層會回RST響應回來,這種情況下,同樣會将連接配接關閉。

SendBufferSize和ReceiveBufferSize

TCP發送緩存區和接收緩存區,預設是8192,一般情況下足夠了,而且就算你增加了發送緩存區,對方沒有增加它對應的接收緩沖,那麼在TCP三握手時,最後确定的最大發送視窗還是雙方最小的那個緩沖區,就算你無視,發了更多的資料,那麼多出來的資料也會被丢棄。除非雙方都協商好。

SO_REUSEADDR

public boolean getReuseAddress() throws SocketException           
public void setReuseAddress(boolean on) throws SocketException
           

錯誤的說法:

通過這個選項,可以使多個Socket對象綁定在同一個端口上。

正确的說明是:

如果端口忙,但TCP狀态位于 TIME_WAIT ,可以重用 端口。如果端口忙,而TCP狀态位于其他狀态,重用端口時依舊得到一個錯誤資訊, 抛出“Address already in use: JVM_Bind”。如果你的服務程式停止後想立即重新開機,不等60秒,而新套接字依舊 使用同一端口,此時 SO_REUSEADDR 選項非常有用。必須意識到,此時任何非期 望資料到達,都可能導緻服務程式反應混亂,不過這隻是一種可能,事實上很不可能。

這個參數在Windows平台與Linux平台表現的特點不一樣。在Windows平台表現的特點是不正确的, 在Linux平台表現的特點是正确的。

在Windows平台,多個Socket建立立對象可以綁定在同一個端口上,這些新連接配接是非TIME_WAIT狀态的。這樣做并沒有多大意義。

在Linux平台,隻有TCP狀态位于 TIME_WAIT ,才可以重用 端口。這才是正确的行為。

public class Test {
    public static void main(String[] args) {
        try {
            ServerSocket socket1 = new ServerSocket();
            ServerSocket socket2 = new ServerSocket();
            socket1.setReuseAddress(true);
            socket1.bind(new InetSocketAddress("127.0.0.1", ));
            System.out.println("socket1.getReuseAddress():"+ socket1.getReuseAddress());
            socket2.setReuseAddress(true);
            socket2.bind(new InetSocketAddress("127.0.0.1", ));
            System.out.println("socket2.getReuseAddress():"+ socket1.getReuseAddress());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
           

使用SO_REUSEADDR選項時有兩點需要注意:

1. 必須在調用bind方法之前使用setReuseAddress方法來打開SO_REUSEADDR選項。是以,要想使用SO_REUSEADDR選項,就不能通過Socket類的構造方法來綁定端口。

2. 必須将綁定同一個端口的所有的Socket對象的SO_REUSEADDR選項都打開才能起作用。如在例程4-12中,socket1和socket2都使用了setReuseAddress方法打開了各自的SO_REUSEADDR選項。

在Windows作業系統上運作上面的代碼的運作結果如下:

這種結果是不正确的。

socket1.getReuseAddress():true
socket2.getReuseAddress():true
           

在Linux作業系統上運作上面的代碼的運作結果如下:

這種結果是正确的。因為第一個連接配接不是TIME_WAIT狀态的,第二個連接配接就不能使用8899端口;

隻有第一個連接配接是TIME_WAIT狀态的,第二個連接配接就才能使用8899端口;

socket1.getReuseAddress():true
java.net.BindException: Address already in use
    at java.net.PlainSocketImpl.socketBind(Native Method)
    at java.net.PlainSocketImpl.bind(PlainSocketImpl.java:)
    at java.net.ServerSocket.bind(ServerSocket.java:)
    at java.net.ServerSocket.bind(ServerSocket.java:)
    at com.Test.main(Test.java:)
           

SO_SNDBUF

public int getSendBufferSize() throws SocketException
public void setSendBufferSize(int size) throws SocketException
           

在預設情況下,輸出流的發送緩沖區是8096個位元組(8K)。這個值是Java所建議的輸出緩沖區的大小。如果這個預設值不能滿足要求,可以用setSendBufferSize方法來重新設定緩沖區的大小。但最好不要将輸出緩沖區設得太小,否則會導緻傳輸資料過于頻繁,進而降低網絡傳輸的效率。

如果底層的Socket實作不支援SO_SENDBUF選項,這兩個方法将會抛出SocketException例外。必須将size設為正整數,否則setSendBufferedSize方法将抛出IllegalArgumentException例外。

SO_RCVBUF

public int getReceiveBufferSize() throws SocketException
public void setReceiveBufferSize(int size) throws SocketException
           

在預設情況下,輸入流的接收緩沖區是8096個位元組(8K)。這個值是Java所建議的輸入緩沖區的大小。如果這個預設值不能滿足要求,可以用setReceiveBufferSize方法來重新設定緩沖區的大小。但最好不要将輸入緩沖區設得太小,否則會導緻傳輸資料過于頻繁,進而降低網絡傳輸的效率。

如果底層的Socket實作不支援SO_RCVBUF選項,這兩個方法将會抛出SocketException例外。必須将size設為正整數,否則setReceiveBufferSize方法将抛出IllegalArgumentException例外。

SO_OOBINLINE

public boolean getOOBInline() throws SocketException
 public void setOOBInline(boolean on) throws SocketException
           

如果這個Socket選項打開,可以通過Socket類的sendUrgentData方法向伺服器發送一個單位元組的資料。這個單位元組資料并不經過輸出緩沖區,而是立即發出。雖然在用戶端并不是使用OutputStream向伺服器發送資料,但在服務端程式中這個單位元組的資料是和其它的普通資料混在一起的。是以,在服務端程式中并不知道由用戶端發過來的資料是由OutputStream還是由sendUrgentData發過來的。下面是sendUrgentData方法的聲明:

public void sendUrgentData(int data) throws IOException
           

雖然sendUrgentData的參數data是int類型,但隻有這個int類型的低位元組被發送,其它的三個位元組被忽略。下面的代碼示範了如何使用SO_OOBINLINE選項來發送單位元組資料。

package mynet;
import java.net.*;
import java.io.*;
class Server
{
    public static void main(String[] args) throws Exception
    {
        ServerSocket serverSocket = new ServerSocket();
        System.out.println("伺服器已經啟動,端口号:1234");
        while (true)
        {
            Socket socket = serverSocket.accept();
            socket.setOOBInline(true);
            InputStream in = socket.getInputStream();
            InputStreamReader inReader = new InputStreamReader(in);
            BufferedReader bReader = new BufferedReader(inReader);
            System.out.println(bReader.readLine());
            System.out.println(bReader.readLine());
            socket.close();
        }
    }
}
public class Client
{
    public static void main(String[] args) throws Exception
    {
        Socket socket = new Socket("127.0.0.1", );
        socket.setOOBInline(true);
        OutputStream out = socket.getOutputStream();
        OutputStreamWriter outWriter = new OutputStreamWriter(out);
        outWriter.write();              // 向伺服器發送字元"C"
        outWriter.write("hello world\r\n");
        socket.sendUrgentData();        // 向伺服器發送字元"A"
        socket.sendUrgentData();        // 向伺服器發送字元"B"
        outWriter.flush();
        socket.sendUrgentData();       // 向伺服器發送漢字”中”
        socket.sendUrgentData();
        socket.sendUrgentData();       // 向伺服器發送漢字”國”
        socket.sendUrgentData();
        socket.close();
    }
}
           

由于運作上面的代碼需要一個伺服器類,是以,在加了一個類名為Server的伺服器類,關于服務端套接字的使用方法将會在後面的文章中詳細讨論。在類Server類中隻使用了ServerSocket類的accept方法接收用戶端的請求。并從用戶端傳來的資料中讀取兩行字元串,并顯示在控制台上。

測試

由于本例使用了127.0.0.1,因Server和Client類必須在同一台機器上運作。

運作Server

運作Client

在服務端控制台的輸出結果

伺服器已經啟動,端口号:1234
ABChello world
中國
           

在ClienT類中使用了sendUrgentData方法向伺服器發送了字元’A’(65)和’B’(66)。但發送’B’時實際發送的是322,由于sendUrgentData隻發送整型數的低位元組。是以,實際發送的是66.十進制整型322的二進制形式如圖1所示。

圖1 十進制整型322的二進制形式

java.net.ServerSocket詳解先介紹介紹網絡知識ServerSocket重要屬性保持socket長連接配接KeepAlive詳細介紹

從圖1可以看出,雖然322分布在了兩個位元組上,但它的低位元組仍然是66.

在Client類中使用flush将緩沖區中的資料發送到伺服器。我們可以從輸出結果發現一個問題,在Client類中先後向伺服器發送了’C’、”hello world”r”n”、’A’、’B’.而在服務端程式的控制台上顯示的卻是ABChello world.這種現象說明使用sendUrgentData方法發送資料後,系統會立即将這些資料發送出去;而使用write發送資料,必須要使用flush方法才會真正發送資料。

在Client類中向伺服器發送”中國”字元串。由于”中”是由214和208兩個位元組組成的;而”國”是由185和250兩個位元組組成的;是以,可分别發送這四個位元組來傳送”中國”字元串。

注意:在使用setOOBInline方法打開SO_OOBINLINE選項時要注意是必須在用戶端和服務端程式同時使用setOOBInline方法打開這個選項,否則無法命名用sendUrgentData來發送資料。

SO_LINGER/ SO_REUSEADDR

TCP正常的關閉過程如下(四次握手過程):
           

(FIN_WAIT_1) A —FIN—> B(CLOSE_WAIT)

(FIN_WAIT_2) A <–ACK– B(CLOSE_WAIT)

(TIME_WAIT)A <–FIN—- B(LAST_ACK)

(TIME_WAIT)A —ACK-> B(CLOSED)

Ø A端首先發送一個FIN請求給B端,要求關閉,發送後A段的TCP狀态變更為FIN_WAIT_1,接收到FIN請求後B端的TCP狀态變更為CLOSE_WAIT

Ø B接收到ACK請求後,B回一個ACK給A端,确認接收到的FIN請求,接收到ACK請求後,A端的TCP狀态變更為為FIN_WAIT_2。

Ø B端再發送一個FIN請求給A端,與連接配接過程的3次握手過程不一樣,這個FIN請求之是以并不是與上一個請求一起發送,之是以如此處理,是因為TCP是雙通道的,允許在發送ACK請求後,并不馬上發FIN請求,即隻關閉A到B端的資料流,仍然允許B端到A端的資料流。這個ACK請求發送之後,B端的TCP狀态變更為LAST_ACK,A端的狀态變更為TIME_WAIT。

Ø A端接收到B端的FIN請求後,再回B端一個ACK資訊,對上一個FIN請求進行确認,到此時B端狀态變更為CLOSED,Socket可以關閉。

除了如上正常的關閉(優雅關閉)之外,TCP還提供了另外一種非優雅的關閉方式RST(Reset)

(CLOSED) A —RST–> B (CLOSED)

Ø A端發送RST狀态之後,TCP進入CLOSED狀态,B端接收到RST後,也即可進入CLOSED狀态。

在第一種關閉方式上(優雅關閉),非常遺憾,A端在最後發送一個ACK請求後,并不能馬上将該Socket回收,因為A并不能确定B一定能夠接收到這個ACK請求,是以A端必須對這個Socket維持TIME_WAIT狀态2MSL(MSL=Max Segment Lifetime,取決于作業系統和TCP實作,該值為30秒、60秒或2分鐘)。如果A端是用戶端,這并不會成為問題,但如果A端是服務端,那就很危險了,如果連接配接的Socket非常多,而又維持如此多的TIME_WAIT狀态的話,那麼有可能會将Socket耗盡(報Too Many Open File)。

服務端為了解決這個TIME_WAIT問題,可選擇的方式有三種:

Ø 保證由用戶端主動發起關閉(即做為B端)

Ø 關閉的時候使用RST的方式

Ø 對處于TIME_WAIT狀态的TCP允許重用

一般我們當然最好是選擇第一種方式,實在沒有辦法的時候,我們可以使用SO_LINGER選擇第二種方式,使用SO_REUSEADDR選擇第三種方式

Java代碼 收藏代碼

public void setSoLinger(boolean on, int linger) throws SocketException

public void setReuseAddress(boolean on) throws SocketException

第一個on表示是否使用SO_LINGER選項,linger(以秒為機關)表示在發RST之前會等待多久,因為一旦發送RST,還在緩沖區中還沒有發送出去的資料就會直接丢棄。

保持socket長連接配接

在實際開發中我們常常會遇到:如何保證socket長連接配接?

這方面一般來說比較成熟的有兩種方法:

自己心跳

在應用層制定協定,發心跳包,這也是C#,JAVA等進階語言比較常用的方法。用戶端和服務端制定一個通訊協定,每隔一定時間(一般15秒左右),由一方發起,向對方發送協定包;對方收到這個包後,按指定好的通訊協定回一個。若沒收到回複,則判斷網絡出現問題,伺服器可及時的斷開連接配接,用戶端也可以及時重連。

依賴KeepAlive

通過TCP協定層發送KeepAlive包。這個方法隻需設定好你使用的TCP的KeepAlive項就好,其他的作業系統會幫你完成。作業系統會按時發送KeepAlive包,一發現網絡異常,馬上斷開。我就是使用這個方法,也是重點向大家介紹的。

使用第二種方法的好處,是我們在應用層不需自己定協定,通信的兩端,隻要有一端設好這個值,兩邊都能及時檢測出TCP連接配接情況。而且這些都是作業系統幫你自動完成的。有的公司的服務端代碼就是早寫好的,很難改動。以前也沒加入心跳機制,後面要改很麻煩,要求檢測連接配接的工作盡量用戶端單獨完成。

還有一個好處就是節省網絡資源。KeepAlive包,隻有很簡單的一些TCP資訊,無論如何也是比你自己設計的心跳包短小的。然後就是它的發送機制,在TCP空閑XXX秒後才開始發送。自己設計心跳機制的話,很難做到這一點。

這種方法也是有些缺陷的。比如某一時刻,網線松了,如果剛好被KeepAlive包檢測到,它會馬上斷開TCP連接配接。但其實這時候TCP連接配接也算是established的,隻要網線再插好,這個連接配接還是可以正常工作的。

一般KeepAlive設定TCP15秒鐘空閑。

KeepAlive詳細介紹

什麼是keepalive定時器?

在一個空閑的(idle)TCP連接配接上,沒有任何的資料流,許多TCP/IP的初學者都對此感到驚奇。也就是說,如果TCP連接配接兩端沒有任何一個程序在向對方發送資料,那麼在這兩個TCP子產品之間沒有任何的資料交換。你可能在其它的網絡協定中發現有輪詢(polling),但在TCP中它不存在。言外之意就是我們隻要啟動一個用戶端程序,同伺服器建立了TCP連接配接,不管你離開幾小時,幾天,幾星期或是幾個月,連接配接依舊存在。中間的路由器可能崩潰或者重新開機,電話線可能go down或者back up,隻要連接配接兩端的主機沒有重新開機,連接配接依舊保持建立。

這就可以認為不管是用戶端的還是伺服器端的應用程式都沒有應用程式級(application-level)的定時器來探測連接配接的不活動狀态(inactivity),進而引起任何一個應用程式的終止。然而有的時候,伺服器需要知道用戶端主機是否已崩潰并且關閉,或者崩潰但重新開機。許多實作提供了存活定時器來完成這個任務。

存活定時器是一個包含争議的特征。許多人認為,即使需要這個特征,這種對對方的輪詢也應該由應用程式來完成,而不是由TCP中實作。此外,如果兩個終端系統之間的某個中間網絡上有連接配接的暫時中斷,那麼存活選項(option)就能夠引起兩個程序間一個良好連接配接的終止。例如,如果正好在某個中間路由器崩潰、重新開機的時候發送存活探測,TCP就将會認為用戶端主機已經崩潰,但事實并非如此。

存活(keepalive)并不是TCP規範的一部分。在Host Requirements RFC羅列有不使用它的三個理由:(1)在短暫的故障期間,它們可能引起一個良好連接配接(good connection)被釋放(dropped),(2)它們消費了不必要的寬帶,(3)在以資料包計費的網際網路上它們(額外)花費金錢。然而,在許多的實作中提供了存活定時器。

一些伺服器應用程式可能代表用戶端占用資源,它們需要知道用戶端主機是否崩潰。存活定時器可以為這些應用程式提供探測服務。Telnet伺服器和Rlogin伺服器的許多版本都預設提供存活選項。

個人計算機使用者使用TCP/IP協定通過Telnet登入一台主機,這是能夠說明需要使用存活定時器的一個常用例子。如果某個使用者在使用結束時隻是關掉了電源,而沒有登出(log off),那麼他就留下了一個半打開(half-open)的連接配接。在圖18.16,我們看到如何在一個半打開連接配接上通過發送資料,得到一個複位(reset)傳回,但那是在用戶端,是由用戶端發送的資料。如果用戶端消失,留給了伺服器端半打開的連接配接,并且伺服器又在等待用戶端的資料,那麼等待将永遠持續下去。存活特征的目的就是在伺服器端檢測這種半打開連接配接。

keepalive如何工作?

在此描述中,我們稱使用存活選項的那一段為伺服器,另一端為用戶端。也可以在用戶端設定該選項,且沒有不允許這樣做的理由,但通常設定在伺服器。如果連接配接兩端都需要探測對方是否消失,那麼就可以在兩端同時設定(比如NFS)。

若在一個給定連接配接上,兩小時之内無任何活動,伺服器便向用戶端發送一個探測段。(我們将在下面的例子中看到探測段的樣子。)用戶端主機必須是下列四種狀态之一:

1) 用戶端主機依舊活躍(up)運作,并且從伺服器可到達。從用戶端TCP的正常響應,伺服器知道對方仍然活躍。伺服器的TCP為接下來的兩小時複位存活定時器,如果在這兩個小時到期之前,連接配接上發生應用程式的通信,則定時器重新為往下的兩小時複位,并且接着交換資料。

2) 用戶端已經崩潰,或者已經關閉(down),或者正在重新開機過程中。在這兩種情況下,它的TCP都不會響應。伺服器沒有收到對其發出探測的響應,并且在75秒之後逾時。伺服器将總共發送10個這樣的探測,每個探測75秒。如果沒有收到一個響應,它就認為用戶端主機已經關閉并終止連接配接。

3) 用戶端曾經崩潰,但已經重新開機。這種情況下,伺服器将會收到對其存活探測的響應,但該響應是一個複位,進而引起伺服器對連接配接的終止。

4) 用戶端主機活躍運作,但從伺服器不可到達。這與狀态2類似,因為TCP無法差別它們兩個。它所能表明的僅是未收到對其探測的回複。

伺服器不必擔心用戶端主機被關閉然後重新開機的情況(這裡指的是操作員執行的正常關閉,而不是主機的崩潰)。當系統被操作員關閉時,所有的應用程式程序(也就是用戶端程序)都将被終止,用戶端TCP會在連接配接上發送一個FIN。收到這個FIN後,伺服器TCP向伺服器程序報告一個檔案結束,以允許伺服器檢測這種狀态。

在第一種狀态下,伺服器應用程式不知道存活探測是否發生。凡事都是由TCP層處理的,存活探測對應用程式透明,直到後面2,3,4三種狀态發生。在這三種狀态下,通過伺服器的TCP,傳回給伺服器應用程式錯誤資訊。(通常伺服器向網絡發出一個讀請求,等待用戶端的資料。如果存活特征傳回一個錯誤資訊,則将該資訊作為讀操作的傳回值傳回給伺服器。)在狀态2,錯誤資訊類似于“連接配接逾時”。狀态3則為“連接配接被對方複位”。第四種狀态看起來像連接配接逾時,或者根據是否收到與該連接配接相關的ICMP錯誤資訊,而可能傳回其它的錯誤資訊。

windows 實作:

在一個正常的TCP連接配接上,當我們用無限等待的方式調用下面的Recv或Send的時候:

如果TCP連接配接被對方正常關閉,也就是說,對方是正确地調用了closesocket(s)或者shutdown(s)的話,那麼上面的Recv或Send調用就能馬上傳回,并且報錯。這是由于closesocket(s)或者shutdown(s)有個正常的關閉過程,會告訴對方“TCP連接配接已經關閉,你不需要再發送或者接受消息了”。但是,如果是網線突然被拔掉,TCP連接配接的任何一端的機器突然斷電或重新開機動,那麼這時候正在執行Recv或Send操作的一方就會因為沒有任何連接配接中斷的通知而一直等待下去,也就是會被長時間卡住。這種情形解決的辦法是啟動TCP程式設計裡的keepAlive機制。

struct TCP_KEEPALIVE inKeepAlive = {};
    unsigned long ulInLen = sizeof(struct TCP_KEEPALIVE);
    struct TCP_KEEPALIVE utKeepAlive = {};
    unsigned long ulOutLen = sizeof(struct TCP_KEEPALIVE);
    unsigned long ulBytesReturn = ;
    inKeepAlive.onoff=;
    inKeepAlive.keepaliveinterval=; //機關為毫秒
    inKeepAlive.keepalivetime=;      //機關為毫秒
    ret=WSAIoctl(s, SIO_KEEPALIVE_VALS, (LPVOID)&inKeepAlive, ulInLen, 
                          (LPVOID)&outKeepAlive, ulOutLen, &ulBytesReturn, NULL, NULL);
           

此處的keepalivetime表示的是TCP連接配接處于暢通時候的探測頻率,一旦探測包沒有傳回,就以keepaliveinterval的頻率發送,經過若幹次的重試,如果探測包都沒有傳回,那麼就得出結論:TCP連接配接已經斷開,于是上面的Recv或Send調用也就能馬上傳回,不會無限制地卡住了。

什麼是HTTP Keep Alive

HTTP Keep-Alive 很大程式上被誤解了,下面介紹一下它在HTTP/1.0和HTTP/1.1版本下是如何工作的,以及其在JAVA中的運作原理及優化建議。

HTTP是一個請求<->響應模式的典型範例,即用戶端向伺服器發送一個請求資訊,伺服器來響應這個資訊。在老的HTTP版本中,每個請求都将被建立一個新的用戶端->伺服器的連接配接,在這個連接配接上發送請求,然後接收請求。這樣的模式有一個很大的優點就是,它很簡單,很容易了解和程式設計實作;它也有一個很大的缺點就是,它效率很低,是以Keep-Alive被提出用來解決效率低的問題。

具體說,HTTP建構在TCP之上。在HTTP早期實作中,每個HTTP請求都要打開一個socket連接配接。這種做效率很低,因為一個Web 頁面中的很多HTTP請求都指向同一個伺服器。例如,很多為Web頁面中的圖檔發起的請求都指向一個通用的圖檔伺服器。持久連接配接的引入解決了多對已請求伺服器導緻的socket連接配接低效性的問題。它使浏覽器可以再一個單獨的連接配接上進行多個請求。浏覽器和伺服器使用Connection頭ilai指出對Keep-Alive的支援。

HTTP/1.0

在HTTP/1.0版本中,并沒有官方的标準來規定Keep-Alive如何工作,是以實際上它是被附加到HTTP/1.0協定上,如果用戶端浏覽器支援Keep-Alive,那麼就在HTTP請求頭中添加一個字段 Connection: Keep-Alive,當伺服器收到附帶有Connection: Keep-Alive的請求時,它也會在響應頭中添加一個同樣的字段來使用Keep-Alive。這樣一來,用戶端和伺服器之間的HTTP連接配接就會被保持,不會斷開(超過Keep-Alive規定的時間,意外斷電等情況除外),當用戶端發送另外一個請求時,就使用這條已經建立的連接配接

HTTP/1.1

在HTTP/1.1版本中,官方規定的Keep-Alive使用标準和在HTTP/1.0版本中有些不同,預設情況下所在HTTP1.1中所有連接配接都被保持,除非在請求頭或響應頭中指明要關閉:Connection: Close ,這也就是為什麼Connection: Keep-Alive字段再沒有意義的原因。另外,還添加了一個新的字段Keep-Alive:,因為這個字段并沒有較長的描述用來做什麼,可忽略它

HTTP Keep Alive的注意點

Not reliable(不可靠)

HTTP是一個無狀态協定,這意味着每個請求都是獨立的,Keep-Alive沒能改變這個結果。另外,Keep-Alive也不能保證用戶端和伺服器之間的連接配接一定是活躍的,在HTTP1.1版本中也如此。唯一能保證的就是當連接配接被關閉時你能得到一個通知,是以不應該讓程式依賴于Keep-Alive的保持連接配接特性,否則會有意想不到的後果

Keep-Alive和POST

在HTTP1.1細則中規定了在一個POST消息體後面不能有任何字元,還指出了對于某一個特定的浏覽器可能并不遵循這個标準(比如在POST消息體的後面放置一個CRLF符)。而據我所知,大部分浏覽器在POST消息體後都會自動跟一個CRLF符再發送,如何解決這個問題呢?根據上面的說明在POST請求頭中禁止使用Keep-Alive,或者由伺服器自動忽略這個CRLF,大部分伺服器都會自動忽略,但是在未經測試之前是不可能知道一個伺服器是否會這樣做。

HTTP Keep Alive 優化例子與總結

例子分析

問題現象: 一個JSP頁面,居然要耗時40多秒。網頁中有大量的圖檔的CSS

問題解決: 原因也找了半天,原來Apache配置裡面,把Keep-Alive的開關關閉了。這個是個大問題,工程師為什麼要關閉它,原來他考慮的太簡單了,我們知道Apache适合處于短連接配接的請求,處理時間越短,并發數才能上去,原來他是這麼考慮,但是沒有辦法,隻能這樣了,還是打開Keep-Alive開關吧。

當然,不是所有的情況都設定KeepAlive為On,下面的文字總結比較好:

【在使用apache的過程中,KeepAlive屬性我一直保持為預設值On,其實,該屬性設定為On還是Off還是要具體問題具體分析的,在生産環境中的影響還是蠻大的。

KeepAlive選項到底有什麼用處?如果你用過Mysql ,應該知道Mysql的連接配接屬性中有一個與KeepAlive 類似的Persistent Connection,即:長連接配接(PConnect)。該屬性打開的話,可以使一次TCP連接配接為同一使用者的多次請求服務,提高了響應速度。

比如很多網頁中圖檔、CSS、JS、Html都在一台Server上,當使用者通路其中的Html網頁時,網頁中的圖檔、Css、Js都構成了通路請求,打開KeepAlive 屬性可以有效地降低TCP握手的次數(當然浏覽器對同一域下同時請求的圖檔數有限制,一般是2 見下文章節 減少域名解釋的開銷),減少httpd程序數,進而降低記憶體的使用(假定prefork模式)。MaxKeepAliveRequests 和KeepAliveTimeOut 兩個屬性在KeepAlive =On時起作用,可以控制持久連接配接的生存時間和最大服務請求數。

不過,上面說的隻是一種情形,那就是靜态網頁居多的情況下,并且網頁中的其他請求與網頁在同一台Server上。當你的應用動态程式(比如:php )居多,使用者通路時由動态程式即時生成html内容,html内容中圖檔素材和Css、Js等比較少或者散列在其他Server上時,KeepAlive =On反而會降低Apache 的性能。為什麼呢?

前面提到過,KeepAlive =On時,每次使用者通路,打開一個TCP連接配接,Apache 都會保持該連接配接一段時間,以便該連接配接能連續為同一client服務,在KeepAliveTimeOut還沒到期并且MaxKeepAliveRequests還沒到門檻值之前,Apache 必然要有一個httpd程序來維持該連接配接,httpd程序不是廉價的,他要消耗記憶體和CPU時間片的。假如目前Apache 每秒響應100個使用者通路,KeepAliveTimeOut=5,此時httpd程序數就是100*5=500個(prefork 模式),一個httpd程序消耗5M記憶體的話,就是500*5M=2500M=2.5G,誇張吧?當然,Apache 與Client隻進行了100次TCP連接配接。如果你的記憶體夠大,系統負載不會太高,如果你的記憶體小于2.5G,就會用到Swap,頻繁的Swap切換會加重CPU的Load。

現在我們關掉KeepAlive ,Apache 仍然每秒響應100個使用者通路,因為我們将圖檔、js、css等分離出去了,每次通路隻有1個request,此時httpd的程序數是100*1=100個,使用記憶體100*5M=500M,此時Apache 與Client也是進行了100次TCP連接配接。性能卻提升了太多。

總結

1、當你的Server記憶體充足時,KeepAlive =On還是Off對系統性能影響不大。

2、當你的Server上靜态網頁(Html、圖檔、Css、Js)居多時,建議打開KeepAlive 。

3、當你的Server多為動态請求(因為連接配接資料庫,對檔案系統通路較多),KeepAlive 關掉,會節省一定的記憶體,節省的記憶體正好可以作為檔案系統的Cache(vmstat指令中cache一列),降低I/O壓力。

PS:當KeepAlive =On時,KeepAliveTimeOut的設定其實也是一個問題,設定的過短,會導緻Apache 頻繁建立連接配接,給Cpu造成壓力,設定的過長,系統中就會堆積無用的Http連接配接,消耗掉大量記憶體,具體設定多少,可以進行不斷的調節,因你的網站浏覽和伺服器配置 而異。

減少域名解釋的開銷

對于HTTP/1.0來說可以充分利用浏覽器預設最大并發連接配接數比HTTP/1.1多的好 處,實作不增加新域名的開銷而更高的并行下載下傳,減少域名解釋的開銷(注:IE 6,7在HTTP/1.0中預設最大并發連接配接數為4,在HTTP/1.1中預設最大并發連接配接數為2,IE8都為6,Firefox2在HTTP/1.0中 預設最大并發連接配接數為2 在HTTP/1.1中預設最大并發連接配接數為8,firefox 3預設都是6),根據10年7月Google索引的42億個網頁的統計報告,每張網頁裡包含29.39個圖檔,7.09個外部腳本,3.22個外部CSS 樣式表,如果設定了Keep-Alive并且合理控制Keep-Alive TimeOut這個參數可以大量的節約連接配接的開銷,提高相應速度。如果設定不好,在大并發的情況小,因維持大量連接配接而使伺服器資源耗盡,而對于目前國内大 部分的使用者使用的還是IE6,7的情況。