一、序

OkHttp 应该算是 Android 中使用最广泛的网络库了,我们通常会利用它来实现 HTTP 请求,但是实际上它还可以支持 WebSocket,并且使用起来还非常的便捷。

那本文就来聊聊,利用 OkHttp 实现 WebSocket 的一些细节,包括对 WebSocket 的介绍,以及在传输前如何做到鉴权、长连接保活及其原理。

二、WebSocket 简介

2.1 为什么使用 WebSocket?

我们做客户端开发时,接触最多的应用层网络协议,就是 HTTP 协议,而今天介绍的 WebSocket,下层和 HTTP 一样也是基于 TCP 协议,这是一种轻量级网络通信协议,也属于应用层协议。

WebSocket 与 HTTP/2 一样,其实都是为了解决 HTTP/1.1 的一些缺陷而诞生的,而 WebSocket 针对的就是「请求-应答」这种"半双工"的模式的通信缺陷。

「请求-应答」是"半双工"的通信模式,数据的传输必须经过一次请求应答,这个完整的通信过程,通信的同一时刻数据只能在一个方向上传递。它最大的问题在于,HTTP 是一种被动的通信模式,服务端必须等待客户端请求才可以返回数据,无法主动向客户端发送数据。

这也导致在 WebSocket 出现之前,一些对实时性有要求的服务,通常是基于轮询(Polling)这种简单的模式来实现。轮询就是由客户端定时发起请求,如果服务端有需要传递的数据,可以借助这个请求去响应数据。

轮询的缺点也非常明显,大量空闲的时间,其实是在反复发送无效的请求,这显然是一种资源的损耗。

虽然在之后的 HTTP/2、HTTP/3 中,针对这种半双工的缺陷新增了 Stream、Server Push 等特性,但是「请求-应答」依然是 HTTP 协议主要的通信方式。

WebSocket 协议是由 HTML5 规范定义的,原本是为了浏览器而设计的,可以避免同源的限制,浏览器可以与任意服务端通信,现代浏览器基本上都已经支持 WebSocket。

虽然 WebSocket 原本是被定义在 HTML5 中,但它也适用于移动端,尽管移动端也可以直接通过 Socket 与服务端通信,但借助 WebSocket,可以利用 80(HTTP) 或 443(HTTPS)端口通信,有效的避免一些防火墙的拦截。

WebSocket 是真正意义上的全双工模式,也就是我们俗称的「长连接」。当完成握手连接后,客户端和服务端均可以主动的发起请求,回复响应,并且两边的传输都是相互独立的。

2.2 WebSocket 的特点

WebSocket 的数据传输,是基于 TCP 协议,但是在传输之前,还有一个握手的过程,双方确认过眼神,才能够正式的传输数据。

WebSocket 的握手过程,符合其 "Web" 的特性,是利用 HTTP 本身的 "协议升级" 来实现。

在建立连接前,客户端还需要知道服务端的地址,WebSocket 并没有另辟蹊径,而是沿用了 HTTP 的 URL 格式,但协议标识符变成了 "ws" 或者 "wss",分别表示明文和加密的 WebSocket 协议,这一点和 HTTP 与 HTTPS 的关系类似。

以下是一些 WebSocket 的 URL 例子:

ws://cxmydev.com/some/path
ws://cxmydev.com:8080/some/path
wss://cxmydev.com:443?uid=xxx

而在连接建立后,WebSocket 采用二进制帧的形式传输数据,其中常用的包括用于数据传输的数据帧 MESSAGE 以及 3 个控制帧:

  • PING:主动保活的 PING 帧;
  • PONG:收到 PING 帧后回复;
  • CLOSE:主动关闭 WebSocket 连接;

更多 WebSocket 的协议细节,可以参考《WebSocket Protocol 规范》,具体细节,有机会为什么再开单篇文章讲解。

了解这些基本知识,我们基本上就可以把 WebSocket 使用起来,并且不会掉到坑里。

我们再小结一下 WebSocket 的特性:

  1. WebSocket 建立在 TCP 协议之上,对服务器端友好;
  2. 默认端口采用 80 或 443,握手阶段采用 HTTP 协议,不容易被防火墙屏蔽,能够通过各种 HTTP 代理服务器;
  3. 传输数据相比 HTTP 更轻量,少了 HTTP Header,性能开销更小,通信更高效;
  4. 通过 MESSAGE 帧发送数据,可以发送文本或者二进制数据,如果数据过大,会被分为多个 MESSAGE 帧发送;
  5. WebSocket 沿用 HTTP 的 URL,协议标识符是 "ws" 或 "wss"。

那接下来我们就看看如何利用 OkHttp 使用 WebSocket。

三、WebSocket之OkHttp

3.1 建立 WebSocket 连接

借助 OkHttp 可以很轻易的实现 WebSocket,它的 OkHttpClient 中,提供了 newWebSocket() 方法,可以直接建立一个 WebSocket 连接并完成通信。

fun connectionWebSockt(hostName:String,port:Int){
val httpClient = OkHttpClient.Builder()
.pingInterval(40, TimeUnit.SECONDS) // 设置 PING 帧发送间隔
.build()
val webSocketUrl = "ws://${hostName}:${port}"
val request = Request.Builder()
.url(webSocketUrl)
.build()
httpClient.newWebSocket(request, object:WebSocketListener(){
// ...
})
}

我想熟悉 OkHttp 的朋友,对上面这端代码不会有疑问,只是 URL 换成了 "ws" 协议标识符。另外,还需要配置 pingInterval(),这个细节后文会讲解。

调用 newWebSocket() 后,就会开始 WebSocket 连接,但是核心操作都在 WebSocketListener 这个抽象类中。

3.2 使用 WebSocketListener

WebSocketListener 是一个抽象类,其中定义了比较多的方法,借助这些方法回调,就可以完成对 WebSocket 的所有操作。

var mWebSocket : WebSocket? = null
fun connectionWebSockt(hostName:String,port:Int){
// ...
httpClient.newWebSocket(request, object:WebSocketListener(){
override fun onOpen(webSocket: WebSocket, response: Response) {
super.onOpen(webSocket, response)
// WebSocket 连接建立
mWebSocket = webSocket
} override fun onMessage(webSocket: WebSocket, text: String) {
super.onMessage(webSocket, text)
// 收到服务端发送来的 String 类型消息
} override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
super.onClosing(webSocket, code, reason)
// 收到服务端发来的 CLOSE 帧消息,准备关闭连接
} override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
super.onClosed(webSocket, code, reason)
// WebSocket 连接关闭
} override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
super.onFailure(webSocket, t, response)
// 出错了
}
})
}

WebSocketListener 的所有方法回调中,都包含了 WebSocket 类型的对象,它就是当前建立的 WebSocket 连接实体,通过它就可以向服务端发送 WebSocket 消息。

如果需要在其他时机发送消息,可以在回调 onOpen() 这个建立连接完成的时机,保存 webSocket 对象,以备后续使用。

OkHttp 中的 WebSocket 本身是一个接口,它的实现类是 RealWebSocket,它定义了一些发送消息和关闭连接的方法:

  • send(text):发送 String 类型的消息;
  • send(bytes):发送二进制类型的消息;
  • close(code, reason):主动关闭 WebSocket 连接;

利用这些回调和 WebSocket 提供的方法,我们就可以完成 WebSocket 通信了。

3.3 Mock WebSocket

有时候为了方便我们测试,OkHttp 还提供了扩展的 MockWebSocket 服务,来模拟服务端。

MockWebSocket 需要添加额外的 Gradle 引用,最好和 OkHttp 版本保持一致:

api 'com.squareup.okhttp3:okhttp:3.9.1'
api 'com.squareup.okhttp3:mockwebserver:3.9.1'

MockWebServer 的使用也非常简单,只需要利用 MockWebSocket 类即可。

var mMockWebSocket: MockWebServer? = null
fun mockWebSocket() {
if (mMockWebSocket != null) {
return
}
mMockWebSocket = MockWebServer()
mMockWebSocket?.enqueue(MockResponse().withWebSocketUpgrade(object : WebSocketListener() { override fun onOpen(webSocket: WebSocket, response: Response) {
super.onOpen(webSocket, response)
// 有客户端连接时回调
} override fun onMessage(webSocket: WebSocket, text: String) {
super.onMessage(webSocket, text)
// 收到新消息时回调
} override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
super.onClosing(webSocket, code, reason)
// 客户端主动关闭时回调
} override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
super.onClosed(webSocket, code, reason)
// WebSocket 连接关闭
} override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
super.onFailure(webSocket, t, response)
// 出错了
}
}))
}

Mock WebSocket 服务端,依然需要用到我们前面讲到的 WebSocketListener,这个就比较熟悉,不再赘述了。

之后就可以通过 mMockWebSocket 获取到这个 Mock 的服务的 IP 和端口。

val hostName = mMockWebSocket?.getHostName()
val port = mMockWebSocket?.getPort()
val url = "ws:${hostName}:${port}"

需要注意的是,这两个方法需要在子线程中调用,否者会收到一个异常。

虽然有时候在服务端完善的情况下,我们并不需要使用 Mock 的手段,但是在学习阶段,依然推荐大家在本地 Mock 一个服务端,打一些日志,观察一个完整的 WebSocket 连接和发送消息的过程。

3.4 WebSocket 如何鉴权

接下来我们聊聊 WebSocket 连接的鉴权问题。

所谓鉴权,其实就是为了安全考虑,避免服务端启动 WebSocket 的连接服务后,任谁都可以连接,这肯定会引发一些安全问题。其次,服务端还需要将 WebSocket 的连接实体与一个真是的用户对应起来,否者业务无法保证了。

那么问题就回到了,WebSocket 通信的完整过程中,如何以及何时将一些业务数据传递给服务端?当然在 WebSocket 连接建立之后,立即给服务端发送一些鉴权的数据,必然是可以做到业务实现的,但是这样明显是不够优雅的。

前文提到,WebSocket 在握手阶段,使用的是 HTTP 的 "协议升级",它本质上还是 HTTP 的报文头发送一些特殊的头数据,来完成协议升级。

例如在 RealWebSocket 中,就有构造 Header 的过程,如 Upgrade、Connection 等等。

public void connect(OkHttpClient client) {
// ...
final Request request = originalRequest.newBuilder()
.header("Upgrade", "websocket")
.header("Connection", "Upgrade")
.header("Sec-WebSocket-Key", key)
.header("Sec-WebSocket-Version", "13")
.build();
//....
}

那么实际我们在 WebSocket 阶段,也可以通过 Header 传输一些鉴权的数据,例如 uid、token 之类,具体方法就是在构造 Request 的时候,为其增加 Header,这里就不举例说明了。

另外 WebSocket 的 URL 也是可以携带参数的。

wss://cxmydev.com:443?uid=xxx&token=xxx

3.5 WebSocket 保活

WebSocket 建立的连接就是我们所谓的长连接,每个连接对于服务器而言,都是资源。但服务器倾向于在一个连接长时间没有消息往来的时候,将其关闭。而 WebSocket 的保活,实际上就是定时向服务端发送一个空消息,来保证连接不会被服务端主动断开。

那么我们自己写个定时器,固定间隔向服务端 mWebSocket.send() 一个消息,就可以达到保活的目的,但这样发送的其实是 MESSAGE 帧数据,如果使用 WebSocket 还有更优雅的方式。

前文我们提到,WebSocket 采用二进制帧的形式传输数据,其中就包括了用于保活的 PING 帧,而 OkHttp 只需要简单的配置,就可以自动的间隔发送 PING 帧和数据。

我们只需要在构造 OkHttpClient 的时候,通过 pingInterval() 设置 PING 帧发送的时间间隔,它的默认值为 0,所以不设置不发送。

val httpClient = OkHttpClient.Builder()
.pingInterval(40, TimeUnit.SECONDS) // 设置 PING 帧发送间隔
.build()

这里设置的时长,需要和服务端商议,通常建议最好设置一个小于 60s 的值。

具体的逻辑在 RealWebSocket 类中。

public void initReaderAndWriter(String name, Streams streams) throws IOException {
synchronized (this) {
// ...
if (pingIntervalMillis != 0) {
executor.scheduleAtFixedRate(
new PingRunnable(), pingIntervalMillis, pingIntervalMillis, MILLISECONDS);
}
// ...
}
// ...
}

PingRunnabel 最终会去间隔调用 writePingFrame() 用以向 WebSocketWriter 中写入 PING 帧,来达到服务端长连接保活的效果。

四、小结

到这里本文就介绍清楚 WebSocket 以及如何使用 OkHttp 实现 WebSocket 支持。

这里还是简单小结一下:

  1. WebSocket 是一个全双工的长连接应用层协议,可以通过它实现服务端到客户端主动的推送通信。
  2. OkHttp 中使用 WebSocket 的关键在于 newWebSocket() 方法以及 WebSocketListener 这个抽象类,最终连接建立完毕后,可以通过 WebSocket 对象向对端发送消息;
  3. WebSocket 鉴权,可以利用握手阶段的 HTTP 请求中,添加 Header 或者 URL 参数来实现;
  4. WebSocket 的保活,需要定时发送 PING 帧,发送的时间间隔,在 OkHttp 中可以通过 pingInterval() 方法设置;

额外提一句,OkHttp 在 v3.4.1 中添加的 WebSocket 的支持,之前的版本需要 okhttp-ws 扩展库来支持,但是那毕竟已经是 2016 年的事了,我想现在应该没有人在用那么老版本的 OkHttp 了。

本文对你有帮助吗?留言、转发、收藏是最大的支持,谢谢!如果本文各项数据好,之后会再分享一篇 OkHttp 中针对 WebSocket 的实现以及 WebSocket 协议的讲解。

参考:


热文推荐:

公众号后台回复成长『成长』,将会得到我准备的学习资料。

聊聊OkHttp实现WebSocket细节,包括鉴权和长连接保活及其原理!的更多相关文章

  1. Vue+WebSocket 实现页面实时刷新长连接

    最近vue项目要做数据实时刷新,折线图每秒重画一次,数据每0.5秒刷新一次,说白了就是实时刷新,因为数据量较大,用定时器估计页面停留一会就会卡死... 与后台人员讨论过后决定使用h5新增的WebSoc ...

  2. WebSocket IO和后端建立长连接,即时接受后端消息

    https://github.com/TooTallNate/Java-WebSocket ant得到java_websocket.jar改名为WebSocket.jar放到 https://gith ...

  3. 雨露均沾的OkHttp—WebSocket长连接的使用&源码解析

    前言 最近老板又来新需求了,要做一个物联网相关的app,其中有个需求是客户端需要收发服务器不定期发出的消息. 内心OS:

  4. go-zero docker-compose 搭建课件服务(六):完善jwt鉴权和返回结构

    0.转载 go-zero docker-compose 搭建课件服务(六):完善jwt鉴权和返回结构 0.1源码地址 https://github.com/liuyuede123/go-zero-co ...

  5. 微信小程序中如何使用WebSocket实现长连接(含完整源码)

    本文由腾讯云技术团队原创,感谢作者的分享. 1.前言   微信小程序提供了一套在微信上运行小程序的解决方案,有比较完整的框架.组件以及 API,在这个平台上面的想象空间很大.腾讯云研究了一番之后,发现 ...

  6. 轮询、长轮询、长连接、websocket

    Web端即时通讯技术:即时通讯技术简单的说就是实现这样一种功能:服务器端可以即时地将数据的更新或变化反应到客户端,例如消息即时推送等功能都是通过这种技术实现的.但是在Web中,由于浏览器的限制,实现即 ...

  7. 聊聊iOS中网络编程长连接的那些事

    1.长连接在iOS开发中的应用 常见的短连接应用场景:一般的App的网络请求都是基于Http1.0进行的,使用的是NSURLConnection.NSURLSession或者是AFNetworking ...

  8. WebSocket长连接

    WebSocket长连接 1.概述 1.1 定义 1.2 原理 2.Django中配置WebSocket 2.1安装第三方法包 pip install channels 2.2 Django 中的配置 ...

  9. 浅谈Websocket、Ajax轮询和长连接(long pull)

    最近看到了一些介绍Websocket的文章,觉得挺有用,所以在这里将自己的对其三者的理解记录一下. 1.什么是Websocket Websocket是HTML5中提出的新的协议,注意,这里是协议,可以 ...

随机推荐

  1. RPC 框架性能大比拼

    Dubbo 是阿里巴巴公司开源的一个Java高性能优秀的服务框架,使得应用可通过高性能的 RPC 实现服务的输出和输入功能,可以和 Spring框架无缝集成. Motan 是新浪微博开源的一个Java ...

  2. POJ 3678 2-SAT

    题意:有n个顶点里面可以放数字1或0,给m个限制,每个限制给出两个顶点编号和两编号内数字运算后的结果 思路:很直接的2-SAT,每个点分为1和0两种情况,按限制要求建边,跑tarjan然后判断点是否在 ...

  3. Leetcode 063 不同路径二

    一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为"Start" ). 机器人每次只能向下或者向右移动一步.机器人试图达到网格的右下角(在下图中标记为" ...

  4. numpy的基础运算1

    import numpy as np #int16和int32内存少,int64内存大但精度高 a = np.array([1,23,4],dtype=np.int32) b = np.zeros(( ...

  5. http概述——http笔记一

    之前想深入的了解学习下HTTP,所以就买了本砖头<HTTP权威指南>.最近一直在看,可是发现看书只有输入并没有输出,不行.所以就打算把自己的理解写成一系列的笔记,供以后翻阅. 大概画了张本 ...

  6. Jenkins+Git+Fastlane+Fir CI集成

    上一篇有讲关于fastlane自动化部署,本篇将会着重讲关于fastlane的实际应用. 目标: 利用自动化jenkins打包工具,自动拉取git仓库代码 不需要通过手动检查修改xcode中项目配置修 ...

  7. Java入门教程五(数字和日期处理)

    Java 提供了处理相关问题的类,包括 Math 类.Random 类.BigInteger 类.Date 类等. Math类 Math 类封装了常用的数学运算,提供了基本的数学操作,如指数.对数.平 ...

  8. Oracle密码验证函数与Create Profile

    今天看到了一个oracle密码函数的东西,就在网上找文档自己做测试,刚开始看不懂,最后做完记录一下 密码函数的作用就是要将用户密码进行限制,比如申请一个网站的账号的时候,密码会要求你不少于8位,必须要 ...

  9. CVE-2020-7245 CTFd v2.0.0 – v2.2.2漏洞分析复现

    CVE-2020-7245 CTFd v2.0.0 – v2.2.2漏洞分析复现 一.漏洞介绍 ​ 在 CTFd v2.0.0 - v2.2.2 的注册过程中,如果知道用户名并在 CTFd 实例上启用 ...

  10. vue+express+mysql项目总结(node项目部署阿里云通用)

    原文发布于我的个人博客上:原文点这里   前面经历千辛万苦,终于把博客的所有东西都准备好了,现在就只等部署了.下面我介绍下我的部署过程: 一.购买服务器和域名   如果需要域名(不用域名通过ip也可以 ...