很早之前就对 WebSocket 协议非常感兴趣,今天有空时看了一下 RFC6455, 发现其实是一个很简单的协议。于是尝试着实现了一个客户端。这里摘取一些关键部分的代码。
WebSocket 和普通的 tcp 连接很类似,可以双向发送消息(区别于 http的request-response模式)。
首先第一步是建立 tcp 连接,然后发送 http 协议升级消息:
defp upgrade_msg(uri, nonce) do
"""
GET / HTTP/1.1\r
Host: #{uri.authority}\r
Upgrade: websocket\r
Connection: Upgrade\r
Sec-WebSocket-Key: #{nonce}\r
Sec-WebSocket-Version: 13\r\n
"""
end
之后服务器会返回一些内容,我们校验过后websocket连接就算建立成功了:
defp handle_tcp(:handshake, data, %{challenge: challenge}) do
{:ok, {:http_response, _, 101, "Switching Protocols"}, rest} =
:erlang.decode_packet(:http_bin, data, []) |> IO.inspect()
case validate_headers(rest, fn
{:Connection, up} ->
String.downcase(up) == "upgrade"
{:Upgrade, ws} ->
String.downcase(ws) == "websocket"
{"Sec-WebSocket-Accept", ch} ->
ch == challenge
_ ->
true
end) do
:ok ->
IO.inspect("goto data_framing")
{:data_framing, ""}
{:error, wrong_header} ->
IO.inspect("falied because: #{inspect(wrong_header)}")
{:failed, :close}
end
end
连接建立之后就可以以特定的格式收发消息了,消息的最小单位是 frame,它的结构是这样的:
def encode(%{opcode: op, mask: mask} = meta) do
op = enop(op)
payload = if mask, do: meta.masked_payload, else: meta.payload
mask_key = if mask, do: meta.mask_key, else: <<>>
mask = if mask, do: 1, else: 0
<<1::size(1), 0::size(3), op::size(4), mask::size(1),
encode_payload_length(byte_size(payload))::bitstring, mask_key::bytes, payload::bytes>>
end
客户端发送给服务器的内容需要 mask,而服务端发给客户端的则不需要。
opcode 常用的有 text,binary,ping,pong,close. 分别代表不同的消息类型。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。