在 Android 聊天页里,点"发送"后服务器要马上回一条消息。有人会说"用 Socket",也有人会说"用 WebSocket"。两者都能保持连接、双向传消息,名字又很像,选型时很容易把它们当成同一层的东西。
一句话结论:Socket 是应用访问网络连接的编程接口;WebSocket 是建立在 HTTP 握手之上的消息协议。常见的 WebSocket 连接最终也会用到 TCP Socket,但它额外规定了握手、消息帧和关闭方式。
本文以 Android 上的 TCP java.net.Socket 和 OkHttp 4.12.0 的 WebSocket API 为例。示例只演示文本回显;生产环境中的认证、断线重连、心跳策略和 wss:// 证书配置需要按业务补齐。
1. 先把层级摆正
"Socket 和 WebSocket 哪个更快"这个问法有点偏:一个是 API,另一个是协议。实际开发中,大家说的"原生 Socket"通常是指用 java.net.Socket 直接读写 TCP 字节流;说 WebSocket 则是让库完成握手和帧编解码,业务层收发消息。
#mermaid-svg-ionLxOzbr8ttVAYx{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ionLxOzbr8ttVAYx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ionLxOzbr8ttVAYx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ionLxOzbr8ttVAYx .error-icon{fill:#552222;}#mermaid-svg-ionLxOzbr8ttVAYx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ionLxOzbr8ttVAYx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ionLxOzbr8ttVAYx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ionLxOzbr8ttVAYx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ionLxOzbr8ttVAYx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ionLxOzbr8ttVAYx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ionLxOzbr8ttVAYx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ionLxOzbr8ttVAYx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ionLxOzbr8ttVAYx .marker.cross{stroke:#333333;}#mermaid-svg-ionLxOzbr8ttVAYx svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ionLxOzbr8ttVAYx p{margin:0;}#mermaid-svg-ionLxOzbr8ttVAYx .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ionLxOzbr8ttVAYx .cluster-label text{fill:#333;}#mermaid-svg-ionLxOzbr8ttVAYx .cluster-label span{color:#333;}#mermaid-svg-ionLxOzbr8ttVAYx .cluster-label span p{background-color:transparent;}#mermaid-svg-ionLxOzbr8ttVAYx .label text,#mermaid-svg-ionLxOzbr8ttVAYx span{fill:#333;color:#333;}#mermaid-svg-ionLxOzbr8ttVAYx .node rect,#mermaid-svg-ionLxOzbr8ttVAYx .node circle,#mermaid-svg-ionLxOzbr8ttVAYx .node ellipse,#mermaid-svg-ionLxOzbr8ttVAYx .node polygon,#mermaid-svg-ionLxOzbr8ttVAYx .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ionLxOzbr8ttVAYx .rough-node .label text,#mermaid-svg-ionLxOzbr8ttVAYx .node .label text,#mermaid-svg-ionLxOzbr8ttVAYx .image-shape .label,#mermaid-svg-ionLxOzbr8ttVAYx .icon-shape .label{text-anchor:middle;}#mermaid-svg-ionLxOzbr8ttVAYx .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ionLxOzbr8ttVAYx .rough-node .label,#mermaid-svg-ionLxOzbr8ttVAYx .node .label,#mermaid-svg-ionLxOzbr8ttVAYx .image-shape .label,#mermaid-svg-ionLxOzbr8ttVAYx .icon-shape .label{text-align:center;}#mermaid-svg-ionLxOzbr8ttVAYx .node.clickable{cursor:pointer;}#mermaid-svg-ionLxOzbr8ttVAYx .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ionLxOzbr8ttVAYx .arrowheadPath{fill:#333333;}#mermaid-svg-ionLxOzbr8ttVAYx .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ionLxOzbr8ttVAYx .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ionLxOzbr8ttVAYx .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ionLxOzbr8ttVAYx .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ionLxOzbr8ttVAYx .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ionLxOzbr8ttVAYx .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ionLxOzbr8ttVAYx .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ionLxOzbr8ttVAYx .cluster text{fill:#333;}#mermaid-svg-ionLxOzbr8ttVAYx .cluster span{color:#333;}#mermaid-svg-ionLxOzbr8ttVAYx div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ionLxOzbr8ttVAYx .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ionLxOzbr8ttVAYx rect.text{fill:none;stroke-width:0;}#mermaid-svg-ionLxOzbr8ttVAYx .icon-shape,#mermaid-svg-ionLxOzbr8ttVAYx .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ionLxOzbr8ttVAYx .icon-shape p,#mermaid-svg-ionLxOzbr8ttVAYx .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ionLxOzbr8ttVAYx .icon-shape .label rect,#mermaid-svg-ionLxOzbr8ttVAYx .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ionLxOzbr8ttVAYx .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ionLxOzbr8ttVAYx .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ionLxOzbr8ttVAYx :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 原生 TCP
WebSocket
Android 业务代码
选择通信方式
java.net.Socket
OkHttp WebSocket API
自定义消息格式与边界
HTTP Upgrade 后收发 WebSocket 帧
TCP 连接
IP 网络
在左边,应用自己决定如何从连续字节中切出一条消息;在右边,WebSocket 协议和库已经处理了消息帧。图里展示的是常见的 ws://、wss:// over TCP 路径;WebSocket 也有其他传输映射,不能把"永远只跑在一条独占 TCP 连接上"当成协议定义。
| 对比项 | TCP Socket |
WebSocket |
|---|---|---|
| 所在层 | 操作系统连接的编程接口,本文使用 TCP 字节流 | 应用层消息协议,Android 通常通过库使用 |
| 建立过程 | TCP 建连后即可按自定义格式收发 | 通常先走 HTTP Upgrade 握手,再收发 WebSocket 帧 |
| 数据边界 | TCP 只保证有序字节流;一次 read 不等于一条消息 |
协议定义文本或二进制消息;库负责帧重组 |
| 双向通信 | TCP 全双工 | 握手后双方均可主动发送消息 |
| 浏览器互通 | 浏览器 JS 不能直接打开任意原生 TCP Socket | 浏览器原生支持 WebSocket API |
| 适用场景 | 自定义设备协议、已有 TCP 私有协议、精细控制字节格式 | 聊天、协同编辑、状态推送,尤其要与 Web 前端共用协议时 |
注意:Socket 不是 TCP 的别名,Android/Java 还有用于 UDP 的 DatagramSocket。下文提到的"Socket 示例"特指 TCP Socket。
2. 同一条消息,实际怎么走?
下面是两个示例各发一次 hello 的路径。TCP 侧用换行符 \n 作为应用自己约定的消息边界;WebSocket 侧把 hello 交给库的 send(),由库编码成协议帧。
回显服务 Android 回显服务 Android #mermaid-svg-mzpjMmvd3pKqYIwG{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-mzpjMmvd3pKqYIwG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-mzpjMmvd3pKqYIwG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-mzpjMmvd3pKqYIwG .error-icon{fill:#552222;}#mermaid-svg-mzpjMmvd3pKqYIwG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-mzpjMmvd3pKqYIwG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-mzpjMmvd3pKqYIwG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-mzpjMmvd3pKqYIwG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-mzpjMmvd3pKqYIwG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-mzpjMmvd3pKqYIwG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-mzpjMmvd3pKqYIwG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-mzpjMmvd3pKqYIwG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-mzpjMmvd3pKqYIwG .marker.cross{stroke:#333333;}#mermaid-svg-mzpjMmvd3pKqYIwG svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-mzpjMmvd3pKqYIwG p{margin:0;}#mermaid-svg-mzpjMmvd3pKqYIwG .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-mzpjMmvd3pKqYIwG text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-mzpjMmvd3pKqYIwG .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-mzpjMmvd3pKqYIwG .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-mzpjMmvd3pKqYIwG .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-mzpjMmvd3pKqYIwG .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-mzpjMmvd3pKqYIwG #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-mzpjMmvd3pKqYIwG .sequenceNumber{fill:white;}#mermaid-svg-mzpjMmvd3pKqYIwG #sequencenumber{fill:#333;}#mermaid-svg-mzpjMmvd3pKqYIwG #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-mzpjMmvd3pKqYIwG .messageText{fill:#333;stroke:none;}#mermaid-svg-mzpjMmvd3pKqYIwG .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-mzpjMmvd3pKqYIwG .labelText,#mermaid-svg-mzpjMmvd3pKqYIwG .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-mzpjMmvd3pKqYIwG .loopText,#mermaid-svg-mzpjMmvd3pKqYIwG .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-mzpjMmvd3pKqYIwG .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-mzpjMmvd3pKqYIwG .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-mzpjMmvd3pKqYIwG .noteText,#mermaid-svg-mzpjMmvd3pKqYIwG .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-mzpjMmvd3pKqYIwG .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-mzpjMmvd3pKqYIwG .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-mzpjMmvd3pKqYIwG .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-mzpjMmvd3pKqYIwG .actorPopupMenu{position:absolute;}#mermaid-svg-mzpjMmvd3pKqYIwG .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-mzpjMmvd3pKqYIwG .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-mzpjMmvd3pKqYIwG .actor-man circle,#mermaid-svg-mzpjMmvd3pKqYIwG line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-mzpjMmvd3pKqYIwG :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} TCP Socket 路径 WebSocket 路径 TCP 建连 写入 UTF-8 的 hello 与换行 读到换行,回写 hello 与换行 关闭 TCP 连接 TCP 建连,发送 HTTP Upgrade 请求 101 Switching Protocols 发送文本消息 hello 返回文本消息 hello 发送 WebSocket Close 帧
WebSocket 握手成功后,这条连接不再按普通 HTTP 请求与响应的方式交换业务数据。图中 101 是典型的 HTTP/1.1 Upgrade 结果;服务器必须理解 WebSocket 握手,普通 TCP 回显服务和普通 HTTP 接口都不能直接充当 WebSocket 服务端。
3. Android 上跑一个最小回显例子
先在电脑上运行下方 Python 3 服务。它分别监听 9000(TCP)和 9001(WebSocket)。这里固定 websockets==13.1,使服务端代码与安装命令对应;示例只监听本机,Android 模拟器用 10.0.2.2 访问宿主机回环地址。
bash
python -m pip install websockets==13.1
python echo_server.py
python
# echo_server.py
import asyncio
import socketserver
from threading import Thread
from websockets.asyncio.server import serve
class TcpEcho(socketserver.StreamRequestHandler):
def handle(self):
for line in self.rfile:
self.wfile.write(line)
async def ws_echo(websocket):
async for message in websocket:
await websocket.send(message)
async def main():
with socketserver.ThreadingTCPServer(("127.0.0.1", 9000), TcpEcho) as tcp:
Thread(target=tcp.serve_forever, daemon=True).start()
async with serve(ws_echo, "127.0.0.1", 9001):
print("TCP :9000 and WebSocket :9001 are ready")
await asyncio.Future()
asyncio.run(main())
TCP 服务端把收到的每一行原样写回;WebSocket 服务端把收到的每条消息原样发回。它们业务结果相同,处理消息边界的责任不同。
Android 工程加入网络权限和 OkHttp 依赖:
xml
<!-- AndroidManifest.xml:将权限和属性合并到工程现有的清单中 -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application android:usesCleartextTraffic="true" />
</manifest>
kotlin
// app/build.gradle.kts
dependencies {
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
}
ws:// 是明文连接,Android 9(API 28)及以上的默认明文策略可能拦截它;这里打开明文仅为本机演示。上线应使用 wss://,并把明文例外限制到确有需要的目标。真实设备不能使用模拟器专用的 10.0.2.2,需要换成电脑的局域网地址并调整服务监听地址与防火墙。
TCP:自己定义"读到哪里算一条消息"
把下面的函数放到 Android 工程的 Kotlin 文件中,并在协程里调用。它在 I/O 线程连接服务、写入一行、读取一行;两个超时分别限制建连等待和读取等待。
kotlin
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.io.BufferedWriter
import java.io.InputStreamReader
import java.io.OutputStreamWriter
import java.net.InetSocketAddress
import java.net.Socket
suspend fun echoTcp(message: String): String = withContext(Dispatchers.IO) {
require('\n' !in message && '\r' !in message) {
"演示协议用换行分隔消息,内容不能包含换行"
}
Socket().use { socket ->
socket.connect(InetSocketAddress("10.0.2.2", 9000), 5_000)
socket.soTimeout = 5_000
val writer = BufferedWriter(OutputStreamWriter(socket.getOutputStream(), Charsets.UTF_8))
writer.write(message)
writer.newLine()
writer.flush()
val reader = BufferedReader(InputStreamReader(socket.getInputStream(), Charsets.UTF_8))
reader.readLine() ?: error("服务端在返回消息前关闭了连接")
}
}
调用 echoTcp("hello") 会得到 hello。这段示例每次调用创建并关闭一条连接,方便看清过程;长连接应用还要设计读写循环、发送队列、断线处理和取消时关闭 Socket。尤其不要把 read() 返回的一块字节当成完整业务消息:TCP 可能拆开或合并多次写入。
WebSocket:把消息交给协议库
下面的类保留一条 WebSocket 连接。onOpen 后发送 hello,收到服务端回显时进入 onMessage。OkHttp 的监听回调不保证运行在主线程,更新 Android UI 时要切回主线程。
kotlin
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
class EchoWebSocket(
private val client: OkHttpClient,
private val onText: (String) -> Unit,
private val onError: (Throwable) -> Unit,
) {
private var socket: WebSocket? = null
fun connect() {
val request = Request.Builder()
.url("ws://10.0.2.2:9001/")
.build()
socket = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
webSocket.send("hello")
}
override fun onMessage(webSocket: WebSocket, text: String) {
onText(text)
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
onError(t)
}
})
}
fun send(text: String): Boolean = socket?.send(text) ?: false
fun close() {
socket?.close(1000, "页面结束")
socket = null
}
}
例如在 Activity 的 onCreate() 中创建一次 EchoWebSocket(OkHttpClient(), { text -> runOnUiThread { /* 显示 text */ } }, { error -> runOnUiThread { /* 显示 error */ } }) 并调用 connect();在 onDestroy() 调用 close()。实际项目通常复用应用级 OkHttpClient,并按连接所属的页面或业务作用域管理生命周期。send() 返回 false 表示消息未进入发送队列,不能把它当成发送成功的确认;需要交付保证时,应设计业务层确认。
4. 选型时真正要问什么?
如果后端已经有自定义 TCP 协议,或者你需要与设备约定字节级格式,就用 TCP Socket,同时把长度字段、分隔符或其他封包规则写清楚。上面的换行协议简单,但无法直接承载任意包含换行的文本或二进制数据。
如果要做聊天、状态推送,并希望 Android 与浏览器沿用同一个实时协议,我会先选 WebSocket:消息边界和跨端支持已经有标准,OkHttp 也替你处理握手与帧。这个建议的前提是服务端支持 WebSocket;只做偶发请求时,普通 HTTP 往往更省事。
两者都不是"连上就永不掉线"。移动网络切换、页面退出和服务端重启都会打断连接。应把重连退避、心跳、鉴权刷新及消息补偿放到业务设计中,而不是期待换一个 API 自动解决。
回到最开始的 hello:TCP 示例传的是按换行切开的字节流;WebSocket 示例传的是有边界的文本消息。先确认对端说什么协议,再决定 Android 端用哪个 API。
参考资料
- RFC 9293:Transmission Control Protocol:TCP 字节流与连接语义。
- RFC 6455:The WebSocket Protocol:握手、消息帧和关闭过程。
- Android Emulator networking:模拟器的
10.0.2.2地址。 - Android Network Security Configuration:明文流量策略。