1、Socket簡單介紹java
Socket通訊做爲Java網絡通信的基礎內容,集中了異常、I/O流模式等衆多知識點。學習Socket通訊,既可以瞭解真正的網絡通信原理,也可以加強對I/O流模式的理解。編程
1)Socket通訊分類數組
(一)基於TCP的Socket通訊:使用流式套接字,提供可靠、面向鏈接的通訊流。服務器
(二)基於UDP的Socket通訊:使用數據報套接字,定義一種無鏈接服務,數據之間經過相互獨立的報文進行傳輸,是無序的,而且不保證可靠、無差錯。網絡
2)Socket概念理解多線程
金山詞霸中對Socket名詞解釋:插座、燈座、窩,引伸到計算機科學稱爲"套接字"。至於爲何要翻譯成"套接字",能夠參考:https://www.zhihu.com/question/21383903/answer/18347271z對Socket歷史較爲詳細考證。socket
Socket曾經被翻譯爲"軟插座",代表此處說的插座不是實際生活中的那種插座(硬插座),而是在計算機領域抽象出來的接口。若是在客戶端插座和服務器端插座之間連一條線(也就是數據交互的信道),那麼客戶端就可以與服務器端進行數據交互。學習
2、基於TCP的Socket通訊理論基礎編碼
基於TCP/IP協議的網絡編程,就是利用TCP/IP協議在客戶端和服務器端之間創建通訊連接來實現數據交換。 具體的編程實現步驟以下:spa
1)服務器端建立其提供服務的端口號,即服務器端中提供服務的應用程序接口名稱。
服務器端ServerSocket: ServerSocket serverSocket = new ServerSocket(int port, int backlog); ServerSocket做用是向操做系統註冊相應協議服務,申請端口並監聽這個端口是否有連接請求。其中port是端口號,backlog是服務器最多容許連接的客戶端數。註冊完成後,服務器分配此端口用於提供某一項進程服務。
2)服務器端(Server)和客戶端(Client)都建立各自的Socket對象。
服務器端Socket: Socket socket = serverSocket.accept(); 服務器端建立一個socket對象用於等待客戶端socket的連接(accept方法是建立一個阻塞隊列,只有客戶端socket申請連接到服務器後,服務器端socket才能收到消息) 。若是服務器端socket收到客戶端的連接請求,那麼通過"三次握手"過程,創建客戶端與服務器端的鏈接。若是鏈接不成功,則拋出異常(詳見模塊三)。
客戶端Socket: Socket socket = new Socket(String host, int port); 客戶端建立按一個socket對象用於連接具體服務器host的具體服務端口port,用於得到服務器進程的相應服務。
通過三次握手後,一個Socket通路就創建起來。此時,服務器端和客戶端就能夠開始通信了。
3)服務器端和客戶端打開連接到Socket通路的I/O流,按照必定協議進行數據通訊。
協議就是指發送與接受數據的編碼格式(計算機網絡中爲:語義、同步)。簡單說就是輸入和輸出的流必須匹配。
開啓網絡輸入流:網絡輸入流指的是從socket通道進入計算機內存的流。 socket.getInputStream(); 返回值InputStream 輸入字節流
開啓網絡輸出流:網絡輸出流指的是從計算機內存走出到socket通道的流。 socket.getOutputStream(); 返回值OutputStream 輸出字節流
爲了通信方便,每每將低級流包裝成高級流進行服務端與客戶端之間的交互。
4)通訊完畢,關閉網絡流
通常而言,服務器端的流失不用關閉的,固然在某些條件下(好比服務器須要維護)也是須要關閉的。而客戶端通常都須要關閉。
3、Socket異常類
網絡通信中會遇到不少種錯誤,好比通信中斷、服務器維護拒絕訪問等等。下面稍微總結一下Socket通信中常見的異常類。
1)java.net.SocketTimeoutException套接字超時異常。常見緣由:網絡通路中斷,連接超時;
2)java.net.UnknowHostException未知主機異常。常見緣由:客戶端綁定的服務器IP或主機名不存在;
3)java.net.BindException綁定異常。常見緣由:端口被佔用;
4)java.net.ConnectException鏈接異常。常見緣由:服務器未啓動,客戶端申請服務;服務器拒絕服務,即服務器正在維護;
4、Java創建Socket通信
1)服務器端與客戶端創建鏈接
1 package day05; 2 3 import java.io.IOException; 4 import java.net.ServerSocket; 5 import java.net.Socket; 6 7 /** 8 * 服務器端 9 * @author forget406 10 * 11 */ 12 public class Server { 13 14 private ServerSocket serverSocket; 15 16 /** 在操做系統中註冊8000端口服務,並監聽8000端口 */ 17 public Server() { 18 try { 19 /* public ServerSocket(int port, int backlog) 20 * port表示端口號,backlog表示最多支持鏈接數 */ 21 serverSocket = new ServerSocket(8000, 3); 22 } catch (IOException e) { 23 e.printStackTrace(); 24 } 25 } 26 27 /** 與客戶端交互 */ 28 public void start() { 29 try { 30 System.out.println("等待用戶連接..."); 31 /* 建立Socket對象: public Socket accept() 32 * 等待客戶端連接,直到客戶端連接到此端口 */ 33 Socket socket = serverSocket.accept(); 34 System.out.println("連接成功,能夠通信!"); 35 } catch (IOException e) { 36 // TODO Auto-generated catch block 37 e.printStackTrace(); 38 } 39 } 40 41 public static void main(String[] args) { 42 Server server = new Server(); 43 server.start(); 44 } 45 } 46 47 ============================================== 48 49 package day05; 50 51 import java.io.IOException; 52 import java.net.Socket; 53 import java.net.UnknownHostException; 54 55 /** 56 * 客戶端 57 * @author forget406 58 * 59 */ 60 public class Client { 61 62 private Socket socket; 63 64 /** 申請與服務器端口鏈接 */ 65 public Client() { 66 try { 67 /* 請求與服務器端口創建鏈接 68 * 並申請服務器8000端口的服務*/ 69 socket = new Socket("localhost", 8000); 70 } catch (UnknownHostException e) { 71 e.printStackTrace(); 72 } catch (IOException e) { 73 e.printStackTrace(); 74 } 75 } 76 77 /** 與服務器交互 */ 78 public void start() { 79 80 } 81 82 public static void main(String[] args) { 83 Client client = new Client(); 84 client.start(); 85 } 86 }
服務器端結果:
5、Java實現C/S模式Socket通信
1)客戶端向服務器端發送消息(單向通訊):服務器只能接受數據,客戶端只能發送數據。這是因爲socket綁定了從客戶端到服務器的一條通訊通路。
1 package day05; 2 3 import java.io.BufferedReader; 4 import java.io.IOException; 5 import java.io.InputStream; 6 import java.io.InputStreamReader; 7 import java.net.ServerSocket; 8 import java.net.Socket; 9 10 /** 11 * 服務器端 12 * @author forget406 13 * 14 */ 15 public class Server { 16 17 private ServerSocket serverSocket; 18 19 /** 在操做系統中註冊8000端口服務,並監聽8000端口 */ 20 public Server() { 21 try { 22 /* public ServerSocket(int port, int backlog) 23 * port表示端口號,backlog表示最多支持鏈接數 */ 24 serverSocket = new ServerSocket(8000, 3); 25 } catch (IOException e) { 26 e.printStackTrace(); 27 } 28 } 29 30 /** 與客戶端單向交互 */ 31 public void start() { 32 System.out.println("等待用戶連接..."); 33 try { 34 /* 建立Socket對象: public Socket accept() 35 * 等待客戶端連接,直到客戶端連接到此端口 */ 36 Socket socket = serverSocket.accept(); 37 System.out.println("用戶連接成功,開始通信!"); 38 39 /* 服務器開始與客戶端通信 */ 40 while(true) { 41 // 開啓服務器socket端口到服務器內存的網路輸入字節流 42 InputStream is 43 = socket.getInputStream(); 44 // 在服務器內存中將網絡字節流轉換成字符流 45 InputStreamReader isr 46 = new InputStreamReader( 47 is, "UTF-8" 48 ); 49 // 包裝成按行讀取字符流 50 BufferedReader br 51 = new BufferedReader(isr); 52 53 /* 中途網絡可能斷開 54 * 1)Windows的readLine會直接拋出異常 55 * 2)Linux的readLine則會返回null*/ 56 String msg = null; 57 if((msg = br.readLine()) != null) { 58 System.out.println("客戶端說:" + 59 msg 60 ); 61 } 62 63 } 64 65 } catch (IOException e) { 66 System.out.println("連接失敗"); 67 e.printStackTrace(); 68 } 69 } 70 71 public static void main(String[] args) { 72 Server server = new Server(); 73 server.start(); 74 } 75 } 76 77 =========================================== 78 79 package day05; 80 81 import java.io.IOException; 82 import java.io.OutputStream; 83 import java.io.OutputStreamWriter; 84 import java.io.PrintWriter; 85 import java.net.Socket; 86 import java.net.UnknownHostException; 87 import java.util.Scanner; 88 89 /** 90 * 客戶端 91 * @author forget406 92 * 93 */ 94 public class Client { 95 96 private Socket socket; 97 98 /** 申請與服務器端口鏈接 */ 99 public Client() { 100 try { 101 /* 請求與服務器端口創建鏈接 102 * 並申請服務器8000端口的服務*/ 103 socket = new Socket("localhost", 8000); 104 } catch (UnknownHostException e) { 105 e.printStackTrace(); 106 } catch (IOException e) { 107 e.printStackTrace(); 108 } 109 } 110 111 /** 與服務器單向交互 */ 112 public void start() { 113 try { 114 // 開啓客戶端內存到客戶端socket端口的網絡輸出流 115 OutputStream os 116 = socket.getOutputStream(); 117 // 將客戶端網絡輸出字節流包裝成網絡字符流 118 OutputStreamWriter osw 119 = new OutputStreamWriter(os, "UTF-8"); 120 // 將輸出字符流包裝成字符打印流 121 PrintWriter pw 122 = new PrintWriter(osw, true); 123 // 來自鍵盤的標準輸入字節流 124 Scanner sc = new Scanner(System.in); 125 while(true) { 126 // 打印來自鍵盤的字符串(字節數組) 127 pw.println(sc.nextLine()); 128 } 129 130 } catch (IOException e) { 131 e.printStackTrace(); 132 } 133 } 134 135 public static void main(String[] args) { 136 Client client = new Client(); 137 client.start(); 138 } 139 }
客戶端輸入:
服務器端結果:
2)客戶端與服務器端雙向通訊:客戶端與服務器交互,可以實現服務器對客戶端的應答,這更像是P2P模式。此時,雙方socket端口均綁定來回一對通訊通路。
1 package day05; 2 3 import java.io.BufferedReader; 4 import java.io.IOException; 5 import java.io.InputStreamReader; 6 import java.io.OutputStreamWriter; 7 import java.io.PrintWriter; 8 import java.net.ServerSocket; 9 import java.net.Socket; 10 import java.util.Scanner; 11 12 /** 13 * 服務器端 14 * @author forget406 15 * 16 */ 17 public class Server { 18 19 private ServerSocket serverSocket; 20 21 /** 在操做系統中註冊8000端口服務,並監聽8000端口 */ 22 public Server() { 23 try { 24 /* public ServerSocket(int port, int backlog) 25 * port表示端口號,backlog表示最多支持鏈接數 */ 26 serverSocket = new ServerSocket(8000, 3); 27 } catch (IOException e) { 28 e.printStackTrace(); 29 } 30 } 31 32 /** 與客戶端單向交互 */ 33 @SuppressWarnings("resource") 34 public void start() { 35 System.out.println("等待用戶連接..."); 36 try { 37 /* 建立Socket對象: public Socket accept() 38 * 等待客戶端連接,直到客戶端連接到此端口 */ 39 Socket socket = serverSocket.accept(); 40 System.out.println("用戶連接成功,開始通信!"); 41 42 /* 服務器接收客戶端數據 */ 43 InputStreamReader isr 44 = new InputStreamReader( 45 socket.getInputStream(), 46 "UTF-8" 47 ); 48 BufferedReader br 49 = new BufferedReader(isr); 50 String msgReceive = null; 51 String msgSend = null; 52 53 /* 服務器向客戶端發送數據 */ 54 OutputStreamWriter osw 55 = new OutputStreamWriter( 56 socket.getOutputStream(), 57 "UTF-8" 58 ); 59 PrintWriter pw 60 = new PrintWriter(osw, true); 61 Scanner sc = new Scanner(System.in); 62 63 while(true) { 64 if((msgReceive = br.readLine()) != null) { 65 System.out.println("客戶端說:" + msgReceive); 66 } 67 68 if((msgSend = sc.nextLine()) != null) { 69 pw.println(msgSend); 70 } 71 } 72 73 } catch (IOException e) { 74 System.out.println("連接失敗"); 75 e.printStackTrace(); 76 } 77 } 78 79 public static void main(String[] args) { 80 Server server = new Server(); 81 server.start(); 82 } 83 } 84 85 ============================================ 86 87 package day05; 88 89 import java.io.BufferedReader; 90 import java.io.IOException; 91 import java.io.InputStreamReader; 92 import java.io.OutputStreamWriter; 93 import java.io.PrintWriter; 94 import java.net.Socket; 95 import java.net.UnknownHostException; 96 import java.util.Scanner; 97 98 /** 99 * 客戶端 100 * @author forget406 101 * 102 */ 103 public class Client { 104 105 private Socket socket; 106 107 /** 申請與服務器端口鏈接 */ 108 public Client() { 109 try { 110 /* 請求與服務器端口創建鏈接 111 * 並申請服務器8000端口的服務*/ 112 socket = new Socket("localhost", 8000); 113 } catch (UnknownHostException e) { 114 e.printStackTrace(); 115 } catch (IOException e) { 116 e.printStackTrace(); 117 } 118 } 119 120 /** 與服務器單向交互 */ 121 @SuppressWarnings("resource") 122 public void start() { 123 try { 124 /* 客戶端向服務器發送數據 */ 125 OutputStreamWriter osw 126 = new OutputStreamWriter( 127 socket.getOutputStream(), 128 "UTF-8" 129 ); 130 PrintWriter pw 131 = new PrintWriter(osw, true); 132 Scanner sc = new Scanner(System.in); 133 134 /* 客戶端接收服務器數據 */ 135 InputStreamReader isr 136 = new InputStreamReader( 137 socket.getInputStream(), 138 "UTF-8" 139 ); 140 BufferedReader br 141 = new BufferedReader(isr); 142 String msgReceive = null; 143 String msgSend = null; 144 145 while(true) { 146 if((msgSend = sc.nextLine()) != null) { 147 pw.println(msgSend); 148 } 149 if((msgReceive = br.readLine()) != null) { 150 System.out.println("服務器說:" + msgReceive); 151 } 152 } 153 154 } catch (IOException e) { 155 System.out.println("連接失敗!"); 156 e.printStackTrace(); 157 } 158 } 159 160 161 public static void main(String[] args) { 162 Client client = new Client(); 163 client.start(); 164 165 } 166 }
PS: 只是初步實現,有些bug沒有改進。相似QQ的完善版本代碼會在後續的文章中更新。
6、心得體會
上述代碼實現的是C/S模型的簡化版本,即P2P模式---客戶端與服務器端一對一進行交互通訊。事實上,服務器能夠並行與多臺客戶機進行數據收發與交互,這須要運用到Java多線程的知識,這將會在後續文章中分析。
I/O流模式的選取原則:
1. 選擇合適的節點流。在Socket網絡編程中,節點流分別是socket.getInputStream和socket.getOutputStream,均爲字節流。
1.1)選擇合適方向的流。輸入流socket.getInputStream、InputStreamReader、BufferedReader;輸出流socket.getOutputStream、OutputStreamWriter、PrintWriter。
1.2)選擇字節流和字符流。網絡通訊在實際通訊線路中傳遞的是比特流(字節流);而字符流只會出如今計算機內存中。
2. 選擇合適的包裝流。在選擇I/O流時,節點流是必須的,而包裝流則是可選的;節點流類型只能存在一種,而包裝流則能存在多種(注意區分:是一種或一對,而不是一個)。
2.1)選擇符合功能要求的流。若是須要讀寫格式化數據,選擇DataInputStream/DataOutputStream;而BufferedReader/BufferedWriter則提供緩衝區功能,可以提升格式化讀寫的效率。
2.2)選擇合適方向的包裝流。基本與節點流一致。當選擇了多個包裝流後,可使用流之間的多層嵌套功能,不過流的嵌套在物理實現上是組合關係,所以彼此之間沒有順序。