Skip to content

Commit 955c5ff

Browse files
authored
Merge pull request #194 from ZipFile/tcp-cleanup
TCP improvements
2 parents dc00755 + 60b94ba commit 955c5ff

11 files changed

Lines changed: 446 additions & 331 deletions

examples/async_simple_tcp_client.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77

88
import argparse
99
import asyncio
10+
import logging
1011
import random
11-
import sys
1212

1313
from pythonosc import tcp_client
1414

@@ -47,10 +47,6 @@ async def init_main():
4747
tg.create_task(send_messages(client))
4848

4949

50-
if sys.version_info >= (3, 7):
51-
asyncio.run(init_main())
52-
else:
53-
# TODO(python-upgrade): drop this once 3.6 is no longer supported
54-
event_loop = asyncio.get_event_loop()
55-
event_loop.run_until_complete(init_main())
56-
event_loop.close()
50+
if __name__ == "__main__":
51+
logging.basicConfig(level=logging.DEBUG)
52+
asyncio.run(init_main(), debug=True)

examples/async_tcp_server.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import argparse
22
import asyncio
3-
import sys
3+
import logging
44

55
from pythonosc.dispatcher import Dispatcher
66
from pythonosc.osc_tcp_server import AsyncOSCTCPServer
@@ -42,10 +42,6 @@ async def init_main():
4242
tg.create_task(loop())
4343

4444

45-
if sys.version_info >= (3, 7):
46-
asyncio.run(init_main())
47-
else:
48-
# TODO(python-upgrade): drop this once 3.6 is no longer supported
49-
event_loop = asyncio.get_event_loop()
50-
event_loop.run_until_complete(init_main())
51-
event_loop.close()
45+
if __name__ == "__main__":
46+
logging.basicConfig(level=logging.DEBUG)
47+
asyncio.run(init_main(), debug=True)

examples/simple_tcp_client.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,15 @@
55
"""
66

77
import argparse
8+
import logging
89
import random
10+
from random import randint
11+
from zlib import crc32
912

1013
from pythonosc import tcp_client
1114

1215
if __name__ == "__main__":
16+
logging.basicConfig(level=logging.DEBUG)
1317
parser = argparse.ArgumentParser()
1418
parser.add_argument("--ip", default="127.0.0.1", help="The ip of the OSC server")
1519
parser.add_argument(
@@ -27,6 +31,9 @@
2731
n = random.random()
2832
print(f"Sending /filter {n}")
2933
client.send_message("/filter", n)
34+
data = bytes(random.randint(0, 255) for i in range(randint(1, 10000)))
35+
print(f"Sending /crc {len(data)} bytes (should be {crc32(data):08X})")
36+
client.send_message("/crc", data)
3037
resp = client.get_messages(1)
3138
for r in resp:
3239
try:

examples/simple_tcp_server.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
"""
66

77
import argparse
8+
import logging
89
import math
10+
from zlib import crc32
911

1012
from pythonosc import osc_tcp_server
1113
from pythonosc.dispatcher import Dispatcher
@@ -23,6 +25,7 @@ def print_compute_handler(unused_addr, args, volume):
2325

2426

2527
if __name__ == "__main__":
28+
logging.basicConfig(level=logging.DEBUG)
2629
parser = argparse.ArgumentParser()
2730
parser.add_argument("--ip", default="127.0.0.1", help="The ip to listen on")
2831
parser.add_argument("--port", type=int, default=5005, help="The port to listen on")
@@ -36,6 +39,7 @@ def print_compute_handler(unused_addr, args, volume):
3639

3740
dispatcher = Dispatcher()
3841
dispatcher.map("/filter", print)
42+
dispatcher.map("/crc", lambda _, v: f"{crc32(v):08X}")
3943
dispatcher.map("/volume", print_volume_handler, "Volume")
4044
dispatcher.map("/logvolume", print_compute_handler, "Log volume", math.log)
4145

pythonosc/osc_tcp_server.py

Lines changed: 49 additions & 165 deletions
Original file line numberDiff line numberDiff line change
@@ -33,68 +33,37 @@
3333
# mypy: disable-error-code="attr-defined"
3434

3535
import asyncio
36-
import logging
3736
import os
3837
import socket
3938
import socketserver
40-
import struct
41-
from typing import List, Tuple
39+
from typing import Tuple
4240

43-
from pythonosc import osc_message_builder, slip
41+
from pythonosc import osc_message_builder
4442
from pythonosc.dispatcher import Dispatcher
43+
from pythonosc.parsing.framing import (
44+
BINARY_LENGTH_FRAMING,
45+
SLIP_FRAMING,
46+
Framing,
47+
FramingParser,
48+
)
4549

46-
LOG = logging.getLogger()
4750
MODE_1_0 = "1.0"
4851
MODE_1_1 = "1.1"
52+
FRAMING_BY_MODE: dict[str, Framing] = {
53+
MODE_1_0: BINARY_LENGTH_FRAMING,
54+
MODE_1_1: SLIP_FRAMING,
55+
}
4956

5057

51-
class _TCPHandler1_0(socketserver.BaseRequestHandler):
52-
"""Handles correct OSC1.0 messages.
58+
def get_framing(mode: str) -> Framing:
59+
try:
60+
return FRAMING_BY_MODE[mode]
61+
except KeyError:
62+
raise ValueError(f"Unsupported OSC mode: {mode}") from None
5363

54-
Whether this will be run on its own thread, the server's or a whole new
55-
process depends on the server you instantiated, look at their documentation.
5664

57-
This method is called after a basic sanity check was done on the datagram,
58-
basically whether this datagram looks like an osc message or bundle,
59-
if not the server won't even bother to call it and so no new
60-
threads/processes will be spawned.
61-
"""
62-
63-
def handle(self) -> None:
64-
LOG.debug("handle OSC 1.0 protocol")
65-
while True:
66-
lengthbuf = self.recvall(4)
67-
if lengthbuf == b"":
68-
break
69-
(length,) = struct.unpack("!I", lengthbuf)
70-
data = self.recvall(length)
71-
if data == b"":
72-
break
73-
74-
resp = self.server.dispatcher.call_handlers_for_packet(
75-
data, self.client_address
76-
)
77-
# resp = _call_handlers_for_packet(data, self.server.dispatcher)
78-
for r in resp:
79-
if not isinstance(r, tuple):
80-
r = [r]
81-
msg = osc_message_builder.build_msg(r[0], r[1:])
82-
b = struct.pack("!I", len(msg.dgram))
83-
self.request.sendall(b + msg.dgram)
84-
85-
def recvall(self, count: int) -> bytes:
86-
buf = b""
87-
while count > 0:
88-
newbuf = self.request.recv(count)
89-
if not newbuf:
90-
return b""
91-
buf += newbuf
92-
count -= len(newbuf)
93-
return buf
94-
95-
96-
class _TCPHandler1_1(socketserver.BaseRequestHandler):
97-
"""Handles correct OSC1.1 messages.
65+
class TCPHandler(socketserver.BaseRequestHandler):
66+
"""Handles correct OSC messages.
9867
9968
Whether this will be run on its own thread, the server's or a whole new
10069
process depends on the server you instantiated, look at their documentation.
@@ -106,42 +75,28 @@ class _TCPHandler1_1(socketserver.BaseRequestHandler):
10675
"""
10776

10877
def handle(self) -> None:
109-
LOG.debug("handle OSC 1.1 protocol")
110-
while True:
111-
packets = self.recvall()
112-
if not packets:
113-
break
114-
115-
for p in packets:
116-
# resp = _call_handlers_for_packet(p, self.server.dispatcher)
78+
assert isinstance(self.server, OSCTCPServer)
79+
80+
parser = FramingParser(self.server.framing)
81+
82+
while chunk := self.request.recv(16384):
83+
for p in parser.feed(chunk):
11784
resp = self.server.dispatcher.call_handlers_for_packet(
11885
p, self.client_address
11986
)
12087
for r in resp:
12188
if not isinstance(r, tuple):
12289
r = [r]
12390
msg = osc_message_builder.build_msg(r[0], r[1:])
124-
self.request.sendall(slip.encode(msg.dgram))
125-
126-
def recvall(self) -> List[bytes]:
127-
buf = self.request.recv(4096)
128-
if not buf:
129-
return []
130-
# If the last byte is not an END marker there could be more data coming
131-
while buf[-1] != 192:
132-
newbuf = self.request.recv(4096)
133-
if not newbuf:
134-
# Maybe should raise an exception here?
135-
break
136-
buf += newbuf
137-
138-
packets = [slip.decode(p) for p in buf.split(slip.END_END)]
139-
return packets
91+
self.request.sendall(self.server.framing.encode(msg.dgram))
14092

14193

14294
class OSCTCPServer(socketserver.TCPServer):
14395
"""Superclass for different flavors of OSCTCPServer"""
14496

97+
dispatcher: Dispatcher
98+
framing: Framing
99+
145100
def __init__(
146101
self,
147102
server_address: Tuple[str | bytes | bytearray, int],
@@ -150,9 +105,8 @@ def __init__(
150105
family: socket.AddressFamily | None = None,
151106
):
152107
self.request_queue_size = 300
153-
self.mode = mode
154-
if mode not in [MODE_1_0, MODE_1_1]:
155-
raise ValueError("OSC Mode must be '1.0' or '1.1'")
108+
self.dispatcher = dispatcher
109+
self.framing = get_framing(mode)
156110

157111
if family is not None:
158112
self.address_family = family
@@ -171,16 +125,7 @@ def __init__(
171125
# Fallback to default if resolution fails
172126
pass
173127

174-
if self.mode == MODE_1_0:
175-
super().__init__(server_address, _TCPHandler1_0)
176-
else:
177-
super().__init__(server_address, _TCPHandler1_1)
178-
self._dispatcher = dispatcher
179-
180-
@property
181-
def dispatcher(self):
182-
"""Dispatcher accessor for handlers to dispatch osc messages."""
183-
return self._dispatcher
128+
super().__init__(server_address, TCPHandler)
184129

185130

186131
class BlockingOSCTCPServer(OSCTCPServer):
@@ -230,8 +175,8 @@ def __init__(
230175
"""
231176
self._port = port
232177
self._server_address = server_address
233-
self._dispatcher = dispatcher
234-
self._mode = mode
178+
self.dispatcher = dispatcher
179+
self.framing = get_framing(mode)
235180

236181
async def __aenter__(self):
237182
return self
@@ -245,102 +190,41 @@ async def start(self) -> None:
245190
self.handle, self._server_address, self._port
246191
)
247192

248-
addrs = ", ".join(str(sock.getsockname()) for sock in self._server.sockets)
249-
LOG.debug(f"Serving on {addrs}")
250-
251193
async with self._server:
252194
await self._server.serve_forever()
253195

254196
async def stop(self) -> None:
255197
self._server.close()
256198
await self._server.wait_closed()
257199

258-
@property
259-
def dispatcher(self):
260-
return self._dispatcher
261-
262200
async def handle(
263201
self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
264202
) -> None:
203+
try:
204+
await self._handle(reader, writer)
205+
finally:
206+
if writer.can_write_eof():
207+
writer.write_eof()
208+
writer.close()
209+
await writer.wait_closed()
210+
211+
async def _handle(
212+
self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
213+
) -> None:
214+
parser = FramingParser(self.framing)
265215
client_address = ("", 0)
266216
sock = writer.transport.get_extra_info("socket")
267217
if sock is not None:
268218
client_address = sock.getpeername()
269219

270-
if self._mode == MODE_1_1:
271-
await self.handle_1_1(reader, writer, client_address)
272-
else:
273-
await self.handle1_0(reader, writer, client_address)
274-
writer.write_eof()
275-
LOG.debug("Close the connection")
276-
writer.close()
277-
await writer.wait_closed()
278-
279-
async def handle1_0(
280-
self,
281-
reader: asyncio.StreamReader,
282-
writer: asyncio.StreamWriter,
283-
client_address: Tuple[str, int],
284-
) -> None:
285-
LOG.debug("Incoming socket open 1.0")
286-
while True:
287-
try:
288-
buf = await reader.read(4)
289-
except Exception as e:
290-
LOG.exception("Read error", e)
291-
return
292-
if buf == b"":
293-
break
294-
(length,) = struct.unpack("!I", buf)
295-
buf = b""
296-
while length > 0:
297-
newbuf = await reader.read(length)
298-
if not newbuf:
299-
break
300-
buf += newbuf
301-
length -= len(newbuf)
302-
303-
result = await self.dispatcher.async_call_handlers_for_packet(
304-
buf, client_address
305-
)
306-
for r in result:
307-
if not isinstance(r, tuple):
308-
r = [r]
309-
msg = osc_message_builder.build_msg(r[0], r[1:])
310-
b = struct.pack("!I", len(msg.dgram))
311-
writer.write(b + msg.dgram)
312-
await writer.drain()
313-
314-
async def handle_1_1(
315-
self,
316-
reader: asyncio.StreamReader,
317-
writer: asyncio.StreamWriter,
318-
client_address: Tuple[str, int],
319-
) -> None:
320-
LOG.debug("Incoming socket open 1.1")
321-
while True:
322-
try:
323-
buf = await reader.read(4096)
324-
except Exception as e:
325-
LOG.exception("Read error", e)
326-
return
327-
if buf == b"":
328-
break
329-
while len(buf) > 0 and buf[-1] != 192:
330-
newbuf = await reader.read(4096)
331-
if not newbuf:
332-
# Maybe should raise an exception here?
333-
break
334-
buf += newbuf
335-
336-
packets = [slip.decode(p) for p in buf.split(slip.END_END)]
337-
for p in packets:
220+
while chunk := await reader.read(16384):
221+
for p in parser.feed(chunk):
338222
result = await self.dispatcher.async_call_handlers_for_packet(
339223
p, client_address
340224
)
341225
for r in result:
342226
if not isinstance(r, tuple):
343227
r = [r]
344228
msg = osc_message_builder.build_msg(r[0], r[1:])
345-
writer.write(slip.encode(msg.dgram))
229+
writer.write(self.framing.encode(msg.dgram))
346230
await writer.drain()

0 commit comments

Comments
 (0)