WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。它允许客户端和服务器建立持久连接,实现低延迟的实时数据交互,允许客户端和服务器之间进行实时数据交换,非常适合需要频繁更新数据的应用,如聊天应用、实时通知系统、在线游戏等。它在 2011 年成为国际标准(RFC 6455),为浏览器和服务器之间提供了实时通信的能力。
WebSocket 的特点:
全双工通信:客户端和服务器可以同时发送和接收数据,不需要像 HTTP 那样请求-响应的模式。
持久连接:一旦连接建立,连接保持打开状态,直到被显式关闭。
实时性:由于不需要不断地建立新的连接,WebSocket 能实现实时的数据传输,非常适合实时聊天、实时游戏、实时数据更新等场景。
低延迟:相比 HTTP 轮询,WebSocket 提供了更低的延迟。
事件驱动:基于事件模型,易于处理异步数据流。
较少的开销:WebSocket 握手阶段使用 HTTP 协议,之后的数据传输不再带有 HTTP 头信息,减少了数据传输的开销。
跨域支持:WebSocket 可以轻松地处理跨域通信,而不像传统的 HTTP 请求那样受到同源策略的严格限制。
WebSocket 的生命周期:
握手:客户端通过 HTTP 请求发起握手,服务器响应后升级为 WebSocket 连接。
消息交换:连接建立后,双方可以发送和接收消息。
关闭连接:任何一方都可以关闭连接。
//在build.gradle文件中添加依赖
dependencies {
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation ("org.java-websocket:Java-WebSocket:1.5.3")
}
//在类文件中实现websockt通信
// 导入 OkHttp3 库的所有类和接口。
// OkHttp3 是一个高效的 HTTP 客户端,也支持 WebSocket 协议,为创建 WebSocket 连接提供了便捷的 API。
import okhttp3.*
// 导入 Okio 库中的 ByteString 类。
// ByteString 用于处理二进制数据,在 WebSocket 通信中可用于接收和发送二进制消息。
import okio.ByteString
// 定义了一个名为 WebSocketClient 的类,该类封装了与 WebSocket 服务器进行通信的功能。
class WebSocketClient {
private lateinit var webSocket: WebSocket
fun connect() {
// 创建一个 OkHttpClient 实例,该实例是 OkHttp3 库中用于发起 HTTP 和 WebSocket 请求的核心类。
val client = OkHttpClient()
// 定义一个字符串变量 serverUrl,存储要连接的 WebSocket 服务器的地址。
// ws:// 是 WebSocket 协议的前缀,echo.websocket.org 是一个公共的 WebSocket 测试服务器,它会将客户端发送的消息原样返回。
val serverUrl = "ws://echo.websocket.org"
// 使用 Request.Builder() 创建一个 Request 对象的构建器。
val request: Request = Request.Builder()
// 通过 url(serverUrl) 方法指定请求的目标 URL
.url(serverUrl)
// 调用 build() 方法构建出一个完整的 Request 对象,该对象包含了连接 WebSocket 服务器所需的请求信息。
.build()
// 创建一个匿名对象,继承自 WebSocketListener 类。
// WebSocketListener 是 OkHttp3 库中用于处理 WebSocket 事件的抽象类,通过重写其方法可以处理不同的 WebSocket 事件。
val webSocketListener: WebSocketListener = object : WebSocketListener() {
override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
super.onMessage(webSocket, bytes)
println("onMessage...bytes:$bytes")
}
override fun onMessage(webSocket: WebSocket, text: String) {
super.onMessage(webSocket, text)
println("onMessage...text:$text")
}
override fun onOpen(webSocket: WebSocket, response: Response) {
super.onOpen(webSocket, response)
webSocket.send("Hello from webSocket client")
println("onOpen...response:$response")
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
super.onClosed(webSocket, code, reason)
webSocket.close(code, reason)
println("onClosed...code:$code,reason:$reason")
}
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
super.onClosing(webSocket, code, reason)
println("onClosing...code:$code,reason:$reason")
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
super.onFailure(webSocket, t, response)
println("onFailure...throwable:$t,response:$response")
}
}
// 调用 OkHttpClient 的 newWebSocket 方法,传入之前创建的 Request 对象和 WebSocketListener 对象,建立 WebSocket 连接,并将返回的 WebSocket 实例赋值给 webSocket 成员变量。
webSocket = client.newWebSocket(request, webSocketListener);
}
}
fun main() {
WebSocketClient().connect()
}
上面的代码输出如下:
onOpen...response:Response{protocol=http/1.1, code=101, message=Switching Protocols, url=https://echo.websocket.org/}
onMessage...text:Request served by 1781505b56ee58
onMessage...text:Hello from webSocket client
出现两条 onMessage 输出的原因:
服务器响应:
第一条 onMessage 输出 "onMessage...text:Request served by 1781505b56ee58" 很可能是服务器在连接建立之后自动发送的信息,用于告知客户端本次请求是由哪个服务实例处理的,这属于服务器的一种标准响应。
客户端消息回显:
第二条 onMessage 输出 "onMessage...text:Hello from webSocket client" 是服务器把客户端发送的消息原样返回了。ws://echo.websocket.org 是一个测试用的 WebSocket 服务器,它的主要功能就是将客户端发送的消息原样回显给客户端。所以,当客户端在 onOpen 方法里发送 "Hello from webSocket client" 之后,服务器会把这个消息再回传给客户端,进而触发了第二次 onMessage 回调。
也可以采用另外的方法,即使用org.java_websocket.client.WebSocketClient.
//在build.gradle文件中添加依赖
dependencies {
implementation("org.java-websocket:Java-WebSocket:1.4.0")
}
//定义类文件
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake
import java.net.URI
class CustomJavaWebSocketClient(serverUri: URI) : WebSocketClient(serverUri) {
override fun onOpen(handshakedata: ServerHandshake?) {
println("WebSocket 连接已打开")
// 连接打开后可以发送消息
send("Hello, server!")
}
override fun onMessage(message: String?) {
message?.let {
println("接收到消息: $it")
// 在这里可以进行数据解析
parseReceivedData(it)
}
}
override fun onClose(code: Int, reason: String?, remote: Boolean) {
println("WebSocket 连接已关闭,代码: $code,原因: $reason,是否远程关闭: $remote")
}
override fun onError(ex: Exception?) {
ex?.let {
println("WebSocket 连接发生错误: ${it.message}")
}
}
private fun parseReceivedData(data: String) {
}
}
上面的代码定义了一个继承自WebSocketClient的类,下面对之进行调用。
fun main() {
var serverUri = URI("ws://echo.websocket.org")
try {
// 处理协议替换,同时支持 ws 和 wss
// 传入的 serverUri 进行判断,若为 ws 协议则替换为 http,若为 wss 协议则替换为 https,这样就能同时处理两种协议。
// 这样就可以避免出现下面的错误:
//WebSocket 连接发生错误: unknown scheme: https
//WebSocket 连接已关闭,代码: -1,原因: unknown scheme: https,是否远程关闭: false
val httpUrl = if (serverUri.scheme == "ws") {
serverUri.toString().replace("ws://", "http://")
} else if (serverUri.scheme == "wss") {
serverUri.toString().replace("wss://", "https://")
} else {
serverUri.toString()
}
val url = URL(httpUrl)
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "HEAD"
connection.instanceFollowRedirects = false
// 发送一个 HTTP HEAD 请求来检查是否存在重定向。
val responseCode = connection.responseCode
// 如果服务器返回 301 或 302 状态码,我们就从响应头中获取新的 URL,并将其协议替换回 ws,更新 serverUri。
if (responseCode == HttpURLConnection.HTTP_MOVED_PERM
|| responseCode == HttpURLConnection.HTTP_MOVED_TEMP) {
val newUrl = connection.getHeaderField("Location")
// 还原为 WebSocket 协议
val newWsUrl = when {
newUrl.startsWith("http://") -> newUrl.replace("http://", "ws://")
newUrl.startsWith("https://") -> newUrl.replace("https://", "wss://")
else -> newUrl
}
serverUri = URI(newWsUrl)
println("检测到重定向,新的 WebSocket URL: $serverUri")
}
} catch (e: Exception) {
println("处理重定向时发生错误: ${e.message}")
}
val client = CustomJavaWebSocketClient(serverUri)
println("call client.connect()")
// 进行连接
client.connect()
println("call client.connect(),isOpen:${client.isOpen}")
while (!client.isOpen) {
Thread.sleep(100)
}
client.send("Another message from client")
try {
Thread.sleep(5000)
} catch (e: InterruptedException) {
e.printStackTrace()
}
client.close()
}
上面代码的输出如下:
检测到重定向,新的 WebSocket URL: wss://echo.websocket.org/
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
call client.connect()
call client.connect(),isOpen:false
WebSocket 连接已打开
接收到消息: Request served by 9e7840027f4383
接收到消息: Hello, server!
接收到消息: Another message from client
WebSocket 连接已关闭,代码: 1000,原因: ,是否远程关闭: false
上面的代码为什么需要进行重定向呢?
是因为遇到了:“WebSocket 连接已关闭,代码: 1002,原因: Invalid status code received: 301 Status line: HTTP/1.1 301 Moved Permanently,是否远程关闭: false” 这样的错误信息。
Invalid status code received: 301 Status line: HTTP/1.1 301 Moved Permanently 表明在建立 WebSocket 连接时,客户端收到了一个 HTTP 301 状态码。HTTP 301 状态码代表请求的资源已被永久移动到了新的 URL。
具体可能的原因如下:
服务器端配置变更:服务器管理员可能对 WebSocket 服务的配置进行了调整,将原本的端点迁移到了新的地址。
URL 错误:你所使用的 WebSocket URL 可能已经过时或者存在拼写错误,服务器会返回 301 状态码来指引你访问新的地址。
代理或负载均衡器的影响:如果使用了代理服务器或者负载均衡器,它们的配置可能存在问题,从而将 WebSocket 请求重定向到了错误的地址。
为了解决这个问题,可以在代码中添加处理重定向的逻辑。