神奇好望角 The Magical Cape of Good Hope

          庸人不必自擾,智者何需千慮?
          posts - 26, comments - 50, trackbacks - 0, articles - 11
            BlogJava :: 首頁 ::  :: 聯系 :: 聚合  :: 管理

          JDK 提供了對 TCP(Transmission Control Protocol,傳輸控制協議)和 UDP(User Datagram Protocol,用戶數據報協議)這兩個數據傳輸協議的支持。本文開始探討 TCP。

          TCP 基礎知識

          在“服務器-客戶端”這種架構中,服務器和客戶端各自維護一個端點,兩個端點需要通過網絡進行數據交換。TCP 為這種需求提供了一種可靠的流式連接,流式的意思是傳出和收到的數據都是連續的字節,沒有對數據量進行大小限制。一個端點由 IP 地址和端口構成(專業術語為“元組 {IP 地址, 端口}”)。這樣,一個連接就可以由元組 {本地地址, 本地端口, 遠程地址, 遠程端口} 來表示。

          連接過程

          在 TCP 編程接口中,端點體現為 TCP 套接字。共有兩種 TCP 套接字:主動和被動,“被動”狀態也常被稱為“偵聽”狀態。服務器和客戶端利用套接字進行連接的過程如下:

          1. 服務器創建一個被動套接字,開始循環偵聽客戶端的連接。
          2. 客戶端創建一個主動套接字,連接服務器。
          3. 服務器接受客戶端的連接,并創建一個代表該連接的主動套接字。
          4. 服務器和客戶端通過步驟 2 和 3 中創建的兩個主動套接字進行數據傳輸。

          下面是連接過程的圖解:

          TCP 連接
          TCP 連接

          一個簡單的 TCP 服務器

          JDK 提供了 ServerSocket 類來代表 TCP 服務器的被動套接字。下面的代碼演示了一個簡單的 TCP 服務器(多線程阻塞模式),它不斷偵聽并接受客戶端的連接,然后將客戶端發送過來的文本按行讀取,全文轉換為大寫后返回給客戶端,直到客戶端發送文本行 bye

          public class TcpServer implements Runnable {
              private ServerSocket serverSocket;
          
              public TcpServer(int port) throws IOException {
                  // 創建綁定到某個端口的 TCP 服務器被動套接字。
                  serverSocket = new ServerSocket(port);
              }
          
              @Override
              public void run() {
                  while (true) {
                      try {
                          // 以阻塞的方式接受一個客戶端連接,返回代表該連接的主動套接字。
                          Socket socket = serverSocket.accept();
                          // 在新線程中處理客戶端連接。
                          new Thread(new ClientHandler(socket)).start();
                      } catch (IOException ex) {
                          ex.printStackTrace();
                      }
                  }
              }
          }
          
          public class ClientHandler implements Runnable {
              private Socket socket;
          
              public ClientHandler(Socket socket) {
                  this.socket = Objects.requireNonNull(socket);
              }
          
              @Override
              public void run() {
                  try (Socket s = socket) {  // 減少代碼量的花招……
                      // 包裝套接字的輸入流以讀取客戶端發送的文本行。
                      BufferedReader in = new BufferedReader(new InputStreamReader(
                              s.getInputStream(), StandardCharsets.UTF_8));
                      // 包裝套接字的輸出流以向客戶端發送轉換結果。
                      PrintWriter out = new PrintWriter(new OutputStreamWriter(
                              s.getOutputStream(), StandardCharsets.UTF_8), true);
          
                      String line = null;
                      while ((line = in.readLine()) != null) {
                          if (line.equals("bye")) {
                              break;
                          }
          
                          // 將轉換結果輸出給客戶端。
                          out.println(line.toUpperCase(Locale.ENGLISH));
                      }
                  } catch (IOException ex) {
                      ex.printStackTrace();
                  }
              }
          }
              

          阻塞模式的編程方式簡單,但存在性能問題,因為服務器線程會卡死在接受客戶端的 accept() 方法上,不能有效利用資源。套接字支持非阻塞模式,現在暫時略過。

          一個簡單的 TCP 客戶端

          JDK 提供了 Socket 類來代表 TCP 客戶端的主動套接字。下面的代碼演示了上述服務器的客戶端:

          public class TcpClient implements Runnable {
              private Socket socket;
          
              public TcpClient(String host, int port) throws IOException {
                  // 創建連接到服務器的套接字。
                  socket = new Socket(host, port);
              }
          
              @Override
              public void run() {
                  try (Socket s = socket) {  // 再次減少代碼量……
                      // 包裝套接字的輸出流以向服務器發送文本行。
                      PrintWriter out = new PrintWriter(new OutputStreamWriter(
                              s.getOutputStream(), StandardCharsets.UTF_8), true);
                      // 包裝套接字的輸入流以讀取服務器返回的文本行。
                      BufferedReader in = new BufferedReader(new InputStreamReader(
                              s.getInputStream(), StandardCharsets.UTF_8));
          
                      Console console = System.console();
                      String line = null;
                      while ((line = console.readLine()) != null) {
                          if (line.equals("bye")) {
                              break;
                          }
          
                          // 將文本行發送給服務器。
                          out.println(line);
                          // 打印服務器返回的文本行。
                          console.writer().println(in.readLine());
                      }
          
                      // 通知服務器關閉連接。
                      out.println("bye");
                  } catch (IOException ex) {
                      ex.printStackTrace();
                  }
              }
          }
              

          從 JDK 文檔可以看到,ServerSocketSocket 在初始化的時候,可以設定一些參數,還支持延遲綁定。這些東西對性能和行為都有所影響。下一篇文章將詳解這兩個類的初始化。


          評論

          # re: Java 網絡編程從菜鳥到叫獸 2:TCP 和套接字入門  回復  更多評論   

          2012-01-05 17:13 by 何楊
          標題起得不錯。

          # re: Java 網絡編程從菜鳥到叫獸 2:TCP 和套接字入門[未登錄]  回復  更多評論   

          2012-01-05 21:06 by song
          還不夠菜 嘿嘿

          # re: Java 網絡編程從菜鳥到叫獸 2:TCP 和套接字入門  回復  更多評論   

          2012-01-10 14:35 by #
          上面對節省代碼地方,一直未搞明白!

          # re: Java 網絡編程從菜鳥到叫獸 2:TCP 和套接字入門  回復  更多評論   

          2012-01-10 15:38 by 蜀山兆孨龘
          @#
          建議你先看看 Java SE 7 的新功能“帶資源的 try”,就容易明白了。直接寫 try (socket) 是語法錯誤,因為 try 后面的括號里必須是賦值表達式,所以才引入臨時變量 s 暗度陳倉。

          # re: Java 網絡編程從菜鳥到叫獸 2:TCP 和套接字入門  回復  更多評論   

          2012-01-10 16:09 by 開啟
          學習
          主站蜘蛛池模板: 读书| 黄平县| 湖北省| 遵义市| 武邑县| 利川市| 偃师市| 鄂尔多斯市| 庆云县| 本溪市| 扎鲁特旗| 辛集市| 昭平县| 威海市| 承德县| 布尔津县| 湘乡市| 敦化市| 保山市| 武平县| 邛崃市| 常州市| 始兴县| 饶河县| 平舆县| 金堂县| 河东区| 仪征市| 福鼎市| 含山县| 丰都县| 宜宾县| 澄江县| 达孜县| 社旗县| 营口市| 乡宁县| 那曲县| 邹平县| 历史| 麻城市|