前言
在前一篇文章《多进程:为什么要把消息服务拆分到一个独立的进程?》中我们出于保证连接的稳定性的目的,将应用拆分成了「主进程」和「通讯进程」,并为二者定义了相互通信的接口。即便如此,我们也只是实现了客户端一侧的进程间通信,而要实现与完整聊天系统中另一端的角色——服务端的通信,则需依靠「网络通信协议」来协助完成,在此我们选用的是WebSocket协议。
什么是WebSocket?
WebSocket一词,从词面上可以拆解为 Web & Socket 两个单词,Socket我们并不陌生,其是对处于网络中不同主机上的应用进程之间进行双向通信的端点的抽象,是应用程序通过网络协议进行通信的接口,一个Socket对应着通信的一端,由IP地址和端口组合而成。需要注意的是,Socket并不是具体的一种协议,而是一个逻辑上的概念。
那么WebSocket和Socket之间存在着什么联系呢,是否可以理解为是Socket概念在Web环境的移植呢?为了解答这个疑惑,我们先来回顾一下,在Java平台上进行Socket编程的流程:
- 服务端创建ServerSocket实例并绑定本地端口进行监听
- 客户端创建Socket实例并指定要连接的服务端的IP地址和端口
- 客户端发起连接请求,服务端成功接受之后,双方就建立了一个端对端的TCP连接,在该连接上可以双向通信。而后服务端继续处于监听状态,接受其他客户端的连接请求。
上述流程还可以简化为:
- 服务端监听
- 客户端请求
- 连接确认
与之类似,WebSocket服务端与客户端之间的通信过程可以描述为:
- 服务端创建包含有效主机与端口的WebSocket实例,随后启动并等待客户端连接
- 客户端创建WebSocket实例,并为该实例提供一个URL,该URL代表希望连接的服务器端点
-
客户端通过HTTP请求握手建立连接之后,后面就使用刚才发起HTTP请求的TCP连接进行双向通信。
WebSocket协议最初是HTML5规范的一部分,但后来移至单独的标准文档中以使规范集中化,其借鉴了Socket的思想,通过单个TCP连接,为Web浏览器端与服务端之间提供了一种全双工通信机制。WebSocket协议旨在与现有的Web基础体系结构良好配合,基于此设计原则,协议规范定义了WebSocket协议握手流程需借助HTTP协议进行,并被设计工作在与HTTP(80)和HTTPS(443)相同的端口,也支持HTTP代理和中间件,以保证能完全向后兼容。
由于WebSocket本身只是一个应用层协议,原则上只要遵循这个协议的客户端均可使用,因此我们才得以将其运用到我们的Android客户端。
什么是全双工通信?
简单来讲,就是通信双方(客户端和服务端)可同时向对方发送消息。为什么这一点很重要呢?因为传统的基于HTTP协议的通信是单向的,只能由客户端发起,服务端无法主动向客户端推送信息。一旦面临即时通讯这种对数据实时性要求很高的场景,当服务端有数据更新而客户端要获知,就只能通过客户端轮询的方式,具体又可分为以下两种轮询策略:
- 短轮询
即客户端定时向服务端发送请求,服务端收到请求后马上返回响应并关闭连接。
优点:实现简单
缺点:
1.并发请求对服务端造成较大压力
2.数据可能没有更新,造成无效请求
3.频繁的网络请求导致客户端设备电量、流量快速消耗
4.定时操作存在时间差,可能造成数据同步不及时
5.每次请求都需要携带完整的请求头
- 长轮询
即服务端在收到请求之后,如果数据无更新,会阻塞请求,直至数据更新或连接超时才返回。
优点:相较于短轮询减少了HTTP请求的次数,节省了部分资源。
缺点:
1.连接挂起同样会消耗资源
2.冗余请求头问题依旧存在
与上述两个方案相比,WebSocket的优势在于,当连接建立之后,后续的数据都是以帧的形式发送。除非某一端主动断开连接,否则无需重新建立连接。因此可以做到:
1.减轻服务器的负担
2.极大地减少不必要的流量、电量消耗
3.提高实时性,保证客户端和服务端数据的同步
4.减少冗余请求头造成的开销
除了WebSocket,实现移动端即时通讯的还有哪些技术?
- XMPP
全称(Extensible Messaging and Presence Protocol,可扩展通讯和表示协议),是一种基于XML的协议,它继承了在XML环境中灵活的发展性。
XMPP中定义了三个角色,客户端,服务器,网关。通信能够在这三者的任意两个之间双向发生。服务器同时承担了客户端信息记录,连接管理和信息的路由功能。网关承担着与异构即时通信系统的互联互通,异构系统可以包括SMS(短信),MSN,ICQ等。基本的网络形式是单客户端通过TCP/IP连接到单服务器,然后在之上传输XML。
优点
1.超强的可扩展性。经过扩展以后的XMPP可以通过发送扩展的信息来处理用户的需求。
2.易于解析和阅读。方便了开发和查错。
3.开源。在客户端、服务器、组件、源码库等方面,都已经各自有多种实现。
缺点
1.数据负载太重。过多的冗余标签、低效的解析效率使得XMPP在移动设备上表现不佳。
应用场景举例:点对点单聊约球
我刚毕业时入职的公司曾接手开发一个线上足球约战的社交平台APP项目,当时为了提高约球时的沟通效率,考虑为应用引入聊天模块,并优先实现点对点单聊功能。那时市面上的即时通讯SDK方案还尚未成熟,综合当时团队成员的技术栈,决定采用XMPP+Openfire+Smack作为自研技术搭建聊天框架。
Openfire基于XMPP协议,采用Java开发,可用于构建高效的即时通信服务器端,单台服务器可支持上万并发用户。Openfire安装和使用都非常简单,并利用Web进行管理。由于是采用开放的XMPP协议,因此可以使用各种支持XMPP协议的IM客户端软件登录服务。
Smack是一个开源的、易于使用的XMPP客户端Java类库,提供了一套可扩展的API。
- MQTT
全称(Message Queuing Telemetry Transport,消息队列遥测传输协议),是一种基于发布/订阅模式的“轻量级”通讯协议,其构建于TCP/IP协议之上。MQTT最大优点在于,可以以极少的代码和有限的带宽,为连接远程设备提供实时可靠的消息服务。作为一种低开销、低带宽占用的即时通讯协议,使其在物联网、小型设备、移动应用等方面有较广泛的应用。
特点
1.基于发布/订阅模型。提供一对多的消息发布,解除应用程序耦合。
2.低开销。MQTT客户端很轻巧,只需要最少的资源,同时MQTT消息头也很小,可以优化网络带宽。
3.可靠的消息传递。MQTT定义了3种消息发布服务质量,以支持消息可靠性:至多一次,至少一次,只有一次。
4.对不可靠网络的支持。专为受限设备和低带宽、高延迟或不可靠的网络而设计。
应用场景举例:赔率更新、赛事直播聊天室
我第二家入职的公司的主打产品是一款提供模拟竞猜、赛事直播的体育类APP,其中核心的功能模块就是提供各种赛事的最新比分赔率数据,最初采用的即是上文所说的低效的HTTP轮询方案,效果可想而知。后面技术重构后改用了MQTT,极大地减少了对网络环境的依赖,提高了数据的实时性和可靠性。再往后搭建直播模块时,考虑到聊天室这种一对多的消息发布场景同样适合用MQTT解决,于是沿用了原先的技术方案扩展了新的聊天室模块。
- WebSocket
而相较之下,WebSocket的特点包括:
1.较少的控制开销。在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。
2.更好的二进制支持。Websocket定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容。
3.可以支持扩展。Websocket定义了扩展,用户可以扩展协议、实现部分自定义的子协议,如以上所说的XMPP协议、MQTT协议等。
WebSocket协议在Android客户端的实现
实现WebSocket协议很简单,广为Android开发者使用的网络请求框架——OkHttp对WebSocket通信流程进行了封装,提供了简明的接口用于WebSocket的连接建立、数据收发、连接保活、连接关闭等,使我们可以专注于业务实现而无须关注通信细节,简单到我们只需要实现以下两步:
- 创建WebSocket实例并提供一个URL以指定要连接的服务器地址
- 提供一个WebSocket连接事件监听器,用于监听事件回调以处理连接生命周期的每个阶段
WebSocket URL的构成与Http URL很相似,都是由协议、主机、端口、路径等构成,区别就是WebSocket URL的协议名采用的是ws://和wss://,wss://表明是安全的WebSocket连接。
首先我们在项目中引入OkHttp库的依赖:
implementation("com.squareup.okhttp3:okhttp:4.9.0")
其次,我们须指定要连接的服务器地址,此处可以使用WebSocket的官方服务器地址:
/** WebSocket服务器地址 */
private var serverUrl: String = "ws://echo.websocket.org"
@Synchronized
fun connect() {
val request = Request.Builder().url(serverUrl).build()
val okHttpClient = OkHttpClient.Builder().callTimeout(20, TimeUnit.SECONDS).build()
...
}
接着,我们调用OkHttpClient实例的newWebSocket(request: Request, listener: WebSocketListener)方法,该方法需传入两个参数,第一个是上文构建的Request对象,第二个是WebSocket连接事件的监听器,WebSocket协议包含四个主要的事件:
- Open:客户端和服务器之间建立了连接后触发
- Message:服务端向客户端发送数据时触发。发送的数据可以是纯文本或二进制数据
- Close:服务端与客户端之间的通信结束时触发。
- Error:通信过程中发生错误时触发。
每个事件都通过分别实现对应的回调来进行处理。OkHttp提供的监听器包含以下回调:
abstract class WebSocketListener {
open fun onOpen(webSocket: WebSocket, response: Response) {}
open fun onMessage(webSocket: WebSocket, text: String) {}
open fun onMessage(webSocket: WebSocket, bytes: ByteString) {}
open fun onClosing(webSocket: WebSocket, code: Int, reason: String) {}
open fun onClosed(webSocket: WebSocket, code: Int, reason: String) {}
open fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {}
}
object WebSocketConnection : WebSocketListener()
@Synchronized
fun connect() {
...
webSocketClient = okHttpClient.newWebSocket(request, this)
}
...
}
以上的事件通常在连接状态发生变化时被动触发,另一方面,如果用户想主动执行某些操作,WebSocket也提供了相应的接口以给用户显式调用。WebSocket协议包含两个主要的操作:
- send( ) :向服务端发送消息,包括文本或二进制数据
- close( ):主动请求关闭连接。
可以看到,OkHttp提供的WebSocket接口也提供了这两个方法:
interface WebSocket {
...
fun send(text: String): Boolean
fun send(bytes: ByteString): Boolean
fun close(code: Int, reason: String?): Boolean
...
}
当onOpen方法回调时,即是连接建立成功,可以传输数据了。此时我们便可以调用WebSocket实例的send()方法发送文本消息或二进制消息,WebSocket官方服务器会将数据通过onMessage(webSocket: WebSocket, bytes: ByteString)或onMessage(webSocket: WebSocket, text: String)回调原样返回给我们。
WebSocket是如何建立连接的?
我们可以通过阅读OkHttp源码获知,newWebSocket(request: Request, listener: WebSocketListener)方法内部是创建了一个RealWebSocket实例,该类是WebSocket接口的实现类,创建实例成功后便调用connect(client: OkHttpClient)方法开始异步建立连接。
override fun newWebSocket(request: Request, listener: WebSocketListener): WebSocket {
val webSocket = RealWebSocket(
taskRunner = TaskRunner.INSTANCE,
originalRequest = request,
listener = listener,
random = Random(),
pingIntervalMillis = pingIntervalMillis.toLong(),
extensions = null, // Always null for clients.
minimumDeflateSize = minWebSocketMessageToCompress
)
webSocket.connect(this)
return webSocket
}
连接建立的过程主要是向服务器发送了一个HTTP请求,该请求包含了额外的一些请求头信息:
val request = originalRequest.newBuilder()
.header("Upgrade", "websocket")
.header("Connection", "Upgrade")
.header("Sec-WebSocket-Key", key)
.header("Sec-WebSocket-Version", "13")
.header("Sec-WebSocket-Extensions", "permessage-deflate")
.build()
这些请求头的意义如下:
Connection: Upgrade:表示要升级协议
Upgrade: websocket:表示要升级到websocket协议。
Sec-WebSocket-Version:13:表示websocket的版本。如果服务端不支持该版本,需要返回一个Sec-WebSocket-Versionheader,里面包含服务端支持的版本号。
Sec-WebSocket-Key:与后面服务端响应首部的Sec-WebSocket-Accept是配套的,提供基本的防护,比如恶意的连接,或者无意的连接。
当返回的状态码为101时,表示服务端同意客户端协议转换请求,并将其转换为Websocket协议,该过程称之为Websocket协议握手(websocket Protocol handshake),协议升级完成后,后续的数据交换则遵照WebSocket的协议。
前面我们一直说「握手」,握手究竟指的是什么呢?在计算机领域的语境中,握手通常是指确保服务器与其客户端同步的过程。握手是WebSocket协议的基本概念。
为了直观展示,以上实例中传输的消息均以文本为例,WebSocket还支持二进制数据的传输,而这就要依靠「数据传输协议」来完成了,这是下一篇文章的内容,敬请期待。
总结
为了完成与服务端的双向通信,我们选取了WebSocket协议作为网络通信协议,并通过对比传统HTTP协议和其他相关的即时通讯技术,总结出,在为移动设备下应用选择的合适的网络通信协议时,可以有以下的参考标准:
- 支持全双工通信
- 支持二进制数据传输
- 支持扩展
- 跨语言、跨平台实现
同时,也对WebSocket协议在Android端的实现提供了示例,并对WebSocket协议握手流程进行了初步窥探,当然,这只是第一步,往后的心跳保活、断线重连、消息队列等每一个都可以单独作为一个课题,会在后面陆续推出的。
参考
WebSocket 官网
http://www.websocket.org/index.html
WebSocket 百度百科
https://baike.baidu.com/item/WebSocket/1953845?fr=aladdin
学习WebSocket简单易学
https://www.tutorialspoint.com/websockets/index.htm
WebSocket详解(一):初步认识WebSocket技术
http://www.52im.net/thread-331-1-1.html
轮询、长轮询、长连接、websocket
https://www.cnblogs.com/huchong/p/8595644.html
WEB端即时通讯:HTTP长连接、长轮询(long polling)详解
http://www.52im.net/thread-224-1-1.html
WebSockets vs 长轮询(一)
https://webrtc.org.cn/20190704-webrtc-protocol/
MQTT 入门介绍
https://www.runoob.com/w3cnote/mqtt-intro.html
Android网络编程要学的东西与Http协议学习
https://www.kancloud.cn/kancloud/android-tutorial/87221