天天看點

Socket類的getter和setter方法

        在Java類中,getter和setter方法占了很大的比重。由于Java中沒有定義屬性的關鍵字;是以,getter和setter方法用于獲得和設定Java類的屬性值;如getName和setName方法用于設定name屬性的值。如果某個屬性隻有getter方法,那這個屬性是隻讀的;如果隻有setter方法,那麼這個屬性是隻寫的。在Socket類中也有很多這樣的屬性來獲得和Socket相關的資訊,以及對Socket對象的狀态進行設定。

一、用于獲得資訊的getter方法

        我們可以從Socket對象中獲得3種資訊。

1、伺服器資訊

 對于用戶端來說,伺服器的資訊隻有3個:域名、IP位址)和端口。Socket類為我們提供了3個方法來得到這3個資訊。

        (1)public  InetAddress getInetAddress()

        這個方法傳回一個InetAddress對象。通過這個對象,可以得到伺服器的IP、域名等資訊。

Socket socket = new Socket("www.ptpress.com.cn", 80);
//IP 位址(字元串形式)
System.out.println(socket.getInetAddress().getHostAddress());
//域名
System.out.println(socket.getInetAddress().getHostName());
           

        (2)public int getPort()

        這個方法可以以整數形式獲得伺服器的端口号。

Socket socket = new Socket("www.ptpress.com.cn", 80);
System.out.println(socket.getPort());
           

        (3)public SocketAddressgetRemoteSocketAddress()

        這個方法是将getInetAddress和getPort方法結合在了一起;利用這個方法可以同時得到伺服器的IP和端口号。但這個方法傳回了一個SocketAddress對象,這個對象隻能作為connect方法的參數用于連接配接伺服器;而要想獲得伺服器的IP和端口号,必須得将SocketAddress轉換為它的子類InetSocketAddress。

Socket socket = new Socket("www.ptpress.com.cn", 80);    
System.out.println(((InetSocketAddress)socket.getRemoteSocketAddress()).getHostName());
System.out.println(((InetSocketAddress)socket.getRemoteSocketAddress()).getPort());
           

注意:以上3個方法都可以在調用Socket對象關閉後調用。它們所獲得的資訊在Socket對象關閉後仍然有效。如果直接使用IP連接配接伺服器時,getHostName和getHostAddress的傳回值是一樣的,都是伺服器的IP。

2、本機資訊

        與伺服器資訊一樣,本機資訊也有3個:本地IP、域名和綁定的本地端口号。這些資訊也可以通過3個方法來獲得。

        (1)publicInetAddress getLocalAddress()

        這個方法傳回了本機的InetAddress對象。通過這個方法可以得到本機的IP和機器名。當本機綁定了多個IP時,Socket對象使用哪一個IP連接配接伺服器,就傳回哪個IP。如果本機使用ADSL上網,并且通過Socket對象連接配接到Internet上的某一個IP或域名上(如www.ptpress.com.cn),則getLocalAddress将傳回“ADSL連接配接”所臨時綁定的IP;是以,我們可以通過getLocalAddress得到ADSL的臨時IP。

Socket socket = new Socket();
socket.connect(new InetSocketAddress("www.ptpress.com.cn", 80));
System.out.println(socket.getLocalAddress().getHostAddress());
System.out.println(socket.getLocalAddress().getHostName()); 
           

        (2)publicint getLocalPort()

        通過這個方法可以得到Socket對象所綁定的本機的一個端口号;如果未綁定端口号,則傳回一個從1024到65,535之間的随機數。是以,使用這個方法可能每次得到的端口号不一樣。

Socket socket = new Socket();
// 如果使用下面的bind方法進行端口綁定的話,getLocalPort方法将傳回100
// socket.bind(new InetSocketAddress("127.0.0.1", 100));
socket.connect(new InetSocketAddress("www.ptpress.com.cn" 80));
System.out.println(socket.getLocalPort())                   
           

        (3)public SocketAddressgetLocalSocketAddress()

        這個方法和getRemoteSocketAddress方法類似,也是同時得到了本地IP和Socket對象所綁定的端口号。如果要得到本地IP和端口号,必須将這個方法的傳回值轉換為InetSocketAddress對象。

Socket socket = new Socket("www.ptpress.com.cn", 80);    
System.out.println(((InetSocketAddress)socket.getLocalSocketAddress()).getHostName());       
System.out.println(((InetSocketAddress)socket.getLocalSocketAddress()).getPort());
           

3、用于傳輸資料的輸入、輸出流

        輸入、輸出流在前面的章節已經被多次用到。在這裡讓我們來簡單回顧一下。

        (1)public InputStream getInputStream() throws IOException

 用于獲得從伺服器讀取資料的輸入流。它所得的流是最原始的源。為了操作更友善,我們經常使用InputStreamReader和BufferedReader來讀取從伺服器傳過來的字元串資料。

Socket socket = new Socket("www.ptpress.com.cn", 80);
InputStream inputStream = socket.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
System.out.println(bufferedReader.readLine());
           

        (2)public OutputStream getOutputStream() throws IOException

        用于獲得向伺服器發送資料的輸出流。輸出流可以通過OutputStreamWriter和BufferedWriter向伺服器寫入字元串資料。

Socket socket = new Socket("www.ptpress.com.cn", 80);
OutputStream outputStream  = socket.getOutputStream();        
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream);
BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter);
bufferedWriter.write("你好");
bufferedWriter.flush();
           

注意:在使用OutputStream的write方法輸出資料後,必須使用flush方法重新整理輸出緩沖區,以便将輸出緩沖區中的資料發送出去。如果要輸出字元串,使用OutputStreamWriter和BufferedWriter都可以;它們的write方法都可以直接使用字元串作為參數來輸出資料。而這一點與相應的InputStreamReader和BufferedReader不同;它們中隻有BufferedReader有readLine方法,是以,必須使用BufferedReader才能直接讀取字元串資料。

二、用于擷取和設定Socket選項的getter和setter方法

        Socket選項可以指定Socket類發送和接收資料的方式。在JDK1.4中共有8個Socket選項可以設定。這8個選項都定義在java.net.SocketOptions接口中。定義如下:

public final static int TCP_NODELAY = 0x0001;

    public final static int SO_REUSEADDR = 0x04;

    public final static int SO_LINGER = 0x0080;

    public final static int SO_TIMEOUT = 0x1006;

    public final static int SO_SNDBUF = 0x1001;

    public final static int SO_RCVBUF = 0x1002;

    public final static int SO_KEEPALIVE = 0x0008;

    public final static int SO_OOBINLINE = 0x1003;
           

        有 趣的是,這8個選項除了第一個沒有SO字首外,其他7個選項都以SO作為字首。其實這個SO就是SocketOption的縮寫;是以,在Java中約定所有以SO為字首的常量都表示Socket選項;當然,也有例外,如TCP_NODELAY。在Socket類中為每一個選項提供了一對get和set方法,分别用來獲得和設定這些選項。

1、TCP_NODELAY

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

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

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

2、SO_REUSEADDR

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

 通過這個選項,可以使多個Socket對象綁定在同一個端口上。其實這樣做并沒有多大意義,但當使用close方法關閉Socket連接配接後,Socket對象所綁定的端口并不一定馬上釋放;系統有時在Socket連接配接關閉才會再确認一下是否有因為延遲而未到達的資料包,這完全是在底層處理的,也就是說對使用者是透明的;是以,在使用Socket類時完全不會感覺到。

        這種處理機制對于随機綁定端口的 Socket對象沒有什麼影響,但對于綁定在固定端口的Socket對象就可能會抛出“Address already in use: JVM_Bind”例外。是以,使用這個選項可以避免個例外的發生。

package mynet;

import java.net.*;
import java.io.*;

public class Test
{
    public static void main(String[] args)
    {
        Socket socket1 = new Socket();
        Socket socket2 = new Socket();
        try
        {
            socket1.setReuseAddress(true);
            socket1.bind(new InetSocketAddress("127.0.0.1", 88));
            System.out.println("socket1.getReuseAddress():"
                    + socket1.getReuseAddress());
            socket2.bind(new InetSocketAddress("127.0.0.1", 88));
        }
        catch (Exception e)
        {
            System.out.println("error:" + e.getMessage());
            try
            {
                socket2.setReuseAddress(true);
                socket2.bind(new InetSocketAddress("127.0.0.1", 88));
                System.out.println("socket2.getReuseAddress():"
                        + socket2.getReuseAddress());
                System.out.println("端口88第二次綁定成功!");
            }
            catch (Exception e1)
            {
                System.out.println(e.getMessage());
            }
        }
    }
}
           

        上面 的代碼的運作結果如下:

socket1.getReuseAddress():true
error:Address already in use: JVM_Bind
socket2.getReuseAddress():true
端口88第二次綁定成功!
           

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

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

b.必須将綁定同一個端口的所有的Socket對象的SO_REUSEADDR選項都打開才能起作用。如在上面的代碼中,socket1和socket2都使用了setReuseAddress方法打開了各自的SO_REUSEADDR選項。

3、SO_LINGER

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() == -1) socket.setSoLinger(true, 60);
           

4、SO_TIMEOUT

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秒:

socket1.setSoTimeout(30 * 1000);
           

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

5、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例外。

6、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例外。

7、SO_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);
           

8、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(1234);
        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", 1234);
        socket.setOOBInline(true);
        OutputStream out = socket.getOutputStream();
        OutputStreamWriter outWriter = new OutputStreamWriter(out);
        outWriter.write(67);              // 向伺服器發送字元"C"
        outWriter.write("hello world/r/n");
        socket.sendUrgentData(65);        // 向伺服器發送字元"A"
        socket.sendUrgentData(322);        // 向伺服器發送字元"B"
        outWriter.flush();
        socket.sendUrgentData(214);       // 向伺服器發送漢字”中”
        socket.sendUrgentData(208);
        socket.sendUrgentData(185);       // 向伺服器發送漢字”國”
        socket.sendUrgentData(250);
        socket.close();
    }
}
           

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

測試:

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

       1)運作Server

java mynet.Server
           

       2)運作Client

java mynet.Client
           

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

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

        在ClienT類中使用了sendUrgentData 方法向伺服器發送了字元'A'(65) 和'B'(66) 。但發送'B'時 實際發送的是322 ,由于sendUrgentData 隻發送整型數的低位元組。是以,實際發送的是66 。

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

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

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