Java Web 基礎(一) 基於TCP的Socket網絡編程


一、Socket簡單介紹

  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曾經被翻譯為"軟插座",表明此處說的插座不是實際生活中的那種插座(硬插座),而是在計算機領域抽象出來的接口。如果在客戶端插座和服務器端插座之間連一條線(也就是數據交互的信道),那么客戶端就能夠與服務器端進行數據交互。

二、基於TCP的Socket通信理論基礎

  基於TCP/IP協議的網絡編程,就是利用TCP/IP協議在客戶端和服務器端之間建立通信鏈接實現數據交換。 具體的編程實現步驟如下:

  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)通信完畢,關閉網絡流

     一般而言,服務器端的流失不用關閉的,當然在某些條件下(比如服務器需要維護)也是需要關閉的。而客戶端一般都需要關閉。

 

三、Socket異常類

  網絡通訊中會遇到很多種錯誤,比如通訊中斷、服務器維護拒絕訪問等等。下面稍微總結一下Socket通訊中常見的異常類。

  1)java.net.SocketTimeoutException套接字超時異常。常見原因:網絡通路中斷,鏈接超時;

  2)java.net.UnknowHostException未知主機異常。常見原因:客戶端綁定的服務器IP或主機名不存在;

  3)java.net.BindException綁定異常。常見原因:端口被占用;

  4)java.net.ConnectException連接異常。常見原因:服務器未啟動,客戶端申請服務;服務器拒絕服務,即服務器正在維護;

 

四、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 }

服務器端結果:

 

五、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的完善版本代碼會在后續的文章中更新。

 

六、心得體會

  上述代碼實現的是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)選擇合適方向的包裝流。基本與節點流一致。當選擇了多個包裝流后,可以使用流之間的多層嵌套功能,不過流的嵌套在物理實現上是組合關系,因此彼此之間沒有順序

 

 注明:文章系作者原創,轉載請注明出處 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM