什么是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 { private static final int PORT = 9999; public static void main(String[] args) throws IOException { ServerSocket serverSocket = null; //實例化一個服務器端的socket連接 try { serverSocket = new ServerSocket(PORT); } catch (IOException e) { System.err.println("Could not listen on port:9999"); System.exit(1); e.printStackTrace(); } Socket clientSocket = null; try { //用於接收來自客戶端的連接 clientSocket = serverSocket.accept(); } catch (IOException e) { System.err.println("accept failed"); System.exit(1); e.printStackTrace(); } //客戶端有數據了就向屏幕打印Hello World System.out.println("hello world"); clientSocket.close(); serverSocket.close(); } }
此代碼的作用就是構造出服務端Socket,並等待來自客戶端的消息。當然,此時運行代碼是沒有任何反應的,因為服務端在等待客戶端的連接。下面看一下客戶端代碼如何寫:

public class HelloClient { private static final int PORT = 9999; public static void main(String[] args) throws IOException{ Socket socket = null; BufferedReader br = null; //下面這段程序,用於將輸入輸出流和Socket相關聯 try { socket = new Socket("localhost", PORT); br = new BufferedReader(new InputStreamReader(socket.getInputStream())); } catch (UnknownHostException e) { System.err.println("Don't know about host:localhost"); System.exit(1); e.printStackTrace(); } catch (IOException e) { System.err.println("Could not get I/O for the connection"); System.exit(1); } System.out.println(br.readLine()); br.close(); socket.close(); } }
此時只需要先運行HelloServer,再運行HelloClient,保證服務器先監聽,客戶端后發送,就可以在控制台上看到"Hello World"了。
改進版本的Socket
上面的Socket演示的效果是,服務器端Socket收到了來自客戶端Socket的數據,但是並沒有真正地體現服務器端Socket和客戶端Socket的交互,下面演示一下利用Socket進行服務器端和客戶端的交互,首先是服務器端的:

public class EchoServer { private static final int PORT = 9998; public static void main(String[] args) throws IOException { ServerSocket serverSocket = null; PrintWriter pw = null; BufferedReader br = null; try { //實例化監聽端口 serverSocket = new ServerSocket(PORT); } catch (IOException e) { System.err.println("Could not listen on port:9998"); System.exit(1); e.printStackTrace(); } Socket incoming = null; while(true){ incoming = serverSocket.accept(); pw = new PrintWriter(incoming.getOutputStream(), true); //先將字節流通過InputStreamReader轉換為字符流,之后將字符流放入緩沖之中 br = new BufferedReader(new InputStreamReader(incoming.getInputStream())); //提示信息 pw.println("hello!..."); pw.println("Enter BYE to exit!"); pw.flush(); //沒有異常則不斷循環 while(true){ //只有當用戶輸入時才返回數據 String str = br.readLine(); //當用戶連接斷掉時會返回空值null if(null == str){ //退出循環 break; }else{ //對用戶輸入字符串加前綴Echo並將此信息打印到客戶端 pw.println("Echo: " + str); pw.flush(); //退出命令,equalsIgnoreCase()是不區分大小寫的 if("BYE".equalsIgnoreCase(str.trim())){ break; } } } pw.close(); br.close(); incoming.close(); serverSocket.close(); } } }
接着是客戶端的:

public class EchoClient { private static final int PORT = 9998; private static final String HOST = "localhost"; public static void main(String[] args) throws IOException { Socket socket = null; PrintWriter pw = null; BufferedReader br = null; try { socket = new Socket(HOST, PORT); pw = new PrintWriter(socket.getOutputStream(), true); br = new BufferedReader(new InputStreamReader(socket.getInputStream())); } catch (UnknownHostException e) { System.err.println("Don't know abount host:localhost"); System.exit(1); e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } System.out.println(br.readLine()); BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in)); String userInput = null; //將客戶端Socket輸入流(即服務器端Socket的輸出流)輸出到標准輸出上 while((userInput = stdIn.readLine()) != null){ pw.println(userInput); System.out.println(br.readLine()); } pw.close(); br.close(); socket.close(); } }
看一下運行結果:
這正是我們程序要達到的效果,客戶端不管輸入什么,服務器端都給輸入拼接上"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交給線程處理。
由服務端多監聽程序看IO模型
上面的代碼,用一張圖來表示一下這種IO模型:
即由一個獨立的Acceptor線程負責監聽客戶端的連接,它接收到客戶端連接之后為每個客戶端創建一個新的線程進行鏈路處理,處理完成之后,通過輸出流返回應答給客戶端,線程銷毀。這就是典型的一請求一應答通信模型,也就是Blocking IO模型即BIO。
該模型最大的問題就是缺乏彈性伸縮能力,當客戶端並發訪問量增大后,服務端的線程個數和客戶端並發訪問數呈1:1的正比關系,由於線程是Java虛擬機非常寶貴的系統資源,當線程數膨脹之后,系統的性能將極具下降,隨着並發訪問量的繼續增大,系統將會發生線程堆棧溢出、創建新線程失敗等問題,並最終導致進程宕機或者僵死,不能對外提供服務。
在高性能服務器應用領域,往往要面向成千上萬個客戶端的並發連接,這種模型顯然無法滿足高性能、高並發接入的場景。
當然具體問題具體分析,BIO性能雖然差,但是編程簡單,如果客戶端並發連接數不多,周邊對接的網元不多,服務器的負載也不重,那么完全可以使用BIO進行作為服務器的IO模型。