Skip to content

Commit

Permalink
[#37] chore: merge pull request #47
Browse files Browse the repository at this point in the history
- Socket.io server for real-time communication in the game
- currently only some events `init` (join room), `deinit` (leave room), `chat` are implemented
  • Loading branch information
metalalive authored Dec 11, 2023
2 parents b867ea5 + 38c4f56 commit 45e3ec8
Show file tree
Hide file tree
Showing 10 changed files with 904 additions and 133 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/backend_ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,7 @@ jobs:
- name: Run test by pytest
run: |
cd backend
mkdir -p log/dev
poetry run socketio-app-dev &
poetry run pytest -v tests
kill -s sigterm $(cat ./pid.log)
5 changes: 5 additions & 0 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,14 @@ poetry install
```

### Run
#### web application server for REST endpoints
```bash
poetry run webapp-dev
```
#### socket.io server
```bash
poetry run socketio-app-dev
```

Once the server started, you can send HTTP request to the server.

Expand Down
9 changes: 8 additions & 1 deletion backend/app/adapter/api.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import asyncio
import logging
from contextlib import asynccontextmanager

from fastapi import FastAPI, APIRouter, status as FastApiHTTPstatus
from fastapi.responses import JSONResponse
from hypercorn.config import Config
from hypercorn.asyncio import serve
from app.config import LOG_FILE_PATH

from app.dto import (
CreateGameReqDto,
Expand All @@ -20,10 +22,15 @@
SwitchInvestigatorUseCase,
UpdateGameDifficultyUseCase,
)
from app.config import REST_HOST, REST_PORT
from app.domain import GameError
from app.adapter.repository import get_repository
from app.adapter.presenter import read_investigator_presenter, create_game_presenter

_logger = logging.getLogger(__name__)
_logger.setLevel(logging.WARNING)
_logger.addHandler(logging.FileHandler(LOG_FILE_PATH["REST"], mode="a"))

_router = APIRouter(
prefix="", # could be API versioning e.g. /v0.0.1/* , /v2.0.1/*
dependencies=[],
Expand Down Expand Up @@ -106,6 +113,6 @@ def init_app_server() -> FastAPI:
def start_web_app() -> None:
# TODO, parameterize with separate python module or `toml` file
cfg = Config()
cfg.bind = ["localhost:8081"]
cfg.bind = ["%s:%s" % (REST_HOST, REST_PORT)]
app = init_app_server()
asyncio.run(serve(app, cfg))
116 changes: 116 additions & 0 deletions backend/app/adapter/sio_srv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import logging
import asyncio
import os
from typing import Dict

import socketio
from hypercorn.config import Config
from hypercorn.asyncio import serve
from pydantic import BaseModel, ConfigDict, ValidationError

from app.constant import RealTimeCommConst as RtcConst
from app.config import RTC_HOST, RTC_PORT, LOG_FILE_PATH

srv = socketio.AsyncServer(async_mode="asgi")
# currently the logger is configured in simple way, if someone needs to run it
# in production environment, maybe they can switch to more advanced architecture
# e.g. centralized logging architecture, ELK stack, EFK stack ...etc
_logger = logging.getLogger(__name__)
_logger.setLevel(logging.WARNING)
_logger.addHandler(logging.FileHandler(LOG_FILE_PATH["RTC"], mode="a"))


class ChatMsgData(BaseModel):
model_config = ConfigDict(extra="forbid")
msg: str
nickname: str
gameID: str
client: str ## client session ID


class RtcInitMsgData(BaseModel):
model_config = ConfigDict(extra="forbid")
nickname: str
gameID: str
client: str


@srv.on(RtcConst.EVENTS.CHAT.value, namespace=RtcConst.NAMESPACE)
async def _forward_chat_msg(sid, data: Dict):
try:
ChatMsgData(**data)
await srv.emit(
RtcConst.EVENTS.CHAT.value,
data,
namespace=RtcConst.NAMESPACE,
room=data["gameID"],
skip_sid=sid,
)
except ValidationError as e:
error = e.errors(include_url=False, include_input=False)
await srv.emit(
RtcConst.EVENTS.CHAT.value, data=error, namespace=RtcConst.NAMESPACE, to=sid
)


@srv.on(RtcConst.EVENTS.INIT.value, namespace=RtcConst.NAMESPACE)
async def init_communication(sid, data: Dict):
try:
RtcInitMsgData(**data)
await srv.enter_room(sid, room=data["gameID"], namespace=RtcConst.NAMESPACE)
data["succeed"] = True
await srv.emit(
RtcConst.EVENTS.INIT.value,
data,
namespace=RtcConst.NAMESPACE,
room=data["gameID"],
)
except ValidationError as e:
_logger.error("%s", e)
error = e.errors(include_url=False, include_input=False)
error["succeed"] = False
await srv.emit(
RtcConst.EVENTS.INIT.value,
namespace=RtcConst.NAMESPACE,
data=error,
to=sid,
)


@srv.on(RtcConst.EVENTS.DEINIT.value, namespace=RtcConst.NAMESPACE)
async def deinit_communication(sid, data: Dict):
try:
RtcInitMsgData(**data)
await srv.leave_room(sid, room=data["gameID"], namespace=RtcConst.NAMESPACE)
data["succeed"] = True
await srv.emit(
RtcConst.EVENTS.DEINIT.value,
data,
namespace=RtcConst.NAMESPACE,
room=data["gameID"],
)
except ValidationError as e:
_logger.error("%s", e)
error = e.errors(include_url=False, include_input=False)
data["succeed"] = False
await srv.emit(
RtcConst.EVENTS.DEINIT.value,
namespace=RtcConst.NAMESPACE,
data=error,
to=sid,
)


def gen_srv_task(host: str):
cfg = Config()
cfg.bind = [host]
app = socketio.ASGIApp(srv, static_files={})
return serve(app, cfg)


def entry() -> None:
with open("pid.log", "w") as f:
pid = os.getpid()
f.write(str(pid))
url = "%s:%s" % (RTC_HOST, RTC_PORT)
asyncio.run(gen_srv_task(url))
7 changes: 7 additions & 0 deletions backend/app/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
REST_HOST = "localhost"
REST_PORT = 8081

RTC_HOST = "localhost"
RTC_PORT = 8082

LOG_FILE_PATH = {"REST": "./log/dev/rest-server.log", "RTC": "./log/dev/rtc-server.log"}
16 changes: 16 additions & 0 deletions backend/app/constant.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from enum import Enum


class GameRtcEvent(Enum):
"""
events which synchronize game state in real-time communication
"""

INIT = "init"
DEINIT = "deinit"
CHAT = "chat"


class RealTimeCommConst:
EVENTS = GameRtcEvent
NAMESPACE = "/game"
Loading

0 comments on commit 45e3ec8

Please sign in to comment.