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程式設計是基於ServerSocket和Socket來實現的。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 設定接收資料的緩衝區的大小。