Socket 与 WebSocket 完全指南

26 阅读44分钟

一、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,对应到接口里通常有两个类型常量。

类型底层协议常见常量名通俗理解
流式 SocketTCPSOCK_STREAM像打电话:先拨号接通(建连接),再说话,说完挂断(断连接);顺序一致、一般不丢。
数据报 SocketUDPSOCK_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 五方面对比如下:

特性TCPUDP
连接方式面向连接。通信前必须先建立连接(三次握手),通信结束后要释放连接(四次挥手);双方在连接期间维护状态(序号、窗口等)。无连接。不需要事先建连,也不存在“断开连接”;发数据时带上目标地址和端口即可,每个数据报独立传输。
可靠性保证可靠传输。通过序号、确认、重传、校验和等机制,确保数据不丢、不重复、按序到达;若丢包会重传,应用层拿到的是完整、有序的字节流。不保证可靠。发出去就不管,不确认、不重传;可能丢包、乱序、重复,需要可靠时要在应用层自己做确认与重传。
数据形式字节流,没有报文边界。多次 write 可能被合并成一次送达,一次 read 可能读到多次 write 的内容;应用层要自己定界(粘包/拆包)。数据报,有边界。一次 send 对应一个报文,一次 recv 对应一个完整报文,不会粘在一起。
开销与性能有建连、维护、断连的开销;有确认、重传、流量控制、拥塞控制,延迟和头部都更大,适合对可靠性要求高的场景。无建连,头部小,没有确认与重传,延迟低、开销小,适合能容忍丢包、更看重实时性的场景。
典型 APISocket / ServerSocketDatagramSocket / 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 客户端编程

步骤概览

  1. 创建 Socketnew Socket() 或先 new Socket()connect();或直接用 new Socket(host, port) 一步完成创建与连接。
  2. 连接:若分步则调用 socket.connect(new InetSocketAddress(host, port), connectTimeout),建议始终传入连接超时(毫秒),避免在不可达地址上长时间阻塞。
  3. 获取流socket.getInputStream()socket.getOutputStream(),用 BufferedInputStream / BufferedOutputStream 或 DataInputStream / DataOutputStream 按需包装,便于按行、按块或按协议读写。
  4. 读写:按应用层协议在流上读写;注意 TCP 是字节流,需自己定界(长度前缀、分隔符等),处理粘包/拆包。
  5. 关闭:用完后调用 socket.close()(触发四次挥手),放在 finally 或 try-with-resources 中,避免异常时未关闭导致泄漏。

超时与线程(必设)

  • 连接超时Socket.connect(address, connectTimeoutMs) 的第二个参数;超时未连上会抛 SocketTimeoutException
  • 读超时Socket.setSoTimeout(readTimeoutMs);在 read() 上阻塞超过该时间会抛 SocketTimeoutException,可用于轮询或心跳场景。
  • 线程connect()read()/write() 都会阻塞,在 Android 上必须在子线程或协程(如 Kotlin Dispatchers.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 服务端编程

步骤概览

  1. 创建 ServerSocketnew ServerSocket()new ServerSocket(port);若只在本机监听可用 new ServerSocket(port, 0, InetAddress.getByName("127.0.0.1"))
  2. 绑定端口:若用无参构造,则需 serverSocket.bind(new InetSocketAddress(port));可指定 backlog(等待连接队列长度)。
  3. 接受连接Socket clientSocket = serverSocket.accept();该方法阻塞,直到有客户端连上才返回。返回的 clientSocket 专门与该客户端通信。
  4. 与客户端通信:用 clientSocket.getInputStream()/getOutputStream() 读写;处理逻辑建议放到单独线程或线程池,避免阻塞下一次 accept()
  5. 关闭:与该客户端的会话结束后 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,系统会在首次发送时分配临时端口)。

发送步骤

  1. 创建 DatagramSocket一次):可先 bind(本机地址, 端口) 固定本端端口,或不 bind 由系统分配临时端口。
  2. 构造 发送用的 DatagramPacketnew DatagramPacket(byte[] data, int length, InetAddress address, int port),传入数据、长度、目标 IP 与端口。
  3. 发送socket.send(packet);每次 send 对应一个独立数据报,无连接概念,可多次 send 到不同目标(同一 Socket 可发往多端)。

接收步骤(用上面同一条已创建的 Socket;若需收包则需先 bind 一次

  1. 绑定(若尚未绑定):若需在本机某端口收包,执行一次 socket.bind(new InetSocketAddress(port))(或构造 Socket 时指定端口)。
  2. 构造 接收缓冲区:new DatagramPacket(byte[] buffer, int length)
  3. 接收socket.receive(packet)(阻塞直到收到一个数据报)。
  4. 解析:从 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_STREAMSOCK_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-finallytry-with-resources 中关闭 SocketServerSocketDatagramSocket,否则会泄漏文件描述符;在 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,再切回主线程(如 runOnUiThreadLiveData、协程 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 里用 SocketServerSocket 就是典型的 BIO。
  • NIO(New I/O,在 Java 里常指非阻塞 IO + 多路复用):用 java.nio.channelsChannel(如 SocketChannelServerSocketChannel)可设为非阻塞,再配合 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)。

典型流程(服务端)

  1. 打开 ServerSocketChannel,bind 端口,设为非阻塞。
  2. 创建 Selector,把 ServerSocketChannel 注册到 Selector,关注 OP_ACCEPT
  3. 循环:selector.select() 等待有事件;返回后遍历 selectedKeys(),若是 ACCEPT 则 accept() 得到 SocketChannel,把该 SocketChannel 也注册到 Selector(关注 OP_READ/OP_WRITE);若是 READ 则对该 Channel 做 read() 等。
  4. 用少量线程(甚至单线程)即可处理大量连接,因为线程不会卡在某个连接的 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 并从池移除,避免再次被取出导致数据错乱。

线程安全

多线程取/还连接时,池的取放逻辑须线程安全(如 BlockingQueuesynchronizedReentrantLock);取出后标记占用、归还后标记空闲,保证一条连接同一时刻只被一个线程使用。

与 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: websocket
    • Connection: Upgrade
    • Sec-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:用于协商子协议(如 chatsoap),服务器在响应头中选一个返回,表示双方使用该子协议。

与轮询、长轮询、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(connectacceptreadreceive 等)必须在非主线程执行,如 ThreadExecutorService、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 需切回主线程(runOnUiThreadHandler、协程 Dispatchers.Main)。
  • 地址与证书:生产环境应使用 wss://;与 HTTPS 一样会校验证书,需保证服务器证书有效、主机名一致,否则会回调 onFailure
  • 重连:在 onFailureonClosed 中根据业务决定是否重连;可采用指数退避(如 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: websocketConnection: UpgradeSec-WebSocket-KeySec-WebSocket-Version: 13;服务器同意则返回 101 Switching ProtocolsSec-WebSocket-Accept
Sec-WebSocket-Accept 计算将客户端的 Sec-WebSocket-Key 与固定 GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" 拼接 → SHA-1 哈希 → Base64 编码。
为什么用 WebSocket 不用轮询全双工、长连接、低延迟、少无效请求;短轮询延迟高、浪费带宽;长轮询仍是一问一答,建连频繁。
帧类型 Opcode0x1 文本、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 销毁时应在 onDestroyclose WebSocket 并置空引用;Listener 若持有 Activity 易造成内存泄漏,可用弱引用或销毁时 cancel。
重连策略onFailure/onClosed 中按业务决定是否重连;常用指数退避(如 1s、2s、4s…)并设最大重试次数;重连前先关闭旧连接,避免重复建连与回调错乱。

对比与选型(一句话)

考点要点
Socket 与 WebSocketSocket 是传输层 API(TCP/UDP),可自定义协议;WebSocket 是应用层协议,跑在 TCP 上,有标准握手与帧格式,浏览器原生支持。
长轮询 / SSE / WebSocket长轮询:一问一答、实现复杂;SSE:仅服务端→客户端单向;WebSocket:全双工、双向实时,适合聊天、推送等。
何时用 WebSocket需要长连接、服务端主动推送、低延迟双向通信(聊天、实时通知、协作等),且希望用标准协议、少写底层时。

五、协议与场景选择

本节说明在不同业务场景下如何选择 HTTP/HTTPSTCP SocketUDP SocketWebSocket,便于面试与实战选型。


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/HTTPSWebSocketTCP SocketUDP Socket
连接与方向短连接为主,请求-响应长连接,全双工长连接,全双工无连接,可单向/多播
可靠性基于 TCP 的请求可可靠送达,应用层可对单次请求重试基于 TCP,可靠可靠、有序不保证
服务端主动推不支持(需轮询/长轮询/SSE)支持支持支持(发即可)
协议与格式固定(请求/响应头+体)固定(帧格式)自定义自定义
典型场景API、网页、下载聊天、推送、实时自研协议、推送、RPC音视频、游戏、DNS

场景 → 方案决策表

场景更合适的方案说明
普通 Web 页面、RESTful API、文件下载、表单提交HTTP/HTTPS无状态、易缓存、工具链成熟
Web/App 聊天、实时通知、推送、行情、协作编辑WebSocket (wss://)长连接、双向、标准协议、浏览器支持
App 自研长连接、自定义二进制协议、内部 RPCTCP Socket协议与细节完全自控
音视频、在线游戏、DNS、广播/组播UDP,或 TCP+UDP 混合实时性优先,可接受丢包;可靠部分可用 TCP
需要可靠且自定义协议、非浏览器TCP Socket可靠 + 自定义,不用受 WebSocket 帧格式限制
只需服务端→客户端单向推送(如日志流、通知)SSE 或 WebSocket 均可SSE 更简单;若后续要双向可一步到位 WebSocket

选型思路(简记)

  1. 是否只需请求-响应、无实时推送? → 是则用 HTTP/HTTPS
  2. 是否在 Web/App 且要长连接 + 双向实时? → 是则用 WebSocket
  3. 是否要完全自定义协议、或非浏览器环境? → 是则用 TCP Socket
  4. 是否实时性优先、可接受丢包? → 是则用 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 间取舍。