TCP、Socket 与 ServerSocket | JavaSE

TCP、Socket 与 ServerSocket

一、学习目标

学完本章,你应该能够:

  1. 能够解释 TCP 与 UDP 在通信模型上的核心区别。
  2. 能够解释 TCP“面向连接、可靠、有序、字节流”的含义。
  3. 能够理解 Java 中 SocketServerSocket 的不同职责。
  4. 能够使用 Socket(host, port) 请求建立 TCP 连接。
  5. 能够使用 ServerSocket(port) 监听服务端端口。
  6. 能够理解 accept() 的阻塞行为及其返回的 Socket 对象。
  7. 能够通过 Socket 的输入流和输出流完成最基本的一发一收。
  8. 能够区分“TCP 字节流”和“应用层消息边界”。

二、核心知识

2.1 从 UDP 切换到 TCP

前面我们使用 UDP:

Client
  │
  │ Datagram
  ▼
Server

UDP 的特点:

无连接
面向数据报
每个数据报独立
不提供可靠交付保证

现在换成 TCP:

Client
     │
     │ 建立连接
     ▼
Server
     │
     │
     ├────可靠字节流────┐
     │                  │
     └──────────────────┘

TCP:

Transmission Control Protocol
传输控制协议

是一个:

面向连接的、可靠的、有序字节流传输协议。


2.2 TCP 的核心特点

可以先掌握四个关键词:

连接
可靠
有序
字节流

2.3 面向连接

UDP 发送时:

知道目标 IP + Port
↓
直接 send

TCP 不一样。

客户端真正传输应用数据前,需要:

与服务端建立 TCP 连接

Java 中最典型的代码:

Socket socket =
        new Socket(
                "127.0.0.1",
                9999
        );

这句话并不是简单:

创建一个普通 Java 对象

它还会尝试:

连接 127.0.0.1:9999

服务端必须有:

ServerSocket

监听对应端口。


2.4 TCP 建立连接

计算机网络课程通常会把 TCP 连接建立概括为:

三次握手

可以用最简模型理解:

Client              Server

   ───── SYN ───────→

   ←── SYN + ACK ────

   ───── ACK ───────→

最终双方确认:

对方存在
双方通信能力基本就绪
连接状态建立

JavaSE 阶段重点不是手写 TCP 报文,而是理解:

Socket 成功建立以后,应用程序才获得了一条可以进行双向字节流通信的 TCP 连接。


2.5 TCP 的可靠性

TCP 协议提供一系列机制来提高数据可靠交付能力,例如:

序号
确认
重传
校验
流量控制
拥塞控制

所以应用层看到的是:

一个可靠、有序的字节流。

如果发送:

A B C D E

正常连接状态下,接收程序不会把它作为:

E C A B D

这样的乱序字节流交给应用。


2.6 “TCP 可靠”不等于“业务绝不会失败”

必须避免一种误解:

用了 TCP
=
消息百分之百业务成功

不是。

例如:

连接中途断开
程序崩溃
服务器宕机
读取逻辑错误
协议设计错误

仍然可能导致业务失败。

TCP 所说的可靠,主要是:

传输层为已建立连接提供可靠、有序的字节流传输语义。

至于:

用户订单是否写入数据库
支付是否真正成功
聊天消息是否真正落库

仍然需要应用层自己保证。


2.7 TCP 是字节流

这是后面非常重要的概念。

UDP:

Packet A

Packet B

Packet C

有天然的数据报边界。

TCP:

byte byte byte byte byte byte byte ...

应用程序看到的是:

连续字节流。

例如客户端连续写:

Hello

Java

TCP

TCP 并不自动保存:

第一条消息
第二条消息
第三条消息

这种业务意义上的边界。

它只保证:

一串按顺序到达的字节

因此以后进行 TCP 多发多收时必须考虑:

消息边界 / 应用层协议。


2.8 Java TCP 核心 API

Java TCP 编程主要使用:

Socket

与:

ServerSocket

二者都位于:

java.net

包。


2.9 Socket 是什么

Java 中的:

Socket

可以理解为:

TCP 通信连接在应用程序中的对象表示。

客户端通常通过:

new Socket(host, port)

连接服务端。

例如:

Socket socket =
        new Socket(
                "127.0.0.1",
                9999
        );

此时如果连接成功:

Client Socket
       │
       │ TCP Connection
       │
Server Socket

双方就拥有一条 TCP 通信通道。


2.10 ServerSocket 是什么

服务端不能使用普通:

Socket

去“等待所有客户端连接”。

Java 提供:

ServerSocket

它的核心职责:

监听某个本地 TCP 端口,等待新的连接请求。

创建:

ServerSocket serverSocket =
        new ServerSocket(9999);

表示:

在当前主机
监听 TCP 9999

模型:

          TCP 9999

             ▲
             │
      ServerSocket
             │
       等待客户端连接

2.11 ServerSocket 和 Socket 的区别

这是本章必须彻底分清的地方。

ServerSocket

负责:

监听
等待新连接
接受新连接

Socket

负责:

代表一条已经建立的 TCP 通信连接
读取数据
写出数据

模型:

                 Server

             ServerSocket
                  │
                  │ accept()
                  ▼
                Socket
                  ▲
                  │ TCP
                  │
                Socket
                 Client

所以:

ServerSocket
≠
真正与客户端交换业务数据的那条 Socket

它主要负责:

接客

accept() 返回的:

Socket

才负责后续:

通信

2.12 accept()

核心方法:

Socket accept()

例如:

Socket socket =
        serverSocket.accept();

含义:

等待客户端连接
        ↓
有客户端连接到达
        ↓
接受该连接
        ↓
返回一个 Socket

如果当前没有客户端连接:

serverSocket.accept();

通常会:

阻塞等待。

与 UDP 的:

receive()

很像:

UDP:
receive() 等数据

TCP:
accept() 等连接

2.13 为什么 accept() 返回 Socket

假设服务端监听:

TCP 9999

客户端:

127.0.0.1:53120

建立连接。

服务端的:

ServerSocket

依旧负责监听:

9999

而:

accept()

返回:

Socket socket

表示:

服务端与这个客户端之间
已经建立的具体连接

因此服务器会同时存在:

ServerSocket
+
Socket

它们职责完全不同。


2.14 TCP Socket 如何传输数据

TCP 不使用:

DatagramPacket

而是使用:

IO 流

Socket 提供:

getInputStream()

获得:

InputStream

用于:

读取对方发送的数据

以及:

getOutputStream()

获得:

OutputStream

用于:

向对方发送数据

所以前面学习的:

File / IO

正式和网络编程合流。


2.15 TCP 通信管道模型

客户端:

Socket
 ├── InputStream
 └── OutputStream

服务端接受连接后:

Socket
 ├── InputStream
 └── OutputStream

于是:

Client OutputStream
          │
          ▼
       TCP 网络
          │
          ▼
Server InputStream

反方向:

Server OutputStream
          │
          ▼
       TCP 网络
          │
          ▼
Client InputStream

因此 TCP Socket 天然支持:

双向通信。


三、使用方法

3.1 TCP 客户端核心 API

客户端:

Socket socket =
        new Socket(
                "127.0.0.1",
                9999
        );

连接成功后:

OutputStream outputStream =
        socket.getOutputStream();

向服务端发送。

以及:

InputStream inputStream =
        socket.getInputStream();

读取服务端发送的数据。


3.2 TCP 服务端核心 API

首先:

ServerSocket serverSocket =
        new ServerSocket(9999);

然后:

Socket socket =
        serverSocket.accept();

连接建立:

Client Socket
       │
       │ TCP
       │
Server Socket

之后服务端也可以:

socket.getInputStream();

和:

socket.getOutputStream();

3.3 TCP 一发一收客户端

为了与原课程中的 IO 知识自然组合,本章使用:

DataOutputStream

发送:

消息类型
+
字符串消息

代码:

import java.io.DataOutputStream;
import java.net.Socket;

public class TCPClient {
    public static void main(String[] args) throws Exception {

        System.out.println("=== TCP 客户端启动 ===");

        try (
                Socket socket =
                        new Socket(
                                "127.0.0.1",
                                9999
                        );

                DataOutputStream output =
                        new DataOutputStream(
                                socket.getOutputStream()
                        )
        ) {

            output.writeInt(1);

            output.writeUTF(
                    "你好,我是 TCP 客户端"
            );

            output.flush();

            System.out.println("消息发送完成");
        }
    }
}

3.4 客户端发生了什么

第一步:

new Socket(
        "127.0.0.1",
        9999
);

尝试建立 TCP 连接。

如果连接成功:

Socket 创建成功

第二步:

socket.getOutputStream()

获得 Socket 字节输出流。

第三步:

new DataOutputStream(...)

进行包装。

第四步:

output.writeInt(1);

写入:

一个 int

第五步:

output.writeUTF(...)

写入:

一个 DataOutputStream 格式的 UTF 字符串

3.5 TCP 一发一收服务端

import java.io.DataInputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class TCPServer {
    public static void main(String[] args) throws Exception {

        System.out.println("=== TCP 服务端启动 ===");

        try (
                ServerSocket serverSocket =
                        new ServerSocket(9999)
        ) {

            System.out.println(
                    "等待客户端连接..."
            );

            try (
                    Socket socket =
                            serverSocket.accept();

                    DataInputStream input =
                            new DataInputStream(
                                    socket.getInputStream()
                            )
            ) {

                System.out.println(
                        "客户端连接成功:"
                        + socket.getInetAddress()
                                .getHostAddress()
                        + ":"
                        + socket.getPort()
                );

                int type =
                        input.readInt();

                String message =
                        input.readUTF();

                System.out.println(
                        "type = " + type
                );

                System.out.println(
                        "message = " + message
                );
            }
        }
    }
}

3.6 服务端发生了什么

第一步:

ServerSocket serverSocket =
        new ServerSocket(9999);

服务器监听:

TCP 9999

第二步:

Socket socket =
        serverSocket.accept();

程序停在这里:

等待客户端

客户端运行:

new Socket(
        "127.0.0.1",
        9999
);

双方完成 TCP 连接建立。

于是:

accept()

返回。


3.7 accept 返回的 Socket 是谁?

假设客户端:

127.0.0.1:53180

连接服务端:

127.0.0.1:9999

服务端:

Socket socket =
        serverSocket.accept();

这个 socket 可以理解为:

服务端这一侧
与 127.0.0.1:53180 客户端
对应的 TCP 通信 Socket

因此可以获取:

socket.getInetAddress()

得到:

远端客户端 IP

以及:

socket.getPort()

得到:

远端客户端端口

3.8 为什么客户端和服务端读取顺序必须一致

客户端写:

output.writeInt(1);
output.writeUTF("Hello");

那么服务端必须按相同协议读取:

input.readInt();
input.readUTF();

如果服务端错误写成:

input.readUTF();
input.readInt();

双方对于字节含义的理解不同:

客户端:
[ int ][ UTF ]

服务端:
[ UTF ][ int ]

就会出现:

数据解析错误
阻塞
异常

这已经是:

应用层协议设计

最简单的形式。


3.9 TCP 通信完整流程

                  TCP Server

          new ServerSocket(9999)
                    │
                    ▼
                 accept()
                    │
                 阻塞等待
                    │
                    │
                    │
TCP Client          │
                    │
new Socket(host,port)
        │           │
        └────连接───┘
                    │
                    ▼
               accept 返回
                    │
                    ▼
               Server Socket


Client:
getOutputStream()
        │
        │ bytes
        ▼
════════ TCP ════════
        │
        ▼
Server:
getInputStream()

四、原理与进阶

4.1 Socket 到底是什么

Socket 可以翻译为:

套接字

它可以理解为:

应用程序访问操作系统网络通信能力的一个抽象端点。

网络程序不是 Java 自己直接:

操控网卡

而是大致:

Java 程序
    ↓
Socket API
    ↓
操作系统网络协议栈
    ↓
TCP / IP
    ↓
网卡

因此 Socket 是应用程序与操作系统网络栈之间非常重要的接口抽象。


4.2 一条 TCP 连接如何区分

一条 TCP 连接通常可以由四个核心信息区分:

源 IP
源 Port
目标 IP
目标 Port

例如:

Client
192.168.1.10:53101

→

Server
192.168.1.100:9999

可以表示成:

192.168.1.10:53101
      ↓
192.168.1.100:9999

另一客户端:

192.168.1.11:53101
      ↓
192.168.1.100:9999

仍然是另一条连接。

这也解释了后面一个重要现象:

一个服务端的 9999 端口可以同时服务很多 TCP 客户端连接。

因为连接不仅仅由:

9999

一个数字决定。


4.3 ServerSocket 为什么不是“只能接一个客户端”

代码:

Socket socket =
        serverSocket.accept();

这一章暂时只执行一次,因此只能接受一个连接。

ServerSocket 本身并不是:

只能服务一个客户端

如果以后写:

while (true) {
    Socket socket =
            serverSocket.accept();
}

就可以持续接受新连接。

问题在于:

接受连接以后,怎么同时处理多个 Socket?

这正是后续:

09-08 多客户端服务端与多线程通信

解决的问题。


4.4 TCP 没有应用层消息边界

非常重要。

例如客户端:

output.write(
        "AAA".getBytes(...)
);

output.write(
        "BBB".getBytes(...)
);

程序员认为发送:

消息 1:AAA

消息 2:BBB

但 TCP 只看到:

AAABBB

这样的连续字节流。

服务端某次:

input.read(buffer);

可能读到:

AAABBB

也可能:

AA

下一次:

ABBB

应用程序不能把:

一次 write

机械等价成:

对方一次 read

所以 TCP 程序需要设计:

消息长度
分隔符
固定长度
结构化协议

等消息边界。


4.5 那为什么 writeUTF / readUTF 可以读一条字符串?

因为:

DataOutputStream.writeUTF()

本身采用了一套确定的数据编码格式。

对应:

DataInputStream.readUTF()

能够按照相同规则读取。

所以:

writeUTF()

和:

readUTF()

解决的是:

在 TCP 字节流之上人为建立一个数据边界协议。

不是 TCP 自己突然变成:

面向消息协议

必须区分:

TCP
提供字节流

和:

DataInputStream / DataOutputStream
在字节流之上定义读取格式

4.6 accept() 和 receive() 的阻塞有什么不同

UDP:

DatagramSocket.receive()

等待:

数据报

TCP:

ServerSocket.accept()

等待:

新的 TCP 连接

TCP 连接建立以后:

input.read(...)

又可能等待:

连接中的数据

所以网络编程中会大量遇到:

阻塞 IO

4.7 close() 会发生什么

客户端:

socket.close();

意味着应用程序关闭 Socket 资源。

对于 TCP:

连接终止流程

会由操作系统 TCP 协议栈处理。

计算机网络课程常将正常 TCP 连接关闭概括为:

四次挥手

Java 程序员通常不需要手工发送这些 TCP 控制报文。

我们的职责主要是:

socket.close();

操作系统负责:

TCP 协议层连接关闭

五、实践应用

5.1 为什么聊天程序通常更适合 TCP

如果业务要求:

A 发送:
“今天晚上 8 点见”

通常希望:

完整收到
顺序正确
不希望中间丢几个字节

TCP 的可靠字节流模型很适合这类需求。


5.2 文件传输

文件:

1 MB
100 MB
10 GB

本质上都可以:

FileInputStream
       ↓
Socket OutputStream
       ↓
TCP
       ↓
Socket InputStream
       ↓
FileOutputStream

于是之前学习的:

File
IO
网络编程

真正串起来。


5.3 Web 开发的基础

浏览器访问网站时:

Browser
   ↓
TCP Connection
   ↓
HTTP
   ↓
Server

现代 HTTP 还涉及 TLS、HTTP/2、HTTP/3 等更复杂机制,但理解经典:

Socket
ServerSocket
TCP
InputStream
OutputStream

有助于理解后面的 Java Web 网络基础。


六、常见问题

6.1 Socket 和 ServerSocket 有什么区别?

最重要的记忆:

ServerSocket
=
监听 / 接受连接

Socket
=
真正的数据通信连接

6.2 为什么服务端需要两个对象?

服务端首先:

ServerSocket serverSocket =
        new ServerSocket(9999);

负责监听。

然后:

Socket socket =
        serverSocket.accept();

负责与某个具体客户端通信。


6.3 accept() 为什么程序像卡死一样?

因为:

accept()

默认会阻塞等待客户端连接。

如果没有客户端:

继续等待

这是正常现象。


6.4 TCP 发送数据为什么没有 DatagramPacket?

因为 TCP 是:

字节流协议

Java 通过:

InputStream
OutputStream

暴露连接中的字节流。

UDP 才主要使用:

DatagramPacket

6.5 Socket 的 getInputStream 是读取谁?

读取:

对方通过这条 TCP 连接发送过来的数据。

客户端:

客户端 InputStream

读取的是:

服务端发送的数据

服务端:

服务端 Socket InputStream

读取的是:

客户端发送的数据

6.6 getOutputStream 是写到本机文件吗?

不是。

它是:

Socket 的网络输出流

向:

TCP 连接另一端

写数据。


6.7 客户端 new Socket() 为什么可能失败?

例如:

new Socket(
        "127.0.0.1",
        9999
);

但:

服务端没启动

或者:

端口错误
网络不通
防火墙阻止
目标不可达

就可能连接失败并抛出网络相关异常。

这与 UDP:

send()

没有建立连接的模型不同。


6.8 TCP 可靠,为什么还会抛异常?

“可靠”不是:

物理网络永远不会断

而是:

TCP 协议在可工作的连接中
提供可靠有序字节流机制

如果:

网线断了
程序崩了
机器关机了
连接被重置

Java 仍然会收到异常或 EOF。


6.9 一次 write() 是否一定对应对方一次 read()?

不是。

这一点必须记住:

write()
≠
对方一定一次 read() 完整得到

因为:

TCP = 字节流

应用层需要自己定义消息边界。


6.10 为什么 DataOutputStream 和 DataInputStream 必须配套?

例如:

writeInt()
writeUTF()

接收端必须:

readInt()
readUTF()

因为双方必须遵守相同:

数据格式
+
数据顺序

否则就是:

通信协议不一致

七、练习与验收

7.1 知识问答

  1. TCP 的全称是什么?
  2. TCP 有哪些核心特点?
  3. UDP 与 TCP 最大的通信模型区别是什么?
  4. Java TCP 客户端核心类是什么?
  5. Java TCP 服务端监听核心类是什么?
  6. ServerSocketSocket 的职责分别是什么?
  7. accept() 为什么会阻塞?
  8. accept() 返回的是什么?
  9. TCP 建立连接以后如何发送和接收数据?
  10. getInputStream()getOutputStream() 分别解决什么问题?
  11. TCP 为什么叫字节流协议?
  12. 为什么一次 write() 不等于对方一次 read()
  13. writeUTF() / readUTF() 为什么能够形成消息边界?
  14. 为什么“TCP 可靠”不代表业务绝不会失败?

7.2 代码阅读

阅读:

ServerSocket serverSocket =
        new ServerSocket(9999);

Socket socket =
        serverSocket.accept();

InputStream input =
        socket.getInputStream();

回答:

  1. serverSocket 的职责是什么?
  2. 9999 是什么端口?
  3. 没有客户端时 accept() 会怎样?
  4. socket 表示什么?
  5. input 读取的数据来自哪里?

再阅读:

Socket socket =
        new Socket(
                "127.0.0.1",
                9999
        );

OutputStream output =
        socket.getOutputStream();

回答:

  1. new Socket(...) 会尝试做什么?
  2. 127.0.0.1 是谁?
  3. 9999 是谁的端口?
  4. output.write(...) 最终把字节发给谁?

7.3 手写代码

关闭 AI 自动补全。

TCP 客户端

从零实现:

TCPClient

要求:

连接 127.0.0.1:9999

发送:

type = 1001

message =
"Hello TCP"

要求使用:

DataOutputStream

TCP 服务端

从零实现:

TCPServer

要求:

  • 监听 TCP 9999。
  • 调用 accept()
  • 获取客户端 IP。
  • 获取客户端端口。
  • 使用 DataInputStream
  • 按顺序读取 int + UTF
  • 输出收到的信息。

7.4 Debug

客户端:

output.writeUTF("Hello");
output.writeInt(100);

服务端:

int id =
        input.readInt();

String message =
        input.readUTF();

判断:

  1. 双方协议是否一致?
  2. 为什么可能产生错误?
  3. 应如何修改?

再分析:

ServerSocket serverSocket =
        new ServerSocket(9999);

InputStream input =
        serverSocket.getInputStream();

有什么问题?

要求解释:

ServerSocket

为什么不能直接通过这种方式读取具体客户端业务数据。

7.5 综合训练

画出:

                Server

            ServerSocket
               :9999
                 │
               accept
                 │
                 ▼
              Socket
                 ▲
                 │ TCP
                 │
              Socket
               Client

再补充:

Client OutputStream
        ↓
Server InputStream

Server OutputStream
        ↓
Client InputStream

并解释每个对象和流的职责。

7.6 本章验收

如果关闭资料后,可以从零写出:

new Socket(
        host,
        port
);

socket.getInputStream();
socket.getOutputStream();

new ServerSocket(
        port
);

serverSocket.accept();

并能完整回答:

ServerSocket、服务端 Socket、客户端 Socket 三个对象究竟分别是什么?

以及:

为什么 TCP 使用 IO 流而不是 DatagramPacket

并且能够解释:

TCP
=
连接
+
可靠
+
有序
+
字节流

那么 TCP 基础已经建立。

最终知识模型:

                         TCP

             ┌────────────┴────────────┐
             │                         │
          Client                     Server
             │                         │
           Socket                 ServerSocket
             │                         │
             │                      accept()
             │                         │
             │                       Socket
             │                         │
             └──────── TCP ────────────┘
                        │
                 Input / Output
                        │
                     IO Stream