使用Socket
用Socket寫入伺服器 構造和連接配接Socket Socket位址 代理伺服器 擷取Socket的資訊 關閉還是連接配接 Socket選項 Socket異常
使用Socket
socket是兩台主機之間的一個連接配接。它可以完成7個基本操作:
連接配接遠端機器;發送資料;接收資料;關閉連接配接;綁定端口;監聽入站資料;在綁定端口上接收來自遠端機器的連接配接。
前4個步驟對應的4個操作方法應用于用戶端(Socket),後面三個操作僅伺服器需要(ServerSocket)
一旦建立了連接配接,本地和遠端主機就從這個socket得到輸入流和輸出流,使用者兩個流互相發送資料。連接配接是全雙工的,兩台主機都可以同時發送和接收資料。資料的含義取決與協定,發送給FTP伺服器的指令與發送給HTTP伺服器的指令有所不同。一般先完成某種協定握手,然後再具體傳輸資料。
當資料傳輸結束後,一端或兩端将關閉連接配接。有些協定,如HTTP1.0要求每次請求得到伺服器後都要關閉連接配接。而FTP或者HTTP1.1則允許在一個連接配接上處理多個請求。
public static void main(String[] args) throws Exception {
try (Socket socket = new Socket("127.0.0.1", 8888)) {
} catch (Exception e) {
System.out.println(e);
}
}// 用戶端
使用setSoTimeout(int m)方法為連接配接設定一個逾時時間,逾時時間的機關是毫秒。
一旦打開Socket并設定其逾時時間後,可以調用getInputStream()傳回一個InputStream,用它從socket中讀取子節。一般來講,伺服器可以發送任意子節。确認讀取完畢後調用shutdownInput()方法關閉輸入流。
public static void main(String[] args) throws Exception {
try (Socket socket = new Socket("127.0.0.1", 8888)) {
socket.setSoTimeout(1000);
InputStream in = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(in));
String line = null;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
socket.shutdownInput();
} catch (Exception e) { System.out.println(e); } }// 用戶端
用Socket寫入伺服器
getOutputStream();傳回一個原始的OutputStream,可以用它從你的應用向Socket的另一端寫資料。确認寫入完畢後調用shutdownOutput();
@RequestMapping(value = "test")
public String g(HttpServletRequest request) throws IOException {
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String key = headerNames.nextElement();
Enumeration<String> headers = request.getHeaders(key);
while (headers.hasMoreElements()) {
String value = headers.nextElement();
System.out.println(key + " " + value);
}
}
return "success1";
}// 服務端
public static void main(String[] args) throws Exception {
try (Socket socket = new Socket("127.0.0.1", 8888)) {
OutputStream out = socket.getOutputStream();
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(out));
bw.append("GET /test HTTP/1.1\r\n");
bw.append("Host: 127.0.0.1:8888\r\n");
bw.append("\r\n");
bw.flush();
socket.shutdownOutput();
InputStream in = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(in));
String line = null;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
socket.shutdownInput();
} catch (Exception e) {
System.out.println(e);
}
}// 用戶端
構造和連接配接Socket
java.net.Socket類是Java完成用戶端TCP操作的基礎類,其他建立TCP網絡連接配接的面向用戶端的類如URL,RULConnection最終都會調用這個類的方法,這個類本身使用原生代碼與主機作業系統的本地TCP棧進行通信。
基本的構造函數
以下兩個構造函數會在構造後立刻與主機建立連接配接。如果出于某種原因未能打開連接配接,構造函數會抛出一個IOException或UnknownHostException異常。
public static void main(String[] args) throws Exception {
Socket socket1 = new Socket("127.0.0.1",8888);
Socket socket2 = new Socket(InetAddress.getByName("127.0.0.1"),8888);
}// 用戶端
也許你需要構造後設定一些參數而不是直接連接配接,可以使用如下方式。
public static void main(String[] args) throws Exception {
Socket socket = new Socket();
SocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
socket.connect(address, 1000);
a(socket);
}// 用戶端
public static void a(Socket socket) {
try {
OutputStream out = socket.getOutputStream();
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(out));
bw.append("GET /test HTTP/1.1\r\n");
bw.append("Host: 127.0.0.1:8888\r\n");
bw.append("\r\n");
bw.flush();
socket.shutdownOutput();
InputStream in = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(in));
String line = null;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
socket.shutdownInput();
} catch (Exception e) {
System.out.println(e);
} finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Socket位址
通常的我們将用戶端或服務端的連接配接位址封裝到SocketAddress類中,達到複用連接配接參數的目的。SocketAddress有唯一的一個子類InetSocketAddress。Socket類的getRemoteSocketAddress()可以擷取服務端的Address。getLocalSocketAddress()可以擷取用戶端的Address。
public static void main(String[] args) throws Exception {
Socket socket = new Socket();
SocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
socket.connect(address);
InetSocketAddress socketAddress = (InetSocketAddress) socket.getRemoteSocketAddress();
System.out.println(socketAddress.getHostName() + ":" + socketAddress.getPort());
InetSocketAddress localSocketAddress = (InetSocketAddress) socket.getLocalSocketAddress();
System.out.println(localSocketAddress.getHostName() + ":" + localSocketAddress.getPort());
a(socket);
}// 用戶端
代理伺服器
要使用某個特定的代理伺服器可以使用以下寫法。
public static void main(String[] args) throws Exception {
SocketAddress proxyAddress = new InetSocketAddress("127.0.0.1",8888);//代理伺服器
Proxy proxy = new Proxy(Proxy.Type.SOCKS,proxyAddress);
Socket socket = new Socket(proxy);
SocketAddress address = new InetSocketAddress("127.0.0.1", 8888);//要連接配接的伺服器
socket.connect(address);
a(socket);
}// 用戶端
擷取Socket的資訊
socket對象有一些屬性可以通過擷取方法來通路。一旦Socket連接配接後這些屬性将不可改變。遠端端口通常是一個已知服務端的端口。而本地端口則是系統運作時從未使用的空閑端口中選擇,通過這種方式多個不同的用戶端就可以同時通路相同的服務。本地端口與本地主機的IP位址一同鑲嵌入在出站IP包中,是以伺服器可以向用戶端上正确的端口傳回資料。
getInetAddress();擷取遠端主機。getPort();擷取遠端端口。
getLocalAddress()擷取本地主機。getLocalPort();擷取本地端口。
關閉還是連接配接
要判斷一個socket是否已經打開關鍵的兩個方法是isConnected();它訓示socket是否連接配接過。isClosed();它表示連接配接是否關閉。
public static void main(String[] args) throws Exception {
Socket socket = new Socket();
SocketAddress address = new InetSocketAddress("127.0.0.1", 8888);//要連接配接的伺服器
System.out.println(socket.isConnected());//true表示已經連接配接過
System.out.println(socket.isClosed());//true表示未關閉
System.out.println(socket.isBound());//true表示已綁定本地端口
socket.connect(address);
System.out.println(socket.isConnected());//true表示已經連接配接過
System.out.println(socket.isClosed());//true表示未關閉
System.out.println(socket.isBound());//true表示已綁定本地端口
a(socket);
System.out.println(socket.isConnected());//true表示已經連接配接過
System.out.println(socket.isClosed());//true表示未關閉
System.out.println(socket.isBound());//true表示已綁定本地端口
}// 用戶端
Socket選項
TCP_NODELAY:設定TCP_NODELAY為true可以確定包會盡可能快的發送,而無論包的大小。正常請求下,小資料包(一位元組)在發送前會組合為更大的包。在發送另一個包之前,本地主機要等待遠端系統對前一個包的确認。這成為Nagle算法。Nagle算法的問題是,如果遠端系統沒有足夠快地将确認發回本地系統,那麼依賴于小資料量資訊穩定傳輸地應用程式會變得很慢。對于GUI程式這個問題尤其嚴重。在一個相當慢地網絡中,即使簡單地打字也會由于持續地緩沖而變得太慢。設定TCP_NODELAY為true可以打破這種緩沖模式,這樣所有包一旦就緒就會立刻發送。
SO_LINGER:SO_LINGER指定了Socket關閉時如何處理未發送地資料封包。預設情況下close()方法将立刻傳回,但系統仍然會嘗試發送剩餘地資料。如果延遲時間設為0,那麼當Socket關閉時,所有未發送地資料包都将被丢棄。如果SO_LINGER打開而且延遲實際設定為任意正數,close()方法會阻塞,阻塞實際為指定的秒數,等待發送資料和接收确認。當過去相當秒數後,Socket關閉,所有剩餘的資料都不會發送,也不會收到确認。如果底層Socket實作不支援SO_LINGER選項,這兩個方法都會抛出SocketException異常。如果試圖将延遲時間設定為一個負數,會抛出一個IllegalArgumentException異常。如果查找該屬性傳回-1則表示這個選項未使用,最大的延遲時間為65535秒,有些平台可能要更短一些。
SO_TIMEOUT:正常情況下,嘗試從Socket讀取資料時,read()調用會阻塞盡可能長的時間來得到足夠的子節。設定SO_TIMEOUT可以確定這次調用阻塞的時間不會超過某個固定的毫秒數。當這個時間到期時就會抛出一個InterruptedIOException異常,你應當準備好捕獲這個異常。不過Socket仍然是連接配接的。雖然這個read()調用失敗了,但可以嘗試讀取該Socket。下一次調用可能會成功。逾時時間按毫秒數給出。0為無限逾時,這是預設值。當底層Socket實作不支援SO_TIMEOUT選項時,這兩個方法都抛出SocketException異常。如果指定為負數,則抛出IllegalArgumentException異常。
SO_RCVBUF和SO_SNDBUF:SO_RCVBUF用于控制網絡輸入的緩沖區大小,SO_SNDBUF用于控制網絡輸出的緩沖區大小。雖然分為輸入和輸出兩個設定,但實際上緩沖區通常會設定兩者之間較小的一個。如果你有一個25Mb/s的Internet連接配接,但是資料傳輸的速率僅為1.5Mb/s那麼可以嘗試增加緩沖區大小,相反如果存在丢包和擁塞現象,則要減少緩沖區的大小。
SO_KEEPALIVE:如果打開了SO_KEEPALIVE用戶端偶爾會通過一個空閑連接配接發送一個資料包,兩小時一次。以確定伺服器未崩潰。如果伺服器沒能相應這個包,用戶端會持續嘗試11分鐘的時間直到接收到響應為止。如果12分鐘内未接收到響應,用戶端就會關閉socket。如果沒有SO_KEEPALIVE,不活動的用戶端可能會永久存在下去,而不會注意到伺服器已經崩潰。
SO_REUSEADDR:一個Socket關閉時,可能不會立即釋放本地端口,尤其是當Socket關閉時若仍有一個打開的連接配接,就不會釋放本地端口。有時會等待一段時間,確定接收到所有要發送到這個端口的延遲資料包.這樣造成的問題時,目前Socket已經關閉但端口在較短時間内不能被别的應用使用。開啟SO_REUSEADDR允許另一個Socket綁定到這個端口,即使此時仍有可能存在前一個Socket未接收的資料。要設定SO_REUSEADDR需要在綁定端口之前使用,這意味着必須要使用無參構造的Socket然後使用connect()方法打開連接配接。
public static void main(String[] args) throws Exception {
Socket socket = new Socket("127.0.0.1", 8888);
//TCP_NODELAY
socket.setTcpNoDelay(true);
socket.getTcpNoDelay();
//SO_LINGER
socket.setSoLinger(true, 10);
socket.getSoLinger();
//SO_TIMEOUT
socket.setSoTimeout(0);
socket.getSoTimeout();
//SO_KEEPALIVE
socket.setKeepAlive(true);
socket.getKeepAlive();
//SO_REUSEADDR
socket.setReuseAddress(true);
socket.getReuseAddress();
//SO_RCVBUF
socket.setReceiveBufferSize(100);
socket.getReceiveBufferSize();
//SO_SNDBUF
socket.setSendBufferSize(100);
socket.getSendBufferSize();
a(socket);
}// 用戶端
Socket異常
如果試圖在一個正在使用的端口上構造Socket或ServerSocket對象或者你沒有權限使用這個端口則會抛出BindException異常。如果連接配接被遠端主機拒絕,而拒絕的原因通常是由于主機忙或者沒有程序在監聽這個端口則會抛出ConnectException異常。如果連接配接已經逾時則抛出NoRouteToHostException異常。當從網絡接收的資料違反TCP/IP規範時,會抛出ProtocolException異常。