概述
什么是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);
1
2
3
上面方法說白了只是設置read方法的超時時間,這個方法是堵塞的!Socket其實還有一個超時時間,即連接超時,可以通過下面方法設置。
Socket設置連接超時:
Socket s = new Socket();
//該Socket超過10s還沒有連接到遠程服務器,認為連接超時
s.connect(new InetAddress(host,port), 10000);
1
2
3
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 設置接收數據的緩沖區的大小。
---------------------
作者:huaxun66
來源:CSDN
原文:https://blog.csdn.net/huaxun66/article/details/53008542
版權聲明:本文為博主原創文章,轉載請附上博文鏈接!