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

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

相關文章
相關標籤/搜索