This commit is contained in:
2026-09-17 16:34:16 +03:00
parent 43f49f4b2a
commit a5011dd94b
28 changed files with 1468 additions and 0 deletions
@@ -0,0 +1,11 @@
FROM golang:1.23-alpine AS builder
WORKDIR /grpc-messenger
COPY proto proto
COPY client client
RUN cd client && go mod download && go build .
FROM alpine:latest
WORKDIR /grpc-messenger
COPY --from=builder /grpc-messenger .
CMD ["./client/client"]
@@ -0,0 +1,12 @@
module github.com/distsys-course/grpc-messenger/client
go 1.23
replace github.com/distsys-course/grpc-messenger/grpc => ../proto
require (
github.com/distsys-course/grpc-messenger/grpc v0.0.0-00010101000000-000000000000
github.com/gin-gonic/gin v1.10.1
github.com/golang/protobuf v1.5.4
google.golang.org/grpc v1.75.0
)
@@ -0,0 +1,80 @@
package main
import (
"context"
"fmt"
"net/http"
"os"
"sync"
"time"
mes_grpc "github.com/distsys-course/grpc-messenger/grpc"
"github.com/gin-gonic/gin"
"github.com/golang/protobuf/jsonpb"
"google.golang.org/grpc"
)
type ChatMessage struct {
Author string `json:"author"`
Text string `json:"text"`
SendTime time.Time `json:"sendTime"`
}
type MessengerClient struct {
pendingMessages []ChatMessage
pendingMutex sync.Mutex
grpcClient YourMessengerServerClient
}
func NewMessengerClient(serverAddr string) *MessengerClient {
// TODO
}
func (c *MessengerClient) ReadMessages() {
// TODO: implement messages consumer here
}
func (c *MessengerClient) GetPending() (messages []ChatMessage) {
c.pendingMutex.Lock()
result := c.pendingMessages
c.pendingMessages = nil
c.pendingMutex.Unlock()
return result
}
type MessageResponse struct {
SendTime *time.Time `json:"sendTime"`
Error *string `json:"error"`
}
func main() {
r := gin.Default()
serverAddr := os.Getenv("MESSENGER_SERVER_ADDR")
if serverAddr == "" {
serverAddr = "localhost:51075"
fmt.Println("Missing MESSENGER_SERVER_ADDR variable, using default value: " + serverAddr)
}
// TODO: create your grpc client with given address
r.POST("/getAndFlushMessages", func(c *gin.Context) {
c.JSON(http.StatusOK, client.GetPending())
})
r.POST("/sendMessage", func(c *gin.Context) {
// TODO: implement send message here, that parses body into protobuf and sends to the server
c.JSON(http.StatusOK, MessageResponse{SendTime: nil}) // TODO: do not forget to fill SendTime
return
})
// TODO: run consumer in a goroutine
addr := os.Getenv("MESSENGER_HTTP_PORT")
if addr == "" {
addr = "0.0.0.0:8080"
fmt.Println("Missing MESSENGER_HTTP_PORT variable, using default value: 8080")
} else {
addr = "0.0.0.0:" + addr
}
if err := r.Run(addr); err != nil {
panic(err)
}
}
@@ -0,0 +1,3 @@
module github.com/distsys-course/grpc-messenger/grpc
go 1.23
@@ -0,0 +1,7 @@
syntax = "proto3";
package mes_grpc;
option go_package = "proto/;mes_grpc";
// TODO: Add messages and service
@@ -0,0 +1,60 @@
import asyncio
import copy
import json
import os
from typing import List
from aiohttp import web
# TODO: implement grpc client for messenger service
class MessengerHandler:
_pendingMessages: List[dict] # list of messages, that have not been requested yet via get_messages
_pendingMessagesLock: asyncio.Lock
_grpcClient = None # grpc client of the messenger service
def __init__(self):
self._pendingMessages = []
self._pendingMessagesLock = asyncio.Lock()
async def send_message(self, request):
"""
Body should be of the form:
{"author": "Ivan", "text": "hey guys"}
:return web.json_response of the form {"sendTime": ... }
"""
j = await request.json() # TODO: use google.protobuf.json_format.ParseDict and raise BadRequest on error
if 'author' not in j or 'text' not in j:
raise web.HTTPBadRequest
print('Got message to send:', json.dumps(j))
# TODO: your rpc call of the messenger here
raise NotImplementedError
return web.json_response({'sendTime': ""}) # TODO: use google.protobuf.json_format.MessageToDict here
async def get_messages(self, _):
async with self._pendingMessagesLock:
res: List[dict] = copy.deepcopy(self._pendingMessages)
self._pendingMessages = []
return web.json_response(res)
# TODO: implement message stream consumer in async method, that fills self._pendingMessages
# btw, consumption can be lazy and happen on get_messages, implement in any suitable way
if __name__ == '__main__':
app = web.Application()
grpcServerAddr = os.environ.get('MESSENGER_SERVER_ADDR', 'localhost:51075')
# TODO: create your grpc client with given address and pass it to MessengerHandler constructor
handler = MessengerHandler()
app.add_routes([web.post('/getAndFlushMessages', handler.get_messages)])
app.add_routes([web.post('/sendMessage', handler.send_message)])
# TODO: run message stream consumer in a background coroutine
httpPort = os.environ.get('MESSENGER_HTTP_PORT', '8080')
web.run_app(app, host='0.0.0.0', port=httpPort)
@@ -0,0 +1,3 @@
aiohttp==3.12.15
grpcio==1.75.0
grpcio-tools==1.75.0
@@ -0,0 +1,11 @@
FROM python:3.12-slim
WORKDIR /grpc-messenger
COPY client/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY client/client.py solution/client/
COPY proto solution/proto/
ENTRYPOINT ["python", "-m", "solution.client.client"]
@@ -0,0 +1,102 @@
import copy
import json
import os
import threading
from http import HTTPStatus
from http.server import HTTPServer, BaseHTTPRequestHandler
from typing import List, Dict
import google.protobuf.empty_pb2 # Empty
import google.protobuf.json_format # ParseDict, MessageToDict
import grpc
from solution.proto import messenger_pb2
from solution.proto import messenger_pb2_grpc
class PostBox:
def __init__(self):
self._messages: List[Dict] = []
self._lock = threading.Lock()
def collect_messages(self) -> List[Dict]:
with self._lock:
messages = copy.deepcopy(self._messages)
self._messages = []
return messages
def put_message(self, message: Dict):
with self._lock:
self._messages.append(message)
class MessageHandler(BaseHTTPRequestHandler):
_stub = None
_postbox: PostBox
def _read_content(self):
content_length = int(self.headers['Content-Length'])
bytes_content = self.rfile.read(content_length)
return bytes_content.decode('ascii')
# noinspection PyPep8Naming
def do_POST(self):
if self.path == '/sendMessage':
response = self._send_message(self._read_content())
elif self.path == '/getAndFlushMessages':
response = self._get_messages()
else:
self.send_error(HTTPStatus.NOT_IMPLEMENTED)
self.end_headers()
return
response_bytes = json.dumps(response).encode('ascii')
self.send_response(HTTPStatus.OK)
self.send_header('Content-Length', str(len(response_bytes)))
self.end_headers()
self.wfile.write(response_bytes)
def _send_message(self, content: str) -> dict:
json_request = json.loads(content)
# TODO: use google.protobuf.json_format.ParseDict
# TODO: your rpc call of the messenger here
# TODO: use google.protobuf.json_format.MessageToDict here
return {'sendTime': ''}
def _get_messages(self) -> List[dict]:
return self._postbox.collect_messages()
def main():
grpc_server_address = os.environ.get('MESSENGER_SERVER_ADDR', 'localhost:51075')
# TODO: create your grpc client and wait for the server to become available.
# The client may start before the server.
stub = None
# A list of messages obtained from the server-py but not yet requested by the user to be shown
# (via the http's /getAndFlushMessages).
postbox = PostBox()
# TODO: Implement and run a messages stream consumer in a background thread here.
# It should fetch messages via the grpc client and store them in the postbox.
# Pass the stub and the postbox to the HTTP server.
# Dirty, but this simple http server doesn't provide interface
# for passing arguments to the handler c-tor.
MessageHandler._stub = stub
MessageHandler._postbox = postbox
http_port = os.environ.get('MESSENGER_HTTP_PORT', '8080')
http_server_address = ('0.0.0.0', int(http_port))
# NB: handler_class is instantiated for every http request. Do not store any inter-request state in it.
httpd = HTTPServer(http_server_address, MessageHandler)
httpd.serve_forever()
if __name__ == '__main__':
main()
@@ -0,0 +1,2 @@
grpcio==1.75.0
grpcio-tools==1.75.0
@@ -0,0 +1,5 @@
syntax = "proto3";
package mes_grpc;
// TODO: Add messages and service