Metadata-Version: 2.4
Name: simple-tcp-server
Version: 0.1.3
Summary: Simple TCP server/client framework.
License: MIT
License-File: LICENSE
Author: GGN_2015
Author-email: neko@jlulug.org
Requires-Python: >=3.10
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Description-Content-Type: text/markdown

# simple_tcp_server
Simple TCP server framework.

## Installation

```bash
pip install simple_tcp_server
```

## Usage

```python
from simple_tcp_server import SimpleTcpServer, SimpleTcpClient
import threading
import time

HOST = "127.0.0.1"
PORT = 9999

def is_prime(msg: bytes) -> bytes:
    n = int(msg.decode())
    if n < 2:
        return b"not prime"
    for i in range(2, int(n**0.5)+1):
        if n % i == 0:
            return b"not prime"
    return b"prime"

server = SimpleTcpServer(HOST, PORT, is_prime, quit_token=b"quit")
server.set_debug_mode(True) # default is False
t = threading.Thread(target=server.mainloop, daemon=True)
t.start()

time.sleep(0.2)

client = SimpleTcpClient(HOST, PORT)

numbers = [b"2", b"10", b"17", b"97", b"1234567"]
for n in numbers:
    res = client.request(n)

# close server by quit_token
client.request(b"quit")
for n in numbers:
    res = client.request(n)

client.close()
```

## Message framing

When a client connects, it sends one strategy byte to negotiate message
framing for that connection. The server stores the negotiated strategy per
client and responds to each client with the same framing. `SimpleTcpServer`
does not need a framing option.

`SimpleTcpClient` uses `framing_strategy="E"` by default. Choose a mode
when creating the client:

```python
legacy_client = SimpleTcpClient(HOST, PORT)
l_client = SimpleTcpClient(HOST, PORT, framing_strategy="L")
d_client = SimpleTcpClient(HOST, PORT, framing_strategy="D")
```

### Negotiation byte

The first byte sent by a new client chooses the framing strategy:

| Byte | Strategy | Meaning |
| --- | --- | --- |
| `b"E"` | E mode | End-marker framing, compatible with the original protocol. |
| `b"L"` | L mode | 4-byte length-prefix framing. |
| `b"D"` | D mode | 8-byte length-prefix framing. |

If the first byte is not `b"E"`, `b"L"`, or `b"D"`, the server defaults
that client to E mode and keeps the byte as part of the first message.
This keeps clients that do not send a negotiation byte compatible with
the default protocol.

### E Mode

E mode is the original end-marker protocol. Each message is sent as:

```text
escaped payload + b"$$"
```

Because `b"$$"` marks the end of a message, every `b"$"` byte inside the
payload is escaped before sending and unescaped after receiving. This mode
is the default and is useful for compatibility with existing clients.

Example payload:

```python
client = SimpleTcpClient(HOST, PORT)  # same as framing_strategy="E"
res = client.request(b"value with $ and $$")
```

### L Mode

L mode uses a 4-byte unsigned big-endian integer before every payload:

```text
4-byte payload length + raw payload
```

The length describes only the payload after the 4-byte prefix. The prefix
allows the receiver to know exactly how many bytes belong to the message,
so the payload is not escaped. This mode is binary-safe: payloads may
contain `b"$"`, `b"$$"`, null bytes, or any other byte value.

Example payload:

```python
client = SimpleTcpClient(HOST, PORT, framing_strategy="L")
res = client.request(b"value with $$ and \x00 bytes")
```

### D Mode

D mode is the same idea as L mode, but the length field is 8 bytes:

```text
8-byte payload length + raw payload
```

The 8-byte length is an unsigned big-endian integer and describes only
the payload after the prefix. Like L mode, D mode does not escape the
payload and is binary-safe. Use D mode when you want the protocol shape
to allow payloads larger than the 4-byte L-mode limit.

Example payload:

```python
client = SimpleTcpClient(HOST, PORT, framing_strategy="D")
res = client.request(b"large or binary payload")
```

