什麼是SocketSocket的概念很簡單,它是網路上運行的兩個程式間雙向通訊的一端,既可以接收請求,也可以發送請求,利用它可以較為方便地編寫網路上數據的傳遞。所以簡而言之,Socket就是進程通信的端點,Socket之間的連接過程可以分為幾步:1、伺服器監聽伺服器端Socket並不定位具體的客戶端...
什麼是Socket
Socket的概念很簡單,它是網路上運行的兩個程式間雙向通訊的一端,既可以接收請求,也可以發送請求,利用它可以較為方便地編寫網路上數據的傳遞。
所以簡而言之,Socket就是進程通信的端點,Socket之間的連接過程可以分為幾步:
1、伺服器監聽
伺服器端Socket並不定位具體的客戶端Socket,而是處於等待連接的狀態,實時監控網路狀態
2、客戶端請求
客戶端Socket發出連接請求,要連接的目標是服務端Socket。為此,客戶端Socket必須首先描述它要連接的服務端Socket,指出服務端Socket的地址和埠號,然後就向服務端Socket提出連接請求
3、連接確認
當服務端Socket監聽到或者說是接收到客戶端Socket的連接請求,它就響應客戶端Socket的請求,建立一個新的線程,把服務端Socket的描述發給客戶端,一旦客戶端確認了此描述,連接就好了。而服務端Socket繼續處於監聽狀態,繼續接收其他客戶端套接字的連接請求
TCP/IP、HTTP、Socket的區別
這三個概念是比較容易混淆的概念,這裡儘量解釋一下三者之間的區別。
隨著電腦網路體繫結構的發展,OSI七層網路模型誕生了,這個模型把開放系統的通信功能劃分為七個層次,一次完整的通信如下圖:
每一層都是相互獨立的,它利用其下一層提供的服務併為其上一層提供服務,而與其它層的具體實現無關,所謂"服務"就是下一層向上一層提供的通信功能和層之間的會話約定,一般用通信原語實現。上圖中,從下至上分別給層編號為1~7,其中1~4層為下層協議,5~7層為上層協議,接著回到我們的概念:
1、TCP/IP講的其實是兩個東西:TCP和IP。IP是一種網路層的協議,用於路由選擇、網路互連
2、TCP是一種傳輸層協議,用於建立、維護和拆除傳送連接,在系統之間提供可靠的透明的數據傳送
3、HTTP是一種應用層協議,提供OSI用戶服務,例如事物處理程式、文件傳送協議和網路管理等,其目的最終是為了實現應用進程之間的信息交換
至於Socket,它只是TCP/IP網路的API而已,Socket介面定義了許多函數,用以開發TCP/IP網路上的應用程式,組織數據,以符合指定的協議。
Socket的兩種模式
Socket有兩種主要的操作方式:面向連接和無連接的。面向連接的Socket操作就像一部電話,必須建立一個連接和一人呼叫,所有事情在達到時的順序與它們出發時的順序一樣,無連接的Socket操作就像是一個郵件投遞,沒有什麼保證,多個郵件可能在達到時的順序與出發時的順序不一樣。
到底使用哪種模式是由應用程式的需要決定的。如果可靠性更重要的話,用面向連接的操作會好一些,比如文件伺服器需要數據的正確性和有序性,如果一些數據丟失了,系統的有效性將會失去;比如一些伺服器間歇性地發送一些數據塊,如果數據丟失了的話,伺服器並不想要再重新發送一次,因為當數據到達的時候,它可能已經過時了。確保數據的有序性和正確性需要額外的操作的記憶體消耗,額外的消耗將會降低系統的回應速率。
無連接的操作使用數據報協議。一個數據報是一個獨立的單元,它包含了所有這次投遞的信息,就像一個信封,它有目的地址和要發送的內容,這個模式下的Socket並不需要連接一個目的Socket,它只是簡單地透出數據報,無連接的操作是快速、高效的,但是數據安全性不佳。
面向連接的操作使用TCP協議。一個這個模式下的Socket必須在發送數據之前與目的地的Socket取得一個連接,一旦連接建立了,Socket就可以使用一個流介面:打開-->讀-->寫-->關閉,所有發送的信息都會在另一端以同樣的順序被接收。面向連接的操作比無連接的操作效率更低,但是數據的安全性更高。
利用Java開發Socket
在Java中面向連接的類有兩種形式,它們分別是客戶端和伺服器端,先看一下伺服器端:
public class HelloServer { public static void main(String[] args) throws IOException { ServerSocket serverSocket = null; try { // 實例化一個伺服器端的Socket連接 serverSocket = new ServerSocket(9999); } catch (IOException e) { System.err.print("Could not listen on port:9999"); System.exit(1); } Socket clientSocket = null; try { // 用於接收來自客戶端的連接 clientSocket = serverSocket.accept(); } catch (IOException e) { System.err.println("Accept failed"); System.exit(1); } // 客戶端有數據了就向屏幕列印Hello World System.out.print("Hello World"); clientSocket.close(); serverSocket.close(); } }
此代碼的作用就是構造出服務端Socket,並等待來自客戶端的消息。當然,此時運行代碼是沒有任何反應的,因為服務端在等待客戶端的連接。下麵看一下客戶端代碼如何寫:
1 public class HelloClient 2 { 3 public static void main(String[] args) throws IOException 4 { 5 Socket socket = null; 6 BufferedReader br = null; 7 8 // 下麵這段程式,用於將輸入輸出流和Socket相關聯 9 try 10 { 11 socket = new Socket("localhost", 9999); 12 br = new BufferedReader(new InputStreamReader(socket.getInputStream())); 13 } 14 catch (UnknownHostException e) 15 { 16 System.err.println("Don't know about host:localhost"); 17 System.exit(1); 18 } 19 catch (IOException e) 20 { 21 System.err.println("Could not get I/O for the connection"); 22 System.exit(1); 23 } 24 25 System.out.print(br.readLine()); 26 br.close(); 27 socket.close(); 28 } 29 }
此時只需要先運行HelloServer,再運行HelloClient,保證伺服器先監聽,客戶端後發送,就可以在控制臺上看到"Hello World"了。
改進版本的Socket
上面的Socket演示的效果是,伺服器端Socket收到了來自客戶端Socket的數據,但是並沒有真正地體現伺服器端Socket和客戶端Socket的交互,下麵演示一下利用Socket進行伺服器端和客戶端的交互,首先是伺服器端的:
1 public class EchoServer 2 { 3 public static void main(String[] args) throws IOException 4 { 5 ServerSocket ss = null; 6 PrintWriter pw = null; 7 BufferedReader br = null; 8 9 try 10 { 11 // 實例化監聽埠 12 ss = new ServerSocket(1111); 13 } 14 catch (IOException e) 15 { 16 System.err.println("Could not listen on port:1111"); 17 System.exit(1); 18 } 19 Socket incoming = null; 20 while (true) 21 { 22 incoming = ss.accept(); 23 pw = new PrintWriter(incoming.getOutputStream(), true); 24 // 先將位元組流通過InputStreamReader轉換為字元流,之後將字元流放入緩衝之中 25 br = new BufferedReader(new InputStreamReader(incoming.getInputStream())); 26 // 提示信息 27 pw.println("Hello!..."); 28 pw.println("Enter BYE to exit"); 29 pw.flush(); 30 // 沒有異常則不斷迴圈 31 while (true) 32 { 33 // 只有當用戶輸入時才返回數據 34 String str = br.readLine(); 35 // 當用戶連接斷掉時會返回空值null 36 if (str == null) 37 { 38 // 退出迴圈 39 break; 40 } 41 else 42 { 43 // 對用戶輸入字元串加首碼Echo並將此信息列印到客戶端 44 pw.println("Echo:" + str); 45 pw.flush(); 46 // 退出命令,equalsIgnoreCase()是不區分大小寫的 47 if ("BYE".equalsIgnoreCase(str.trim())) 48 { 49 break; 50 } 51 } 52 } 53 // 該close的資源都close掉 54 pw.close(); 55 br.close(); 56 incoming.close(); 57 ss.close(); 58 } 59 } 60 }
接著是客戶端的:
1 public class EchoClient 2 { 3 public static void main(String[] args) throws IOException 4 { 5 Socket socket = null; 6 PrintWriter pw = null; 7 BufferedReader br = null; 8 9 try 10 { 11 socket = new Socket("localhost", 1111); 12 pw = new PrintWriter(socket.getOutputStream(), true); 13 br = new BufferedReader(new InputStreamReader(socket.getInputStream())); 14 } 15 catch (UnknownHostException e) 16 { 17 System.err.println("Don't know abount host:localhost"); 18 System.exit(1); 19 } 20 System.out.println(br.readLine()); 21 System.out.println(br.readLine()); 22 BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in)); 23 String userInput; 24 // 將客戶端Socket輸入流(即伺服器端Socket的輸出流)輸出到標準輸出上 25 while ((userInput = stdIn.readLine()) != null) 26 { 27 pw.println(userInput); 28 System.out.println(br.readLine()); 29 } 30 // 同樣的,將該關閉的資源給關閉掉 31 pw.close(); 32 br.close(); 33 socket.close(); 34 } 35 }
看一下運行結果:
這正是我們程式要達到的效果,客戶端不管輸入什麼,伺服器端都給輸入拼接上"Echo:"返還給客戶端並列印在屏幕上。
服務端多監聽
程式寫到上面,已經基本成型了,不過還有一個問題:現實情況中,一個伺服器端的Socket不可能只對應一個客戶端的Socket,必然一個伺服器端的Socket可以接收來自多個客戶端的Socket的請求。
解決上述問題的辦法就是多線程。大致代碼是這樣的:
public class HandleThread extends Thread { private Socket socket; public HandleThread(Socket socket) { this.socket = socket; } public void run() { // Socket處理代碼 } }
public static void main(String[] args) throws IOException { ServerSocket serverSocket = null; try { // 實例化一個伺服器端的Socket連接 serverSocket = new ServerSocket(9999); } catch (IOException e) { System.err.print("Could not listen on port:9999"); System.exit(1); } Socket clientSocket = null; try { while (true) { // 用於接收來自客戶端的連接 clientSocket = serverSocket.accept(); new HandleThread(clientSocket).start(); } } catch (IOException e) { System.err.println("Accept failed"); System.exit(1); } }
即,伺服器端啟動一個永遠運行的線程,監聽來自客戶端的Socket,一旦客戶端有Socket到來,即開啟一個新的線程將Socket交給線程處理。
值得一提的是,這種Socket IO的處理方式,是一種阻塞性的IO操作,也就是說一個客戶端的Socket必然對應一條伺服器端的Socket線程,且Socket未處理完無法釋放該線程,這並不是一種好的辦法。進一步改進,可以使用NIO,NIO採用了Reactor模式,利用事件驅動機制,可以使用幾條較少的線程就處理多客戶端的請求。