一、Socket 基础
1.1 Socket 是什么
一句话:Socket(套接字)是程序和网络之间的一扇门——发数据、收数据都通过它,门怎么开、数据怎么传由操作系统和协议(TCP/UDP)负责。
稍正式:Socket 是操作系统提供的编程接口(API)。调用它,就能在本机进程与“另一台机器、另一个程序”之间建立可收发包的通道;通道两端各有一个 Socket,通过“门牌号”(IP + 端口)寻址。
为什么要有 Socket:应用不能直接操作网卡,Socket 是操作系统给的统一入口;调用 socket()、bind()、connect()、read()、write() 等即可完成“发到谁、从哪收、用 TCP 还是 UDP”。各系统(Windows、Linux、Android、iOS)概念一致,便于跨平台。
易混两点
- Socket 不是协议:没有“Socket 协议”;“用 Socket 编程”= 用这套 API 收发数据,按 TCP 还是 UDP 传由创建时选的类型决定。
- Socket = 接口,TCP/UDP = 协议:接口是你调用的函数,协议是数据如何打包、确认、重传等;同一套 Socket 既可走 TCP 也可走 UDP。
和 HTTP:HTTP 是应用层协议(请求/响应格式),跑在 TCP 上。Socket 是传输层给应用层的接口,更底层;可用 Socket 拼 HTTP,也可自定义应用层协议(游戏、IM)。概括:HTTP 管“说什么”,Socket 管“怎么传”。
1.2 Socket 在协议栈里的位置
分层设计下,每层只做自己的事,下层为上层提供能力;Socket 就是应用层使用传输层时的那套接口。
从下往上看各层:
| 层次 | 主要协议/内容 | 职责简述 |
|---|---|---|
| 应用层 | HTTP、自定义协议等 | 业务逻辑:发什么、收什么、什么格式。 |
| (Socket API) | — | 应用层调用传输层时使用的接口(创建 Socket、绑定、连接、读写);不是独立协议层,而是传输层向上暴露的 API。 |
| 传输层 | TCP、UDP | 端到端传输:用端口区分本机不同进程;TCP 还负责可靠、有序。 |
| 网络层 | IP | 把数据从源主机送到目标主机(多跳),用 IP 地址寻址。 |
| 链路层 | 以太网、Wi-Fi 等 | 在单段物理链路上传数据,关心“这一跳”到下一台设备。 |
对应关系:
应用层 你的程序(HTTP、游戏、聊天等)
↓
Socket API ← 你写代码时用的:socket()、bind()、connect()、read()、write()
↓
传输层 TCP / UDP(选一种,由 Socket 类型决定)
↓
网络层 IP
↓
链路层 以太网、Wi-Fi 等
结论:用 TCP 还是 UDP 由创建 Socket 时选的类型(见 1.3)决定;Socket = 接口,TCP/UDP = 协议。
1.3 两种最常用的 Socket 类型(选哪种、为什么)
创建 Socket 时,必须指定这条通道使用 TCP 还是 UDP,对应到接口里通常有两个类型常量。
| 类型 | 底层协议 | 常见常量名 | 通俗理解 |
|---|---|---|---|
| 流式 Socket | TCP | SOCK_STREAM | 像打电话:先拨号接通(建连接),再说话,说完挂断(断连接);顺序一致、一般不丢。 |
| 数据报 Socket | UDP | SOCK_DGRAM | 像发短信:不用先“接通”,每条消息单独发;不保证一定送到、不保证顺序,但开销小。 |
TCP Socket(SOCK_STREAM)要点
- 使用前必须先建立连接(客户端
connect,服务端accept),底层对应 TCP 的“三次握手”。 - 连接建立后,双方可持续读、写;数据是字节流,没有“一条条消息”的边界,应用层需自己定界(如先发长度再发内容,或固定格式、分隔符),即要处理“粘包/拆包”。
- 断开时调用 close,底层对应“四次挥手”。
- 典型场景:网页、文件传输、登录、支付、数据库连接等,需要可靠、有序、不能丢的场景。
UDP Socket(SOCK_DGRAM)要点
- 无“连接”概念。每次发数据时指定“发给谁”(目标 IP + 端口);收数据时一次 recv 得到一个完整报文,并可拿到“谁发的”(源地址)。
- 不保证送达、不保证顺序;适合能容忍丢包、更看重低延迟、实现简单的场景。
- 典型场景:音视频直播、在线游戏、DNS 查询、内网发现、广播/组播等。
对比小结:TCP Socket = 先连再传、可靠有序、有连接生命周期;UDP Socket = 即发即走、不保证可靠、无连接。一个 Socket 实例只能是其中一种,由创建时选的类型决定。
1.4 Socket 由什么标识(地址 + 端口)
“和谁通信”由两件事决定:哪台机器(IP)和哪个程序(端口)。
- IP:在某一网络范围内唯一标识一台主机;
127.0.0.1(localhost)表示本机。 - 端口:0~65535,区分同一台机器上的不同程序;如
8080表示“监听 8080 端口的进程”。
通信端点:IP:端口(如 192.168.1.100:8080)即一个端点。一次通信有两个端点:本机端点 + 对方端点。Socket 对应本机这一侧;bind = 门开在本机某地址某端口,connect = 连到对方某地址某端口。
端口的习惯划分(约定俗成,非强制):
| 范围 | 名称 | 说明与举例 |
|---|---|---|
| 0~1023 | 知名端口 | 系统或常用服务:80=HTTP,443=HTTPS,22=SSH,53=DNS。 |
| 1024~49151 | 注册端口 | 给各类应用注册使用,如 3306=MySQL,8080 常作开发用 HTTP。 |
| 49152~65535 | 动态端口 | 由系统临时分配给客户端(如浏览器访问网页时本机用的临时端口)。 |
1.5 TCP / UDP 流程概览(帮助理解“连接”)
TCP(有连接)
- 服务端:创建 Socket → bind 端口 → listen → accept(每来一个客户端返回一个新 Socket 专供该客户端)→ 用新 Socket 读写 → close。可循环 accept 服务多客户端。
- 客户端:创建 Socket → connect(服务端地址) → 读写 → close。
- 要点:必须先 connect/accept 成功才有“通道”,之后才能稳定读写;一条连接对应一个对方,直到某一方 close。
UDP(无连接)
- 发/收:创建 Socket → 若需在本机某端口收包则 bind 一次 → sendto(每次指定目标地址与端口)/ recvfrom(收到数据与发送方地址)→ close。
- 要点:无 listen、accept、connect;同一条 Socket 可多次 sendto 到不同目标,也可 recvfrom 收不同来源的包。
1.6 小结与常见问题
小结:Socket = 传输层给应用层的接口(不是协议);TCP/UDP = 传输层协议,创建 Socket 时二选一(SOCK_STREAM / SOCK_DGRAM),一个实例只能是其一。
常见问题
| 问题 | 简要回答 |
|---|---|
| Socket 和 WebSocket 是一回事吗? | 不是。Socket 是传输层 API;WebSocket 是应用层协议,建立在 TCP 之上,见后文“四、WebSocket 与 Android 使用”。 |
| 为什么 accept() 会返回“新”的 Socket? | 因为一个 TCP 连接对应“一个客户端”。监听 Socket 只负责接受新连接;每接受一个客户端就得到一个专用于该客户端的 Socket,才能同时服务多人。 |
| 本机调试时地址写什么? | 服务端 bind 可用 127.0.0.1:端口 或 0.0.0.0:端口;客户端 connect 用 127.0.0.1:端口 即可连到本机服务。 |
二、TCP 与 UDP 的区别,以及三次握手、四次挥手
本节先对比 TCP 与 UDP 的差异,再说明 TCP 建立连接(三次握手)与释放连接(四次挥手)的步骤与原因,最后补充分层里的其他要点及选型提示。
2.1 TCP 与 UDP 的区别
从连接方式、可靠性、数据形式、开销与典型 API 五方面对比如下:
| 特性 | TCP | UDP |
|---|---|---|
| 连接方式 | 面向连接。通信前必须先建立连接(三次握手),通信结束后要释放连接(四次挥手);双方在连接期间维护状态(序号、窗口等)。 | 无连接。不需要事先建连,也不存在“断开连接”;发数据时带上目标地址和端口即可,每个数据报独立传输。 |
| 可靠性 | 保证可靠传输。通过序号、确认、重传、校验和等机制,确保数据不丢、不重复、按序到达;若丢包会重传,应用层拿到的是完整、有序的字节流。 | 不保证可靠。发出去就不管,不确认、不重传;可能丢包、乱序、重复,需要可靠时要在应用层自己做确认与重传。 |
| 数据形式 | 字节流,没有报文边界。多次 write 可能被合并成一次送达,一次 read 可能读到多次 write 的内容;应用层要自己定界(粘包/拆包)。 | 数据报,有边界。一次 send 对应一个报文,一次 recv 对应一个完整报文,不会粘在一起。 |
| 开销与性能 | 有建连、维护、断连的开销;有确认、重传、流量控制、拥塞控制,延迟和头部都更大,适合对可靠性要求高的场景。 | 无建连,头部小,没有确认与重传,延迟低、开销小,适合能容忍丢包、更看重实时性的场景。 |
| 典型 API | Socket / ServerSocket | DatagramSocket / DatagramPacket |
2.2 TCP 三次握手(建立连接的步骤)
何时发生:客户端主动发起连接时(例如调用 connect()),底层会进行三次握手;握手成功后 connect() 才返回,连接建立。
目的:① 确认双方都能正常收发;② 协商各自的初始序号(seq),为后续可靠传输、去重、按序重组做准备;③ 防止历史旧连接请求突然到达导致误建连接(通过序号区分新旧)。
常考:为什么不是两次?两次无法让双方都确认对方已就绪(例如服务器不知道客户端是否收到自己的 SYN+ACK),且难以防止历史旧连接误建。为什么不是四次?三次已足够完成双向确认与序号协商,再多一次无必要。
三步概览:共三次报文交换——第一次 SYN(客户端→服务器),第二次 SYN+ACK(服务器→客户端),第三次 ACK(客户端→服务器)。下面按步说明。
第一次:客户端 → 服务器
- 客户端发出一个 TCP 报文:SYN = 1(请求建立连接),并带上初始序号 seq = x(x 由客户端随机生成)。
- 含义:向服务器表示“请求建连,我这边发数据的起始序号是 x”。
第二次:服务器 → 客户端
- 服务器若同意建连,则回复:SYN = 1,ACK = 1;确认号 ack = x + 1(表示“已收到你的 x,期待你下次从 x+1 开始发”);并带上自己的初始序号 seq = y(y 由服务器随机生成)。
- 含义:向客户端表示“同意建连,已收到你的 x,我这边发数据的起始序号是 y”。
第三次:客户端 → 服务器
- 客户端再发一个报文:ACK = 1;确认号 ack = y + 1(表示“已收到你的 y”)。
- 含义:向服务器确认“已收到你的 y,我这边连接已就绪”。
三次之后:服务器收到第三次 ACK 后也认为连接已建立,双方即可在该连接上正常读写。记:SYN → SYN+ACK → ACK,三次报文完成双方确认与序号协商。
2.3 TCP 四次挥手(释放连接的步骤)
何时发生:任一方主动关闭连接时(例如调用 close()),底层会进行四次挥手;完成后该 TCP 连接被释放,双方不再用该连接收发数据。
为什么是四次:TCP 是全双工的,数据可以双向独立传输。关闭时需要双向都关——先关“我→你”方向(发 FIN,对方回 ACK),再关“你→我”方向(对方发 FIN,我回 ACK)。每一方向上的关闭都要“说一声 + 被确认”,所以是四次报文:FIN → ACK → FIN → ACK。(第二、三次有时可合并成服务器发一个 FIN+ACK,但逻辑上仍是两个动作,习惯上仍称“四次挥手”。)
四步概览:假设客户端先发起关闭(先调用 close / 发 FIN)。第一次和第二次关掉“客户端→服务器”方向;第三次和第四次关掉“服务器→客户端”方向。下面按步说明。
第一次:客户端 → 服务器
- 客户端发送 FIN = 1,并带序号 seq = u(及可能的 ACK)。
- 含义:表示“我这边不再发数据,要关闭我→你这条方向”。客户端进入半关闭:不再发,仍可收。
第二次:服务器 → 客户端
- 服务器回复 ACK,确认号 ack = u + 1。
- 含义:表示“已收到你的关闭请求”。“客户端→服务器”方向就此关闭;服务器若还有数据要发,可继续发,连接尚未完全关闭。
第三次:服务器 → 客户端
- 服务器发完要发的数据后,发送 FIN = 1,并带序号 seq = v(及 ACK)。
- 含义:表示“我这边也不再发数据,要关闭我→你这条方向”。
第四次:客户端 → 服务器
- 客户端回复 ACK,确认号 ack = v + 1。
- 含义:表示“已收到你的关闭请求”。服务器收到该 ACK 后,连接彻底关闭。
小结:谁先 close() 谁先发第一个 FIN(主动关闭方);先关“主动方→对方”方向(FIN + ACK),再关“对方→主动方”方向(FIN + ACK),共四次。第二、三次有时可合并为服务器一次 FIN+ACK,逻辑仍为四次。
2.4 TCP 其他要点
| 要点 | 说明 |
|---|---|
| 可靠性 | 协议栈通过序号、确认、重传、校验和、流量控制(按接收方能力调速)、拥塞控制(按网络状况调速)等保证,应用层得到可靠、有序字节流。 |
| 粘包/拆包 | TCP 无报文边界,多次 write 可能合并送达,一次 read 可能读到多段。应用层须自己定界:固定长度、长度前缀(如 4 字节长度+内容)、分隔符(如 \n)、或自定义协议头。 |
2.5 UDP 其他要点
| 要点 | 说明 |
|---|---|
| 无连接 | 不握手,发时带目标地址与端口即可;每个数据报独立。 |
| 数据报有边界 | 一次 send 对应一个报文,一次 recv 对应一个完整报文,无粘包。 |
| 包大小与 MTU | 单包载荷受 MTU 限制(以太网约 1500 字节);IPv4 建议约 1472 字节(扣 20+8 字节头),IPv6 约 1452 字节(扣 40+8),避免分片。 |
| 可靠性 | 协议不保证;若需可靠,应用层做序号/确认/重传,或选用 QUIC 等基于 UDP 的可靠协议。 |
2.6 TCP / UDP 选型提示
先判断:要可靠、有序还是可接受丢包?要长连接还是即发即走? 再在 TCP 与 UDP 间取舍;若还需选 HTTP/WebSocket 等应用层方案,见第五章「协议与场景选择」。
| 选 TCP | 选 UDP |
|---|---|
| 可靠、有序、不能丢:HTTP、数据库、文件、登录、支付、需可靠的实时信令 | 可容忍丢包、更看重低延迟:音视频、游戏、DNS、广播/组播、传感器 |
| 长连接、服务端主动下发、自定义协议且要可靠 | 无连接、一对多、高频小包、广播/组播 |
三、Socket 编程要点(Java/Android)
本节按** TCP 客户端 → TCP 服务端 → UDP(含广播与组播)→ 异常与资源释放 → Android 注意点 → BIO 与 NIO → 连接池**的顺序,说明在 Java/Android 下用 Socket 编程的步骤与注意点;API 以 Java 为例,Android 上需额外注意线程与权限。
3.1 TCP 客户端编程
步骤概览
- 创建 Socket:
new Socket()或先new Socket()再connect();或直接用new Socket(host, port)一步完成创建与连接。 - 连接:若分步则调用
socket.connect(new InetSocketAddress(host, port), connectTimeout),建议始终传入连接超时(毫秒),避免在不可达地址上长时间阻塞。 - 获取流:
socket.getInputStream()、socket.getOutputStream(),用 BufferedInputStream / BufferedOutputStream 或 DataInputStream / DataOutputStream 按需包装,便于按行、按块或按协议读写。 - 读写:按应用层协议在流上读写;注意 TCP 是字节流,需自己定界(长度前缀、分隔符等),处理粘包/拆包。
- 关闭:用完后调用
socket.close()(触发四次挥手),放在finally或 try-with-resources 中,避免异常时未关闭导致泄漏。
超时与线程(必设)
- 连接超时:
Socket.connect(address, connectTimeoutMs)的第二个参数;超时未连上会抛SocketTimeoutException。 - 读超时:
Socket.setSoTimeout(readTimeoutMs);在read()上阻塞超过该时间会抛SocketTimeoutException,可用于轮询或心跳场景。 - 线程:
connect()和read()/write()都会阻塞,在 Android 上必须在子线程或协程(如 KotlinDispatchers.IO)中执行,否则会阻塞主线程导致 ANR。
示例(流程示意)
Socket socket = null;
try {
socket = new Socket();
socket.connect(new InetSocketAddress(host, port), 5000);
socket.setSoTimeout(8000);
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
// 按协议读写 in / out
} finally {
if (socket != null) socket.close();
}
3.2 TCP 服务端编程
步骤概览
- 创建 ServerSocket:
new ServerSocket()或new ServerSocket(port);若只在本机监听可用new ServerSocket(port, 0, InetAddress.getByName("127.0.0.1"))。 - 绑定端口:若用无参构造,则需
serverSocket.bind(new InetSocketAddress(port));可指定 backlog(等待连接队列长度)。 - 接受连接:
Socket clientSocket = serverSocket.accept();该方法阻塞,直到有客户端连上才返回。返回的clientSocket专门与该客户端通信。 - 与客户端通信:用
clientSocket.getInputStream()/getOutputStream()读写;处理逻辑建议放到单独线程或线程池,避免阻塞下一次accept()。 - 关闭:与该客户端的会话结束后
clientSocket.close();若不再接受新连接则serverSocket.close()。
多客户端怎么处理
- 为什么不能单线程一直读写:
accept()返回后,若在同一线程里用该clientSocket一直 read/write,就会卡住,无法再执行下一次accept(),其他客户端连不进来。所以要“接一个、丢给别处处理”,主线程继续循环 accept。 - 推荐做法:主线程只做一件事——循环
accept();每得到一个clientSocket,就交给线程池或新线程去处理(在该线程里用该 Socket 读写),主线程立刻继续accept()等下一个。这样可同时服务多个客户端。 - 资源:每个客户端对应一个
Socket实例,处理完后必须对该 Socket 调用close(),否则会一直占用文件描述符和连接;建议在处理线程的finally或 try-with-resources 中关闭。
示例(流程示意)
ServerSocket serverSocket = new ServerSocket(port, 50);
while (true) {
Socket client = serverSocket.accept();
// 将 client 交给线程池处理,主线程继续 accept
executor.execute(() -> {
try {
// 用 client.getInputStream() / getOutputStream() 读写
} finally {
client.close();
}
});
}
3.3 UDP 编程(DatagramSocket)
UDP 在 Java 里用 DatagramSocket + DatagramPacket,无连接,发/收都按“数据报”为单位。下面先写发送与接收步骤,再说明广播、组播以及 DatagramSocket 与 Socket 的关系。
说明:UDP 没有像 TCP 那样的“服务端/客户端”角色划分。发送步骤和接收步骤是按“你要发数据还是收数据”来分的——要发就用发送步骤,要收就用接收步骤;同一条 Socket 既可以发也可以收。习惯上,先在某端口 bind 并等着收 的一方常叫“服务端”,不 bind 或后发数据的一方常叫“客户端”,但两边用的都是下面同一套步骤。
次数:创建只需 一次(一个程序里用一条 DatagramSocket 即可既发又收);绑定也只需 一次(若需要在本机某端口收包就 bind 一次,之后发、收都用这条 Socket;若只发不收可不 bind,系统会在首次发送时分配临时端口)。
发送步骤
- 创建
DatagramSocket(一次):可先bind(本机地址, 端口)固定本端端口,或不 bind 由系统分配临时端口。 - 构造 发送用的
DatagramPacket:new DatagramPacket(byte[] data, int length, InetAddress address, int port),传入数据、长度、目标 IP 与端口。 - 发送:
socket.send(packet);每次 send 对应一个独立数据报,无连接概念,可多次 send 到不同目标(同一 Socket 可发往多端)。
接收步骤(用上面同一条已创建的 Socket;若需收包则需先 bind 一次)
- 绑定(若尚未绑定):若需在本机某端口收包,执行一次
socket.bind(new InetSocketAddress(port))(或构造 Socket 时指定端口)。 - 构造 接收缓冲区:
new DatagramPacket(byte[] buffer, int length)。 - 接收:
socket.receive(packet)(阻塞直到收到一个数据报)。 - 解析:从
packet.getData()、packet.getLength()取数据内容,从packet.getAddress()、packet.getPort()取发送方地址与端口,便于回信。
广播与组播:是发送还是接收的一环?
- 本质上是「发送」这一侧的概念:指你把数据发往哪里——发往一个目标(单播)、发往整个子网(广播)、或发往一个组播组(组播)。接收端仍是普通的
receive(packet):收广播时无需额外设置,只要发送方发往广播地址,你 bind 好端口后正常 receive 即可收到;收组播时,需要先joinGroup(组播地址)加入该组,再 receive 才能收到发往该组播地址的包。 - 小结:广播/组播 = 发送时「发给谁」的两种一对多方式;接收只是正常收包,组播多一步“先加入组”。
广播 / 组播是干嘛用的?
| 方式 | 含义 | 典型场景 |
|---|---|---|
| 广播 | 一次发给同一子网内所有主机(目标地址为广播地址) | 内网设备发现(谁在线)、DHCP 请求、局域网唤醒、简单服务发现 |
| 组播 | 一次只发给加入某组播组的主机(目标地址为组播地址),不打扰未加入的主机 | 内网音视频直播、多人会议、按组做服务发现、IoT 分组下发 |
广播(代码要点)
- 目标地址设为子网广播地址(如
255.255.255.255或该网段广播地址),调用socket.setBroadcast(true)后再send(packet)。
组播(代码要点)
- 使用
MulticastSocket(继承自DatagramSocket):发送方把目标地址设为组播地址(224.0.0.0~239.255.255.255)后send(packet);接收方先joinGroup(InetAddress)加入组播组,再receive(packet),不用时leaveGroup(InetAddress)离开。
DatagramSocket 是某一种 Socket 类型吗?
- 在概念上,Socket 有“流式”(TCP)和“数据报”(UDP)两种类型,C 里用
SOCK_STREAM和SOCK_DGRAM区分。在 Java 里没有“一个 Socket 类 + 类型参数”,而是用两套类:TCP 用Socket/ServerSocket,UDP 用DatagramSocket+DatagramPacket。所以 DatagramSocket 就是 Java 里“UDP 那一种”Socket,与 TCP 的 Socket 并列,不是“统一一个 Socket 既能 TCP 又能 UDP”;底层分别对应 TCP 和 UDP。
注意
- 包大小:受 MTU 限制(以太网约 1500 字节),建议单包载荷约 1472 字节以内(扣除 IPv4 头 20 字节 + UDP 头 8 字节),减少分片与丢包。
- 线程:
receive()会阻塞,在 Android 上须在子线程或协程(如Dispatchers.IO)中执行,避免阻塞主线程。
3.4 异常处理与资源释放
常见异常
- SocketTimeoutException:连接超时或读超时;可区分是“连不上”还是“读不到数据”。
- ConnectException / NoRouteToHostException:无法连到目标(网络不可达、对方未监听、被防火墙拒绝等)。
- SocketException:如连接被对端重置(RST)、本地已 close 仍读写等。
- IOException:底层 IO 错误,通常应捕获并做日志或提示,再关闭 Socket。
资源释放
- 务必在 try-finally 或 try-with-resources 中关闭
Socket、ServerSocket、DatagramSocket,否则会泄漏文件描述符;在 Android 上长时间运行可能耗尽资源导致新连接失败。 - try-with-resources 示例:
try (Socket s = new Socket(host, port)) { ... },退出块时自动close()。
3.5 Android 平台注意点
- 权限:Manifest 中需声明
android.permission.INTERNET;若用网络状态做判断,可能还需ACCESS_NETWORK_STATE。 - 线程:所有会阻塞的 Socket 操作(connect、accept、read、receive 等)都应在后台线程或 Kotlin 协程(如
Dispatchers.IO)中执行;结果若需更新 UI,再切回主线程(如runOnUiThread、LiveData、协程Dispatchers.Main)。 - 策略:在 Android 9 及以上,默认禁止明文 HTTP,建议使用 HTTPS / TLS;若必须用 Socket 直连,可配置网络安全策略或使用
CleartextTrafficPermitted(仅调试或内网时考虑)。
3.6 BIO 与 NIO(阻塞与非阻塞)
BIO 和 NIO 分别是什么?
- BIO(Blocking I/O,阻塞 IO):调用
connect()、accept()、read()、write()时,当前线程会一直停在这个调用上,直到本次操作完成(或超时)才返回,等待期间线程不能干别的事。代码好写、顺序执行;但一个连接占一个线程,连接多了就要开很多线程,高并发时成本高。Java 里用Socket、ServerSocket就是典型的 BIO。 - NIO(New I/O,在 Java 里常指非阻塞 IO + 多路复用):用
java.nio.channels的 Channel(如SocketChannel、ServerSocketChannel)可设为非阻塞,再配合 Selector,由一个或少量线程轮询“哪些 Channel 可读/可写/可连接”,再去做对应 IO。等待时线程不用傻等,可去处理其他 Channel,一个线程能管大量连接。适合高并发、长连接服务端;代码是事件驱动,比 BIO 复杂。 - 一句对比:BIO = 阻塞式 IO,一线程一连接,等的时候线程卡住;NIO = 非阻塞 IO + 多路复用,少线程管多连接,等的时候线程不卡住,由 Selector 通知再处理。
3.6.1 阻塞 IO(BIO)要点
哪些调用会阻塞
- 客户端:
Socket.connect()会阻塞到连接成功或超时;InputStream.read()会阻塞到有数据可读或超时。 - 服务端:
ServerSocket.accept()会阻塞到有客户端连上;对每个已连接 Socket 的read()同样会阻塞。
特点与适用场景
- 优点:代码直观,顺序执行,易于理解和调试;配合
setSoTimeout()可以避免无限等待。 - 缺点:一个连接在一个线程上占住不放。若要同时处理很多连接,就要开很多线程(一线程一连接),线程多了会带来上下文切换、栈内存等开销,高并发时成本高。
- 适用:连接数不多(例如几十以内)、逻辑简单、对延迟不极敏感的场景;很多客户端/工具类程序用阻塞 + 单线程或小线程池即可。
3.6.2 非阻塞 NIO 与多路复用要点
核心类与概念
| 类 / 概念 | 作用 |
|---|---|
| SocketChannel | 可非阻塞读写的 TCP 通道,对应一个连接;可配置 configureBlocking(false) 为非阻塞。 |
| ServerSocketChannel | 可非阻塞 accept 的“监听通道”;每 accept 到一个连接就得到一个 SocketChannel。 |
| Selector | 把多个 Channel 注册上去,用一个线程调用 select() 等待“有就绪的 Channel”(可读/可写/可连接),再遍历处理,避免为每个连接单独阻塞。 |
| SelectionKey | 注册时返回的“键”,表示某个 Channel 在 Selector 上的注册关系;可获取就绪事件(OP_READ、OP_WRITE、OP_ACCEPT、OP_CONNECT)。 |
典型流程(服务端)
- 打开
ServerSocketChannel,bind 端口,设为非阻塞。 - 创建
Selector,把ServerSocketChannel注册到 Selector,关注OP_ACCEPT。 - 循环:
selector.select()等待有事件;返回后遍历selectedKeys(),若是 ACCEPT 则accept()得到SocketChannel,把该 SocketChannel 也注册到 Selector(关注 OP_READ/OP_WRITE);若是 READ 则对该 Channel 做read()等。 - 用少量线程(甚至单线程)即可处理大量连接,因为线程不会卡在某个连接的
read()上,而是由 Selector 统一调度。
特点与适用场景
- 优点:单线程或少量线程即可支撑大量连接,减少线程数,适合高并发、长连接(如推送、即时通讯服务端)。
- 缺点:代码结构更复杂,要处理“未读完/未写完”的缓冲、注册与反注册、边界条件;调试难度也更大。
- 适用:服务端需要维持成千上万连接、或希望用少量线程处理多路 IO 时;客户端若连接数不多,用阻塞 IO 通常足够。
3.6.3 阻塞与 NIO 对比小结
| 维度 | 阻塞 IO(BIO) | NIO 多路复用 |
|---|---|---|
| 线程与连接 | 通常一线程一连接(或一线程少量连接) | 少量线程管理大量连接 |
| 调用行为 | connect/accept/read/write 会阻塞当前线程 | Channel 设为非阻塞,由 Selector 通知就绪再 IO |
| 代码复杂度 | 简单,顺序逻辑 | 较复杂,事件驱动、状态要自己维护 |
| 适用场景 | 连接数不多、逻辑简单 | 高并发、长连接、服务端 |
3.7 连接池(针对 TCP 客户端)
是什么、为什么用
客户端频繁访问同一服务端时,若每次请求都 new Socket() + connect(),会反复三次握手,延迟大、服务端压力也大。连接池即:预先与目标 host:port 建立若干条 TCP 连接放入池中,请求时取一条用,用毕归还,供后续复用,从而少建连、降延迟。
基本流程
| 步骤 | 说明 |
|---|---|
| 初始化 | 按配置(最小/最大连接数)预先建连并放入池(队列或列表)。 |
| 取连接 | 从池中取空闲连接;池空且未达上限则新建;达上限则等待或失败。 |
| 使用 | 用该连接的 InputStream/OutputStream 按协议收发。 |
| 归还 | 用完后归还到池并标记为空闲,不要 close。 |
| 健康检查与回收 | 取用前或归还时检查连接是否有效;失效则 close 并移除。空闲过久的连接可定时回收。 |
池大小与超时
- 池大小:按并发量和服务端能力设最小/最大连接数;过小易排队,过大占资源。
- 获取超时:取连接时若超过设定时间(如 3 秒)仍拿不到,可失败或重试。
- 空闲超时:连接空闲超过一定时间可关闭并移出池,需要时再新建。
健康检查
池中连接可能已被对端或中间设备关闭,不检查就拿去用可能读写失败。常见方式:
- 应用层探活:发一条约定好的探活包,有正常响应则视为有效。
- 读探测:短时
setSoTimeout后尝试读;若收到 EOF 或连接异常(如 RST)则移除。 - 用后兜底:若本次 write/read 抛异常,则 close 并移出池,不归还。
失效连接必须 close 并从池移除,避免再次被取出导致数据错乱。
线程安全
多线程取/还连接时,池的取放逻辑须线程安全(如 BlockingQueue、synchronized、ReentrantLock);取出后标记占用、归还后标记空闲,保证一条连接同一时刻只被一个线程使用。
与 HTTP 的关系
- HTTP:直接用 OkHttp 即可,其内部已按 host 做连接池,无需自建。
- 裸 Socket 自建协议:需自己实现“池 + 健康检查 + 线程安全取还”,或参考对象池库(如 Apache Commons Pool)把对象换成 Socket。
四、WebSocket 与 Android 使用
本节覆盖 WebSocket 基础、协议要点、与其它实时方案的对比,以及 Android 上的使用与常考注意点。
4.1 WebSocket 基础
定义:WebSocket 是建立在 TCP 之上的应用层全双工协议。客户端先发一次 HTTP 请求并带上“升级为 WebSocket”的头部,服务器若同意则返回 101,此后同一 TCP 连接上不再走 HTTP,而是按 WebSocket 帧格式收发数据,连接长期保持,双方可随时双向收发。
特点
| 特点 | 说明 |
|---|---|
| 全双工 | 客户端与服务器可同时发送,无需等对方回应再发。 |
| 持久连接 | 一次 HTTP 握手后长期复用,不必每次请求都重新建连,降低延迟与开销。 |
| 低开销 | 握手后以二进制帧传输,帧头较小,适合高频小消息(如聊天、推送、行情)。 |
| 易部署 | 握手使用 HTTP,走 80/443 端口,便于经过网关、代理与防火墙。 |
与 HTTP 的关系
-
握手阶段:使用普通 HTTP 请求,请求头中需包含:
Upgrade: websocketConnection: UpgradeSec-WebSocket-Key(客户端随机 Base64)Sec-WebSocket-Version: 13等
服务器校验通过后返回 101 Switching Protocols,并在响应头中返回Sec-WebSocket-Accept(由 Key 按规范计算得出)。此后同一 TCP 连接上只传输 WebSocket 帧,不再解析为 HTTP。
-
协议与端口:
- ws://:默认 80 端口,明文传输。
- wss://:默认 443 端口,基于 TLS,与 HTTPS 一致;生产环境应使用 wss 以保证机密性与完整性。
握手细节(常考)
- 握手使用 HTTP GET 请求(不是 POST),请求的 URL 即为 WebSocket 的地址(如
wss://example.com/ws)。 - Sec-WebSocket-Accept 的计算方式:将客户端发来的
Sec-WebSocket-Key与固定 GUID 字符串"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"拼接后,做 SHA-1 哈希,再做 Base64 编码;服务器在响应头中返回该值,客户端可校验以确认是合法 WebSocket 服务。 - 可选请求头 Sec-WebSocket-Protocol:用于协商子协议(如
chat、soap),服务器在响应头中选一个返回,表示双方使用该子协议。
与轮询、长轮询、SSE 的对比(常考)
| 方式 | 特点 | 缺点 | 适用场景 |
|---|---|---|---|
| 短轮询 | 客户端定时发 HTTP 请求问“有数据吗” | 无效请求多、延迟高、浪费带宽 | 实时性要求不高的简单查询 |
| 长轮询 | 客户端发请求,服务器有数据才响应,否则挂起 | 仍是一问一答,频繁建连、实现复杂 | 兼容性要求高、不能 WebSocket 时 |
| SSE(Server-Sent Events) | 基于 HTTP,服务器单向推送到客户端,客户端用 EventSource | 仅服务器→客户端单向,且基于 HTTP | 只需服务端推送(如通知、日志流) |
| WebSocket | 全双工、长连接、双向实时、帧格式标准 | 需服务端与客户端都支持 | 聊天、协作、实时行情、游戏等双向实时 |
与 Socket 的区别
Socket 是传输层提供给应用层的编程接口,可基于 TCP 或 UDP 自由定义应用层协议;WebSocket 则是跑在 TCP 之上的既定应用层协议,有标准握手和帧格式。对比如下:
| 维度 | Socket(TCP/UDP API) | WebSocket |
|---|---|---|
| 层次 | 传输层 API,直接面向 TCP/UDP | 应用层协议,建立在 TCP 之上 |
| 协议内容 | 无固定格式,可自定义字节流或文本协议;需自己处理粘包、定界、心跳等 | 固定握手(HTTP 升级)+ 标准帧格式(Opcode、Mask、Payload、Ping/Pong、关闭帧等),规范已定好 |
| 连接建立 | TCP 需自己完成三次握手(调用 connect/accept);UDP 无连接 | 通过一次 HTTP 握手“升级”为 WebSocket,库或浏览器自动完成 |
| 数据形式 | TCP 为字节流无边界,UDP 为数据报有边界;应用层自行约定报文格式 | 以“帧”为单位,每条消息可带类型(文本/二进制),支持分片;有明确的打开/关闭/心跳语义 |
| 典型用途 | 任意网络程序:自定义 RPC、游戏协议、IoT、内网服务等 | Web/App 实时通信:聊天、推送、实时行情、协作编辑、在线状态等 |
| 浏览器支持 | 不直接暴露裸 Socket API,无法在网页中直接使用 | 浏览器原生提供 WebSocket API,可直接在 JS 中建连、收发 |
| 开发成本 | 协议与细节(心跳、重连、断线检测)需自行设计与实现 | 握手、帧解析、Ping/Pong 由库/运行时处理,只需关注业务消息的收发 |
结论与选型:Socket 适合对协议、性能、控制力有更高要求的场景(如自研长连接协议、非浏览器客户端);WebSocket 适合需要「长连接 + 双向实时」且希望少写底层细节的 Web/App 场景,直接使用标准协议即可。
4.2 WebSocket 协议要点
数据帧结构(概念)
- 帧头:包含 FIN(是否最后一帧)、Opcode(帧类型)、Mask(是否掩码)、Payload length 等;扩展长度时还有 2 或 8 字节的扩展长度域。
- Opcode 常见值:0x1 文本、0x2 二进制、0x8 关闭、0x9 Ping、0xA Pong;0x0 表示延续帧(分片时后续帧使用)。
- 掩码:规范要求客户端发往服务器的帧必须带掩码(Mask=1 且带 4 字节掩码键),服务器发往客户端的帧不掩码;用于防止恶意脚本与代理缓存污染。
- 分片:大消息可拆成多帧发送:首帧 Opcode 为文本或二进制且 FIN=0,后续帧 Opcode=0x0,最后一帧 FIN=1;接收方按序重组为一条完整消息。
连接生命周期
- CONNECTING:正在发起 HTTP 握手或等待 101 响应。
- OPEN:握手成功,可正常收发数据帧。
- CLOSING:已发送或收到关闭帧,正在等待对方关闭帧或关闭 TCP。
- CLOSED:连接已关闭,不可再收发。
关闭时应发送关闭帧(Opcode 0x8),可带关闭状态码和简短原因文本;收到对方关闭帧后再关闭底层 TCP,实现优雅关闭。
常见关闭码(常考)
| 码 | 含义 |
|---|---|
| 1000 | 正常关闭 |
| 1001 | 端点“离开”(如页面导航离开) |
| 1002 | 协议错误 |
| 1003 | 不支持的数据类型 |
| 1005 | 未指定状态码(禁止在关闭帧中发送) |
| 1006 | 异常关闭(未发关闭帧即断连) |
| 1011 | 服务器内部错误 |
心跳与保活
长连接场景下,需要定期确认“连接是否还活着”,避免对方已断线或中间设备回收连接而应用层不知道。常见做法有两类:Ping/Pong 帧(协议层)和心跳包(应用层)。
一、Ping(0x9)/ Pong(0xA)
- 是什么:WebSocket 协议规定的两种控制帧,专门用于保活、探活,不携带业务数据。
- Ping 帧(Opcode 0x9):一方主动发出,表示“你还在吗?”;可带一段可选 payload,对方通常原样放在 Pong 里回传。
- Pong 帧(Opcode 0xA):收到 Ping 后应回复 Pong,表示“在,连接正常”;payload 一般与收到的 Ping 一致或为空。
- 作用:① 检测连接是否存活;② 让链路上有数据流动,防止 NAT、代理、负载均衡等因“长时间无数据”而回收连接;③ 可粗略测 RTT(发 Ping 到收 Pong 的耗时)。
- 谁发:很多库(如 OkHttp 的 WebSocket)会自动发 Ping、回 Pong,应用层一般不用自己发;若服务端或库不提供,再考虑用应用层心跳包。
- 来源:Ping/Pong 是 WebSocket 协议自己提供的 控制帧,不是应用层定义的;是否自动发取决于具体库/框架,并非每个框架都自带自动 Ping/Pong。
二、心跳包(应用层)
- 是什么:应用自己定义的业务消息,按约定格式定期发送,对方按约定回复,用来表示“我还活着”或携带少量业务信息。
- 来源与载体:心跳包不是接口,而是在连接通道上发送的应用层消息;通道可以是 TCP Socket 或 WebSocket,心跳包就是在这条通道里发的一种数据,由应用自己定义格式与间隔。
- 常见形式:例如客户端每 30 秒发一条
{"type":"ping","ts":1234567890},服务端回{"type":"pong"};或简单发固定字符串"ping"/"pong"。 - 作用:与 Ping/Pong 类似——探活、保活、防止中间设备断连;此外可顺带带业务数据(如时间戳、版本号、状态等)。
- 谁发:完全由应用层实现:定时器/协程定时发、服务端收到后解析并回包;需要自己处理超时(一定时间内未收到 pong 则判定断连、触发重连)。
二者对比
| 项目 | Ping / Pong | 心跳包 |
|---|---|---|
| 层级 | WebSocket 协议层控制帧 | 应用层自定义消息 |
| 格式 | 固定帧类型(Opcode 0x9/0xA) | 自定义(JSON、二进制等) |
| 实现 | 多数库自动处理 | 需自己写定时发送与解析逻辑 |
| 扩展 | 一般不带业务数据 | 可带业务字段(时间戳、状态等) |
| 适用 | 有 WebSocket 且库支持时优先用 | 协议不支持 Ping/Pong 或需带业务时用 |
心跳间隔可根据网络环境设置(例如 30 秒~60 秒);过短增加流量与 CPU,过长可能被中间设备先回收连接。
4.3 Android 中的使用
TCP / UDP Socket 在 Android 上的注意点
- 线程:所有会阻塞的 IO(
connect、accept、read、receive等)必须在非主线程执行,如Thread、ExecutorService、Kotlin 协程Dispatchers.IO,否则会阻塞主线程导致 ANR。 - 权限:Manifest 中声明
android.permission.INTERNET;若需根据网络状态做判断,可加ACCESS_NETWORK_STATE。 - 超时:建议设置
Socket.connect(address, connectTimeout)、Socket.setSoTimeout(readTimeout),避免在不可达或僵死连接上长时间阻塞。
WebSocket 推荐用法(OkHttp)
- 库:OkHttp 提供
newWebSocket(Request, WebSocketListener),自动完成 HTTP 握手、帧解析、Ping/Pong 与关闭帧,无需手写协议。 - 创建:构建
Request(URL 为ws://或wss://),调用client.newWebSocket(request, listener)得到WebSocket实例,可用send()发文本或二进制。 - 回调:
WebSocketListener中常用onOpen(连接就绪)、onMessage(收文本/二进制)、onClosing/onClosed(关闭过程与结果)、onFailure(握手失败、网络错误等);这些回调可能在工作线程执行,若需更新 UI 需切回主线程(runOnUiThread、Handler、协程Dispatchers.Main)。 - 地址与证书:生产环境应使用 wss://;与 HTTPS 一样会校验证书,需保证服务器证书有效、主机名一致,否则会回调
onFailure。 - 重连:在
onFailure或onClosed中根据业务决定是否重连;可采用指数退避(如 1s、2s、4s…)并设置最大重试次数;重连前确保旧WebSocket已关闭、避免重复注册监听或重复创建连接。
简单示例(流程示意)
Request request = new Request.Builder().url("wss://example.com/ws").build();
WebSocket ws = client.newWebSocket(request, new WebSocketListener() {
@Override public void onOpen(WebSocket webSocket, Response response) { /* 连接就绪,可 send */ }
@Override public void onMessage(WebSocket webSocket, String text) { /* 收到文本,可切主线程更新 UI */ }
@Override public void onClosed(WebSocket webSocket, int code, String reason) { /* 可在此触发重连逻辑 */ }
@Override public void onFailure(WebSocket webSocket, Throwable t, Response response) { /* 可在此触发重连逻辑 */ }
});
// 发送:ws.send("hello"); 或 ws.send(ByteString.of(...));
// 关闭:ws.close(1000, "正常关闭");
生命周期与资源释放(常考)
- 在 Activity / Fragment 中使用时,应在
onDestroy()或onDestroyView()中主动调用webSocket.close(1000, "页面销毁")并置空引用,避免页面销毁后回调仍触发、或持有 Activity 导致内存泄漏。 - Listener 与泄漏:
WebSocketListener若以匿名内部类形式写且持有 Activity 引用,会形成 Activity → WebSocket/Client → Listener → Activity 的引用链;建议用弱引用持有 Activity、或在销毁时cancel()请求并移除 Listener,避免长时间持有 Activity。
网络状态与重连
- 可配合 ConnectivityManager 监听网络变化(如从无网到有网、Wi-Fi 与移动网络切换);在“网络恢复”时主动触发一次重连,提升体验。
- 重连时注意:先关闭旧连接(若仍存在),再建新连接;避免同一 URL 多次
newWebSocket而未关闭上一次,导致多份连接与回调错乱。
4.4 常考知识点速查
WebSocket 协议
| 考点 | 要点 |
|---|---|
| 握手方式 | 使用 HTTP GET 请求,请求头需带 Upgrade: websocket、Connection: Upgrade、Sec-WebSocket-Key、Sec-WebSocket-Version: 13;服务器同意则返回 101 Switching Protocols 及 Sec-WebSocket-Accept。 |
| Sec-WebSocket-Accept 计算 | 将客户端的 Sec-WebSocket-Key 与固定 GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" 拼接 → SHA-1 哈希 → Base64 编码。 |
| 为什么用 WebSocket 不用轮询 | 全双工、长连接、低延迟、少无效请求;短轮询延迟高、浪费带宽;长轮询仍是一问一答,建连频繁。 |
| 帧类型 Opcode | 0x1 文本、0x2 二进制、0x8 关闭、0x9 Ping、0xA Pong、0x0 延续帧(分片时用)。 |
| 客户端发帧必须掩码 | 规范要求客户端→服务器的数据帧 Mask=1 且带 4 字节掩码键;防代理缓存污染与恶意脚本,服务器→客户端不掩码。 |
| 连接状态 | CONNECTING(握手中)→ OPEN(可收发)→ CLOSING(正在关)→ CLOSED(已关闭)。 |
| 常见关闭码 | 1000 正常关闭、1001 端点离开、1002 协议错误、1006 异常关闭(未发关闭帧即断连)。 |
| Ping/Pong 与心跳包 | Ping/Pong 是协议层控制帧,多由库自动处理;心跳包是应用层自定义消息,需自己发与解析,可带业务数据;二者都可做保活、探活。 |
Android 与使用注意
| 考点 | 要点 |
|---|---|
| 网络必须在子线程 | 阻塞 IO(connect、read、accept 等)会卡主线程导致 ANR;必须在子线程、线程池或 Kotlin 协程 Dispatchers.IO 中执行。 |
| 权限 | 需声明 INTERNET;若根据网络状态做判断可加 ACCESS_NETWORK_STATE。 |
| wss 与证书 | 生产环境用 wss://,与 HTTPS 一样校验证书;证书无效、过期或主机名不匹配会连接失败并回调 onFailure。 |
| 生命周期与泄漏 | Activity/Fragment 销毁时应在 onDestroy 中 close WebSocket 并置空引用;Listener 若持有 Activity 易造成内存泄漏,可用弱引用或销毁时 cancel。 |
| 重连策略 | 在 onFailure/onClosed 中按业务决定是否重连;常用指数退避(如 1s、2s、4s…)并设最大重试次数;重连前先关闭旧连接,避免重复建连与回调错乱。 |
对比与选型(一句话)
| 考点 | 要点 |
|---|---|
| Socket 与 WebSocket | Socket 是传输层 API(TCP/UDP),可自定义协议;WebSocket 是应用层协议,跑在 TCP 上,有标准握手与帧格式,浏览器原生支持。 |
| 长轮询 / SSE / WebSocket | 长轮询:一问一答、实现复杂;SSE:仅服务端→客户端单向;WebSocket:全双工、双向实时,适合聊天、推送等。 |
| 何时用 WebSocket | 需要长连接、服务端主动推送、低延迟双向通信(聊天、实时通知、协作等),且希望用标准协议、少写底层时。 |
五、协议与场景选择
本节说明在不同业务场景下如何选择 HTTP/HTTPS、TCP Socket、UDP Socket、WebSocket,便于面试与实战选型。
5.1 何时用 HTTP/HTTPS
适用场景
- 请求-响应、无状态:RESTful API、CRUD 接口、查询类接口;每次请求独立,不需要维持会话状态。
- 静态资源与下载:网页、图片、CSS/JS、文件下载;CDN 与缓存友好。
- 表单提交、登录、支付:标准 Web 表单、OAuth、支付回调等,已有成熟方案与中间件。
- 不需要“服务端主动推”或“双向实时”:客户端发起请求、服务端一次响应即可满足需求。
特点与注意
- 优点:无状态、易缓存、易水平扩展、协议与工具链成熟(浏览器、代理、抓包、网关都支持)。
- 缺点:每次请求可能重新建连(除非 HTTP/1.1 Keep-Alive 或 HTTP/2 复用),不适合高频双向、服务端主动推送;若用轮询模拟实时,延迟高、浪费带宽。
何时不选:需要服务端主动、实时、双向通信时,应优先考虑 WebSocket 或长连接(TCP Socket),而不是用 HTTP 短轮询硬撑。
5.2 何时用 TCP Socket
适用场景
- 自定义应用层协议:游戏协议、IoT 指令、内部 RPC、二进制流等,希望自己定义报文格式与语义。
- 长连接、服务端主动下发:推送通道、指令下发、状态同步,需要一条长期在线的连接,服务端可随时发数据。
- 对可靠性、顺序有要求:不能丢包、不能乱序,且不想在应用层再做一套确认与重传(由 TCP 保证)。
- 非浏览器环境:App 直连后端、服务端间通信、嵌入式设备等,不依赖浏览器提供的 WebSocket API,可自由选传输层。
特点与注意
- 优点:完全自主的协议设计、可做极致优化(包头、压缩、加密方式自定);适合对延迟、带宽、控制力要求高的场景。
- 缺点:需自己处理粘包/拆包、心跳、重连、断线检测等;开发和维护成本高于直接用 HTTP/WebSocket。
何时不选:若在 Web 端且只需“长连接 + 双向实时”,用 WebSocket 更省事;若可接受丢包、更看重低延迟,可考虑 UDP。
5.3 何时用 UDP Socket
适用场景
- 实时性优先、可接受少量丢包:音视频直播、语音通话、在线游戏(位置、技能同步)、实时监控等;偶尔丢一帧可接受,延迟要低。
- 无连接、一对多:DNS 查询、广播、组播(如内网发现、视频分发);不需要建立“会话”,发完即走。
- 高频小包:传感器上报、心跳、状态上报,包小、量大会放大 TCP 头与建连开销时,UDP 更轻。
特点与注意
- 优点:无建连、头部小、延迟低、适合广播/组播。
- 缺点:不保证可靠、不保证顺序;若业务需要可靠,需在应用层做序号、确认、重传(或选用基于 UDP 的可靠协议如 QUIC)。
何时不选:必须可靠、有序、不能丢(如支付、关键指令、文件传输)时,选 TCP 或基于 TCP 的协议。
5.4 何时用 WebSocket
适用场景
- Web 或混合 App 中需要“长连接 + 双向实时”:聊天、即时通讯、实时通知、推送、实时行情、协作编辑、在线状态、游戏大厅等;服务端需主动推,客户端也需随时发。
- 希望用标准协议、少写底层:握手与帧格式由规范与库(如 OkHttp、浏览器
WebSocket)处理,只需关心“发什么消息、收什么消息”;无需自己设计 TCP 粘包、心跳、关闭语义等。
特点与注意
- 优点:一次 HTTP 握手升级后长连接、全双工、帧格式标准、浏览器原生支持、易过网关与代理。
- 缺点:协议与帧格式固定,不能像裸 TCP 那样随意自定义;若在非浏览器环境且已有自研长连接协议,可继续用 TCP Socket。
何时不选:纯请求-响应、无实时推送需求时用 HTTP 即可;需完全自定义二进制协议或对传输层有特殊要求时,用 TCP/UDP Socket。
5.5 选型对比与决策表
四种方式简要对比
| 维度 | HTTP/HTTPS | WebSocket | TCP Socket | UDP Socket |
|---|---|---|---|---|
| 连接与方向 | 短连接为主,请求-响应 | 长连接,全双工 | 长连接,全双工 | 无连接,可单向/多播 |
| 可靠性 | 基于 TCP 的请求可可靠送达,应用层可对单次请求重试 | 基于 TCP,可靠 | 可靠、有序 | 不保证 |
| 服务端主动推 | 不支持(需轮询/长轮询/SSE) | 支持 | 支持 | 支持(发即可) |
| 协议与格式 | 固定(请求/响应头+体) | 固定(帧格式) | 自定义 | 自定义 |
| 典型场景 | API、网页、下载 | 聊天、推送、实时 | 自研协议、推送、RPC | 音视频、游戏、DNS |
场景 → 方案决策表
| 场景 | 更合适的方案 | 说明 |
|---|---|---|
| 普通 Web 页面、RESTful API、文件下载、表单提交 | HTTP/HTTPS | 无状态、易缓存、工具链成熟 |
| Web/App 聊天、实时通知、推送、行情、协作编辑 | WebSocket (wss://) | 长连接、双向、标准协议、浏览器支持 |
| App 自研长连接、自定义二进制协议、内部 RPC | TCP Socket | 协议与细节完全自控 |
| 音视频、在线游戏、DNS、广播/组播 | UDP,或 TCP+UDP 混合 | 实时性优先,可接受丢包;可靠部分可用 TCP |
| 需要可靠且自定义协议、非浏览器 | TCP Socket | 可靠 + 自定义,不用受 WebSocket 帧格式限制 |
| 只需服务端→客户端单向推送(如日志流、通知) | SSE 或 WebSocket 均可 | SSE 更简单;若后续要双向可一步到位 WebSocket |
选型思路(简记)
- 是否只需请求-响应、无实时推送? → 是则用 HTTP/HTTPS。
- 是否在 Web/App 且要长连接 + 双向实时? → 是则用 WebSocket。
- 是否要完全自定义协议、或非浏览器环境? → 是则用 TCP Socket。
- 是否实时性优先、可接受丢包? → 是则用 UDP 或在 UDP 上做可靠层。
小结
本文从 Socket 基础、TCP/UDP 区别与三次握手四次挥手、Java/Android 编程要点、WebSocket 与 Android 使用、协议与场景选择五部分,串联了从传输层到应用层的常用知识点。
- Socket:传输层 API,是“程序与网络之间的门”;可基于 TCP(可靠、有序、需处理粘包)或 UDP(无连接、不保证可靠)实现任意应用协议。
- TCP:面向连接,三次握手建连、四次挥手断连;保证可靠、有序;应用层需自己解决粘包/拆包与定界。
- UDP:无连接,即发即走;不保证可靠与顺序,适合实时、可丢包场景;单包需注意 MTU 限制。
- WebSocket:基于 TCP 的应用层协议,通过 HTTP 握手升级建立,长连接、全双工、标准帧格式;适合 Web/App 中的聊天、推送、实时通知等;常考握手(GET、101、Sec-WebSocket-Accept 计算)、Ping/Pong 与心跳包、关闭码与生命周期。
- 选型:请求-响应用 HTTP;长连接 + 双向实时用 WebSocket;自定义协议或非浏览器用 TCP Socket;实时优先可丢包用 UDP。Android 上网络 IO 须在子线程,注意生命周期与资源释放,生产环境用 wss。
实际选型时:先看是否只需请求-响应 → 否则看是否要长连接 + 双向实时(是则 WebSocket)→ 否则看是否要完全自定义协议或非浏览器(是则 TCP/UDP Socket)→ 再按可靠性/实时性在 TCP 与 UDP 间取舍。