WebSocket协议概述
-
Webscoket是Web浏览器和服务器之间的一种全双工通信协议。比如说,服务器可以在任意时刻发送消息给浏览器。
-
WebSocket并不是全新的协议,而是利用了HTTP协议来建立连接。
-
WebSocket连接由浏览器发起,请求协议是一个标准的HTTP请求,格式如下:
GET ws://localhost:3000/ws/chat HTTP/1.1 Host: localhost Upgrade: websocket Connection: Upgrade Origin: http://localhost:3000 Sec-WebSocket-Key: client-random-string Sec-WebSocket-Version: 13
该请求和普通的HTTP请求有几点不同:
- GET请求的地址不是类似/path/,而是以ws://开头的地址;
- 请求头Upgrade: websocket和Connection: Upgrade表示这个连接将要被转换为WebSocket连接;
- Sec-WebSocket-Key是用于标识这个连接,并非用于加密数据;
- Sec-WebSocket-Version指定了WebSocket的协议版本。
服务器如果接受该请求,就会返回如下响应:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: server-random-string
响应代码101表示本次连接的HTTP协议即将被更改,更改后的协议就是Upgrade: websocket指定的WebSocket协议。
OKHttp实现
连接握手
public class OkHttpClient implements Cloneable, Call.Factory, WebSocket.Factory {
. . . . . .
@Override public WebSocket newWebSocket(Request request, WebSocketListener listener) {
RealWebSocket webSocket = new RealWebSocket(request, listener, new SecureRandom());
webSocket.connect(this);
return webSocket;
}
这里会创建一个 RealWebSocket 对象,然后执行其 connect() 方法建立连接。
RealWebSocket 构造方法如下
public RealWebSocket(Request request, WebSocketListener listener, Random random) {
if (!"GET".equals(request.method())) {
throw new IllegalArgumentException("Request must be GET: " + request.method());
}
this.originalRequest = request;
this.listener = listener;
this.random = random;
byte[] nonce = new byte[16];
random.nextBytes(nonce);
this.key = ByteString.of(nonce).base64();
this.writerRunnable = new Runnable() {
@Override public void run() {
try {
while (writeOneFrame()) {
}
} catch (IOException e) {
failWebSocket(e, null);
}
}
};
}
主要的是初始化了 key,以备后续连接建立及握手之用。Key 是一个16字节长的随机数经过 Base64 编码得到的。此外还初始化了 writerRunnable 等。
连接建立及握手过程如下:
public void connect(OkHttpClient client) {
client = client.newBuilder()
.protocols(ONLY_HTTP1)
.build();
final int pingIntervalMillis = client.pingIntervalMillis();
final Request request = originalRequest.newBuilder()
.header("Upgrade", "websocket")
.header("Connection", "Upgrade")
.header("Sec-WebSocket-Key", key)
.header("Sec-WebSocket-Version", "13")
.build();
call = Internal.instance.newWebSocketCall(client, request);
call.enqueue(new Callback() {
@Override public void onResponse(Call call, Response response) {
try {
checkResponse(response);
} catch (ProtocolException e) {
failWebSocket(e, response);
closeQuietly(response);
return;
}
// Promote the HTTP streams into web socket streams.
StreamAllocation streamAllocation = Internal.instance.streamAllocation(call);
streamAllocation.noNewStreams(); // Prevent connection pooling!
Streams streams = streamAllocation.connection().newWebSocketStreams(streamAllocation);
// Process all web socket messages.
try {
listener.onOpen(RealWebSocket.this, response);
String name = "OkHttp WebSocket " + request.url().redact();
initReaderAndWriter(name, pingIntervalMillis, streams);
streamAllocation.connection().socket().setSoTimeout(0);
loopReader();
} catch (Exception e) {
failWebSocket(e, null);
}
}
@Override public void onFailure(Call call, IOException e) {
failWebSocket(e, null);
}
});
}
其主要完成的动作:
向服务器发送一个HTTP请求,HTTP 请求的特别之处在于,它包含了如下的一些Headers:
Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Key: 7wgaspE0Tl7/66o4Dov2kw==
Sec-WebSocket-Version: 13
其中 Upgrade 和 Connection header 向服务器表明,请求的目的就是要将客户端和服务器端的通讯协议从 HTTP 协议升级到 WebSocket 协议,同时在请求处理完成之后,连接不要断开。
Sec-WebSocket-Key header 值正是我们前面看到的key,它是 WebSocket 客户端发送的一个 base64 编码的密文,要求服务端必须返回一个对应加密的 “Sec-WebSocket-Accept” 应答,否则客户端会抛出 “Error during WebSocket handshake” 错误,并关闭连接。
来自于 HTTP 服务器的响应到达的时,则表明连接已建立。
尽管连接已经建立,还要为数据的收发做一些准备。
这些准备中的第一步就是检查 HTTP 响应:
void checkResponse(Response response) throws ProtocolException {
if (response.code() != 101) {
throw new ProtocolException("Expected HTTP 101 response but was '"
+ response.code() + " " + response.message() + "'");
}
String headerConnection = response.header("Connection");
if (!"Upgrade".equalsIgnoreCase(headerConnection)) {
throw new ProtocolException("Expected 'Connection' header value 'Upgrade' but was '"
+ headerConnection + "'");
}
String headerUpgrade = response.header("Upgrade");
if (!"websocket".equalsIgnoreCase