1. 程式人生 > >Android Socket程式設計實踐

Android Socket程式設計實踐

概述

什麼是Socket

網路上的兩個程式通過一個雙向的通訊連線實現資料的交換,這個雙向鏈路的一端稱為一個Socket。Socket通常用來實現客戶端和服務端的連線。Socket是TCP/IP協議的一個十分流行的程式設計實現,一個Socket由一個IP地址和一個埠號唯一確定。
但是,Socket所支援的協議種類也不光TCP/IP一種,因此兩者之間是沒有必然聯絡的。在Java環境下,Socket程式設計主要是指基於TCP/IP協議的網路程式設計。

Socket通訊的過程

Server端Listen(監聽)某個埠是否有連線請求,Client端向Server端發出Connect(連線)請求,Server端向Client端發回Accept(接受)訊息。一個連線就建立起來了。Server端和Client 端都可以通過Send,Write等方法與對方通訊。
對於一個功能齊全的Socket,都要包含以下基本結構,其工作過程包含以下四個基本的步驟:
  (1) 建立Socket;
  (2) 開啟連線到Socket的輸入/出流;
  (3) 按照一定的協議對Socket進行讀/寫操作;
  (4) 關閉Socket。

InetAdress

首先說明一下InetAdress類的用法,它代表一個IP地址物件,是網路通訊的基礎,後面講TCP/UDP程式設計會大量使用該類。
Java提供了InetAdress類來代表IP地址,InetAdress下還有兩個子類:Inet4Adress(IPv4)和Inet6Adress(IPv6)。
InetAdress類沒有提供構造器,而是提供了下面兩個靜態方法來獲取InetAdress物件:

  • InetAdress getByName(String ip):根據主機IP獲取對應InetAdress物件。
  • InetAdress getByAddress(Byte[] addr ):根據IP地址獲取對應InetAdress物件。
  • InetAdress getLocalHost():獲取本地機器的InetAdress物件。

InetAdress還提供瞭如下幾個方法來獲取IP地址和主機名:

  • String getCanonicalHostName():獲取全限定域名
  • String getHostAdress():獲取IP地址字串
  • String getHostName():獲取主機名
  • Boolean isReachable(int time):測試指定時間內(ms)是否可以到達該地址

網路程式設計中兩個主要的問題

一個是如何準確的定位網路上一臺或多臺主機,另一個就是找到主機後如何可靠高效的進行資料傳輸。
(1)在TCP/IP協議中IP層主要負責網路主機的定位,資料傳輸的路由,由IP地址可以唯一地確定Internet上的一臺主機。
(2)而TCP層則提供面向應用的可靠(TCP)的或非可靠(UDP)的資料傳輸機制,這是網路程式設計的主要物件,一般不需要關心IP層是如何處理資料的。
目前較為流行的網路程式設計模型是客戶機/伺服器(C/S)結構。即通訊雙方一方作為伺服器等待客戶提出請求並予以響應。客戶則在需要服務時向伺服器提出申請。伺服器一般作為守護程序始終執行,監聽網路埠,一旦有客戶請求,就會啟動一個服務程序來響應該客戶,同時自己繼續監聽服務埠,使後來的客戶也能及時得到服務。

兩類傳輸協議:TCP、UDP

TCP是Tranfer Control Protocol的簡稱,是一種面向連線的保證可靠傳輸的協議。通過TCP協議傳輸,得到的是一個順序的無差錯的資料流。傳送方和接收方的成對的兩個socket之間必須建立連線,以便在TCP協議的基礎上進行通訊,當一個socket(通常都是server socket)等待建立連線時,另一個socket可以要求進行連線,一旦這兩個socket連線起來,它們就可以進行雙向資料傳輸,雙方都可以進行傳送或接收操作。
UDP是User Datagram Protocol的簡稱,是一種面向無連線的協議,每個資料報都是一個獨立的資訊,包括完整的源地址或目的地址,它在網路上以任何可能的路徑發往目的地,因此能否到達目的地,到達目的地的時間以及內容的正確性都是不能被保證的。

比較:

  • TCP:

    • 1,面向連線的協議,在socket之間進行資料傳輸之前必然要建立連線,所以在TCP中需要連線時間。
    • 2,TCP傳輸資料無大小限制,一旦連線建立起來,雙方的socket就可以按統一的格式傳輸大的資料。
    • 3,TCP是一個可靠的協議,它的重發機制確保接收方完全正確地獲取傳送方所傳送的全部資料。
  • UDP:

    • 1,每個資料報中都給出了完整的地址資訊,因此無需要建立傳送方和接收方的連線。
    • 2,UDP傳輸資料時是有大小限制的,每個被傳輸的資料報必須限定在64KB之內。
    • 3,UDP是一個不可靠的協議,傳送方所傳送的資料報並不一定以相同的次序到達接收方。

應用:

  • 1,TCP在網路通訊上有極強的生命力,例如遠端連線(Telnet)和檔案傳輸(FTP)都需要不定長度的資料被可靠地傳輸。但是可靠的傳輸是要付出代價的,對資料內容正確性的檢驗必然佔用計算機的處理時間和網路的頻寬,因此TCP傳輸的效率不如UDP高。
  • 2,UDP操作簡單,而且僅需要較少的監護,因此通常用於區域網高可靠性的分散系統中client/server應用程式。例如視訊會議系統,並不要求音訊視訊資料絕對的正確,只要保證連貫性就可以了,這種情況下顯然使用UDP會更合理一些。

TCP程式設計

原理

TCP程式設計是基於ServerSocketSocket來實現的。TCP協議控制兩個通訊實體相互通訊的示意圖如下:
這裡寫圖片描述
從圖上看兩個通訊實體並沒有伺服器、客戶端之分,但那是兩個通訊實體已經建立鏈路後的示意圖。在鏈路建立前,必須要有一個通訊實體做出“主動姿態”,主動接收來自其他通訊實體的連線請求,這方就是伺服器端了。Java中能接收其他通訊實體連線請求的類是ServerSocket,ServerSocket物件用來監聽來自客戶端的Socket連線,如果沒有連線,它將一直處於等待狀態。ServerSocket包含一個監聽來自客戶端連線請求的方法:

  • Socket accept();
    如果接收到一個客戶端的Socket連線請求,會返回一個與客戶端Socket對應的服務端Socket,否則該方法一直處於等待狀態,執行緒阻塞。

ServerSocket類提供如下構造器:

  • ServerSocket(int port);
    使用指定埠來建立,port範圍:0-65535。注意,在選擇埠時,必須小心。每一個埠提供一種特定的服務,只有給出正確的埠,才能獲得相應的服務。0~1023的埠號為系統所保留,例如http服務的埠號為80,telnet服務的埠號為21,ftp服務的埠號為23, 所以我們在選擇埠號時,最好選擇一個大於1023的數以防止發生衝突。
  • ServerSocket(int port, int backlog);
    增加一個用來改變連線佇列長度的引數backlog。
  • ServerSocket(int port, int backlog, InetAddress bindAddr);
    在機器存在多個IP地址的情況下,bindAddr引數指定將ServerSocket繫結到指定IP。

當ServerSocket使用完畢,應使用close()方法來關閉此ServerSocket。通常情況下,伺服器不應該只接收一個客戶端請求,而應該不斷接收來自客戶端的請求,所以程式可以通過迴圈,不斷呼叫ServerSocket的accept方法:

ServerSocket ss = new ServerSocket(30000);
while(true) {
    Socket s = ss.accept();
    ...
}

客戶端通常使用Socket來連線指定伺服器,Socket類提供如下構造器:

  • Socket(InetAddress/String remoteAddress, int port);
    建立連線到指定遠端主機、遠端埠的Socket,本地IP地址和埠使用預設值。
  • Socket(InetAddress/String remoteAddress, int port, InetAddress localAddr, int localPort);
    繫結本地IP地址和埠,適用於本地主機有多個IP地址的情形。

上面兩個構造器指定遠端主機時既可以使用InetAddress來指定,也可以直接使用String物件來指定遠端IP。本地主機只有一個IP地址時,使用第一個方法更簡單。

實踐

我們這裡實現一個功能,客戶端通過TCP協議傳輸一張圖片到伺服器端,並顯示傳輸進度。
這裡將服務端程式寫成一個Android應用:

public class MainActivity extends AppCompatActivity {
    private TextView text;
    public static Toast toast = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        text = (TextView) findViewById(R.id.text);
        text.setText("IP地址:"+getLocalHostIp());

        //啟動伺服器監聽執行緒
        new ServerListener().start();
    }

    public class ServerListener extends Thread {
        @Override
        public void run() {
            try {
                ServerSocket serverSocket = new ServerSocket(30000);
                // 迴圈的監聽
                while (true) {
                    Socket socket = serverSocket.accept();// 阻塞
                    runOnUiThread(new Runnable(){
                       @Override
                       public void run() {
                            showToast("有客戶端連線到本機的30000埠!");
                       }
                    });
                    // 將socket傳給新的執行緒
                    TransportSocket ts = new TransportSocket(socket);
                    ts.start();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public class TransportSocket extends Thread {
        Socket socket;
        int progress = 0;

        public TransportSocket(Socket s) {
            this.socket = s;
        }

        @Override
        public void run() {
            try {
                File file = new File("/sdcard/receive.png"); //接收檔案
                if (!file.exists()) {
                    file.createNewFile();
                }
                BufferedInputStream is = new BufferedInputStream(socket.getInputStream()); // 讀進
                BufferedOutputStream os = new BufferedOutputStream(new FileOutputStream(file));// 寫出
                byte[] data = new byte[1024*5];// 每次讀取的位元組數
                int len= -1;
                while ((len=is.read(data) )!= -1) {
                    os.write(data,0,len);
                    progress+=len;//進度
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            showToast("接收進度:" + progress);
                        }
                    });
                }
                progress = 0;
                is.close();
                os.flush();
                os.close();
                socket.close(); //關閉socket
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        showToast("接收完成!");
                    }
                });
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    //及時toast
    public void showToast(String info) {
        if (toast == null) {
            toast = Toast.makeText(this, "", Toast.LENGTH_SHORT);
        }
        toast.setText(info);
        toast.show();
    }


    // 獲取本機IPv4地址
    public static String getLocalHostIp() {
        String ipaddress = "";
        try {
            Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces();
            // 遍歷所用的網路介面
            while (en.hasMoreElements()) {
                NetworkInterface nif = en.nextElement();// 得到每一個網路介面繫結的所有ip
                Enumeration<InetAddress> inet = nif.getInetAddresses();
                // 遍歷每一個介面繫結的所有ip
                while (inet.hasMoreElements()) {
                    InetAddress ip = inet.nextElement();
                    if (!ip.isLoopbackAddress() && ip instanceof Inet4Address) {
                        return ipaddress = ip.getHostAddress();
                    }
                }
            }
        } catch (SocketException e) {
            System.out.print("獲取IP失敗");
            e.printStackTrace();
        }
        return ipaddress;
    }
}

新增許可權:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>

再來看看客戶端程式碼,同樣是一個Android應用:

public class MainActivity extends AppCompatActivity {
    private Button button;
    private int progress = 0;
    private Toast toast = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button = (Button) findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View v) {
                ClientSocket cs = new ClientSocket();
                cs.start();
            }
        });
    }

    public class ClientSocket extends Thread {
        int progress = 0;

        @Override
        public void run() {
            try {
                Socket socket = new Socket("192.168.1.43", 30000); //伺服器IP及埠
                File file = new File("/sdcard/send.png");//傳送檔案,記得客戶端此路徑下必須要有此檔案存在
                BufferedInputStream is = new BufferedInputStream(new FileInputStream(file));
                BufferedOutputStream os =new BufferedOutputStream(socket.getOutputStream());
                // 讀檔案
                double n = 1;
                byte[] data = new byte[1024*5];//每次讀取的位元組數
                int len=-1;
                while ((len=is.read(data))!= -1) {
                    os.write(data,0,len);
                    progress+=len;//進度
                    runOnUiThread(new Runnable(){
                        @Override
                        public void run() {
                            showToast("傳送進度:"+progress);
                        }
                    });
                }
                progress=0;
                is.close();
                os.flush();
                os.close();
                socket.close(); //關閉socket
                runOnUiThread(new Runnable(){
                    @Override
                    public void run() {
                        showToast("傳送完成!");
                    }
                });
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public void showToast(String info) {
        if (toast == null) {
            toast = Toast.makeText(this, "", Toast.LENGTH_SHORT);
        }
        toast.setText(info);
        toast.show();
    }
}

同樣新增許可權:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>

注意,這裡Socket連線的伺服器IP地址和埠必須保持和服務端應用的一致。上面服務端應用已經將其IP地址首頁顯示出來了。
同時執行服務端和客戶端應用,這裡要確保它們處於聯網狀態且處於同一個區域網。點選客戶端開始傳輸按鈕:
這裡寫圖片描述
服務端顯示:
這裡寫圖片描述
傳送完成後,可以看到,在伺服器sdcard目錄下會生成一張receive.png圖片,內容與客戶端的send.png完全一致。

UDP程式設計

原理

UDP是一種不可靠網路協議,雙方Socket之間並沒有虛擬鏈路,這兩個Socket只是傳送、接收資料報的物件。Java提供了DatagramSocket作為基於UDP協議的Socket,使用DatagramPacket代表DatagramSocket傳送、接收的資料報。
DatagramSocket本身只是碼頭,不維護狀態,不能產生IO流,唯一作用就是接收和傳送資料報。看一下DatagramSocket建構函式:

  • DatagramSocket();
    建立物件,繫結到本機預設IP地址,本機所有可用埠隨機選擇某個埠。
  • DatagramSocket(int port);
    繫結到本機預設IP地址,指定埠。
  • DatagramSocket(int port, InetAdress addr);
    繫結到指定IP地址,指定埠。

通常建立伺服器時,我們建立指定埠DatagramSocket物件—-這樣保證其他客戶端可以將資料傳送到該伺服器。一旦得到DatagramSocket物件之後,可以通過如下兩個方法接收和傳送資料:

  • receive(DatagramPacket p);
    從該DatagramSocket中接收資料。receive將一直等待(也就是說會阻塞呼叫該方法的執行緒),直到收到一個數據報為止。
  • send(DatagramPacket p);
    以該DatagramSocket向外傳送資料

使用DatagramSocket傳送資料時,DatagramSocket並不知道資料的目的地,而是由DatagramPacket自身決定的。
當C/S程式使用UDP時,實際上並沒有明顯的客戶端、伺服器之分,雙方都要建立一個DatagramSocket物件來發送和接收資料,一般把固定IP,固定埠的DatagramSocket所在程式作為伺服器,因為該DatagramSocket可以主動接受客戶端資料。
接下來看看DatagramPacket建構函式:

  • DatagramPacket(byte buf[], int length);
    以一個空陣列來建立DatagramPacket物件,該物件作用是接收DatagramSocket中的資料。
  • DatagramPacket(byte buf[], int offset, int length);
    以一個空陣列來建立DatagramPacket物件,並指定接收到的資料放入buf陣列時從offset開始,最多放length個位元組。
  • DatagramPacket(byte buf[], int length, InetAdress addr, int port);
    以一個包含資料的陣列來建立DatagramPacket物件,還指定了IP和埠—決定該資料目的地。
  • DatagramPacket(byte buf[], int offset, int length, InetAdress addr, int port);
    建立用於傳送的DatagramPacket物件,多指定了一個offset引數。

注:DatagramPacket同時還提供了getData()和setData()函式用來獲取和設定封裝在DatagramPacket物件裡面的位元組陣列,即構造器裡面的位元組陣列實參。

DatagramPacket提供瞭如下三個方法來獲取傳送者的IP和埠:

  • InetAdress getAdress();
    當程式準備傳送此資料報時,返回資料報目標主機的IP地址;當程式接收到一個數據報時,返回該資料報傳送主機的IP地址。
  • int getPort();
    和上面函式類似,只是獲取的是埠。
  • SocketAdress getSocketAdress();
    和上面函式類似,只是獲取的是SocketAdress物件,SocketAdress封裝了一個InetAdress物件和一個整形埠資訊。

實踐

我們接下來實現一個客戶端和伺服器的區域網聊天系統,訊息的傳送和接收是基於UDP協議的。
首先看客戶端程式碼:

public class MainActivity extends AppCompatActivity {
    private Button send_message;
    private EditText edit;
    private TextView content;
    private Toast toast = null;
    private DatagramSocket sendDS; //udp傳送socket
    private DatagramSocket receiveDS; //udp接收socket

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        edit = (EditText) findViewById(R.id.edit);
        send_message = (Button) findViewById(R.id.send_message);
        content = (TextView) findViewById(R.id.content);

        send_message.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View v) {
                UdpSend us = new UdpSend();
                us.start();
            }
        });
        //啟動UDP監聽執行緒
        new UdpReceive().start();
    }

    // 傳送訊息執行緒
    class UdpSend extends Thread {

        @Override
        public void run() {
            try {
                runOnUiThread(new Runnable(){
                    @Override
                    public void run() {
                        if (edit.getText().toString().trim().isEmpty()) {
                            showToast("請輸入訊息!");
                            return;
                        } else {
                            content.setText(content.getText()+"\n我說:"+edit.getText().toString());
                            edit.setText("");
                        }
                    }
                });

                byte[] data = edit.getText().toString().getBytes();
                if (sendDS == null) {
                    sendDS = new DatagramSocket(null);
                    sendDS.setReuseAddress(true);
                    sendDS.bind(new InetSocketAddress(20000)); //傳送埠20000
                }
                DatagramPacket packet = new DatagramPacket(data, data.length, InetAddress.getByName("192.168.1.43"), 25000); //接收埠25000
                packet.setData(data);
                sendDS.send(packet);
//              sendDS.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    // 接收訊息執行緒
    class UdpReceive extends Thread {

        @Override
        public void run() {
            //訊息迴圈
            while(true) {
                try {
                    if(receiveDS == null){
                        receiveDS = new DatagramSocket(null);
                        receiveDS.setReuseAddress(true);
                        receiveDS.bind(new InetSocketAddress(25000)); //接收埠25000
                    }
                    byte[] data = new byte[1024 * 4];
                    DatagramPacket dp = new DatagramPacket(data, data.length);
                    dp.setData(data);
                    receiveDS.receive(dp); //阻塞式,等待伺服器訊息
//                  receiveDS.close();
                    final byte[] data2 = data.clone();
                    runOnUiThread(new Runnable(){
                        @Override
                        public void run() {
                            content.setText(content.getText()+"\n伺服器說:"+new String(data2));
                        }
                    });
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }

        }
    }

    public void showToast(String info) {
        if (toast == null) {
            toast = Toast.makeText(this, "", Toast.LENGTH_SHORT);
        }
        toast.setText(info);
        toast.show();
    }
}

伺服器程式碼如下:

public class MainActivity extends AppCompatActivity {
    private Button send_message;
    private EditText edit;
    private TextView content;
    public static Toast toast = null;
    public InetAddress clientAdress; //記錄客戶端IP地址
    private DatagramSocket sendDS; //udp傳送socket
    private DatagramSocket receiveDS; //udp接收socket

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        edit = (EditText) findViewById(R.id.edit);
        send_message = (Button) findViewById(R.id.send_message);
        content = (TextView) findViewById(R.id.content);

        send_message.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View v) {
                UdpSend us = new UdpSend();
                us.start();
            }
        });
        //啟動UDP伺服器監聽執行緒
        new UdpReceive().start();
    }

    // 傳送訊息執行緒
    class UdpSend extends Thread {

        @Override
        public void run() {
            try {
                runOnUiThread(new Runnable(){
                    @Override
                    public void run() {
                        if (edit.getText().toString().trim().isEmpty()) {
                            showToast("請輸入訊息!");
                            return;
                        } else {
                            if (clientAdress == null) {
                                showToast("未獲取客戶端資訊!");
                                return;
                            }
                            content.setText(content.getText()+"\n我說:"+edit.getText().toString());
                            edit.setText("");
                        }
                    }
                });

                byte[] data = edit.getText().toString().getBytes();
                if (sendDS == null) {
                    sendDS = new DatagramSocket(null);
                    sendDS.setReuseAddress(true);
                    sendDS.bind(new InetSocketAddress(20000)); //傳送埠20000
                }
                DatagramPacket packet = new DatagramPacket(data, data.length, clientAdress, 25000);
                packet.setData(data);
                sendDS.send(packet);
//              sendDS.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }


    // 接收訊息執行緒
    class UdpReceive extends Thread {

        @Override
        public void run() {
            //訊息迴圈
            while(true) {
                try {
                    if(receiveDS == null){
                        receiveDS = new DatagramSocket(null);
                        receiveDS.setReuseAddress(true);
                        receiveDS.bind(new InetSocketAddress(25000)); //接收埠25000
                    }
                    byte[] data = new byte[1024 * 4];
                    DatagramPacket dp = new DatagramPacket(data, data.length);
                    dp.setData(data);
                    receiveDS.receive(dp); //阻塞式,等待客戶端訊息
//                  receiveDS.close();
                    clientAdress = dp.getAddress(); //獲取客戶端IP,後面就可以發訊息給客戶端了
                    final byte[] data2 = data.clone();
                    runOnUiThread(new Runnable(){
                        @Override
                        public void run() {
                            content.setText(content.getText()+"\n客戶端說:"+new String(data2));

                        }
                    });
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }


    public void showToast(String info) {
        if (toast == null) {
            toast = Toast.makeText(this, "", Toast.LENGTH_SHORT);
        }
        toast.setText(info);
        toast.show();
    }
}

可以看到,伺服器端程式碼和客戶端基本一樣,除了下面部分

clientAdress = dp.getAddress(); /**獲取客戶端IP,後面就可以發訊息給客戶端了*/
...
DatagramPacket packet = new DatagramPacket(data, data.length, clientAdress, 25000);

伺服器端IP是固定的,但客戶端卻不是,所以伺服器端需要利用客戶端發過來的資料報獲取客戶端的IP地址並記錄下來,之後就可以使用剛才獲取的客戶端IP地址clientAdress來向客戶端發訊息啦。
同時開啟客戶端和服務端應用,客戶端先發送一條資訊給客戶端,客戶端效果如下圖:
這裡寫圖片描述
服務端效果圖:
這裡寫圖片描述

ServerSocket 選項

ServerSocket 有以下 3 個選項.

  • SO_TIMEOUT: 表示等待客戶連線的超時時間.
  • SO_REUSEADDR: 表示是否允許重用伺服器所繫結的地址.
  • SO_RCVBUF: 表示接收資料的緩衝區的大小.

SO_TIMEOUT 選項

  • 設定該選項: public void setSoTimeout(int timeout) throws SocketException
  • 讀取該選項: public int getSoTimeout() throws SocketException

這個選項與Socket 的選項相同。SO_TIMEOUT,以毫秒為單位,將此選項設為非零的超時值時,在與此 Socket 關聯的 InputStream 上呼叫 read() 將只阻塞此時間長度。如果超過超時值,將引發 java.net.SocketTimeoutException,雖然 Socket 仍舊有效。選項必須在進入阻塞操作前被啟用才能生效。超時值必須是 大於 0 的數。超時值為 0 被解釋為無窮大超時值。

Socket設定讀寫超時:

Socket s = new Socket("172.0.0.1", 30000);
//讀取伺服器的程序一直阻塞,超過10s,丟擲SocketTimeoutException異常
s.setSoTimeout(10000);

上面方法說白了只是設定read方法的超時時間,這個方法是堵塞的!Socket其實還有一個超時時間,即連線超時,可以通過下面方法設定。

Socket設定連線超時:

Socket s = new Socket();
//該Socket超過10s還沒有連線到遠端伺服器,認為連線超時
s.connect(new InetAddress(host,port), 10000);

SO_REUSEADDR 選項

  • 設定該選項: public void setResuseAddress(boolean on) throws SocketException
  • 讀取該選項: public boolean getResuseAddress() throws SocketException

這個選項與Socket 的選項相同, 用於決定如果網路上仍然有資料向舊的 ServerSocket 傳輸資料, 是否允許新的 ServerSocket 繫結到與舊的 ServerSocket 同樣的埠上。SO_REUSEADDR 選項的預設值與作業系統有關, 在某些作業系統中, 允許重用埠, 而在某些作業系統中不允許重用埠。
當 ServerSocket 關閉時, 如果網路上還有傳送到這個 ServerSocket 的資料, 這個ServerSocket 不會立即釋放本地埠, 而是會等待一段時間, 確保接收到了網路上傳送過來的延遲資料, 然後再釋放埠。由於埠已經被佔用, 使得程式無法繫結到該埠, 程式執行失敗, 並丟擲 BindException:
java.net.BindException: bind failed: EADDRINUSE (Address already in use)

為了確保一個程序關閉了 ServerSocket 後, 即使作業系統還沒釋放埠, 同一個主機上的其他程序還可以立即重用該埠, 可以呼叫 ServerSocket 的 setResuseAddress(true) 方法,而且必須在 ServerSocket 還沒有繫結到一個本地埠之前呼叫, 否則執行serverSocket.setReuseAddress(true) 方法無效。 此外, 兩個共用同一個埠的程序必須都呼叫 serverSocket.setResuseAddress(true) 方法, 才能使得一個程序關閉 ServerSocket 後, 另一個程序的 ServerSocket 還能夠立刻重用相同的埠。

if(receiveDS == null){
     receiveDS = new DatagramSocket(null);
     receiveDS.setReuseAddress(true);
     receiveDS.bind(new InetSocketAddress(25000));
}

SO_RCVBUF 選項

  • 設定該選項: public void setReceiveBufferSize(int size) throws SocketException
  • 讀取該選項: public int getReceiveBufferSize() throws SocketException

SO_RCVBUF 表示伺服器端的用於接收資料的緩衝區的大小, 以位元組為單位。 一般說來, 傳輸大的連續的資料塊(基於HTTP 或 FTP 協議的資料傳輸) 可以使用較大的緩衝區, 這可以減少傳輸資料的次數, 從而提高傳輸資料的效率. 而對於互動頻繁且單次傳送數量比較小的通訊(Telnet 和 網路遊戲), 則應該採用小的緩衝區, 確保能及時把小批量的資料傳送給對方。
SO_RCVBUF 的預設值與作業系統有關. 例如, 在Windows 2000 中執行以下程式碼時, 顯示 SO_RCVBUF 的預設值為 8192。

無論在 ServerSocket繫結到特定埠之前或之後, 呼叫 setReceiveBufferSize() 方法都有效. 例外情況下是如果要設定大於 64 KB 的緩衝區, 則必須在 ServerSocket 繫結到特定埠之前進行設定才有效。
執行 serverSocket.setReceiveBufferSize() 方法, 相當於對所有由 serverSocket.accept() 方法返回的 Socket 設定接收資料的緩衝區的大小。