Compare commits
2 Commits
352da0ef54
...
1f5ca02ca4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f5ca02ca4 | ||
|
|
e79c523034 |
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# Byte-compiled / cache
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# Virtual environments
|
||||
.venv/
|
||||
venv/
|
||||
|
||||
# Certs (provide via env variables or local path)
|
||||
*.pem
|
||||
*.crt
|
||||
*.key
|
||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
aioquic>=1.2.0
|
||||
cryptography>=41.0.0
|
||||
36
run.py
36
run.py
@@ -1,10 +1,36 @@
|
||||
from server.server import main
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from server.server import GameServer
|
||||
from server.config import ServerConfig
|
||||
from server.transport import InMemoryTransport
|
||||
|
||||
|
||||
async def main():
|
||||
from server.server import GameServer
|
||||
from server.transport import InMemoryTransport
|
||||
|
||||
cfg = ServerConfig()
|
||||
server = GameServer(transport=InMemoryTransport(lambda d, p: server.on_datagram(d, p)), config=cfg)
|
||||
await asyncio.gather(server.transport.run(), server.tick_loop())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
|
||||
try:
|
||||
asyncio.run(main())
|
||||
# Optional QUIC mode if env vars are provided
|
||||
cert = os.environ.get("QUIC_CERT")
|
||||
key = os.environ.get("QUIC_KEY")
|
||||
host = os.environ.get("QUIC_HOST", "0.0.0.0")
|
||||
port = int(os.environ.get("QUIC_PORT", "4433"))
|
||||
if cert and key:
|
||||
from server.quic_transport import QuicWebTransportServer
|
||||
from server.server import GameServer
|
||||
cfg = ServerConfig()
|
||||
async def start_quic():
|
||||
server = GameServer(transport=QuicWebTransportServer(host, port, cert, key, lambda d, p: server.on_datagram(d, p)), config=cfg)
|
||||
await asyncio.gather(server.transport.run(), server.tick_loop())
|
||||
asyncio.run(start_quic())
|
||||
else:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
|
||||
75
server/quic_transport.py
Normal file
75
server/quic_transport.py
Normal file
@@ -0,0 +1,75 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from typing import Awaitable, Callable, Dict, Optional
|
||||
|
||||
from .transport import DatagramServerTransport, OnDatagram, TransportPeer
|
||||
|
||||
|
||||
try:
|
||||
from aioquic.asyncio import QuicConnectionProtocol, serve
|
||||
from aioquic.quic.configuration import QuicConfiguration
|
||||
from aioquic.quic.events import DatagramFrameReceived, ProtocolNegotiated
|
||||
except Exception: # pragma: no cover - optional dependency not installed in skeleton
|
||||
QuicConnectionProtocol = object # type: ignore
|
||||
QuicConfiguration = object # type: ignore
|
||||
serve = None # type: ignore
|
||||
DatagramFrameReceived = object # type: ignore
|
||||
ProtocolNegotiated = object # type: ignore
|
||||
|
||||
|
||||
class GameQuicProtocol(QuicConnectionProtocol): # type: ignore[misc]
|
||||
def __init__(self, *args, on_datagram: OnDatagram, peers: Dict[int, "GameQuicProtocol"], **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._on_datagram = on_datagram
|
||||
self._peers = peers
|
||||
self._peer_id: Optional[int] = None
|
||||
|
||||
def quic_event_received(self, event) -> None: # type: ignore[override]
|
||||
if isinstance(event, ProtocolNegotiated):
|
||||
# Register by connection id
|
||||
self._peer_id = int(self._quic.connection_id) # type: ignore[attr-defined]
|
||||
self._peers[self._peer_id] = self
|
||||
elif isinstance(event, DatagramFrameReceived):
|
||||
# Schedule async callback
|
||||
if self._peer_id is None:
|
||||
return
|
||||
peer = TransportPeer(addr=self)
|
||||
asyncio.ensure_future(self._on_datagram(bytes(event.data), peer))
|
||||
|
||||
async def send_datagram(self, data: bytes) -> None:
|
||||
self._quic.send_datagram_frame(data) # type: ignore[attr-defined]
|
||||
await self._loop.run_in_executor(None, self.transmit) # type: ignore[attr-defined]
|
||||
|
||||
|
||||
class QuicWebTransportServer(DatagramServerTransport):
|
||||
def __init__(self, host: str, port: int, certfile: str, keyfile: str, on_datagram: OnDatagram):
|
||||
if serve is None:
|
||||
raise RuntimeError("aioquic is not installed. Please `pip install aioquic`.")
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.certfile = certfile
|
||||
self.keyfile = keyfile
|
||||
self._on_datagram = on_datagram
|
||||
self._server = None
|
||||
self._peers: Dict[int, GameQuicProtocol] = {}
|
||||
|
||||
async def send(self, data: bytes, peer: TransportPeer) -> None:
|
||||
proto = peer.addr # expected GameQuicProtocol
|
||||
if isinstance(proto, GameQuicProtocol):
|
||||
await proto.send_datagram(data)
|
||||
|
||||
async def run(self) -> None:
|
||||
configuration = QuicConfiguration(is_client=False, alpn_protocols=["h3"])
|
||||
configuration.load_cert_chain(self.certfile, self.keyfile)
|
||||
|
||||
async def _create_protocol(*args, **kwargs):
|
||||
return GameQuicProtocol(*args, on_datagram=self._on_datagram, peers=self._peers, **kwargs)
|
||||
|
||||
self._server = await serve(self.host, self.port, configuration=configuration, create_protocol=_create_protocol)
|
||||
try:
|
||||
await self._server.wait_closed()
|
||||
finally:
|
||||
self._server.close()
|
||||
|
||||
Reference in New Issue
Block a user