Android 中的 Socket 和 WebSocket:从一条聊天消息看清区别

在 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。

参考资料

相关推荐
mmsx2 小时前
Android 流式对话实现:分层门面与 SSE 手工解析
android·github·sse
墨狂之逸才3 小时前
Android扫码异常排查小白教程
android
.Hypocritical.3 小时前
计算机网络】吃透 OSI 七层模型与 TCP/IP 协议栈
网络协议·tcp/ip·计算机网络
恋猫de小郭3 小时前
Kotlin 2.5 的 Companion 改造,对 KMP 的重要升级
android·前端·flutter
霸道流氓气质4 小时前
Java调用StableDiffusion/ComfyUI图像生成:DAG工作流编排、WebSocket实时进度与Flux模型集成的生产级实战
java·websocket·stable diffusion
Kapaseker4 小时前
写了这么久 Kotlin,你真的理解 in、out 和 * 吗?
android·kotlin
小七在进步4 小时前
C++内存管理:new/delete vs malloc/free深度解析
android·java·c++
Lzg_na15 小时前
ARP 地址解析协议详解
网络·网络协议
北京盛世宏博16 小时前
通信异常深度排查:以太网温湿度设备 TCP 粘包、UDP 丢包根治方案
单片机·网络协议·tcp/ip·udp·以太网·传感器·温湿度