bisocket: Simple, Secure, Bidirectional Python Sockets
bisocket is a high-level Python library that simplifies bidirectional (two-way) communication over sockets. It provides a robust framework for building client-server applications that require sending and receiving data simultaneously without blocking.
It comes with built-in AES-GCM end-to-end encryption and bz2 compression, ensuring your data is secure and transmitted efficiently. The library offers both synchronous (threading-based) and asynchronous (asyncio) APIs, making it versatile for various application architectures.
✨ Features
- True Bidirectional Communication: Uses separate sockets for sending and receiving, enabling non-blocking, full-duplex communication.
- End-to-End Encryption: Automatic AES-GCM encryption for all messages ensures data privacy and integrity.
- Data Compression: Automatic
bz2compression reduces bandwidth usage for large payloads. - Sync & Async Support: Provides both a standard threading API and a modern
asyncioAPI. - Simple Handler-Based API: Use a clean handler function on the server and an
on_receivecallback on the client to process messages. - Unique Client Identification: Manages clients using unique UUIDs, making it easy to track connections.
- Connection Lifecycle Hooks: Optional
on_open,on_closeandon_finallycallbacks on the server.
⚙️ Installation
Install bisocket directly from PyPI:
pip install bisocket
The only runtime dependency is the cryptography library for encryption.
If Cython and a C compiler are available at install time, a compiled build of the library is used automatically. If they are not, the install still succeeds and the identical pure-Python implementation is used instead. Both are built from the same source, so behaviour does not differ. To see which one you got:
python -c "import bisocket; print(bisocket.main.__file__)"
🚀 Quick Start
Here's a simple echo client and server to get you started.
1. Set the Encryption Key
For security, bisocket requires an encryption key. Set it as an environment variable. If it's not set, the library prints a warning and falls back to a default, insecure key suitable only for testing.
export CRYPTO_KEY='your-super-secret-and-long-encryption-key'
2. Synchronous Example
Server (server.py)
from bisocket import Server, ServerRequest
# Define a handler to process incoming requests.
def handler(request: ServerRequest):
print(f"Received method '{request.method}' with data: {request.data.decode()}")
if request.method == 'echo':
# Send the received data back to the client.
request.send_data(request.data)
elif request.method == 'ping':
request.send_data(b'pong')
# Create and start the server.
if __name__ == "__main__":
server = Server(host='127.0.0.1', port=65432, handler=handler)
print("Starting synchronous server on port 65432...")
server.start()
Client (client.py)
import time
from bisocket import Client, Message
# Define a callback to handle messages from the server.
def on_receive(msg: Message):
print(f"Received response for request ID {msg.request_id}: {msg.data.decode()}")
# Use the Client as a context manager for clean connection handling.
with Client(host='127.0.0.1', port=65432, on_receive=on_receive) as client:
print("Client connected.")
# Send an 'echo' request.
request_id_1 = client.send('echo', b'Hello, World!')
print(f"Sent 'echo' request with ID: {request_id_1}")
time.sleep(1) # Wait for the response.
# Send a 'ping' request.
request_id_2 = client.send('ping', b'')
print(f"Sent 'ping' request with ID: {request_id_2}")
time.sleep(2) # Give time for messages to be processed before exiting.
print("Client disconnected.")
3. Asynchronous Example
Use Server.astart() and the client's aopen() / asend() / aclose() (or async with)
for the asyncio API. An async def handler works with either server, but the
synchronous Server.start() has to spin up an event loop per call, so prefer
astart() when your handler is a coroutine.
Async Server (async_server.py)
import asyncio
from bisocket import Server, ServerRequest
# Define an async handler for non-blocking operations.
async def ahandler(request: ServerRequest):
print(f"Received method '{request.method}' with data: {request.data.decode()}")
if request.method == 'echo':
await asyncio.sleep(0.5) # Simulate I/O-bound work.
request.send_data(request.data)
# Create and run the async server.
async def main():
server = Server(host='127.0.0.1', port=65432, handler=ahandler)
print("Starting asynchronous server on port 65432...")
await server.astart()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("Server shutting down.")
Async Client (async_client.py)
import asyncio
from bisocket import Client, Message
# Define an async callback to process server messages.
async def aon_receive(msg: Message):
print(f"Received response for request ID {msg.request_id}: {msg.data.decode()}")
async def main():
# Use the async context manager for the client.
async with Client(host='127.0.0.1', port=65432, on_receive=aon_receive) as client:
print("Async client connected.")
# Send multiple requests concurrently.
tasks = [
client.asend('echo', b'First async message'),
client.asend('echo', b'Second async message')
]
request_ids = await asyncio.gather(*tasks)
print(f"Sent requests with IDs: {request_ids}")
await asyncio.sleep(2) # Keep client running to receive responses.
if __name__ == "__main__":
asyncio.run(main())
📚 API Reference
Client(host, port, on_receive)
on_receive is called with a Message for every message pushed by the server. It
may be a normal function or an async def coroutine function.
| Method | Description |
|---|---|
open() / close() |
Connect and disconnect. Also available as a with block. |
aopen() / aclose() |
Async equivalents. Also available as an async with block. |
send(method, data) -> str |
Send bytes under a method name; returns the request ID. |
send_obj(method, obj) -> str |
Same, but JSON-encodes obj first. |
asend(...) / asend_obj(...) |
Async equivalents. |
ping() / aping() |
Raise ConnectionError if either socket has dropped. |
send() and asend() are safe to call concurrently from multiple threads or tasks;
each call holds the send socket until its acknowledgement returns.
Message
| Attribute / Method | Description |
|---|---|
request_id: str |
ID of the request this message answers. |
data: bytes |
Raw payload. |
get_str() -> str |
Payload decoded as UTF-8. |
get_obj() |
Payload parsed as JSON. |
Server(host, port, handler, ...)
| Argument | Called when |
|---|---|
handler |
A request arrives. Receives a ServerRequest. |
on_open |
A client's send socket connects. Receives OnOpenInfo. |
on_open_receive |
A client's receive socket connects. Receives OnOpenInfo. |
on_close |
A client's send socket closes. Receives OnCloseInfo. |
on_close_receive |
A client's receive socket closes. Receives OnCloseInfo. |
on_finally |
Any client connection ends, for any reason. Receives OnFinallyInfo. |
Every callback, and handler itself, may be a normal function or an async def
coroutine function. OnOpenInfo and OnCloseInfo carry a client_id;
OnFinallyInfo carries client_id: str | None, which is None if the connection
failed before the client identified itself.
Because each client opens two sockets, on_finally fires twice per client — once
per connection.
| Method | Description |
|---|---|
start() |
Run the threaded server. Blocks forever. |
astart() |
Run the asyncio server. Blocks forever. |
ServerRequest
| Attribute / Method | Description |
|---|---|
client_id: str |
UUID of the sending client. |
request_id: str |
UUID of this request. |
method: str |
Method name the client sent. |
data: bytes |
Raw payload. |
send_data(data: bytes) |
Queue a bytes response back to that client. |
send(data: str) |
Same, for a str. |
A handler may call send_data() any number of times, including zero. Responses are
pushed over the client's receive socket, so they are not tied to a request/response
turn.
Aliases
BiClient, BiServer, BiMessage and BiServerRequest are aliases for Client,
Server, Message and ServerRequest.
🧠 How It Works
Traditional socket programming can be tricky when you need to send and receive data at the same time, often leading to blocking calls or complex multiplexing.
bisocket simplifies this by establishing two separate socket connections for each client:
- Send Socket: The client uses this connection exclusively to send data to the server.
- Receive Socket: The client uses this connection exclusively to receive data from the server.
This architecture allows the client and server to communicate in full-duplex mode without one operation blocking the other. The library manages these connections, message framing, encryption, and compression internally, so you can focus on your application logic.
- On the Client: The
Clientruns a background thread (orasynciotask) to listen for incoming messages on the receive socket. These messages are passed to youron_receivecallback. - On the Server: The
Servermanages a pool of client connections. It receives a request from a client's "send" socket, processes it in your handler, and then queues the response to be sent back via that same client's "receive" socket.
Messages are delimited on the wire by a byte token. Payloads are encrypted and compressed before framing, so your own data may contain any bytes, delimiters included.
🔐 Security
All data transmitted by bisocket is encrypted using AES-256-GCM, an authenticated encryption scheme that provides confidentiality and integrity. The 256-bit encryption key is derived from the string you provide via the CRYPTO_KEY environment variable using SHA-256.
⚠️ It is crucial to set a strong, unique secret key for your application.
You can generate a cryptographically secure key using OpenSSL:
# This command generates a 32-byte (256-bit) random key in hex format.
export CRYPTO_KEY=$(openssl rand -hex 32)
If CRYPTO_KEY is not set, a default, insecure key ('secret-lol') is used and a warning is printed to stderr. This is intended only for local testing and development.
Note the current limits of this model, which matter if you expose a server publicly:
- Every client shares one symmetric key, so any client that can connect can read and forge any other client's traffic. There is no per-client authentication.
client_idis chosen by the client and is not verified.- The key is derived by a single SHA-256 pass, not a slow KDF, so a weak
CRYPTO_KEYis cheap to brute force. Use a long random value.
📄 License
This project is licensed under the MIT License. See the LICENSE file for details.
🙏 Contributing
Contributions are welcome! Please feel free to submit a pull request or open an issue to discuss new features or bugs.
Metadata
Release files for bisocket 0.0.8
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| bisocket-0.0.8.tar.gz | 26.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| bisocket-0.0.8-cp314-cp314-macosx_26_0_arm64.whl | CPython 3.14 | CPython 3.14 | macOS 26.0+ ARM64 | Details |
Total release size: 274.0 kB
Release files / bisocket-0.0.8.tar.gz
| Download URL | bisocket-0.0.8.tar.gz |
|---|---|
| Size | 26.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d5c69ccefdb283d6d7cc4f644b3d8fcec7cbf7079fe48b3ac403eb994d062d8d
|
|
BLAKE2b-256 checksum How to use checksums |
368345f3e17d27dda43a5270127b0745f8fd62881cdf9f965d0d25afb7c09409
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.4
|
Release files / bisocket-0.0.8-cp314-cp314-macosx_26_0_arm64.whl
| Download URL | bisocket-0.0.8-cp314-cp314-macosx_26_0_arm64.whl |
|---|---|
| Size | 247.3 kB |
| Tags | CPython 3.14 macOS 26.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
2a681ec527a4cb005821cae72f4697978f1bbc56aa9fb400bf193e002573196c
|
|
BLAKE2b-256 checksum How to use checksums |
4aa9c36a058672f29aa73971d19c1ee108128f2ae245f0e556600be58ba34034
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.4
|