JDK 提供了對(duì) TCP(Transmission Control Protocol,傳輸控制協(xié)議)和 UDP(User Datagram Protocol,用戶數(shù)據(jù)報(bào)協(xié)議)這兩個(gè)數(shù)據(jù)傳輸協(xié)議的支持。本文開始探討 TCP。
TCP 基礎(chǔ)知識(shí)
在“服務(wù)器-客戶端”這種架構(gòu)中,服務(wù)器和客戶端各自維護(hù)一個(gè)端點(diǎn),兩個(gè)端點(diǎn)需要通過(guò)網(wǎng)絡(luò)進(jìn)行數(shù)據(jù)交換。TCP 為這種需求提供了一種可靠的流式連接,流式的意思是傳出和收到的數(shù)據(jù)都是連續(xù)的字節(jié),沒有對(duì)數(shù)據(jù)量進(jìn)行大小限制。一個(gè)端點(diǎn)由 IP 地址和端口構(gòu)成(專業(yè)術(shù)語(yǔ)為“元組 {IP 地址, 端口}
”)。這樣,一個(gè)連接就可以由元組 {本地地址, 本地端口, 遠(yuǎn)程地址, 遠(yuǎn)程端口}
來(lái)表示。
連接過(guò)程
在 TCP 編程接口中,端點(diǎn)體現(xiàn)為 TCP 套接字。共有兩種 TCP 套接字:主動(dòng)和被動(dòng),“被動(dòng)”狀態(tài)也常被稱為“偵聽”狀態(tài)。服務(wù)器和客戶端利用套接字進(jìn)行連接的過(guò)程如下:
- 服務(wù)器創(chuàng)建一個(gè)被動(dòng)套接字,開始循環(huán)偵聽客戶端的連接。
- 客戶端創(chuàng)建一個(gè)主動(dòng)套接字,連接服務(wù)器。
- 服務(wù)器接受客戶端的連接,并創(chuàng)建一個(gè)代表該連接的主動(dòng)套接字。
- 服務(wù)器和客戶端通過(guò)步驟 2 和 3 中創(chuàng)建的兩個(gè)主動(dòng)套接字進(jìn)行數(shù)據(jù)傳輸。
下面是連接過(guò)程的圖解:

TCP 連接
一個(gè)簡(jiǎn)單的 TCP 服務(wù)器
JDK 提供了 ServerSocket
類來(lái)代表 TCP 服務(wù)器的被動(dòng)套接字。下面的代碼演示了一個(gè)簡(jiǎn)單的 TCP 服務(wù)器(多線程阻塞模式),它不斷偵聽并接受客戶端的連接,然后將客戶端發(fā)送過(guò)來(lái)的文本按行讀取,全文轉(zhuǎn)換為大寫后返回給客戶端,直到客戶端發(fā)送文本行 bye
:
public class TcpServer implements Runnable {
private ServerSocket serverSocket;
public TcpServer(int port) throws IOException {
// 創(chuàng)建綁定到某個(gè)端口的 TCP 服務(wù)器被動(dòng)套接字。
serverSocket = new ServerSocket(port);
}
@Override
public void run() {
while (true) {
try {
// 以阻塞的方式接受一個(gè)客戶端連接,返回代表該連接的主動(dòng)套接字。
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) { // 減少代碼量的花招……
// 包裝套接字的輸入流以讀取客戶端發(fā)送的文本行。
BufferedReader in = new BufferedReader(new InputStreamReader(
s.getInputStream(), StandardCharsets.UTF_8));
// 包裝套接字的輸出流以向客戶端發(fā)送轉(zhuǎn)換結(jié)果。
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;
}
// 將轉(zhuǎn)換結(jié)果輸出給客戶端。
out.println(line.toUpperCase(Locale.ENGLISH));
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
阻塞模式的編程方式簡(jiǎn)單,但存在性能問(wèn)題,因?yàn)榉?wù)器線程會(huì)卡死在接受客戶端的 accept()
方法上,不能有效利用資源。套接字支持非阻塞模式,現(xiàn)在暫時(shí)略過(guò)。
一個(gè)簡(jiǎn)單的 TCP 客戶端
JDK 提供了 Socket
類來(lái)代表 TCP 客戶端的主動(dòng)套接字。下面的代碼演示了上述服務(wù)器的客戶端:
public class TcpClient implements Runnable {
private Socket socket;
public TcpClient(String host, int port) throws IOException {
// 創(chuàng)建連接到服務(wù)器的套接字。
socket = new Socket(host, port);
}
@Override
public void run() {
try (Socket s = socket) { // 再次減少代碼量……
// 包裝套接字的輸出流以向服務(wù)器發(fā)送文本行。
PrintWriter out = new PrintWriter(new OutputStreamWriter(
s.getOutputStream(), StandardCharsets.UTF_8), true);
// 包裝套接字的輸入流以讀取服務(wù)器返回的文本行。
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;
}
// 將文本行發(fā)送給服務(wù)器。
out.println(line);
// 打印服務(wù)器返回的文本行。
console.writer().println(in.readLine());
}
// 通知服務(wù)器關(guān)閉連接。
out.println("bye");
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
從 JDK 文檔可以看到,ServerSocket
和 Socket
在初始化的時(shí)候,可以設(shè)定一些參數(shù),還支持延遲綁定。這些東西對(duì)性能和行為都有所影響。下一篇文章將詳解這兩個(gè)類的初始化。