A few days ago, I couldn’t get a WebSocket library working with another library on Python 3.10. So to avoid those dependencies, I implemented my WebSocket client on a low-level socket API. I implemented one in Common Lisp first. Then I translated it to Python. My WebSocket client is very far away from being completed, but at least it can run.
The first part is import. I imported only three things that are socket, ssl, and uuid.
<span>import</span> <span>socket</span><span>import</span> <span>ssl</span><span>import</span> <span>uuid</span><span>import</span> <span>socket</span> <span>import</span> <span>ssl</span> <span>import</span> <span>uuid</span>import socket import ssl import uuid
Enter fullscreen mode Exit fullscreen mode
The second part is based on HTTP headers for telling the server to switch to the WebSocket mode.
<span>def</span> <span>upgrade</span><span>(</span><span>s</span><span>,</span> <span>conn_info</span><span>):</span><span>with</span> <span>s</span><span>.</span><span>makefile</span><span>(</span><span>mode</span> <span>=</span> <span>'rw'</span><span>,</span> <span>encoding</span> <span>=</span> <span>"ISO-8859-1"</span><span>)</span> <span>as</span> <span>f</span><span>:</span><span>f</span><span>.</span><span>write</span><span>(</span><span>f</span><span>'GET </span><span>{</span><span>conn_info</span><span>[</span><span>"path"</span><span>]</span><span>}</span><span> HTTP/1.1</span><span>\r\n</span><span>'</span><span>)</span><span>f</span><span>.</span><span>write</span><span>(</span><span>f</span><span>'Host: </span><span>{</span><span>conn_info</span><span>[</span><span>"host"</span><span>]</span><span>}</span><span>\r\n</span><span>'</span><span>)</span><span>f</span><span>.</span><span>write</span><span>(</span><span>"Connection: Upgrade</span><span>\r\n</span><span>"</span><span>)</span><span>f</span><span>.</span><span>write</span><span>(</span><span>"Upgrade: websocket</span><span>\r\n</span><span>"</span><span>)</span><span>f</span><span>.</span><span>write</span><span>(</span><span>f</span><span>'Sec-Websocket-Key: </span><span>{</span><span>str</span><span>(</span><span>uuid</span><span>.</span><span>uuid1</span><span>())</span><span>}</span><span>\r\n</span><span>'</span><span>)</span><span>f</span><span>.</span><span>write</span><span>(</span><span>"Sec-WebSocket-Version: 13</span><span>\r\n</span><span>"</span><span>)</span><span>f</span><span>.</span><span>write</span><span>(</span><span>"</span><span>\r\n</span><span>"</span><span>)</span><span>f</span><span>.</span><span>flush</span><span>()</span><span># reading response </span> <span>for</span> <span>line</span> <span>in</span> <span>f</span><span>:</span><span>if</span> <span>line</span> <span>==</span> <span>"</span><span>\n</span><span>"</span><span>:</span><span>break</span><span>def</span> <span>upgrade</span><span>(</span><span>s</span><span>,</span> <span>conn_info</span><span>):</span> <span>with</span> <span>s</span><span>.</span><span>makefile</span><span>(</span><span>mode</span> <span>=</span> <span>'rw'</span><span>,</span> <span>encoding</span> <span>=</span> <span>"ISO-8859-1"</span><span>)</span> <span>as</span> <span>f</span><span>:</span> <span>f</span><span>.</span><span>write</span><span>(</span><span>f</span><span>'GET </span><span>{</span><span>conn_info</span><span>[</span><span>"path"</span><span>]</span><span>}</span><span> HTTP/1.1</span><span>\r\n</span><span>'</span><span>)</span> <span>f</span><span>.</span><span>write</span><span>(</span><span>f</span><span>'Host: </span><span>{</span><span>conn_info</span><span>[</span><span>"host"</span><span>]</span><span>}</span><span>\r\n</span><span>'</span><span>)</span> <span>f</span><span>.</span><span>write</span><span>(</span><span>"Connection: Upgrade</span><span>\r\n</span><span>"</span><span>)</span> <span>f</span><span>.</span><span>write</span><span>(</span><span>"Upgrade: websocket</span><span>\r\n</span><span>"</span><span>)</span> <span>f</span><span>.</span><span>write</span><span>(</span><span>f</span><span>'Sec-Websocket-Key: </span><span>{</span><span>str</span><span>(</span><span>uuid</span><span>.</span><span>uuid1</span><span>())</span><span>}</span><span>\r\n</span><span>'</span><span>)</span> <span>f</span><span>.</span><span>write</span><span>(</span><span>"Sec-WebSocket-Version: 13</span><span>\r\n</span><span>"</span><span>)</span> <span>f</span><span>.</span><span>write</span><span>(</span><span>"</span><span>\r\n</span><span>"</span><span>)</span> <span>f</span><span>.</span><span>flush</span><span>()</span> <span># reading response </span> <span>for</span> <span>line</span> <span>in</span> <span>f</span><span>:</span> <span>if</span> <span>line</span> <span>==</span> <span>"</span><span>\n</span><span>"</span><span>:</span> <span>break</span>def upgrade(s, conn_info): with s.makefile(mode = 'rw', encoding = "ISO-8859-1") as f: f.write(f'GET {conn_info["path"]} HTTP/1.1\r\n') f.write(f'Host: {conn_info["host"]}\r\n') f.write("Connection: Upgrade\r\n") f.write("Upgrade: websocket\r\n") f.write(f'Sec-Websocket-Key: {str(uuid.uuid1())}\r\n') f.write("Sec-WebSocket-Version: 13\r\n") f.write("\r\n") f.flush() # reading response for line in f: if line == "\n": break
Enter fullscreen mode Exit fullscreen mode
The third part is reading a content length. I assume that the server keep sending text contents. In read_payload_len(), s.recv(1) read a byte from the socket s. & 0x7F is for masking only 7 bits. If length is 127, the length is in the next 4 bytes instead. If length is 126, the length is in the next 2 bytes. Otherwise the function just returns the length.
<span>def</span> <span>read_payload_len</span><span>(</span><span>s</span><span>):</span><span>match</span> <span>s</span><span>.</span><span>recv</span><span>(</span><span>1</span><span>)[</span><span>0</span><span>]</span> <span>&</span> <span>0x7F</span><span>:</span><span>case</span> <span>127</span><span>:</span><span>return</span> <span>read_extra_len</span><span>(</span><span>s</span><span>,</span> <span>4</span><span>)</span><span>case</span> <span>126</span><span>:</span><span>return</span> <span>read_extra_len</span><span>(</span><span>s</span><span>,</span> <span>2</span><span>)</span><span>case</span> <span>l</span><span>:</span><span>return</span> <span>l</span><span>def</span> <span>read_payload_len</span><span>(</span><span>s</span><span>):</span> <span>match</span> <span>s</span><span>.</span><span>recv</span><span>(</span><span>1</span><span>)[</span><span>0</span><span>]</span> <span>&</span> <span>0x7F</span><span>:</span> <span>case</span> <span>127</span><span>:</span> <span>return</span> <span>read_extra_len</span><span>(</span><span>s</span><span>,</span> <span>4</span><span>)</span> <span>case</span> <span>126</span><span>:</span> <span>return</span> <span>read_extra_len</span><span>(</span><span>s</span><span>,</span> <span>2</span><span>)</span> <span>case</span> <span>l</span><span>:</span> <span>return</span> <span>l</span>def read_payload_len(s): match s.recv(1)[0] & 0x7F: case 127: return read_extra_len(s, 4) case 126: return read_extra_len(s, 2) case l: return l
Enter fullscreen mode Exit fullscreen mode
The read_extra_len reads bytes and turn them to integer.
<span>def</span> <span>read_extra_len</span><span>(</span><span>s</span><span>,</span> <span>num_of_bytes</span><span>):</span><span>buf</span> <span>=</span> <span>s</span><span>.</span><span>recv</span><span>(</span><span>num_of_bytes</span><span>)</span><span>len</span> <span>=</span> <span>0</span><span>for</span> <span>i</span> <span>in</span> <span>range</span><span>(</span><span>num_of_bytes</span><span>):</span><span>len</span> <span>+=</span> <span>buf</span><span>[</span><span>i</span><span>]</span> <span><<</span> <span>(</span><span>8</span> <span>*</span> <span>(</span><span>num_of_bytes</span> <span>-</span> <span>i</span> <span>-</span> <span>1</span><span>))</span><span>return</span> <span>len</span><span>def</span> <span>read_extra_len</span><span>(</span><span>s</span><span>,</span> <span>num_of_bytes</span><span>):</span> <span>buf</span> <span>=</span> <span>s</span><span>.</span><span>recv</span><span>(</span><span>num_of_bytes</span><span>)</span> <span>len</span> <span>=</span> <span>0</span> <span>for</span> <span>i</span> <span>in</span> <span>range</span><span>(</span><span>num_of_bytes</span><span>):</span> <span>len</span> <span>+=</span> <span>buf</span><span>[</span><span>i</span><span>]</span> <span><<</span> <span>(</span><span>8</span> <span>*</span> <span>(</span><span>num_of_bytes</span> <span>-</span> <span>i</span> <span>-</span> <span>1</span><span>))</span> <span>return</span> <span>len</span>def read_extra_len(s, num_of_bytes): buf = s.recv(num_of_bytes) len = 0 for i in range(num_of_bytes): len += buf[i] << (8 * (num_of_bytes - i - 1)) return len
Enter fullscreen mode Exit fullscreen mode
In the fourth part, we read a web socket frame. The program determine a frame type from opcode, which in the last 4 bits of the header. I should implement PING-PONG part but I didn’t. According to RFC6455, which I forgot to mention before, opcode == 0x1 means the frame is a text frame. So the program reads payload length and reads the payload.
<span>def</span> <span>read_frame</span><span>(</span><span>s</span><span>):</span><span>header0</span> <span>=</span> <span>s</span><span>.</span><span>recv</span><span>(</span><span>1</span><span>)[</span><span>0</span><span>]</span><span>opcode</span> <span>=</span> <span>header0</span> <span>&</span> <span>0x0F</span><span>match</span> <span>opcode</span><span>:</span><span>case</span> <span>0x1</span><span>:</span><span>payload_len</span> <span>=</span> <span>read_payload_len</span><span>(</span><span>s</span><span>)</span><span>print</span><span>(</span><span>s</span><span>.</span><span>recv</span><span>(</span><span>payload_len</span><span>))</span><span>case</span> <span>0x9</span><span>:</span><span>print</span><span>(</span><span>"PING"</span><span>)</span><span>case</span> <span>0xA</span><span>:</span><span>print</span><span>(</span><span>"PONG"</span><span>)</span><span>def</span> <span>read_frame</span><span>(</span><span>s</span><span>):</span> <span>header0</span> <span>=</span> <span>s</span><span>.</span><span>recv</span><span>(</span><span>1</span><span>)[</span><span>0</span><span>]</span> <span>opcode</span> <span>=</span> <span>header0</span> <span>&</span> <span>0x0F</span> <span>match</span> <span>opcode</span><span>:</span> <span>case</span> <span>0x1</span><span>:</span> <span>payload_len</span> <span>=</span> <span>read_payload_len</span><span>(</span><span>s</span><span>)</span> <span>print</span><span>(</span><span>s</span><span>.</span><span>recv</span><span>(</span><span>payload_len</span><span>))</span> <span>case</span> <span>0x9</span><span>:</span> <span>print</span><span>(</span><span>"PING"</span><span>)</span> <span>case</span> <span>0xA</span><span>:</span> <span>print</span><span>(</span><span>"PONG"</span><span>)</span>def read_frame(s): header0 = s.recv(1)[0] opcode = header0 & 0x0F match opcode: case 0x1: payload_len = read_payload_len(s) print(s.recv(payload_len)) case 0x9: print("PING") case 0xA: print("PONG")
Enter fullscreen mode Exit fullscreen mode
The last part is for opening connections and SSL/TLS wrapper. The function created a socket and wrapped it with TLS/SSL wrapper. The sending upgrade message to ask the server to switch to Websocket mode and the keep reading frames.
<span>def</span> <span>connect</span><span>(</span><span>conn_info</span><span>):</span><span>ctx</span> <span>=</span> <span>ssl</span><span>.</span><span>create_default_context</span><span>()</span><span>with</span> <span>socket</span><span>.</span><span>create_connection</span><span>((</span><span>conn_info</span><span>[</span><span>"host"</span><span>],</span> <span>conn_info</span><span>[</span><span>"port"</span><span>]))</span> <span>as</span> <span>s</span><span>:</span><span>with</span> <span>ctx</span><span>.</span><span>wrap_socket</span><span>(</span><span>s</span><span>,</span> <span>server_hostname</span> <span>=</span> <span>conn_info</span><span>[</span><span>"host"</span><span>])</span> <span>as</span> <span>ss</span><span>:</span><span>upgrade</span><span>(</span><span>ss</span><span>,</span> <span>conn_info</span><span>)</span><span>while</span> <span>True</span><span>:</span><span>read_frame</span><span>(</span><span>ss</span><span>)</span><span>def</span> <span>connect</span><span>(</span><span>conn_info</span><span>):</span> <span>ctx</span> <span>=</span> <span>ssl</span><span>.</span><span>create_default_context</span><span>()</span> <span>with</span> <span>socket</span><span>.</span><span>create_connection</span><span>((</span><span>conn_info</span><span>[</span><span>"host"</span><span>],</span> <span>conn_info</span><span>[</span><span>"port"</span><span>]))</span> <span>as</span> <span>s</span><span>:</span> <span>with</span> <span>ctx</span><span>.</span><span>wrap_socket</span><span>(</span><span>s</span><span>,</span> <span>server_hostname</span> <span>=</span> <span>conn_info</span><span>[</span><span>"host"</span><span>])</span> <span>as</span> <span>ss</span><span>:</span> <span>upgrade</span><span>(</span><span>ss</span><span>,</span> <span>conn_info</span><span>)</span> <span>while</span> <span>True</span><span>:</span> <span>read_frame</span><span>(</span><span>ss</span><span>)</span>def connect(conn_info): ctx = ssl.create_default_context() with socket.create_connection((conn_info["host"], conn_info["port"])) as s: with ctx.wrap_socket(s, server_hostname = conn_info["host"]) as ss: upgrade(ss, conn_info) while True: read_frame(ss)
Enter fullscreen mode Exit fullscreen mode
In the final part, I cannot find any public Websocket endpoint besides ones from cryptocurrency exchanges. So I put Bitkub API.
<span>connect</span><span>({</span><span>"host"</span><span>:</span> <span>"api.bitkub.com"</span><span>,</span><span>"port"</span><span>:</span> <span>443</span><span>,</span><span>"path"</span><span>:</span> <span>"/websocket-api/market.trade.thb_btc"</span><span>})</span><span>connect</span><span>({</span><span>"host"</span><span>:</span> <span>"api.bitkub.com"</span><span>,</span> <span>"port"</span><span>:</span> <span>443</span><span>,</span> <span>"path"</span><span>:</span> <span>"/websocket-api/market.trade.thb_btc"</span><span>})</span>connect({"host": "api.bitkub.com", "port": 443, "path": "/websocket-api/market.trade.thb_btc"})
Enter fullscreen mode Exit fullscreen mode
And it works, but if you are unlucky, you will get PING instead.
> python3.10 http_ex.pyb'{"amt":0.00004661,"bid":86911896,"rat":2140000,"sid":82187323,"stream":"market.trade.thb_btc","sym":"THB_BTC","ts":1636729677,"txn":"BTCSELL0011078574"}\n{"amt":0.23306074,"bid":86912246,"rat":2140000,"sid":82187325,"stream":"market.trade.thb_btc","sym":"THB_BTC","ts":1636729677,"txn":"BTCSELL0011078576"}'b'{"amt":0.00466121,"bid":86912014,"rat":2140000,"sid":82187324,"stream":"market.trade.thb_btc","sym":"THB_BTC","ts":1636729677,"txn":"BTCSELL0011078575"}'> python3.10 http_ex.py b'{"amt":0.00004661,"bid":86911896,"rat":2140000,"sid":82187323,"stream":"market.trade.thb_btc","sym":"THB_BTC","ts":1636729677,"txn":"BTCSELL0011078574"}\n{"amt":0.23306074,"bid":86912246,"rat":2140000,"sid":82187325,"stream":"market.trade.thb_btc","sym":"THB_BTC","ts":1636729677,"txn":"BTCSELL0011078576"}' b'{"amt":0.00466121,"bid":86912014,"rat":2140000,"sid":82187324,"stream":"market.trade.thb_btc","sym":"THB_BTC","ts":1636729677,"txn":"BTCSELL0011078575"}'> python3.10 http_ex.py b'{"amt":0.00004661,"bid":86911896,"rat":2140000,"sid":82187323,"stream":"market.trade.thb_btc","sym":"THB_BTC","ts":1636729677,"txn":"BTCSELL0011078574"}\n{"amt":0.23306074,"bid":86912246,"rat":2140000,"sid":82187325,"stream":"market.trade.thb_btc","sym":"THB_BTC","ts":1636729677,"txn":"BTCSELL0011078576"}' b'{"amt":0.00466121,"bid":86912014,"rat":2140000,"sid":82187324,"stream":"market.trade.thb_btc","sym":"THB_BTC","ts":1636729677,"txn":"BTCSELL0011078575"}'
Enter fullscreen mode Exit fullscreen mode
原文链接:An incomplete WebSocket client based on only socket, ssl, and uuid in Python
暂无评论内容