亚洲在线久爱草,狠狠天天香蕉网,天天搞日日干久草,伊人亚洲日本欧美

Java TCP Socket 數據收發

1. 前言

TCP 是面向字節流的傳輸協議。所謂字節流是指 TCP 并不理解它所傳輸的數據的含義,在它眼里一切都是字節,1 字節是 8 比特。比如,TCP 客戶端向服務器發送“Hello Server,I’m client。How are you?”,TCP 客戶端發送的是具有一定含義的數據,但是對于 TCP 協議棧來說,傳輸的是一串字節流,具體如何解釋這段數據需要 TCP 服務器的應用程序來完成,這就涉及到“應用層協議設計”的問題。

在 TCP/IP 協議棧的四層協議模型中,操作系統內核協議棧實現了鏈路層、網絡層、傳輸層,將應用層留給了應用程序來實現。在編程實踐中,通常有文本協議二進制協議兩種類型,前者通常通過一個分隔符區分消息語義,而后者通常是需要通過一個 length 字段指定消息體的大小。比如著名的 HTTP 協議就是文本協議,通過 “\r\n” 來區分 HTTP Header 的每一行。而 RTMP 協議是一個二進制協議,通過 length 字段來指定消息體的大小。

解析 TCP 字節流的語義通常叫做消息解析,如果按照傳統 C 語言函數的方式來實現,還是比較麻煩的,有很多細節需要處理。好在 Java 為我們提供了很多工具類,給我們的工作帶來了極大地便利。

2. Java 字節流結構

Java 的 java.io.* 包中包含了 InputStream 和 OutputStream 兩個類,是 Java 字節流 I/O 的基礎類,其他具體的 Java I/O 字節流功能類都派生自這兩個類。

圖片描述

圖中只列出了我們 Socket 編程中常用的 I/O 字節流類。java.net.SocketInputStream 類是 Socket 的輸入流實現類,它繼承了 java.io.FileInputStream 類。java.net.SocketOutputStream 類是 Socket 的輸出流實現類,它繼承了 java.io.FileOutputStream 類,下來我們逐一介紹這些類的基本功能。

2.1 Java InputStream & OutputStream

java.io.InputStream 類是一個抽象超類,它提供最小的編程接口和輸入流的部分實現。java.io.InputStream 類定義的幾類方法:

  • 讀取字節或字節數組,一組 read 方法。
  • 標記流中的位置,mark 方法。
  • 跳過輸入字節,skip 方法。
  • 找出可讀取的字節數,available 方法。
  • 重置流中的當前位置,reset 方法。
  • 關閉流,close 方法。

InputStream 流在創建實例時會自動打開,你可以調用 close 方法顯式關閉流,也可以選擇在垃圾回收 InputStream 時,隱式關閉流。需要注意的是垃圾回收機制關閉流,并不能立刻生效,可能會造成流對象泄漏,所以一般需要主動關閉。

java.io.OutputStream 類同樣是一個抽象超類,它提供最小的編程接口和輸出流的部分實現。java.io.OutputStream 定義的幾類方法:

  • 寫入字節或字節數組,一組 write 方法。
  • 刷新流,flush 方法。
  • 關閉流,close 方法。

OutputStream 流在創建時會自動打開,你可以調用 close 方法顯式關閉流,也可以選擇在垃圾回收 OutputStream 時,隱式關閉流。

2.2 FileInputStream & FileOutputStream

java.io.FileInputStream 和 java.io.FileOutputStream 是文件輸入和輸出流類,用于從本機文件系統上的文件讀取數據或向其寫入數據。你可以通過文件名、java.io.File 對象、java.io.FileDescriptor 對象創建一個 FileInputStream 或 FileOutputStream 流對象。

2.3 SocketOutputStream & SocketInputStream

java.net.SocketInputStream 和 java.net.SocketOutputStream 代表了 Socket 流的讀寫,他們分別繼承自 java.io.FileInputStream 和 java.io.FileOutputStream 類,這說明 Socket 讀寫包含了文件讀寫的特性。另外,這兩個類是定義在 java.net.* 包中,并沒有對外公開。

2.4 FilterInputStream & FilterOutputStream

java.io.FilterInputStream 和 java.io.FilterOutputStream 分別是 java.io.InputStream 和 java.io.OutputStream 的子類,并且它們本身都是抽象類,為被過濾的流定義接口。java.io.FilterInputStream 和 java.io.FilterOutputStream 的主要作用是為基礎流提供一些額外的功能,這些不同的功能都是單獨的類,繼承了他們的接口。例如,過濾后的流 BufferedInputStream 和BufferedOutputStream 在讀寫時會緩沖數據,以加快數據傳輸速度。

2.5 BufferedInputStream & BufferedOutputStream

java.io.BufferedInputStream 類繼承自 java.io.FilterInputStream 類,它的作用是為 java.io.FileInputStream、java.net.SocketInputStream 等輸入流提供緩沖功能。一般通過 java.io.BufferedInputStream 的構造方法傳入具體的輸入流,同時可以指定緩沖區的大小。java.io.BufferedInputStream 會從底層 Socket 讀取一批數據保存到內部緩沖區中,后續通過 java.io.BufferedInputStream 的 read 方法讀取數據,實際上都從緩沖區中讀取,等讀完緩沖中的這部分數據之后,再從底層 Socket 中讀取下一部分的數據。

  • 注意:
  • 當你調用 java.io.BufferedInputStream 的 read 方法讀取一個數組時,只有當讀取的數據達到數組長度時才會返回,否則線程會被阻塞。

java.io.BufferedOutputStream 類繼承自 java.io.FilterOutputStream 類,它的作用是為 java.io.FileOutputStream、java.net.SocketOutputStream 等輸出流提供緩沖功能。一般通過 java.io.BufferedOutputStream 的構造方法傳入底層輸出流,同時可以指定緩沖區的大小。每次調用 java.io.BufferedOutputStream 的 write 方法寫數據時,實際上是寫入它的內部緩沖區中,當內部緩沖區寫滿或者調用了 flush 方法,才會將數據寫入底層 Socket 的緩沖區。

BufferedInputStream 和 BufferedOutputStream 在讀取或寫入時緩沖數據,從而減少了對原始數據源所需的訪問次數。緩沖流通常比類似的非緩沖流效率更高。

2.6 DataInputStream & DataOutputStream

java.io.DataInputStream 和 java.io.DataOutputStream 類繼承自 java.io.FilterInputStream 和 java.io.FilterOutputStream 類,同時實現了 java.io.DataInput 和 java.io.DataOutput 接口,功能是以機器無關的格式讀取或寫入原始 Java 數據類型。

3. 數據讀寫的案例程序

我們設計一個簡單的協議,每個消息的開頭 4 字節表示消息體的長度,格式如下:

+-----------------+
| 4 字節消息長度   |
+-----------------+
|                 |
|   消息體         |
|                 |
+-----------------+

我們通過這個簡單的協議演示 java.io.DataInputStream 、java.io.DataOutputStream 和 java.io.BufferedInputStream、java.io.BufferedOutputStream 類的具體用法。TCP 客戶端和服務器的編寫可以參考上一節內容,本節僅展示數據讀寫的代碼片段。

客戶端數據讀寫代碼:

import java.io.*;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;

public class TCPClientIO {
    // 服務器監聽的端口號
    private static final int PORT = 56002;
    private static final int TIMEOUT = 15000;

    public static void main(String[] args) {
        Socket client = null;
        try {
            // 調用無參構造方法
            client = new Socket();
            // 構造服務器地址結構
            SocketAddress serverAddr = new InetSocketAddress("127.0.0.1", PORT);
            // 連接服務器,超時時間是 15 毫秒
            client.connect(serverAddr, TIMEOUT);

            System.out.println("Client start:" + client.getLocalSocketAddress().toString());

            // 向服務器發送數據
            DataOutputStream out = new DataOutputStream(
                    new BufferedOutputStream(client.getOutputStream()));
            String req = "Hello Server!\n";
            out.writeInt(req.getBytes().length);
            out.write(req.getBytes());
            // 不能忘記 flush 方法的調用
            out.flush();
            System.out.println("Send to server:" + req + " length:" +req.getBytes().length);

            // 接收服務器的數據
            DataInputStream in = new DataInputStream(
                    new BufferedInputStream(client.getInputStream()));

            int msgLen = in.readInt();
            byte[] inMessage = new byte[msgLen];
            in.read(inMessage);
            System.out.println("Recv from server:" + new String(inMessage) + " length:" + msgLen);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (client != null){
                try {
                    client.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

服務端數據讀寫代碼:

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.BufferedOutputStream;
import java.io.DataOutputStream;

public class TCPServerIO {
    private static final int PORT =56002;

    public static void main(String[] args) {
        ServerSocket ss = null;
        try {
            // 創建一個服務器 Socket
            ss = new ServerSocket(PORT);
            // 監聽新的連接請求
            Socket conn = ss.accept();
            System.out.println("Accept a new connection:" + conn.getRemoteSocketAddress().toString());

            // 讀取客戶端數據
            DataInputStream in = new DataInputStream(
                    new BufferedInputStream(conn.getInputStream()));
            int msgLen = in.readInt();
            byte[] inMessage = new byte[msgLen];
            in.read(inMessage);
            System.out.println("Recv from client:" + new String(inMessage) + "length:" + msgLen);

            // 向客戶端發送數據
            String rsp = "Hello Client!\n";

            DataOutputStream out = new DataOutputStream(
                    new BufferedOutputStream(conn.getOutputStream()));
            out.writeInt(rsp.getBytes().length);
            out.write(rsp.getBytes());
            out.flush();
            System.out.println("Send to client:" + rsp + " length:" + rsp.getBytes().length);
            conn.close();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (ss != null){
                try {
                    ss.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        System.out.println("Server exit!");
    }
}

注意讀寫消息長度需要用 readInt 和 writeInt 方法。

4. 總結

通過本節學習,你需要樹立一個觀念:TCP 是面向字節流的協議,TCP 傳輸數據的時候并不保證消息邊界,消息邊界需要程序員設計應用層協議來保證。將字節流解析成自定義的協議格式,需要借助 java.io.* 中提供的工具類,一般情況下,java.io.DataInputStream 、java.io.DataOutputStream 和 java.io.BufferedInputStream、java.io.BufferedOutputStream 四個類就足以滿足你的需求了。DataInputStream 和 DataOutputStream 類主要是用以讀寫 java 相關的數據類型,BufferedInputStream 和 BufferedOutputStream 解決緩沖讀寫的問題,目的是提高讀寫效率。

本節簡要介紹了 Socket 編程中常用的 I/O 流類,關于 java.io.* 包中的各種 I/O 流類不是本節的重點,需要你自己參考相關 Java 書籍。