Java TCP Socket 數(shù)據(jù)收發(fā)
1. 前言
TCP 是面向字節(jié)流的傳輸協(xié)議。所謂字節(jié)流是指 TCP 并不理解它所傳輸?shù)臄?shù)據(jù)的含義,在它眼里一切都是字節(jié),1 字節(jié)是 8 比特。比如,TCP 客戶端向服務(wù)器發(fā)送“Hello Server,I’m client。How are you?”,TCP 客戶端發(fā)送的是具有一定含義的數(shù)據(jù),但是對(duì)于 TCP 協(xié)議棧來說,傳輸?shù)氖且淮?strong>字節(jié)流,具體如何解釋這段數(shù)據(jù)需要 TCP 服務(wù)器的應(yīng)用程序來完成,這就涉及到“應(yīng)用層協(xié)議設(shè)計(jì)”的問題。
在 TCP/IP 協(xié)議棧的四層協(xié)議模型中,操作系統(tǒng)內(nèi)核協(xié)議棧實(shí)現(xiàn)了鏈路層、網(wǎng)絡(luò)層、傳輸層,將應(yīng)用層留給了應(yīng)用程序來實(shí)現(xiàn)。在編程實(shí)踐中,通常有文本協(xié)議和二進(jìn)制協(xié)議兩種類型,前者通常通過一個(gè)分隔符區(qū)分消息語義,而后者通常是需要通過一個(gè) length 字段指定消息體的大小。比如著名的 HTTP 協(xié)議就是文本協(xié)議,通過 “\r\n” 來區(qū)分 HTTP Header 的每一行。而 RTMP 協(xié)議是一個(gè)二進(jìn)制協(xié)議,通過 length 字段來指定消息體的大小。
解析 TCP 字節(jié)流的語義通常叫做消息解析,如果按照傳統(tǒng) C 語言函數(shù)的方式來實(shí)現(xiàn),還是比較麻煩的,有很多細(xì)節(jié)需要處理。好在 Java 為我們提供了很多工具類,給我們的工作帶來了極大地便利。
2. Java 字節(jié)流結(jié)構(gòu)
Java 的 java.io.* 包中包含了 InputStream 和 OutputStream 兩個(gè)類,是 Java 字節(jié)流 I/O 的基礎(chǔ)類,其他具體的 Java I/O 字節(jié)流功能類都派生自這兩個(gè)類。
圖中只列出了我們 Socket 編程中常用的 I/O 字節(jié)流類。java.net.SocketInputStream 類是 Socket 的輸入流實(shí)現(xiàn)類,它繼承了 java.io.FileInputStream 類。java.net.SocketOutputStream 類是 Socket 的輸出流實(shí)現(xiàn)類,它繼承了 java.io.FileOutputStream 類,下來我們逐一介紹這些類的基本功能。
2.1 Java InputStream & OutputStream
java.io.InputStream 類是一個(gè)抽象超類,它提供最小的編程接口和輸入流的部分實(shí)現(xiàn)。java.io.InputStream 類定義的幾類方法:
- 讀取字節(jié)或字節(jié)數(shù)組,一組 read 方法。
- 標(biāo)記流中的位置,mark 方法。
- 跳過輸入字節(jié),skip 方法。
- 找出可讀取的字節(jié)數(shù),available 方法。
- 重置流中的當(dāng)前位置,reset 方法。
- 關(guān)閉流,close 方法。
InputStream 流在創(chuàng)建實(shí)例時(shí)會(huì)自動(dòng)打開,你可以調(diào)用 close 方法顯式關(guān)閉流,也可以選擇在垃圾回收 InputStream 時(shí),隱式關(guān)閉流。需要注意的是垃圾回收機(jī)制關(guān)閉流,并不能立刻生效,可能會(huì)造成流對(duì)象泄漏,所以一般需要主動(dòng)關(guān)閉。
java.io.OutputStream 類同樣是一個(gè)抽象超類,它提供最小的編程接口和輸出流的部分實(shí)現(xiàn)。java.io.OutputStream 定義的幾類方法:
- 寫入字節(jié)或字節(jié)數(shù)組,一組 write 方法。
- 刷新流,flush 方法。
- 關(guān)閉流,close 方法。
OutputStream 流在創(chuàng)建時(shí)會(huì)自動(dòng)打開,你可以調(diào)用 close 方法顯式關(guān)閉流,也可以選擇在垃圾回收 OutputStream 時(shí),隱式關(guān)閉流。
2.2 FileInputStream & FileOutputStream
java.io.FileInputStream 和 java.io.FileOutputStream 是文件輸入和輸出流類,用于從本機(jī)文件系統(tǒng)上的文件讀取數(shù)據(jù)或向其寫入數(shù)據(jù)。你可以通過文件名、java.io.File 對(duì)象、java.io.FileDescriptor 對(duì)象創(chuàng)建一個(gè) FileInputStream 或 FileOutputStream 流對(duì)象。
2.3 SocketOutputStream & SocketInputStream
java.net.SocketInputStream 和 java.net.SocketOutputStream 代表了 Socket 流的讀寫,他們分別繼承自 java.io.FileInputStream 和 java.io.FileOutputStream 類,這說明 Socket 讀寫包含了文件讀寫的特性。另外,這兩個(gè)類是定義在 java.net.* 包中,并沒有對(duì)外公開。
2.4 FilterInputStream & FilterOutputStream
java.io.FilterInputStream 和 java.io.FilterOutputStream 分別是 java.io.InputStream 和 java.io.OutputStream 的子類,并且它們本身都是抽象類,為被過濾的流定義接口。java.io.FilterInputStream 和 java.io.FilterOutputStream 的主要作用是為基礎(chǔ)流提供一些額外的功能,這些不同的功能都是單獨(dú)的類,繼承了他們的接口。例如,過濾后的流 BufferedInputStream 和BufferedOutputStream 在讀寫時(shí)會(huì)緩沖數(shù)據(jù),以加快數(shù)據(jù)傳輸速度。
2.5 BufferedInputStream & BufferedOutputStream
java.io.BufferedInputStream 類繼承自 java.io.FilterInputStream 類,它的作用是為 java.io.FileInputStream、java.net.SocketInputStream 等輸入流提供緩沖功能。一般通過 java.io.BufferedInputStream 的構(gòu)造方法傳入具體的輸入流,同時(shí)可以指定緩沖區(qū)的大小。java.io.BufferedInputStream 會(huì)從底層 Socket 讀取一批數(shù)據(jù)保存到內(nèi)部緩沖區(qū)中,后續(xù)通過 java.io.BufferedInputStream 的 read 方法讀取數(shù)據(jù),實(shí)際上都從緩沖區(qū)中讀取,等讀完緩沖中的這部分?jǐn)?shù)據(jù)之后,再從底層 Socket 中讀取下一部分的數(shù)據(jù)。
- 注意:
- 當(dāng)你調(diào)用 java.io.BufferedInputStream 的 read 方法讀取一個(gè)數(shù)組時(shí),只有當(dāng)讀取的數(shù)據(jù)達(dá)到數(shù)組長(zhǎng)度時(shí)才會(huì)返回,否則線程會(huì)被阻塞。
java.io.BufferedOutputStream 類繼承自 java.io.FilterOutputStream 類,它的作用是為 java.io.FileOutputStream、java.net.SocketOutputStream 等輸出流提供緩沖功能。一般通過 java.io.BufferedOutputStream 的構(gòu)造方法傳入底層輸出流,同時(shí)可以指定緩沖區(qū)的大小。每次調(diào)用 java.io.BufferedOutputStream 的 write 方法寫數(shù)據(jù)時(shí),實(shí)際上是寫入它的內(nèi)部緩沖區(qū)中,當(dāng)內(nèi)部緩沖區(qū)寫滿或者調(diào)用了 flush 方法,才會(huì)將數(shù)據(jù)寫入底層 Socket 的緩沖區(qū)。
BufferedInputStream 和 BufferedOutputStream 在讀取或?qū)懭霑r(shí)緩沖數(shù)據(jù),從而減少了對(duì)原始數(shù)據(jù)源所需的訪問次數(shù)。緩沖流通常比類似的非緩沖流效率更高。
2.6 DataInputStream & DataOutputStream
java.io.DataInputStream 和 java.io.DataOutputStream 類繼承自 java.io.FilterInputStream 和 java.io.FilterOutputStream 類,同時(shí)實(shí)現(xiàn)了 java.io.DataInput 和 java.io.DataOutput 接口,功能是以機(jī)器無關(guān)的格式讀取或?qū)懭朐?Java 數(shù)據(jù)類型。
3. 數(shù)據(jù)讀寫的案例程序
我們?cè)O(shè)計(jì)一個(gè)簡(jiǎn)單的協(xié)議,每個(gè)消息的開頭 4 字節(jié)表示消息體的長(zhǎng)度,格式如下:
+-----------------+
| 4 字節(jié)消息長(zhǎng)度 |
+-----------------+
| |
| 消息體 |
| |
+-----------------+
我們通過這個(gè)簡(jiǎn)單的協(xié)議演示 java.io.DataInputStream 、java.io.DataOutputStream 和 java.io.BufferedInputStream、java.io.BufferedOutputStream 類的具體用法。TCP 客戶端和服務(wù)器的編寫可以參考上一節(jié)內(nèi)容,本節(jié)僅展示數(shù)據(jù)讀寫的代碼片段。
客戶端數(shù)據(jù)讀寫代碼:
import java.io.*;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
public class TCPClientIO {
// 服務(wù)器監(jiān)聽的端口號(hào)
private static final int PORT = 56002;
private static final int TIMEOUT = 15000;
public static void main(String[] args) {
Socket client = null;
try {
// 調(diào)用無參構(gòu)造方法
client = new Socket();
// 構(gòu)造服務(wù)器地址結(jié)構(gòu)
SocketAddress serverAddr = new InetSocketAddress("127.0.0.1", PORT);
// 連接服務(wù)器,超時(shí)時(shí)間是 15 毫秒
client.connect(serverAddr, TIMEOUT);
System.out.println("Client start:" + client.getLocalSocketAddress().toString());
// 向服務(wù)器發(fā)送數(shù)據(jù)
DataOutputStream out = new DataOutputStream(
new BufferedOutputStream(client.getOutputStream()));
String req = "Hello Server!\n";
out.writeInt(req.getBytes().length);
out.write(req.getBytes());
// 不能忘記 flush 方法的調(diào)用
out.flush();
System.out.println("Send to server:" + req + " length:" +req.getBytes().length);
// 接收服務(wù)器的數(shù)據(jù)
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();
}
}
}
}
}
服務(wù)端數(shù)據(jù)讀寫代碼:
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 {
// 創(chuàng)建一個(gè)服務(wù)器 Socket
ss = new ServerSocket(PORT);
// 監(jiān)聽新的連接請(qǐng)求
Socket conn = ss.accept();
System.out.println("Accept a new connection:" + conn.getRemoteSocketAddress().toString());
// 讀取客戶端數(shù)據(jù)
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);
// 向客戶端發(fā)送數(shù)據(jù)
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!");
}
}
注意讀寫消息長(zhǎng)度需要用 readInt 和 writeInt 方法。
4. 總結(jié)
通過本節(jié)學(xué)習(xí),你需要樹立一個(gè)觀念:TCP 是面向字節(jié)流的協(xié)議,TCP 傳輸數(shù)據(jù)的時(shí)候并不保證消息邊界,消息邊界需要程序員設(shè)計(jì)應(yīng)用層協(xié)議來保證。將字節(jié)流解析成自定義的協(xié)議格式,需要借助 java.io.* 中提供的工具類,一般情況下,java.io.DataInputStream 、java.io.DataOutputStream 和 java.io.BufferedInputStream、java.io.BufferedOutputStream 四個(gè)類就足以滿足你的需求了。DataInputStream 和 DataOutputStream 類主要是用以讀寫 java 相關(guān)的數(shù)據(jù)類型,BufferedInputStream 和 BufferedOutputStream 解決緩沖讀寫的問題,目的是提高讀寫效率。
本節(jié)簡(jiǎn)要介紹了 Socket 編程中常用的 I/O 流類,關(guān)于 java.io.* 包中的各種 I/O 流類不是本節(jié)的重點(diǎn),需要你自己參考相關(guān) Java 書籍。