1. 程式人生 > >java Socket用法詳解

java Socket用法詳解

     在客戶/伺服器通訊模式中, 客戶端需要主動建立與伺服器連線的 Socket(套接字), 伺服器端收到了客戶端的連線請求, 也會建立與客戶連線的 Socket. Socket可看做是通訊連線兩端的收發器, 伺服器與客戶端都通過 Socket 來收發資料.

這篇文章首先介紹Socket類的各個構造方法, 以及成員方法的用法, 接著介紹 Socket的一些選項的作用, 這些選項可控制客戶建立與伺服器的連線, 以及接收和傳送資料的行為.

一. 構造Socket

     Socket的構造方法有以下幾種過載形式:  

Socket() 
Socket(InetAddress address, int port) throws UnknowHostException, IOException 
Socket(InetAddress address, int port, InetAddress localAddr, int localPort) throws IOException 
Socket(String host, int port) throws UnknowHostException, IOException 
Socket(String host, int port, InetAddress localAddr, int localPort) throws IOException 
除了第一個不帶引數的構造方法以外, 其他構造方法都會試圖建立與伺服器的連線, 如果連線成功, 就返回 Socket物件; 如果因為某些原因連線失敗, 就會丟擲IOException .

1.1 使用無引數構造方法, 設定等待建立連線的超時時間   

Socket socket = new Socket();
SocketAddress remoteAddr = new InetSocketAddress("localhost",8000);
socket.connect(remoteAddr, 60000); //等待建立連線的超時時間為1分鐘

      以上程式碼用於連線到本地機器上的監聽8000埠的伺服器程式, 等待連線的最長時間為1分鐘. 如果在1分鐘內連線成功則connet()方法順利返回; 如果在1分鐘內出現某種異常, 則丟擲該異常; 如果超過1分鐘後, 即沒有連線成功, 也沒有出現其他異常, 那麼會丟擲 SocketTimeoutException. Socket 類的 connect(SocketAddress endpoint, int timeout) 方法負責連線伺服器, 引數endpoint 指定伺服器的地址, 引數timeout 設定超時資料, 以毫秒為單位. 如果引數timeout 設為0, 表示永遠不會超時, 預設是不會超時的.

1.2 設定伺服器的地址

      除了第一個不帶引數的構造方法, 其他構造方法都需要在引數中設定伺服器的地址, 包括伺服器的IP地址或主機名, 以及埠:

Socket(InetAddress address, int port)              //第一個引數address 表示主機的IP地址
Socket(String host, int port)                              //第一個引數host 表示主機的名字

      InetAddress 類表示伺服器的IP地址, InetAddress 類提供了一系列靜態工廠方法, 用於構造自身的例項, 例如:

//返回本地主機的IP地址
InetAddress addr1 = InetAddress.getLocalHost();
//返回代表 "222.34.5.7"的 IP地址
InetAddress addr2 = InetAddress.getByName("222.34.5.7");
//返回域名為"www.javathinker.org"的 IP地址
InetAddress addr3 = InetAddress.getByName("www.javathinker.org");

1.3 設定客戶端的地址

      在一個Socket 物件中, 即包含遠端伺服器的IP 地址和埠資訊, 也包含本地客戶端的IP 地址和埠資訊. 預設情況下, 客戶端的IP 地址來自於客戶程式所在的主機, 客戶端的埠則由作業系統隨機分配. Socket類還有兩個構造方法允許顯式地設定客戶端的IP 地址和埠:

//引數localAddr 和 localPort 用來設定客戶端的IP 地址和埠
Socket(InetAddress address, int port, InetAddress localAddr, int localPort) throws IOException
Socket(String host, int port, InetAddress localAddr, int localPort) throws IOException

     如果一個主機同時屬於兩個以上的網路, 它就可能擁有兩個以上的IP 地址. 例如, 一個主機在Internet 網路中的IP 地址為 "222.67.1.34", 在一個區域網中的IP 地址為 "112.5.4.3". 假定這個主機上的客戶程式希望和同一個區域網的一個伺服器程式(地址為:"112.5.4.45: 8000")通訊, 客戶端可按照如下方式構造Socket 物件:     

InetAddress remoteAddr1 = InetAddress.getByName("112.5.4.45");
InetAddress localAddr1 = InetAddress.getByName("112.5.4.3");
Socket socket1 = new Socket(remoteAddr1, 8000, localAddr1, 2345);   //客戶端使用埠2345

1.4 客戶連線伺服器時可能丟擲的異常

       當Socket 的構造方法請求連線伺服器時, 可能會丟擲下面的異常.

UnKnownHostException: 如果無法識別主機的名字或IP 地址, 就會丟擲這種異常. 
ConnectException: 如果沒有伺服器程序監聽指定的埠, 或者伺服器程序拒絕連線, 就會丟擲這種異常. 
SocketTimeoutException: 如果等待連線超時, 就會丟擲這種異常. 
BindException: 如果無法把Socket 物件與指定的本地IP 地址或埠繫結, 就會丟擲這種異常.
以上4中異常都是IOException的直接或間接子類.      如圖2-1所示.

        IOException------- UnknownHostException

                              |---- InterruptedIOException ----------- SocketTimeoutException

                              |---- SocketException              ----------- BindException

                                                                             |---------- ConnectException

                                    圖2-1 客戶端連線伺服器時可能丟擲的異常

二. 獲取Socket 的資訊    

      在一個Socket 物件中同時包含了遠端伺服器的IP 地址和埠資訊, 以及客戶本地的IP 地址和埠資訊. 此外, 從Socket 物件中還可以獲得輸出流和輸入流, 分別用於向伺服器傳送資料, 以及接收從伺服器端發來的資料. 以下方法用於獲取Socket的有關資訊.

getInetAddress(): 獲得遠端伺服器的IP 地址. 
getPort(): 獲得遠端伺服器的埠. 
getLocalAddress(): 獲得客戶本地的IP 地址. 
getLocalPort(): 獲得客戶本地的埠. 
getInputStream(): 獲得輸入流. 如果Socket 還沒有連線, 或者已經關閉, 或者已經通過 shutdownInput() 方法關閉輸入流, 那麼此方法會丟擲IOException. 
getOutputStream(): 獲得輸出流, 如果Socket 還沒有連線, 或者已經關閉, 或者已經通過 shutdownOutput() 方法關閉輸出流, 那麼此方法會丟擲IOException. 
這裡有個HTTPClient 類的例子, 程式碼我是寫好了, 也測試過了, 因為篇幅原因就不貼了. 這個HTTPClient 類用於訪問網頁 www.javathinker.org/index.jsp. 該網頁位於一個主機名(也叫域名)為 www.javathinker.org 的遠端HTTP伺服器上, 它監聽 80 埠. 在HTTPClient 類中, 先建立了一個連線到該HTTP伺服器的Socket物件, 然後傳送符合HTTP 協議的請求, 接著接收從HTTP 伺服器上發回的響應結果.

三. 關閉Socket

     當客戶與伺服器的通訊結束, 應該及時關閉Socket , 以釋放Socket 佔用的包括埠在內的各種資源. Socket 的 close() 方法負責關閉Socket. 當一個Socket物件被關閉, 就不能再通過它的輸入流和輸出流進行I/O操作, 否則會導致IOException.

      為了確保關閉Socket 的操作總是被執行, 強烈建議把這個操作放在finally 程式碼塊中:

   Socket socket = null;
try{
socket = new Socket(www.javathinker.org,80);
//執行接收和傳送資料的操作
..........
}catch(IOException e){
e.printStackTrace();
}finally{
try{
if(socket != null) socket.close();
}catch(IOException e){e.printStackTrace();}
}

    Socket 類提供了3 個狀態測試方法.

isClosed(): 如果Socket已經連線到遠端主機, 並且還沒有關閉, 則返回true , 否則返回false . 
isConnected(): 如果Socket曾經連線到遠端主機, 則返回true , 否則返回false . 
isBound(): 如果Socket已經與一個本地埠繫結, 則返回true , 否則返回false . 
如果要判斷一個Socket 物件當前是否處於連線狀態, 可採用以下方式:

      boolean isConnected = socket.isConnected() && !socket.isClosed();                                      

四. 半關閉Socket

     程序A 與程序B 通過Socket 通訊, 假定程序A 輸出資料, 程序B 讀入資料. 程序A 如何告訴程序B 所有資料已經輸出完畢? 下文略......

五. 設定Socket 的選項

     Socket 有以下幾個選項.

TCP_NODELAY: 表示立即傳送資料. 
SO_RESUSEADDR: 表示是否允許重用Socket 所繫結的本地地址. 
SO_TIMEOUT: 表示接收資料時的等待超時資料. 
SO_LINGER: 表示當執行Socket 的 close()方法時, 是否立即關閉底層的Socket. 
SO_SNFBUF: 表示傳送資料的緩衝區的大小. 
SO_RCVBUF: 表示接收資料的緩衝區的大小. 
SO_KEEPALIVE: 表示對於長時間處於空閒狀態的Socket , 是否要自動把它關閉. 
OOBINLINE: 表示是否支援傳送一個位元組的TCP 緊急資料. 
5.1 TCP_NODELAY 選項

設定該選項: public void setTcpNoDelay(boolean on) throws SocketException 
讀取該選項: public boolean getTcpNoDelay() throws SocketException 
預設情況下, 傳送資料採用Negale 演算法. Negale 演算法是指傳送方傳送的資料不會立即發出, 而是先放在緩衝區, 等快取區滿了再發出. 傳送完一批資料後, 會等待接收方對這批資料的迴應, 然後再發送下一批資料. Negale 演算法適用於傳送方需要傳送大批量資料, 並且接收方會及時作出迴應的場合, 這種演算法通過減少傳輸資料的次數來提高通訊效率.

     如果傳送方持續地傳送小批量的資料, 並且接收方不一定會立即傳送響應資料, 那麼Negale 演算法會使傳送方執行很慢. 對於GUI 程式, 如網路遊戲程式(伺服器需要實時跟蹤客戶端滑鼠的移動), 這個問題尤其突出. 客戶端滑鼠位置改動的資訊需要實時傳送到伺服器上, 由於Negale 演算法採用緩衝, 大大減低了實時響應速度, 導致客戶程式執行很慢.

      TCP_NODELAY 的預設值為 false, 表示採用 Negale 演算法. 如果呼叫setTcpNoDelay(true)方法, 就會關閉 Socket的緩衝, 確保資料及時傳送:

       if(!socket.getTcpNoDelay()) socket.setTcpNoDelay(true);                                                                                   

      如果Socket 的底層實現不支援TCP_NODELAY 選項, 那麼getTcpNoDelay() 和 setTcpNoDelay 方法會丟擲 SocketException.

5.2 SO_RESUSEADDR 選項

設定該選項: public void setResuseAddress(boolean on) throws SocketException 
讀取該選項: public boolean getResuseAddress() throws SocketException 
當接收方通過Socket 的close() 方法關閉Socket 時, 如果網路上還有傳送到這個Socket 的資料, 那麼底層的Socket 不會立即釋放本地埠, 而是會等待一段時間, 確保接收到了網路上傳送過來的延遲資料, 然後再釋放埠. Socket接收到延遲資料後, 不會對這些資料作任何處理. Socket 接收延遲資料的目的是, 確保這些資料不會被其他碰巧繫結到同樣埠的新程序接收到.

     客戶程式一般採用隨機埠, 因此出現兩個客戶程式繫結到同樣埠的可能性不大. 許多伺服器程式都使用固定的埠. 當伺服器程式關閉後, 有可能它的埠還會被佔用一段時間, 如果此時立刻在同一個主機上重啟伺服器程式, 由於埠已經被佔用, 使得伺服器程式無法繫結到該埠, 啟動失敗. (第三篇文章會對此作出介紹).

     為了確保一個程序關閉Socket 後, 即使它還沒釋放埠, 同一個主機上的其他程序還可以立即重用該埠, 可以呼叫Socket 的setResuseAddress(true) 方法:

        if(!socket.getResuseAddress()) socket.setResuseAddress(true);                                                                            

    值得注意的是 socket.setResuseAddress(true) 方法必須在 Socket 還沒有繫結到一個本地埠之前呼叫, 否則執行 socket.setResuseAddress(true) 方法無效. 因此必須按照以下方式建立Socket 物件, 然後再連線遠端伺服器:

Socket socket = new Socket();            //此時Socket物件未繫結本地埠,並且未連線遠端伺服器
socket.setReuseAddress(true);
SocketAddress remoteAddr = new InetSocketAddress("localhost",8000);
socket.connect(remoteAddr);              //連線遠端伺服器, 並且繫結匿名的本地埠

    或者:

Socket socket = new Socket();              //此時Socke 物件為繫結本地埠, 並且未連線遠端伺服器
socket.setReuseAddress(true);
SocketAddress localAddr = new InetSocketAddress("localhost",9000);
SocketAddress remoteAddr = new InetSocketAddress("localhost",8000);
socket.bind(localAddr);             //與本地埠繫結
socket.connect(remoteAddr); //連線遠端伺服器

    此外, 兩個共用同一個埠的程序必須都呼叫 socket.setResuseAddress(true) 方法, 才能使得一個程序關閉 Socket後, 另一個程序的 Socket 能夠立即重用相同埠.

5.3 SO_TIMEOUT 選項

設定該選項: public void setSoTimeout(int milliseconds) throws SocketException 
讀取該選項: public int getSoTimeout() throws SocketException 
當通過Socket 的輸入流讀資料時, 如果還沒有資料, 就會等待. 例如, 在以下程式碼中, in.read(buff) 方法從輸入流中讀入 1024個位元組:

byte[] buff = new byte[1024];
InputStream in = socket.getInputStream();
in.read(buff);

     如果輸入流中沒有資料, in.read(buff) 就會等待發送方傳送資料, 直到滿足以下情況才結束等待:

     略...............

     Socket 類的 SO_TIMEOUT 選項用於設定接收資料的等待超時時間, 單位為毫秒, 它的預設值為 0, 表示會無限等待, 永遠不會超時. 以下程式碼把接收資料的等待超時時間設為 3 分鐘:

        if(socket.getSoTimeout() == 0) socket.setSoTimeout(60000 * 3);   //注意, 原書中這裡的程式碼錯誤, 裡面的方法名字都少了"So"   

     Socket 的 setSoTimeout() 方法必須在接收資料之前執行才有效. 此外, 當輸入流的 read()方法丟擲 SocketTimeoutException 後, Socket 仍然是連線的, 可以嘗試再次讀資料:     

socket.setSoTimeout(180000);
byte[] buff = new byte[1024];
InputStream in = socket.getInputStream();
int len = -1;
do{
try{
len = in.read(buff);
//處理讀到的資料
//.........
}catch(SocketTimeoutException e){
//e.printStackTrace(); 
System.out.println("等待讀超時!");
len = 0;
}    
}while(len != -1);

     例子ReceiveServer.java 和 SendClient.java 是一對簡單的伺服器/客戶程式. sendClient 傳送字串 "hello everyone" ,接著睡眠 1 分鐘, 然後關閉 Socket. ReceiveServer 讀取 SendClient 傳送來的資料, 直到抵達輸入流的末尾, 最後列印 SendClient 傳送來的資料.

     ReceiveServer.java 略....... ,         SendClient.java 略..........

     在 SendClient 傳送字串 "hello everyone" 後, 睡眠 1 分鐘. 當 SendClient 在睡眠時, ReceiveServer 在執行 in.read(buff) 方法, 不能讀到足夠的資料填滿 buff 緩衝區, 因此會一直等待 SendClient 傳送資料. 如果在 ReceiveServer 類中 socket.setSoTimeout(20000) , 從而把等待接收資料的超時時間設為 20 秒, 那麼 ReceiveServer 在等待資料時, 每當超過 20 秒, 就會丟擲SocketTimeoutException . 等到 SendClient 睡眠 1 分鐘後, SendClient 呼叫 Socket 的 close() 方法關閉 Socket, 這意味著 ReceiveServer 讀到了輸入流的末尾, ReceiveServer 立即結束讀等待, read() 方法返回 -1 . ReceiveServer最後列印接收到的字串 "hello everyone", 結果如下:

等待讀超時!
等待讀超時!
hello everyone

5.4 SO_LINGER 選項

設定該選項: public void setSoLinger(boolean on, int seconds) throws SocketException 
讀取該選項: public int getSoLinger() throws SocketException 
SO_LINGER 選項用來控制 Socket 關閉時的行為. 預設情況下, 執行 Socket 的 close() 方法, 該方法會立即返回, 但底層的 Socket 實際上並不立即關閉, 它會延遲一段時間, 直到傳送完所有剩餘的資料, 才會真正關閉 Socket, 斷開連線.

      如果執行以下方法:

      socket.setSoLinger(true, 0);                                                                                               

      那麼執行Socket 的close() 方法, 該方法也會立即返回, 並且底層的 Socket 也會立即關閉, 所有未傳送完的剩餘資料被丟棄.

      如果執行以下方法:

      socket.setSoLinger(true, 3600);                                                                                          

      那麼執行Socket 的 close() 方法, 該方法不會立即返回, 而是進入阻塞狀態. 同時, 底層的 Socket 會嘗試傳送剩餘的資料. 只有滿足以下兩個條件之一, close() 方法才返回:

      ⑴ 底層的 Socket 已經發送完所有的剩餘資料;

      ⑵ 儘管底層的 Socket 還沒有傳送完所有的剩餘資料, 但已經阻塞了 3600 秒(注意這裡是秒, 而非毫秒), close() 方法的阻塞時間超過 3600 秒, 也會返回, 剩餘未傳送的資料被丟棄.

      值得注意的是, 在以上兩種情況內, 當close() 方法返回後, 底層的 Socket 會被關閉, 斷開連線. 此外, setSoLinger(boolean on, int seconds) 方法中的 seconds 引數以秒為單位, 而不是以毫秒為單位.    

      如果未設定 SO_LINGER 選項, getSoLinger() 返回的結果是 -1, 如果設定了 socket.setSoLinger(true, 80) , getSoLinger() 返回的結果是 80.

Tips: 當程式通過輸出流寫資料時, 僅僅表示程式向網路提交了一批資料, 由網路負責輸送到接收方. 當程式關閉 Socket, 有可能這批資料還在網路上傳輸, 還未到達接收方. 這裡所說的 "未傳送完的資料" 就是指這種還在網路上傳輸, 未被接收方接收的資料.

    例子 SimpleClient.java 與 SimpleServer.java 所示是一對簡單的客戶/伺服器程式. SimpleClient 類傳送一萬個字元給 SimpleServer, 然後呼叫Socket 的 close() 方法關閉 Socket.

    SimpleServer 通過 ServerSocket 的 accept() 方法接受了 SimpleClient 的連線請求後, 並不立即接收客戶傳送的資料, 而是睡眠 5 秒鐘後再接收資料. 等到 SimpleServer 開始接收資料時, SimpleClient 有可能已經執行了 Socket 的close() 方法, 那麼 SimpleServer 還能接收到 SimpleClient 傳送的資料嗎?

    SimpleClient.java 略..., SimpleServer.java 略......

    SimpleClient.java中

System.out.println("開始關閉 Socket");
long begin = System.currentTimeMillis();
socket.close();
long end = System.currentTimeMillis();
System.out.println("關閉Socket 所用的時間為:" + (end - begin) + "ms");

    下面分 3 種情況演示 SimpleClient 關閉 Socket 的行為.  

    ⑴ 未設定 SO_LINGER 選項, 當 SimpleClient 執行 Socket 的close() 方法時, 立即返回, SimpleClient 的列印結果如下:

開始關閉 Socket
關閉Socket 所用的時間為:0ms

     等到 SimpleClient 結束執行, SimpleServer 可能才剛剛結束睡眠, 開始接收 SimpleClient 傳送的資料. 此時儘管 SimpleClient 已經執行了 Socket 的 close() 方法, 並且 SimpleClient 程式本身也執行結束了, 但從 SimpleServer 的列印結果可以看出, SimpleServer 仍然接收到了所有的資料. 之所以出現這種情況, 是因為當 SimpleClient 執行了 Socket 的 close() 方法後, 底層的 Socket 實際上並沒有真正關閉, 與 SimpleServer 的連線依然存在. 底層的 Socket 會存在一段時間, 直到傳送完所有的資料.

     ⑵ 設定SO_LINGER 選項, socket.setSoLinger(true, 0). 這次當 SimpleClient 執行 Socket 的 close() 方法時, 會強行關閉底層的 Socket, 所有未傳送完的資料丟失. SimpleClient 的列印結果如下:

開始關閉 Socket
關閉Socket 所用的時間為:0ms

     從列印結果看出, SimpleClient 執行 Socket 的 close() 方法時, 也立即返回. 當 SimpleServer 結束睡眠, 開始接收 SimpleClient 傳送的資料時, 由於 SimpleClient 已經關閉底層 Socket, 斷開連線, 因此 SimpleServer 在讀資料時會丟擲 SocketException:

        java.net.SocketException: Connection reset                                       

     ⑶ 設定SO_LINGER 選項, socket.setSoLinger(true, 3600). 這次當 SimpleClient 執行 Socket 的close() 方法時, 會進入阻塞狀態, 知道等待了 3600 秒, 或者底層 Socket 已經把所有未傳送的剩餘資料傳送完畢, 才會從 close() 方法返回. SimpleClient 的列印結果如下:

開始關閉 Socket
關閉Socket 所用的時間為:5648ms

     當 SimpleServer 結束了 5 秒鐘的睡眠, 開始接收 SimpleClient 傳送的資料時, SimpleClient 還在這些 Socket 的close() 方法, 並且處於阻塞狀態. SimpleClient 與 SimpleServer 之間的連線依然存在, 因此 SimpleServer 能夠接收到 SimpleClient 傳送的所有資料.

5.5 SO_RCVBUF 選項

設定該選項: public void setReceiveBufferSize(int size) throws SocketException 
讀取該選項: public int getReceiveBufferSize() throws SocketException 
SO_RCVBUF 表示 Socket 的用於輸入資料的緩衝區的大小. 一般說來, 傳輸大的連續的資料塊(基於HTTP 或 FTP 協議的通訊) 可以使用較大的緩衝區, 這可以減少傳輸資料的次數, 提高傳輸資料的效率. 而對於互動頻繁且單次傳送資料量比較小的通訊方式(Telnet 和 網路遊戲), 則應該採用小的緩衝區, 確保小批量的資料能及時傳送給對方. 這種設定緩衝區大小的原則也同樣適用於 Socket 的 SO_SNDBUF 選項.

      如果底層 Socket 不支援 SO_RCVBUF 選項, 那麼 setReceiveBufferSize() 方法會丟擲 SocketException.

5.6 SO_SNDBUF 選項

設定該選項: public void setSendBufferSize(int size) throws SocketException 
讀取該選項: public int getSendBufferSize() throws SocketException 
SO_SNDBUF 表示 Socket 的用於輸出資料的緩衝區的大小. 如果底層 Socket 不支援 SO_SNDBUF 選項, setSendBufferSize() 方法會丟擲 SocketException.

5.7 SO_KEEPALIVE 選項

設定該選項: public void setKeepAlive(boolean on) throws SocketException 
讀取該選項: public boolean getKeepAlive() throws SocketException //原書中這個方法返回的型別是int 
當 SO_KEEPALIVE 選項為 true 時, 表示底層的TCP 實現會監視該連線是否有效. 當連線處於空閒狀態(連線的兩端沒有互相傳送資料) 超過了 2 小時時, 本地的TCP 實現會發送一個數據包給遠端的 Socket. 如果遠端Socket 沒有發回響應, TCP實現就會持續嘗試 11 分鐘, 直到接收到響應為止. 如果在 12 分鐘內未收到響應, TCP 實現就會自動關閉本地Socket, 斷開連線. 在不同的網路平臺上, TCP實現嘗試與遠端Socket 對話的時限有所差別.

      SO_KEEPALIVE 選項的預設值為 false, 表示TCP 不會監視連線是否有效, 不活動的客戶端可能會永遠存在下去, 而不會注意到伺服器已經崩潰.

      以下程式碼把 SO_KEEPALIVE 選項設為 true:

        if(!socket.getKeepAlive()) socket.setKeepAlive(true);                                                              

5.8 OOBINLINE 選項

設定該選項: public void setOOBInline(boolean on) throws SocketException 
讀取該選項: public boolean getOOBInline() throws SocketException //原書中這個方法返回的型別是int 
當 OOBINLINE 為 true 時, 表示支援傳送一個位元組的 TCP 緊急資料. Socket 類的 sendUrgentData(int data) 方法用於傳送一個位元組的 TCP緊急資料.

     OOBINLINE 的預設值為 false, 在這種情況下, 當接收方收到緊急資料時不作任何處理, 直接將其丟棄. 如果使用者希望傳送緊急資料, 應該把 OOBINLINE 設為 true:

          socket.setOOBInline(true);                                                             

      此時接收方會把接收到的緊急資料