Socket是Java網絡編程的基礎,瞭解仍是有好處的,html
這篇文章主要講解Socket的基礎編程。Socket用在哪呢,主要用在進程間,網絡間通訊。本篇比較長,特別作了個目錄:java
2、消息通訊優化算法
3、服務端優化編程
這種模式是基礎,必須掌握,後期對Socket的優化都是在這個基礎上的,也是爲之後學習NIO作鋪墊。安全
package yiwangzhibujian.onlysend; import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket; public class SocketServer { public static void main(String[] args) throws Exception { // 監聽指定的端口 int port = 55533; ServerSocket server = new ServerSocket(port); // server將一直等待鏈接的到來 System.out.println("server將一直等待鏈接的到來"); Socket socket = server.accept(); // 創建好鏈接後,從socket中獲取輸入流,並創建緩衝區進行讀取 InputStream inputStream = socket.getInputStream(); byte[] bytes = new byte[1024]; int len; StringBuilder sb = new StringBuilder(); while ((len = inputStream.read(bytes)) != -1) { //注意指定編碼格式,發送方和接收方必定要統一,建議使用UTF-8 sb.append(new String(bytes, 0, len,"UTF-8")); } System.out.println("get message from client: " + sb); inputStream.close(); socket.close(); server.close(); } }
服務端監聽一個端口,等待鏈接的到來。服務器
package yiwangzhibujian.onlysend; import java.io.OutputStream; import java.net.Socket; public class SocketClient { public static void main(String args[]) throws Exception { // 要鏈接的服務端IP地址和端口 String host = "127.0.0.1"; int port = 55533; // 與服務端創建鏈接 Socket socket = new Socket(host, port); // 創建鏈接後得到輸出流 OutputStream outputStream = socket.getOutputStream(); String message="你好 yiwangzhibujian"; socket.getOutputStream().write(message.getBytes("UTF-8")); outputStream.close(); socket.close(); } }
客戶端經過ip和端口,鏈接到指定的server,而後經過Socket得到輸出流,並向其輸出內容,服務器會得到消息。最終服務端控制檯打印以下:網絡
server將一直等待鏈接的到來
get message from client: 你好 yiwangzhibujian
經過這個例子應該掌握並瞭解:
這個例子作爲學習的基本例子,實際開發中會有各類變形,好比客戶端在發送完消息後,須要服務端進行處理並返回,以下。
這個也是作爲Socket編程的基本,應該掌握,例子以下:
package yiwangzhibujian.waitreceive; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; public class SocketServer { public static void main(String[] args) throws Exception { // 監聽指定的端口 int port = 55533; ServerSocket server = new ServerSocket(port); // server將一直等待鏈接的到來 System.out.println("server將一直等待鏈接的到來"); Socket socket = server.accept(); // 創建好鏈接後,從socket中獲取輸入流,並創建緩衝區進行讀取 InputStream inputStream = socket.getInputStream(); byte[] bytes = new byte[1024]; int len; StringBuilder sb = new StringBuilder(); //只有當客戶端關閉它的輸出流的時候,服務端才能取得結尾的-1 while ((len = inputStream.read(bytes)) != -1) { // 注意指定編碼格式,發送方和接收方必定要統一,建議使用UTF-8 sb.append(new String(bytes, 0, len, "UTF-8")); } System.out.println("get message from client: " + sb); OutputStream outputStream = socket.getOutputStream(); outputStream.write("Hello Client,I get the message.".getBytes("UTF-8")); inputStream.close(); outputStream.close(); socket.close(); server.close(); } }
與以前server的不一樣在於,當讀取完客戶端的消息後,打開輸出流,將指定消息發送回客戶端,客戶端程序爲:
package yiwangzhibujian.waitreceive; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; public class SocketClient { public static void main(String args[]) throws Exception { // 要鏈接的服務端IP地址和端口 String host = "127.0.0.1"; int port = 55533; // 與服務端創建鏈接 Socket socket = new Socket(host, port); // 創建鏈接後得到輸出流 OutputStream outputStream = socket.getOutputStream(); String message = "你好 yiwangzhibujian"; socket.getOutputStream().write(message.getBytes("UTF-8")); //經過shutdownOutput高速服務器已經發送完數據,後續只能接受數據 socket.shutdownOutput(); InputStream inputStream = socket.getInputStream(); byte[] bytes = new byte[1024]; int len; StringBuilder sb = new StringBuilder(); while ((len = inputStream.read(bytes)) != -1) { //注意指定編碼格式,發送方和接收方必定要統一,建議使用UTF-8 sb.append(new String(bytes, 0, len,"UTF-8")); } System.out.println("get message from server: " + sb); inputStream.close(); outputStream.close(); socket.close(); } }
客戶端也有相應的變化,在發送完消息時,調用關閉輸出流方法,而後打開輸出流,等候服務端的消息。
這個模式的使用場景通常用在,客戶端發送命令給服務器,而後服務器相應指定的命令,若是隻是客戶端發送消息給服務器,而後讓服務器返回收到消息的消息,這就有點過度了,這就是徹底不相信Socket的傳輸安全性,要知道它的底層但是TCP,若是沒有發送到服務器端是會拋異常的,這點徹底不用擔憂。
其實這個問題仍是比較重要的,正常來講,客戶端打開一個輸出流,若是不作約定,也不關閉它,那麼服務端永遠不知道客戶端是否發送完消息,那麼服務端會一直等待下去,直到讀取超時。因此怎麼告知服務端已經發送完消息就顯得特別重要。
這個是第一章介紹的方式,當Socket關閉的時候,服務端就會收到響應的關閉信號,那麼服務端也就知道流已經關閉了,這個時候讀取操做完成,就能夠繼續後續工做。
可是這種方式有一些缺點
這種方式調用的方法是:
socket.shutdownOutput();
而不是(outputStream爲發送消息到服務端打開的輸出流):
outputStream.close();
若是關閉了輸出流,那麼相應的Socket也將關閉,和直接關閉Socket一個性質。
調用Socket的shutdownOutput()方法,底層會告知服務端我這邊已經寫完了,那麼服務端收到消息後,就能知道已經讀取完消息,若是服務端有要返回給客戶的消息那麼就能夠經過服務端的輸出流發送給客戶端,若是沒有,直接關閉Socket。
這種方式經過關閉客戶端的輸出流,告知服務端已經寫完了,雖然能夠讀到服務端發送的消息,可是仍是有一點點缺點:
這個缺點,在訪問頻率比較高的狀況下將是一個須要優化的地方。
這種方式的用法,就是雙方約定一個字符或者一個短語,來當作消息發送完成的標識,一般這麼作就須要改造讀取方法。
假如約定單端的一行爲end,表明發送完成,例以下面的消息,end則表明消息發送完成:
hello yiwangzhibujian
end
那麼服務端響應的讀取操做須要進行以下改造:
Socket socket = server.accept(); // 創建好鏈接後,從socket中獲取輸入流,並創建緩衝區進行讀取 BufferedReader read=new BufferedReader(new InputStreamReader(socket.getInputStream(),"UTF-8")); String line; StringBuilder sb = new StringBuilder(); while ((line = read.readLine()) != null && "end".equals(line)) { //注意指定編碼格式,發送方和接收方必定要統一,建議使用UTF-8 sb.append(line); }
能夠看見,服務端不只判斷是否讀到了流的末尾,還判斷了是否讀到了約定的末尾。
這麼作的優缺點以下:
通過了這麼多的優化仍是有缺點,難道就沒有完美的解決方案嗎,答案是有的,看接下來的內容。
若是你瞭解一點class文件的結構(後續會寫,敬請期待),那麼你就會佩服這麼設計方式,也就是說咱們能夠在此找靈感,就是咱們能夠先指定後續命令的長度,而後讀取指定長度的內容作爲客戶端發送的消息。
如今首要的問題就是用幾個字節指定長度呢,咱們能夠算一算:
這個時候是否是很糾結,最大的固然是最保險的,可是真的有必要選擇最大的嗎,其實若是你稍微瞭解一點UTF-8的編碼方式(字符編碼後續會寫,敬請期待),那麼你就應該能想到爲何必定要固定表示長度字節的長度呢,咱們可使用變長方式來表示長度的表示,好比:
上面提到的這種用法適合高富帥的程序員使用,通常呢,若是用做命名發送,兩個字節就夠了,若是還不放心4個字節基本就能知足你的全部要求,下面的例子咱們將採用2個字節表示長度,目的只是給你一種思路,讓你知道有這種方式來獲取消息的結尾:
服務端程序:
package yiwangzhibujian.waitreceive2; import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket; public class SocketServer { public static void main(String[] args) throws Exception { // 監聽指定的端口 int port = 55533; ServerSocket server = new ServerSocket(port); // server將一直等待鏈接的到來 System.out.println("server將一直等待鏈接的到來"); Socket socket = server.accept(); // 創建好鏈接後,從socket中獲取輸入流,並創建緩衝區進行讀取 InputStream inputStream = socket.getInputStream(); byte[] bytes; // 由於能夠複用Socket且能判斷長度,因此能夠一個Socket用到底 while (true) { // 首先讀取兩個字節表示的長度 int first = inputStream.read(); //若是讀取的值爲-1 說明到了流的末尾,Socket已經被關閉了,此時將不能再去讀取 if(first==-1){ break; } int second = inputStream.read(); int length = (first << 8) + second; // 而後構造一個指定長的byte數組 bytes = new byte[length]; // 而後讀取指定長度的消息便可 inputStream.read(bytes); System.out.println("get message from client: " + new String(bytes, "UTF-8")); } inputStream.close(); socket.close(); server.close(); } }
此處的讀取步驟爲,先讀取兩個字節的長度,而後讀取消息,客戶端爲:
package yiwangzhibujian.waitreceive2; import java.io.OutputStream; import java.net.Socket; public class SocketClient { public static void main(String args[]) throws Exception { // 要鏈接的服務端IP地址和端口 String host = "127.0.0.1"; int port = 55533; // 與服務端創建鏈接 Socket socket = new Socket(host, port); // 創建鏈接後得到輸出流 OutputStream outputStream = socket.getOutputStream(); String message = "你好 yiwangzhibujian"; //首先須要計算得知消息的長度 byte[] sendBytes = message.getBytes("UTF-8"); //而後將消息的長度優先發送出去 outputStream.write(sendBytes.length >>8); outputStream.write(sendBytes.length); //而後將消息再次發送出去 outputStream.write(sendBytes); outputStream.flush(); //==========此處重複發送一次,實際項目中爲多個命名,此處只爲展現用法 message = "第二條消息"; sendBytes = message.getBytes("UTF-8"); outputStream.write(sendBytes.length >>8); outputStream.write(sendBytes.length); outputStream.write(sendBytes); outputStream.flush(); //==========此處重複發送一次,實際項目中爲多個命名,此處只爲展現用法 message = "the third message!"; sendBytes = message.getBytes("UTF-8"); outputStream.write(sendBytes.length >>8); outputStream.write(sendBytes.length); outputStream.write(sendBytes); outputStream.close(); socket.close(); } }
客戶端要多作的是,在發送消息以前先把消息的長度發送過去。
這種事先約定好長度的作法解決了以前提到的種種問題,Redis的Java客戶端Jedis就是用這種方式實現的這種方式的缺點:
固然若是是須要服務器返回結果,那麼也依然使用這種方式,服務端也是先發送結果的長度,而後客戶端進行讀取。固然如今流行的就是,長度+類型+數據模式的傳輸方式。
在上面的例子中,服務端僅僅只是接受了一個Socket請求,並處理了它,而後就結束了,可是在實際開發中,一個Socket服務每每須要服務大量的Socket請求,那麼就不能再服務完一個Socket的時候就關閉了,這時候能夠採用循環接受請求並處理的邏輯:
package yiwangzhibujian.multiserver; import java.io.IOException; import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket; public class SocketServer { public static void main(String args[]) throws IOException { // 監聽指定的端口 int port = 55533; ServerSocket server = new ServerSocket(port); // server將一直等待鏈接的到來 System.out.println("server將一直等待鏈接的到來"); while(true){ Socket socket = server.accept(); // 創建好鏈接後,從socket中獲取輸入流,並創建緩衝區進行讀取 InputStream inputStream = socket.getInputStream(); byte[] bytes = new byte[1024]; int len; StringBuilder sb = new StringBuilder(); while ((len = inputStream.read(bytes)) != -1) { // 注意指定編碼格式,發送方和接收方必定要統一,建議使用UTF-8 sb.append(new String(bytes, 0, len, "UTF-8")); } System.out.println("get message from client: " + sb); inputStream.close(); socket.close(); } } }
這種通常也是新手寫法,可是可以循環處理多個Socket請求,不過當一個請求的處理比較耗時的時候,後面的請求將被阻塞,因此通常都是用多線程的方式來處理Socket,即每有一個Socket請求的時候,就建立一個線程來處理它。
不過在實際生產中,建立的線程會交給線程池來處理,爲了:
package yiwangzhibujian.threadserver; import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class SocketServer { public static void main(String args[]) throws Exception { // 監聽指定的端口 int port = 55533; ServerSocket server = new ServerSocket(port); // server將一直等待鏈接的到來 System.out.println("server將一直等待鏈接的到來"); //若是使用多線程,那就須要線程池,防止併發太高時建立過多線程耗盡資源 ExecutorService threadPool = Executors.newFixedThreadPool(100); while (true) { Socket socket = server.accept(); Runnable runnable=()->{ try { // 創建好鏈接後,從socket中獲取輸入流,並創建緩衝區進行讀取 InputStream inputStream = socket.getInputStream(); byte[] bytes = new byte[1024]; int len; StringBuilder sb = new StringBuilder(); while ((len = inputStream.read(bytes)) != -1) { // 注意指定編碼格式,發送方和接收方必定要統一,建議使用UTF-8 sb.append(new String(bytes, 0, len, "UTF-8")); } System.out.println("get message from client: " + sb); inputStream.close(); socket.close(); } catch (Exception e) { e.printStackTrace(); } }; threadPool.submit(runnable); } } }
使用線程池的方式,算是一種成熟的方式。能夠應用在生產中。
ServerSocket有如下3個屬性。
具體詳細的解釋能夠參照下面。
當如今的性能還不能知足需求的時候,就須要考慮使用NIO,這不是本篇的內容,後續會貼出。
其實若是常常看有關網絡編程的源碼的話,就會發現Socket仍是有不少設置的,能夠學着用,可是仍是要有一些基本的瞭解比較好。下面就對Socket的Java API中涉及到的進行簡單講解。首先呢Socket有哪些能夠設置的選項,其實在SocketOptions接口中已經都列出來了:
上面只是簡單介紹了下(來源Java API),下面有對其中的某些的詳細講解,沒講到的後續若是用到會補上。
服務端綁定端口是能夠理解的,由於要監聽指定的端口,可是客戶端爲何要綁定端口,說實話我以爲這麼作的人有點2,或許有的網絡安全策略配置了端口訪出,使用戶只能使用指定的端口,那麼這樣的配置也是挺2的,直接說就能夠不要留面子。
固然首先要理解的是,若是沒有指定端口的話,Socket會自動選取一個能夠用的端口,不用瞎操心的。
可是你非得指定一個端口也是能夠的,作法以下,這時候就不能用Socket的構造方法了,要一步一步來:
// 要鏈接的服務端IP地址和端口 String host = "localhost"; int port = 55533; // 與服務端創建鏈接 Socket socket = new Socket(); socket.bind(new InetSocketAddress(55534)); socket.connect(new InetSocketAddress(host, port));
這樣作就能夠了,可是當這個程序執行完成之後,再次執行就會報,端口占用異常:
java.net.BindException: Address already in use: connect
明明上一個Socket已經關閉了,爲何再次使用還會說已經被佔用了呢?若是你是用netstat 命令來查看端口的使用狀況:
netstat -n|findstr "55533"
TCP 127.0.0.1:55534 127.0.0.1:55533 TIME_WAIT
就會發現端口的使用狀態爲TIME_WAIT,說到這你須要有一點TCP鏈接的基本常識,建議看《》,這是其中的一點摘抄筆記,或許對理解有一些幫助。
簡單來講,當鏈接主動關閉後,端口狀態變爲TIME_WAIT,其餘程序依然不能使用這個端口,防止服務端由於超時從新發送的確認鏈接斷開對新鏈接的程序形成影響。
TIME_WAIT的時間通常有底層決定,通常是2分鐘,還有1分鐘和30秒的。
因此,客戶端不要綁定端口,不要綁定端口,不要綁定端口。
讀超時這個屬性仍是比較重要的,當Socket優化到最後的時候,每每一個Socket鏈接會一直用下去,那麼當一端由於異常致使鏈接沒有關閉,另外一方是不該該持續等下去的,因此應該設置一個讀取的超時時間,當超過指定的時間後,尚未讀到數據,就假定這個鏈接無用,而後拋異常,捕獲異常後關閉鏈接就能夠了,調用方法爲:
public void setSoTimeout(int timeout) throws SocketException
timeout - 指定的以毫秒爲單位的超時值。設置0爲持續等待下去。建議根據網絡環境和實際生產環境選擇。
這個選項設置的值將對如下操做有影響:
這個鏈接超時和上面說的讀超時不同,讀超時是在創建鏈接之後,讀數據時使用的,而鏈接超時是在進行鏈接的時候,等待的時間。
當須要判斷一個Socket是否可用的時候,不能簡簡單單判斷是否爲null,是否關閉,下面給出一個比較全面的判斷Socket是否可用的表達式,這是根據Socket自身的一些狀態進行判斷的,它的狀態有:
socket != null && socket.isBound() && !socket.isClosed() && socket.isConnected()&& !socket.isInputShutdown() && !socket.isOutputShutdown()
建議如此使用,但這只是第一步,保證Socket自身的狀態是可用的,可是當鏈接正常建立後,上面的屬性若是不調用本方相應的方法是不會改變的,也就是說若是網絡斷開、服務器主動斷開,Java底層是不會檢測到鏈接斷開並改變Socket的狀態,因此,真實的檢測鏈接狀態仍是得經過額外的手段,有兩種方式。
雙方須要約定,什麼樣的消息屬於心跳包,什麼樣的消息屬於正常消息,假設你看了上面的章節如今說就容易理解了,咱們定義前兩個字節爲消息的長度,那麼咱們就能夠定義第3個字節爲消息的屬性,能夠指定一位爲消息的類型,1爲心跳,0爲正常消息。那麼要作的有以下:
具體的編碼再也不貼出,本身實現便可。
Socket自帶一種模式,那就是發送緊急數據,這有一個前提,那就是服務端的OOBINLINE不能設置爲true,它的默認值是false。
OOBINLINE的true和false影響了什麼:
發送緊急數據經過調用Socket的方法:
socket.sendUrgentData(0);
發送數據任意便可,由於OOBINLINE爲false的時候,服務端會丟棄掉緊急數據。
當發送緊急數據報錯之後,咱們就會知道鏈接不通了。
經過上面的兩種方式已經能夠判斷出鏈接是否可用,而後咱們就能夠進行後續操做,但是請你們認真考慮下面的問題:
若是你認真考慮了上面的問題,那麼你就會以爲發送心跳包徹底是沒有必要的操做,經過發送心跳包來判斷鏈接是否可用是經過捕獲異常來判斷的。那麼咱們徹底能夠在發送消息報出IO異常的時候,在異常中從新發送一次便可,這兩種方式的編碼有什麼不一樣呢,下面寫一寫僞代碼。
提早檢測鏈接是否可用:
//有一個鏈接中的socket Socket socket=... //要發送的數據 String data=""; try{ //發送心跳包或者緊急數據,來檢測鏈接的可用性 }catch (Excetption e){ //打印日誌,並重連Socket socket=new Socket(host,port); } socket.write(data);
直接發送數據,出異常後從新鏈接再次發送:
//有一個鏈接中的socket Socket socket=... //要發送的數據 String data=""; try{ socket.write(data); }catch (Excetption e){ //打印日誌,並重連Socket socket=new Socket(host,port); socket.write(data); }
經過比較能夠發現兩種方式的特色,如今簡單介紹下:
但願你們認真考慮,作出本身的選擇。
首先,建立Socket時,默認是禁止的,設置true有什麼做用呢,Java API中是這麼介紹的:
關閉 TCP 鏈接時,該鏈接可能在關閉後的一段時間內保持超時狀態(一般稱爲 TIME_WAIT 狀態或 2MSL 等待狀態)。對於使用已知套接字地址或端口的應用程序而言,若是存在處於超時狀態的鏈接(包括地址和端口),可能不能將套接字綁定到所需的 SocketAddress 上。
使用 bind(SocketAddress) 綁定套接字前啓用 SO_REUSEADDR 容許在上一個鏈接處於超時狀態時綁定套接字。
通常是用在綁定端口的時候使用,可是通過個人測試建議以下:
綜上所述,不建議綁定端口,也不必設置ReuseAddress,固然ReuseAddress的底層仍是和硬件有關係的,或許在你的機器上測試結果和我不同,如果如此和平臺相關性差別這麼大配置更是不建議使用了。
Java API的介紹是:啓用/禁用具備指定逗留時間(以秒爲單位)的 SO_LINGER。最大超時值是特定於平臺的。 該設置僅影響套接字關閉。
你們都是這麼說的,當調用Socket的close方法後,沒有發送的數據將再也不發送,設置這個值的話,Socket會等待指定的時間發送完數據包。說實話,通過我簡單的測試,對於通常數據量來講,幾十K左右,即使直接關閉Socket的鏈接,服務端也是能夠收到數據的。
因此對於通常應用不必設置這個值,當數據量發送過大拋出異常時,再來設置這個值也不晚。那麼到達逗留超時值時,套接字將經過 TCP RST 強制性 關閉。啓用超時值爲零的選項將當即強制關閉。若是指定的超時值大於 65,535,則其將被減小到 65,535。
通常來講當客戶端想服務器發送數據的時候,會根據當前數據量來決定是否發送,若是數據量太小,那麼系統將會根據Nagle 算法(暫時還沒研究),來決定發送包的合併,也就是說發送會有延遲,這在有時候是致命的,好比說對實時性要求很高的消息發送,在線對戰遊戲等,即使數據量很小也要求當即發送,若是稍有延遲就會感受到卡頓,默認狀況下Nagle 算法是開啓的,因此若是不打算有延遲,最好關閉它。這樣一旦有數據將會當即發送而不會寫入緩衝區。
可是對延遲要求不是特別高下仍是可使用的,仍是能夠提高網絡傳輸效率的。
默認都是8K,若是有須要能夠修改,經過相應的set方法。不建議修改的過小,設置過小數據傳輸將過於頻繁。太大了將會形成消息停留。
不過我對這個通過測試後有如下結論:
雖說當設置鏈接鏈接的讀超時爲0,即無限等待時,Socket不會被主動關閉,可是總會有莫名其妙的軟件來檢測你的鏈接是否有數據發送,長時間沒有數據傳輸的鏈接會被它們關閉掉。
所以經過設置這個選項爲true,能夠有以下效果:當2個小時(具體的實現而不一樣)內在任意方向上都沒有跨越套接字交換數據,則 TCP 會自動發送一個保持存活的消息到對面。將會有如下三種響應:
因此對於構建長時間鏈接的Socket仍是配置上SO_KEEPALIVE比較好。
這個異常的含義是,我正在寫數據的時候,你把鏈接給關閉了。這個異常在通常正常的編碼是不會出現這個異常的,由於用戶一般會判斷是否讀到流的末尾了,讀到末尾纔會進行關閉操做,若是出現這個異常,那就檢查一下判斷是否讀到流的末尾邏輯是否正確。
最近在看《TCP/IP詳解 卷1:協議》,關於TCP/IP我以爲講解的很是詳細,我作了點摘抄,能夠大體看看,很是建議你們閱讀下這本書。一般TCP/IP分爲四層:
也就是說Socket其實是歸屬於應用層,使用的事運輸層的TCP,使用SocketServer監聽的端口,也是能夠被Telnet鏈接的。能夠看下面兩行代碼:
ServerSocket server = new ServerSocket(port); Socket socket = server.accept();
在什麼狀況獲取到這個Socket呢,經過理論加測試,結論是在三次握手操做後,系統纔會將這個鏈接交給應用層,ServerSocket 才知道有一個鏈接過來了。那麼系統當接收到一個TCP鏈接請求後,若是上層尚未接受它(假如SocketServer循環處理Socket,一次一個),那麼系統將緩存這個鏈接請求,既然是緩存那麼就是有限度的,書上介紹的是緩存3個,可是通過個人本機測試是50個,也就是說,系統將會爲應用層的Socket緩存50和TCP鏈接(這是和系統底層有關係的),當超過指定數量後,系統將會拒絕鏈接。
假如緩存的TCP鏈接請求發送來數據,那麼系統也會緩存這些數據,等待SocketServer得到這個鏈接的時候一併交給它,這個會在後期學習NIO進行詳解。
換句話說,系統接收TCP鏈接請求放入緩存隊列,而SocketServer從緩存隊列獲取Socket。
而上面例子中的爲了讓服務端知道發送完消息的,關閉輸出流的操做:
socket.shutdownOutput();
實際上是對應着四次揮手的第一次:
也就是上面說的主動關閉,FIN_WAIT_1,這樣服務端就能得知客戶端發送完消息,此時服務端能夠選擇關閉鏈接,也能夠選擇發送數據後關閉鏈接:
這就是TCP所說的半關閉。其實不少知識都是想通的,多學點基礎知識仍是有必要的。
RMI基礎知識就很少介紹了(後續會寫,敬請期待),如今假定你對RMI有所瞭解,那麼通常就會對這兩種技術有所比較。或者說在應用的時候就會想用那種技術比較好。
RMI全稱:Remote Method Invocation-遠程方法調用,經過名字其實就能對這種技術有個初步的瞭解。如今我就簡單說說我對這兩種技術的想法。
這個待寫,等我寫完RMI博客的時候補上,那時候會更細緻的瞭解下。
這一段涉及到UDP,依然和上面同樣,後續會補上。
使用Socket通訊的時候,或多或少都聽過拆包和黏包,若是沒聽過而去貿然編程那麼偶爾就會碰到一些莫名其妙的問題,全部有這方面的知識仍是比較重要的,至少知道怎麼發生,怎麼防範。
如今先簡單說明下拆包和黏包的緣由:
首先能夠明確的是,大部分狀況下咱們是不但願發生拆包和黏包的(若是但願發生,什麼都去作便可),那麼怎麼去避免呢,下面進行詳解?
首先咱們應該正確看待黏包,黏包其實是對網絡通訊的一種優化,假如說上層只發送一個字節數據,而底層卻發送了41個字節,其中20字節的I P首部、 20字節的T C P首部和1個字節的數據,並且發送完後還須要確認,這麼作浪費了帶寬,量大時還會形成網絡擁堵。固然它仍是有必定的缺點的,就是由於它會合並一些包會致使數據不能當即發送出去,會形成延遲,若是能接受(通常延遲爲200ms),那麼仍是不建議關閉這種優化,若是由於黏包會形成業務上的錯誤,那麼請改正你的服務端讀取算法(協議),由於即使不發生黏包,在服務端緩存區也可能會合並起來一塊兒提交給上層,推薦使用長度+類型+數據模式。
若是不但願發生黏包,那麼經過禁用TCP_NODELAY便可,Socket中也有相應的方法:
void setTcpNoDelay(boolean on)
經過設置爲true便可防止在發送的時候黏包,可是當發送的速率大於讀取的速率時,在服務端也會發生黏包,即因服務端讀取過慢,致使它一次可能讀取多個包。
這個問題應該引發重視,在TCP/IP詳解中說過:最大報文段長度(MSS)表示TCP傳往另外一端的最大塊數據的長度。當一個鏈接創建時,鏈接的雙方都要通告各自的 MSS。客戶端會盡可能知足服務端的要求且不能大於服務端的MSS值,當沒有協商時,會使用值536字節。雖然看起來MSS值越大越好,可是考慮到一些其餘狀況,這個值仍是不太好肯定,具體詳見《TCP/IP詳解 卷1:協議》。
如何應對拆包,其實在上面2.3節已經介紹過了,那就是如何代表發送完一條消息了,對於已知數據長度的模式,能夠構造相同大小的數組,循環讀取,示例代碼以下:
int length=1024;//這個是讀取的到數據長度,現假定1024 byte[] data=new byte[1024]; int readLength=0; while(readLength<length){ int read = inputStream.read(data, readLength, length-readLength); readLength+=read; }
這樣當循環結束後,就能讀取到完整的一條數據,而不須要考慮拆包了。
單單關於Java的Socket編程已經基本介紹完成了,固然還有更深層次的知識沒有涉及到,後續若是能有接觸也會寫出來,但願個人文章能幫助到有須要的人,若是有什麼不對的地方請指出,禁止轉載。