picows 0.2.0__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {picows-0.2.0 → picows-0.2.2}/PKG-INFO +21 -8
- {picows-0.2.0 → picows-0.2.2}/README.rst +20 -7
- picows-0.2.2/picows/__init__.py +23 -0
- {picows-0.2.0 → picows-0.2.2}/picows/picows.pxd +1 -1
- {picows-0.2.0 → picows-0.2.2}/picows/picows.pyx +73 -16
- {picows-0.2.0 → picows-0.2.2}/picows.egg-info/PKG-INFO +21 -8
- {picows-0.2.0 → picows-0.2.2}/picows.egg-info/SOURCES.txt +2 -1
- {picows-0.2.0 → picows-0.2.2}/setup.py +8 -4
- picows-0.2.2/tests/test_echo.py +76 -0
- picows-0.2.0/picows/__init__.py +0 -12
- {picows-0.2.0 → picows-0.2.2}/MANIFEST.in +0 -0
- {picows-0.2.0 → picows-0.2.2}/picows.egg-info/dependency_links.txt +0 -0
- {picows-0.2.0 → picows-0.2.2}/picows.egg-info/requires.txt +0 -0
- {picows-0.2.0 → picows-0.2.2}/picows.egg-info/top_level.txt +0 -0
- {picows-0.2.0 → picows-0.2.2}/pyproject.toml +0 -0
- {picows-0.2.0 → picows-0.2.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: picows
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Fast websocket client and server for asyncio
|
|
5
5
|
Author-email: Taras Kozlov <tarasko.projects@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/tarasko/picows
|
|
@@ -28,6 +28,18 @@ Description-Content-Type: text/x-rst
|
|
|
28
28
|
Provides-Extra: dev
|
|
29
29
|
Requires-Dist: pytest; extra == "dev"
|
|
30
30
|
|
|
31
|
+
.. image:: https://github.com/tarasko/picows/workflows/run%20tests/badge.svg
|
|
32
|
+
:target: https://github.com/tarasko/picows/actions?query=workflow%3Arun-tests
|
|
33
|
+
:alt: GitHub Actions status for master branch
|
|
34
|
+
|
|
35
|
+
.. image:: https://badge.fury.io/py/picows.svg
|
|
36
|
+
:target: https://pypi.org/project/picows
|
|
37
|
+
:alt: Latest PyPI package version
|
|
38
|
+
|
|
39
|
+
.. image:: https://img.shields.io/pypi/dm/picows
|
|
40
|
+
:target: https://pypistats.org/packages/picows
|
|
41
|
+
:alt: Downloads count
|
|
42
|
+
|
|
31
43
|
picows is a library for building WebSocket clients and servers with a focus on performance.
|
|
32
44
|
|
|
33
45
|
Performance
|
|
@@ -57,9 +69,11 @@ These features come with a significant cost even when messages are small, unfrag
|
|
|
57
69
|
|
|
58
70
|
API Design
|
|
59
71
|
----------
|
|
60
|
-
The API follows low-level `transport/protocol design from asyncio <https://docs.python.org/3/library/asyncio-protocol.html#asyncio-transports-protocols>`_
|
|
61
|
-
It passes frames instead of messages to a user handler. A message can potentially consist of multiple
|
|
62
|
-
|
|
72
|
+
The API follows the low-level `transport/protocol design from asyncio <https://docs.python.org/3/library/asyncio-protocol.html#asyncio-transports-protocols>`_.
|
|
73
|
+
It passes frames instead of messages to a user handler. A message can potentially consist of multiple frames but it is up to user to choose the best strategy for merging them.
|
|
74
|
+
Same principle applies for compression and flow control. User can implement their own strategies using the most appropriate tools.
|
|
75
|
+
|
|
76
|
+
That being said that the most common use-case is when messages and frames are the same, i.e. a message consists of only a single frame, and no compression is being used.
|
|
63
77
|
|
|
64
78
|
Getting started
|
|
65
79
|
---------------
|
|
@@ -76,6 +90,7 @@ Connects to an echo server, sends a message and disconnect upon reply.
|
|
|
76
90
|
|
|
77
91
|
class ClientListener(WSListener):
|
|
78
92
|
def on_ws_connected(self, transport: WSTransport):
|
|
93
|
+
self.transport = transport
|
|
79
94
|
transport.send(WSMsgType.TEXT, b"Hello world")
|
|
80
95
|
|
|
81
96
|
def on_ws_frame(self, transport: WSTransport, frame: WSFrame):
|
|
@@ -84,9 +99,8 @@ Connects to an echo server, sends a message and disconnect upon reply.
|
|
|
84
99
|
|
|
85
100
|
|
|
86
101
|
async def main(endpoint):
|
|
87
|
-
# ClientListener instance will be created after successfull accept and http upgrade.
|
|
88
102
|
(_, client) = await ws_connect(endpoint, ClientListener, "client")
|
|
89
|
-
await client.
|
|
103
|
+
await client.transport.wait_until_closed()
|
|
90
104
|
|
|
91
105
|
|
|
92
106
|
if __name__ == '__main__':
|
|
@@ -106,7 +120,7 @@ Echo server
|
|
|
106
120
|
|
|
107
121
|
import asyncio
|
|
108
122
|
import uvloop
|
|
109
|
-
from picows import WSFrame, WSTransport, WSListener,
|
|
123
|
+
from picows import WSFrame, WSTransport, WSListener, ws_create_server, WSMsgType
|
|
110
124
|
|
|
111
125
|
class ServerClientListener(WSListener):
|
|
112
126
|
def on_ws_connected(self, transport: WSTransport):
|
|
@@ -119,7 +133,6 @@ Echo server
|
|
|
119
133
|
|
|
120
134
|
async def main():
|
|
121
135
|
url = "ws://127.0.0.1:9001"
|
|
122
|
-
# ServerClientListener instance will be created for each client after accept and successfull http upgrade.
|
|
123
136
|
server = await ws_create_server(url, ServerClientListener, "server")
|
|
124
137
|
print(f"Server started on {url}")
|
|
125
138
|
await server.serve_forever()
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
.. image:: https://github.com/tarasko/picows/workflows/run%20tests/badge.svg
|
|
2
|
+
:target: https://github.com/tarasko/picows/actions?query=workflow%3Arun-tests
|
|
3
|
+
:alt: GitHub Actions status for master branch
|
|
4
|
+
|
|
5
|
+
.. image:: https://badge.fury.io/py/picows.svg
|
|
6
|
+
:target: https://pypi.org/project/picows
|
|
7
|
+
:alt: Latest PyPI package version
|
|
8
|
+
|
|
9
|
+
.. image:: https://img.shields.io/pypi/dm/picows
|
|
10
|
+
:target: https://pypistats.org/packages/picows
|
|
11
|
+
:alt: Downloads count
|
|
12
|
+
|
|
1
13
|
picows is a library for building WebSocket clients and servers with a focus on performance.
|
|
2
14
|
|
|
3
15
|
Performance
|
|
@@ -27,9 +39,11 @@ These features come with a significant cost even when messages are small, unfrag
|
|
|
27
39
|
|
|
28
40
|
API Design
|
|
29
41
|
----------
|
|
30
|
-
The API follows low-level `transport/protocol design from asyncio <https://docs.python.org/3/library/asyncio-protocol.html#asyncio-transports-protocols>`_
|
|
31
|
-
It passes frames instead of messages to a user handler. A message can potentially consist of multiple
|
|
32
|
-
|
|
42
|
+
The API follows the low-level `transport/protocol design from asyncio <https://docs.python.org/3/library/asyncio-protocol.html#asyncio-transports-protocols>`_.
|
|
43
|
+
It passes frames instead of messages to a user handler. A message can potentially consist of multiple frames but it is up to user to choose the best strategy for merging them.
|
|
44
|
+
Same principle applies for compression and flow control. User can implement their own strategies using the most appropriate tools.
|
|
45
|
+
|
|
46
|
+
That being said that the most common use-case is when messages and frames are the same, i.e. a message consists of only a single frame, and no compression is being used.
|
|
33
47
|
|
|
34
48
|
Getting started
|
|
35
49
|
---------------
|
|
@@ -46,6 +60,7 @@ Connects to an echo server, sends a message and disconnect upon reply.
|
|
|
46
60
|
|
|
47
61
|
class ClientListener(WSListener):
|
|
48
62
|
def on_ws_connected(self, transport: WSTransport):
|
|
63
|
+
self.transport = transport
|
|
49
64
|
transport.send(WSMsgType.TEXT, b"Hello world")
|
|
50
65
|
|
|
51
66
|
def on_ws_frame(self, transport: WSTransport, frame: WSFrame):
|
|
@@ -54,9 +69,8 @@ Connects to an echo server, sends a message and disconnect upon reply.
|
|
|
54
69
|
|
|
55
70
|
|
|
56
71
|
async def main(endpoint):
|
|
57
|
-
# ClientListener instance will be created after successfull accept and http upgrade.
|
|
58
72
|
(_, client) = await ws_connect(endpoint, ClientListener, "client")
|
|
59
|
-
await client.
|
|
73
|
+
await client.transport.wait_until_closed()
|
|
60
74
|
|
|
61
75
|
|
|
62
76
|
if __name__ == '__main__':
|
|
@@ -76,7 +90,7 @@ Echo server
|
|
|
76
90
|
|
|
77
91
|
import asyncio
|
|
78
92
|
import uvloop
|
|
79
|
-
from picows import WSFrame, WSTransport, WSListener,
|
|
93
|
+
from picows import WSFrame, WSTransport, WSListener, ws_create_server, WSMsgType
|
|
80
94
|
|
|
81
95
|
class ServerClientListener(WSListener):
|
|
82
96
|
def on_ws_connected(self, transport: WSTransport):
|
|
@@ -89,7 +103,6 @@ Echo server
|
|
|
89
103
|
|
|
90
104
|
async def main():
|
|
91
105
|
url = "ws://127.0.0.1:9001"
|
|
92
|
-
# ServerClientListener instance will be created for each client after accept and successfull http upgrade.
|
|
93
106
|
server = await ws_create_server(url, ServerClientListener, "server")
|
|
94
107
|
print(f"Server started on {url}")
|
|
95
108
|
await server.serve_forever()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
__version__ = "0.2.2"
|
|
2
|
+
|
|
3
|
+
from .picows import (
|
|
4
|
+
WSMsgType,
|
|
5
|
+
WSCloseCode,
|
|
6
|
+
WSFrame,
|
|
7
|
+
WSTransport,
|
|
8
|
+
WSListener,
|
|
9
|
+
ws_connect,
|
|
10
|
+
ws_create_server,
|
|
11
|
+
PICOWS_DEBUG_LL
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
'WSMsgType',
|
|
16
|
+
'WSCloseCode',
|
|
17
|
+
'WSFrame',
|
|
18
|
+
'WSTransport',
|
|
19
|
+
'WSListener',
|
|
20
|
+
'ws_connect',
|
|
21
|
+
'ws_create_server',
|
|
22
|
+
'PICOWS_DEBUG_LL'
|
|
23
|
+
]
|
|
@@ -59,8 +59,8 @@ cdef class WSFrame:
|
|
|
59
59
|
readonly uint8_t fin
|
|
60
60
|
readonly uint8_t last_in_buffer
|
|
61
61
|
|
|
62
|
-
# Creates a new python object every time
|
|
63
62
|
cpdef bytes get_payload_as_bytes(self)
|
|
63
|
+
cpdef str get_payload_as_utf8_text(self)
|
|
64
64
|
cpdef str get_payload_as_ascii_text(self)
|
|
65
65
|
cpdef object get_payload_as_memoryview(self)
|
|
66
66
|
|
|
@@ -23,21 +23,55 @@ from libc.stdlib cimport rand
|
|
|
23
23
|
PICOWS_DEBUG_LL = 9
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
cdef extern from
|
|
26
|
+
cdef extern from * nogil:
|
|
27
|
+
"""
|
|
28
|
+
#if (defined(_WIN16) || defined(_WIN32) || defined(_WIN64)) && !defined(__WINDOWS__)
|
|
29
|
+
# define __WINDOWS__
|
|
30
|
+
#endif
|
|
31
|
+
|
|
32
|
+
#if defined(__linux__)
|
|
33
|
+
#include <arpa/inet.h>
|
|
34
|
+
#include <endian.h>
|
|
35
|
+
#elif defined(__APPLE__)
|
|
36
|
+
#include <arpa/inet.h>
|
|
37
|
+
#include <libkern/OSByteOrder.h>
|
|
38
|
+
#define be64toh(x) OSSwapBigToHostInt64(x)
|
|
39
|
+
#define htobe64(x) OSSwapHostToBigInt64(x)
|
|
40
|
+
#elif defined(__OpenBSD__)
|
|
41
|
+
#include <arpa/inet.h>
|
|
42
|
+
#include <sys/endian.h>
|
|
43
|
+
#elif defined(__NetBSD__) || defined(__FreeBSD__) || defined(__DragonFly__)
|
|
44
|
+
#include <arpa/inet.h>
|
|
45
|
+
#include <sys/endian.h>
|
|
46
|
+
#define be64toh(x) betoh64(x)
|
|
47
|
+
#elif defined(__WINDOWS__)
|
|
48
|
+
#include <winsock2.h>
|
|
49
|
+
#if BYTE_ORDER == LITTLE_ENDIAN
|
|
50
|
+
#define be64toh(x) ntohll(x)
|
|
51
|
+
#define htobe64(x) htonll(x)
|
|
52
|
+
#elif BYTE_ORDER == BIG_ENDIAN
|
|
53
|
+
#define be64toh(x) (x)
|
|
54
|
+
#define htobe64(x) (x)
|
|
55
|
+
#endif
|
|
56
|
+
#else
|
|
57
|
+
error byte order not supported
|
|
58
|
+
#endif
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
# Network order is big-endian
|
|
62
|
+
|
|
27
63
|
uint32_t ntohl(uint32_t)
|
|
28
64
|
uint32_t htonl(uint32_t)
|
|
29
65
|
uint16_t ntohs(uint16_t)
|
|
30
66
|
uint16_t htons(uint16_t)
|
|
31
67
|
|
|
32
|
-
|
|
33
|
-
cdef extern from "endian.h" nogil:
|
|
34
|
-
# Network order is big-endian
|
|
35
68
|
uint64_t be64toh(uint64_t)
|
|
36
69
|
uint64_t htobe64(uint64_t)
|
|
37
70
|
|
|
38
71
|
|
|
39
72
|
cdef extern from "Python.h":
|
|
40
73
|
PyObject *PyUnicode_FromStringAndSize(const char *u, Py_ssize_t size)
|
|
74
|
+
PyObject *PyUnicode_DecodeASCII(char *s, Py_ssize_t size, char *errors)
|
|
41
75
|
|
|
42
76
|
|
|
43
77
|
class PicowsError(Exception):
|
|
@@ -79,15 +113,40 @@ cdef _mask_payload(uint8_t* input, size_t input_len, uint32_t mask):
|
|
|
79
113
|
@cython.freelist(64)
|
|
80
114
|
cdef class WSFrame:
|
|
81
115
|
cpdef bytes get_payload_as_bytes(self):
|
|
116
|
+
"""
|
|
117
|
+
Returns a new bytes object with a copy of frame payload.
|
|
118
|
+
"""
|
|
82
119
|
return PyBytes_FromStringAndSize(self.payload_ptr, <Py_ssize_t>self.payload_size)
|
|
83
120
|
|
|
121
|
+
cpdef str get_payload_as_utf8_text(self):
|
|
122
|
+
"""
|
|
123
|
+
Interpret payload as UTF8 text and returns a new str object.
|
|
124
|
+
Behaviour is underfined (most likely python will crash) if payload doesn't contain a valid UTF8
|
|
125
|
+
"""
|
|
126
|
+
cdef str s = <str>PyUnicode_FromStringAndSize(self.payload_ptr, <Py_ssize_t>self.payload_size)
|
|
127
|
+
# Workaround for broken cython reference counting
|
|
128
|
+
Py_DECREF(s)
|
|
129
|
+
return s
|
|
130
|
+
|
|
84
131
|
cpdef str get_payload_as_ascii_text(self):
|
|
132
|
+
"""
|
|
133
|
+
Interpret payload as UTF8 text and returns a new str object.
|
|
134
|
+
Behaviour is underfined (most likely python will crash) if payload doesn't contain a valid UTF8
|
|
135
|
+
"""
|
|
136
|
+
cdef PyObject* ptr = PyUnicode_DecodeASCII(self.payload_ptr, <Py_ssize_t>self.payload_size, NULL)
|
|
137
|
+
if ptr == NULL:
|
|
138
|
+
raise PicowsError("payload doesn't contain ASCII string")
|
|
139
|
+
cdef str s = <str>ptr
|
|
85
140
|
# Workaround for broken cython reference counting
|
|
86
|
-
cdef str s = <str> PyUnicode_FromStringAndSize(self.payload_ptr, <Py_ssize_t>self.payload_size)
|
|
87
141
|
Py_DECREF(s)
|
|
88
142
|
return s
|
|
89
143
|
|
|
90
144
|
cpdef object get_payload_as_memoryview(self):
|
|
145
|
+
"""
|
|
146
|
+
Return continous memoryview to a parser buffer with payload.
|
|
147
|
+
Memoryview content will be invalidated after on_ws_frame is complete.
|
|
148
|
+
Please process payload or copy it as soon as possible.
|
|
149
|
+
"""
|
|
91
150
|
return PyMemoryView_FromMemory(self.payload_ptr, <Py_ssize_t>self.payload_size, PyBUF_READ)
|
|
92
151
|
|
|
93
152
|
cpdef WSCloseCode get_close_code(self):
|
|
@@ -264,7 +323,7 @@ cdef class WSFrameParser:
|
|
|
264
323
|
if rsv1 or rsv2 or rsv3:
|
|
265
324
|
mem_dump = PyBytes_FromStringAndSize(
|
|
266
325
|
self._buffer.data + self._f_curr_state_start_pos,
|
|
267
|
-
max(self._f_new_data_start_pos - self._f_curr_state_start_pos, 64)
|
|
326
|
+
max(self._f_new_data_start_pos - self._f_curr_state_start_pos, <size_t>64)
|
|
268
327
|
)
|
|
269
328
|
raise PicowsError(
|
|
270
329
|
WSCloseCode.PROTOCOL_ERROR,
|
|
@@ -627,7 +686,6 @@ cdef class WSProtocol:
|
|
|
627
686
|
WSFrameParser _frame_parser
|
|
628
687
|
object _loop
|
|
629
688
|
object _handshake_timeout_handle
|
|
630
|
-
object _listener_factory
|
|
631
689
|
bint _is_client_side
|
|
632
690
|
bint _log_debug_enabled
|
|
633
691
|
|
|
@@ -641,12 +699,11 @@ cdef class WSProtocol:
|
|
|
641
699
|
self._frame_parser = None
|
|
642
700
|
self._loop = asyncio.get_running_loop()
|
|
643
701
|
self._handshake_timeout_handle = None
|
|
644
|
-
self._listener_factory = ws_listener_factory
|
|
645
702
|
self._is_client_side = is_client_side
|
|
646
703
|
self._log_debug_enabled = self._logger.isEnabledFor(PICOWS_DEBUG_LL)
|
|
647
704
|
|
|
648
705
|
self.transport = None
|
|
649
|
-
self.listener =
|
|
706
|
+
self.listener = ws_listener_factory()
|
|
650
707
|
|
|
651
708
|
def connection_made(self, transport: asyncio.Transport):
|
|
652
709
|
sock = transport.get_extra_info('socket')
|
|
@@ -654,24 +711,27 @@ cdef class WSProtocol:
|
|
|
654
711
|
sockname = transport.get_extra_info('sockname')
|
|
655
712
|
|
|
656
713
|
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
|
657
|
-
|
|
714
|
+
if hasattr(socket, "TCP_QUICKACK"):
|
|
715
|
+
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_QUICKACK, 1)
|
|
658
716
|
|
|
659
717
|
self._logger = self._logger.getChild(str(sock.fileno()))
|
|
660
718
|
self._frame_parser = WSFrameParser(self._logger)
|
|
661
719
|
|
|
720
|
+
quickack = sock.getsockopt(socket.IPPROTO_TCP, socket.TCP_QUICKACK) if hasattr(socket, "TCP_QUICKACK") else False
|
|
721
|
+
|
|
662
722
|
if self._is_client_side:
|
|
663
723
|
self._logger.info("WS connection established: %s -> %s, recvbuf=%d, sendbuf=%d, quickack=%d, nodelay=%d",
|
|
664
724
|
peername, sockname,
|
|
665
725
|
sock.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF),
|
|
666
726
|
sock.getsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF),
|
|
667
|
-
|
|
727
|
+
quickack,
|
|
668
728
|
sock.getsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY))
|
|
669
729
|
else:
|
|
670
730
|
self._logger.info("New connection accepted: %s -> %s, recvbuf=%d, sendbuf=%d, quickack=%d, nodelay=%d",
|
|
671
731
|
peername, sockname,
|
|
672
732
|
sock.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF),
|
|
673
733
|
sock.getsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF),
|
|
674
|
-
|
|
734
|
+
quickack,
|
|
675
735
|
sock.getsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY))
|
|
676
736
|
|
|
677
737
|
|
|
@@ -717,8 +777,6 @@ cdef class WSProtocol:
|
|
|
717
777
|
self._frame_parser.handshake_complete_future.set_result(None)
|
|
718
778
|
self._handshake_timeout_handle.cancel()
|
|
719
779
|
self._handshake_timeout_handle = None
|
|
720
|
-
self.listener = self._listener_factory()
|
|
721
|
-
self._listener_factory = None
|
|
722
780
|
self.listener.on_ws_connected(self.transport)
|
|
723
781
|
|
|
724
782
|
cdef WSFrame frame = self._get_next_frame()
|
|
@@ -793,7 +851,7 @@ async def ws_connect(str url, ws_listener_factory, str logger_name, ssl_context=
|
|
|
793
851
|
else:
|
|
794
852
|
raise ValueError(f"invalid url scheme: {url}")
|
|
795
853
|
|
|
796
|
-
ws_protocol_factory = lambda: WSProtocol(url_parts.netloc, url_parts.path, True,
|
|
854
|
+
ws_protocol_factory = lambda: WSProtocol(url_parts.netloc, url_parts.path, True, ws_listener_factory, logger_name)
|
|
797
855
|
|
|
798
856
|
cdef WSProtocol ws_protocol
|
|
799
857
|
|
|
@@ -802,7 +860,6 @@ async def ws_connect(str url, ws_listener_factory, str logger_name, ssl_context=
|
|
|
802
860
|
ssl_handshake_timeout=ssl_handshake_timeout, ssl_shutdown_timeout=ssl_shutdown_timeout)
|
|
803
861
|
|
|
804
862
|
await ws_protocol.wait_until_handshake_complete()
|
|
805
|
-
ws_protocol.listener = ws_listener_factory()
|
|
806
863
|
ws_protocol.listener.on_ws_connected(ws_protocol.transport)
|
|
807
864
|
|
|
808
865
|
return ws_protocol.transport, ws_protocol.listener
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: picows
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Fast websocket client and server for asyncio
|
|
5
5
|
Author-email: Taras Kozlov <tarasko.projects@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/tarasko/picows
|
|
@@ -28,6 +28,18 @@ Description-Content-Type: text/x-rst
|
|
|
28
28
|
Provides-Extra: dev
|
|
29
29
|
Requires-Dist: pytest; extra == "dev"
|
|
30
30
|
|
|
31
|
+
.. image:: https://github.com/tarasko/picows/workflows/run%20tests/badge.svg
|
|
32
|
+
:target: https://github.com/tarasko/picows/actions?query=workflow%3Arun-tests
|
|
33
|
+
:alt: GitHub Actions status for master branch
|
|
34
|
+
|
|
35
|
+
.. image:: https://badge.fury.io/py/picows.svg
|
|
36
|
+
:target: https://pypi.org/project/picows
|
|
37
|
+
:alt: Latest PyPI package version
|
|
38
|
+
|
|
39
|
+
.. image:: https://img.shields.io/pypi/dm/picows
|
|
40
|
+
:target: https://pypistats.org/packages/picows
|
|
41
|
+
:alt: Downloads count
|
|
42
|
+
|
|
31
43
|
picows is a library for building WebSocket clients and servers with a focus on performance.
|
|
32
44
|
|
|
33
45
|
Performance
|
|
@@ -57,9 +69,11 @@ These features come with a significant cost even when messages are small, unfrag
|
|
|
57
69
|
|
|
58
70
|
API Design
|
|
59
71
|
----------
|
|
60
|
-
The API follows low-level `transport/protocol design from asyncio <https://docs.python.org/3/library/asyncio-protocol.html#asyncio-transports-protocols>`_
|
|
61
|
-
It passes frames instead of messages to a user handler. A message can potentially consist of multiple
|
|
62
|
-
|
|
72
|
+
The API follows the low-level `transport/protocol design from asyncio <https://docs.python.org/3/library/asyncio-protocol.html#asyncio-transports-protocols>`_.
|
|
73
|
+
It passes frames instead of messages to a user handler. A message can potentially consist of multiple frames but it is up to user to choose the best strategy for merging them.
|
|
74
|
+
Same principle applies for compression and flow control. User can implement their own strategies using the most appropriate tools.
|
|
75
|
+
|
|
76
|
+
That being said that the most common use-case is when messages and frames are the same, i.e. a message consists of only a single frame, and no compression is being used.
|
|
63
77
|
|
|
64
78
|
Getting started
|
|
65
79
|
---------------
|
|
@@ -76,6 +90,7 @@ Connects to an echo server, sends a message and disconnect upon reply.
|
|
|
76
90
|
|
|
77
91
|
class ClientListener(WSListener):
|
|
78
92
|
def on_ws_connected(self, transport: WSTransport):
|
|
93
|
+
self.transport = transport
|
|
79
94
|
transport.send(WSMsgType.TEXT, b"Hello world")
|
|
80
95
|
|
|
81
96
|
def on_ws_frame(self, transport: WSTransport, frame: WSFrame):
|
|
@@ -84,9 +99,8 @@ Connects to an echo server, sends a message and disconnect upon reply.
|
|
|
84
99
|
|
|
85
100
|
|
|
86
101
|
async def main(endpoint):
|
|
87
|
-
# ClientListener instance will be created after successfull accept and http upgrade.
|
|
88
102
|
(_, client) = await ws_connect(endpoint, ClientListener, "client")
|
|
89
|
-
await client.
|
|
103
|
+
await client.transport.wait_until_closed()
|
|
90
104
|
|
|
91
105
|
|
|
92
106
|
if __name__ == '__main__':
|
|
@@ -106,7 +120,7 @@ Echo server
|
|
|
106
120
|
|
|
107
121
|
import asyncio
|
|
108
122
|
import uvloop
|
|
109
|
-
from picows import WSFrame, WSTransport, WSListener,
|
|
123
|
+
from picows import WSFrame, WSTransport, WSListener, ws_create_server, WSMsgType
|
|
110
124
|
|
|
111
125
|
class ServerClientListener(WSListener):
|
|
112
126
|
def on_ws_connected(self, transport: WSTransport):
|
|
@@ -119,7 +133,6 @@ Echo server
|
|
|
119
133
|
|
|
120
134
|
async def main():
|
|
121
135
|
url = "ws://127.0.0.1:9001"
|
|
122
|
-
# ServerClientListener instance will be created for each client after accept and successfull http upgrade.
|
|
123
136
|
server = await ws_create_server(url, ServerClientListener, "server")
|
|
124
137
|
print(f"Server started on {url}")
|
|
125
138
|
await server.serve_forever()
|
|
@@ -7,8 +7,13 @@ vi = sys.version_info
|
|
|
7
7
|
if vi < (3, 8):
|
|
8
8
|
raise RuntimeError('picows requires Python 3.8 or greater')
|
|
9
9
|
|
|
10
|
+
if os.name == 'nt':
|
|
11
|
+
libraries = ["Ws2_32"]
|
|
12
|
+
else:
|
|
13
|
+
libraries = None
|
|
14
|
+
|
|
10
15
|
cython_modules = [
|
|
11
|
-
Extension("picows.picows", ["picows/picows.pyx"])
|
|
16
|
+
Extension("picows.picows", ["picows/picows.pyx"], libraries=libraries)
|
|
12
17
|
]
|
|
13
18
|
|
|
14
19
|
if os.getenv("PICOWS_BUILD_EXAMPLES") is not None:
|
|
@@ -17,8 +22,8 @@ if os.getenv("PICOWS_BUILD_EXAMPLES") is not None:
|
|
|
17
22
|
setup(
|
|
18
23
|
ext_modules=cythonize(
|
|
19
24
|
cython_modules,
|
|
20
|
-
compiler_directives
|
|
21
|
-
'language_level'
|
|
25
|
+
compiler_directives={
|
|
26
|
+
'language_level': sys.version_info[0],
|
|
22
27
|
'profile': False,
|
|
23
28
|
'nonecheck': False,
|
|
24
29
|
'boundscheck': False,
|
|
@@ -31,4 +36,3 @@ setup(
|
|
|
31
36
|
gdb_debug=False
|
|
32
37
|
)
|
|
33
38
|
)
|
|
34
|
-
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
import pytest_asyncio
|
|
5
|
+
|
|
6
|
+
import picows
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
URL = "ws://127.0.0.1:9001"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class WSFrameMaterialized:
|
|
13
|
+
def __init__(self, frame: picows.WSFrame):
|
|
14
|
+
self.opcode = frame.opcode
|
|
15
|
+
self.payload_as_bytes = frame.get_payload_as_bytes()
|
|
16
|
+
self.payload_as_bytes_from_mv = bytes(frame.get_payload_as_memoryview())
|
|
17
|
+
self.fin = frame.fin
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
#@pytest.fixture(scope="module")
|
|
21
|
+
@pytest.fixture
|
|
22
|
+
async def echo_server():
|
|
23
|
+
class PicowsServerListener(picows.WSListener):
|
|
24
|
+
def on_ws_connected(self, transport: picows.WSTransport):
|
|
25
|
+
print("echo_server:on_ws_connected")
|
|
26
|
+
self._transport = transport
|
|
27
|
+
|
|
28
|
+
def on_ws_frame(self, transport: picows.WSTransport, frame: picows.WSFrame):
|
|
29
|
+
print("echo_server:on_ws_frame")
|
|
30
|
+
self._transport.send(frame.opcode, frame.get_payload_as_bytes())
|
|
31
|
+
if frame.opcode == picows.WSMsgType.CLOSE:
|
|
32
|
+
self._transport.disconnect()
|
|
33
|
+
|
|
34
|
+
server = await picows.ws_create_server(URL, PicowsServerListener, "server")
|
|
35
|
+
task = asyncio.create_task(server.serve_forever())
|
|
36
|
+
print("initiated module level echo server")
|
|
37
|
+
yield server
|
|
38
|
+
|
|
39
|
+
# Teardown server
|
|
40
|
+
task.cancel()
|
|
41
|
+
try:
|
|
42
|
+
await(task)
|
|
43
|
+
except:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
print("stopped module level echo server")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# @pytest.fixture(scope="module")
|
|
50
|
+
@pytest.fixture
|
|
51
|
+
async def echo_client(echo_server):
|
|
52
|
+
class PicowsClientListener(picows.WSListener):
|
|
53
|
+
def on_ws_connected(self, transport: picows.WSTransport):
|
|
54
|
+
self.transport = transport
|
|
55
|
+
self.msg_queue = asyncio.Queue()
|
|
56
|
+
|
|
57
|
+
def on_ws_frame(self, transport: picows.WSTransport, frame: picows.WSFrame):
|
|
58
|
+
self.msg_queue.put_nowait(WSFrameMaterialized(frame))
|
|
59
|
+
|
|
60
|
+
(_, client) = await picows.ws_connect(URL, PicowsClientListener, "client")
|
|
61
|
+
yield client
|
|
62
|
+
|
|
63
|
+
# Teardown client
|
|
64
|
+
client.transport.disconnect()
|
|
65
|
+
await client.transport.wait_until_closed()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@pytest.mark.parametrize("msg_size", [32, 1024, 20000])
|
|
69
|
+
async def test_echo(echo_client, msg_size):
|
|
70
|
+
msg = os.urandom(msg_size)
|
|
71
|
+
echo_client.transport.send(picows.WSMsgType.BINARY, msg)
|
|
72
|
+
frame: WSFrameMaterialized = await echo_client.msg_queue.get()
|
|
73
|
+
assert frame.opcode == picows.WSMsgType.BINARY
|
|
74
|
+
assert frame.payload_as_bytes == msg
|
|
75
|
+
# assert frame.payload_as_ascii_text == msg.decode("ascii")
|
|
76
|
+
assert frame.payload_as_bytes_from_mv == msg
|
picows-0.2.0/picows/__init__.py
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|