在 Android 聊天页里,点“发送”后服务器要马上回一条消息。有人会说“用 Socket”,也有人会说“用 WebSocket”。两者都能保持连接、双向传消息,名字又很像,选型时很容易把它们当成同一层的东西。
一句话结论:Socket是应用访问网络连接的编程接口;WebSocket 是建立在 HTTP 握手之上的消息协议。常见的 WebSocket 连接最终也会用到 TCP Socket,但它额外规定了握手、消息帧和关闭方式。
本文以 Android 上的 TCPjava.net.Socket和 OkHttp 4.12.0 的 WebSocket API 为例。示例只演示文本回显;生产环境中的认证、断线重连、心跳策略和wss://证书配置需要按业务补齐。
1. 先把层级摆正
“Socket 和 WebSocket 哪个更快”这个问法有点偏:一个是 API,另一个是协议。实际开发中,大家说的“原生 Socket”通常是指用java.net.Socket直接读写 TCP 字节流;说 WebSocket 则是让库完成握手和帧编解码,业务层收发消息。
在左边,应用自己决定如何从连续字节中切出一条消息;在右边,WebSocket 协议和库已经处理了消息帧。图里展示的是常见的ws://、wss://over TCP 路径;WebSocket 也有其他传输映射,不能把“永远只跑在一条独占 TCP 连接上”当成协议定义。
| 对比项 | TCPSocket | WebSocket |
|---|---|---|
| 所在层 | 操作系统连接的编程接口,本文使用 TCP 字节流 | 应用层消息协议,Android 通常通过库使用 |
| 建立过程 | TCP 建连后即可按自定义格式收发 | 通常先走 HTTP Upgrade 握手,再收发 WebSocket 帧 |
| 数据边界 | TCP 只保证有序字节流;一次read不等于一条消息 | 协议定义文本或二进制消息;库负责帧重组 |
| 双向通信 | TCP 全双工 | 握手后双方均可主动发送消息 |
| 浏览器互通 | 浏览器 JS 不能直接打开任意原生 TCP Socket | 浏览器原生支持 WebSocket API |
| 适用场景 | 自定义设备协议、已有 TCP 私有协议、精细控制字节格式 | 聊天、协同编辑、状态推送,尤其要与 Web 前端共用协议时 |
注意:Socket不是 TCP 的别名,Android/Java 还有用于 UDP 的DatagramSocket。下文提到的“Socket 示例”特指 TCPSocket。
2. 同一条消息,实际怎么走?
下面是两个示例各发一次hello的路径。TCP 侧用换行符\n作为应用自己约定的消息边界;WebSocket 侧把hello交给库的send(),由库编码成协议帧。
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访问宿主机回环地址。
python-mpipinstallwebsockets==13.1python echo_server.py# echo_server.pyimportasyncioimportsocketserverfromthreadingimportThreadfromwebsockets.asyncio.serverimportserveclassTcpEcho(socketserver.StreamRequestHandler):defhandle(self):forlineinself.rfile:self.wfile.write(line)asyncdefws_echo(websocket):asyncformessageinwebsocket:awaitwebsocket.send(message)asyncdefmain():withsocketserver.ThreadingTCPServer(("127.0.0.1",9000),TcpEcho)astcp:Thread(target=tcp.serve_forever,daemon=True).start()asyncwithserve(ws_echo,"127.0.0.1",9001):print("TCP :9000 and WebSocket :9001 are ready")awaitasyncio.Future()asyncio.run(main())TCP 服务端把收到的每一行原样写回;WebSocket 服务端把收到的每条消息原样发回。它们业务结果相同,处理消息边界的责任不同。
Android 工程加入网络权限和 OkHttp 依赖:
<!-- AndroidManifest.xml:将权限和属性合并到工程现有的清单中 --><manifestxmlns:android="http://schemas.android.com/apk/res/android"><uses-permissionandroid:name="android.permission.INTERNET"/><applicationandroid:usesCleartextTraffic="true"/></manifest>// app/build.gradle.ktsdependencies{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 线程连接服务、写入一行、读取一行;两个超时分别限制建连等待和读取等待。
importkotlinx.coroutines.Dispatchersimportkotlinx.coroutines.withContextimportjava.io.BufferedReaderimportjava.io.BufferedWriterimportjava.io.InputStreamReaderimportjava.io.OutputStreamWriterimportjava.net.InetSocketAddressimportjava.net.SocketsuspendfunechoTcp(message:String):String=withContext(Dispatchers.IO){require('\n'!inmessage&&'\r'!inmessage){"演示协议用换行分隔消息,内容不能包含换行"}Socket().use{socket->socket.connect(InetSocketAddress("10.0.2.2",9000),5_000)socket.soTimeout=5_000valwriter=BufferedWriter(OutputStreamWriter(socket.getOutputStream(),Charsets.UTF_8))writer.write(message)writer.newLine()writer.flush()valreader=BufferedReader(InputStreamReader(socket.getInputStream(),Charsets.UTF_8))reader.readLine()?:error("服务端在返回消息前关闭了连接")}}调用echoTcp("hello")会得到hello。这段示例每次调用创建并关闭一条连接,方便看清过程;长连接应用还要设计读写循环、发送队列、断线处理和取消时关闭 Socket。尤其不要把read()返回的一块字节当成完整业务消息:TCP 可能拆开或合并多次写入。
WebSocket:把消息交给协议库
下面的类保留一条 WebSocket 连接。onOpen后发送hello,收到服务端回显时进入onMessage。OkHttp 的监听回调不保证运行在主线程,更新 Android UI 时要切回主线程。
importokhttp3.OkHttpClientimportokhttp3.Requestimportokhttp3.Responseimportokhttp3.WebSocketimportokhttp3.WebSocketListenerclassEchoWebSocket(privatevalclient:OkHttpClient,privatevalonText:(String)->Unit,privatevalonError:(Throwable)->Unit,){privatevarsocket:WebSocket?=nullfunconnect(){valrequest=Request.Builder().url("ws://10.0.2.2:9001/").build()socket=client.newWebSocket(request,object:WebSocketListener(){overridefunonOpen(webSocket:WebSocket,response:Response){webSocket.send("hello")}overridefunonMessage(webSocket:WebSocket,text:String){onText(text)}overridefunonFailure(webSocket:WebSocket,t:Throwable,response:Response?){onError(t)}})}funsend(text:String):Boolean=socket?.send(text)?:falsefunclose(){socket?.close(1000,"页面结束")socket=null}}例如在Activity的onCreate()中创建一次EchoWebSocket(OkHttpClient(), { text -> runOnUiThread { /* 显示 text */ } }, { error -> runOnUiThread { /* 显示 error */ } })并调用connect();在onDestroy()调用close()。实际项目通常复用应用级OkHttpClient,并按连接所属的页面或业务作用域管理生命周期。send()返回false表示消息未进入发送队列,不能把它当成发送成功的确认;需要交付保证时,应设计业务层确认。
4. 选型时真正要问什么?
如果后端已经有自定义 TCP 协议,或者你需要与设备约定字节级格式,就用 TCPSocket,同时把长度字段、分隔符或其他封包规则写清楚。上面的换行协议简单,但无法直接承载任意包含换行的文本或二进制数据。
如果要做聊天、状态推送,并希望 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:明文流量策略。