ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [AWS] Serverless 채팅 구현하기
    AWS 2023. 9. 4. 22:58

     

     webSocket API, Lambda, DynamoDB를 이용해서 서버리스 채팅 앱을 구현해보았다.

     

    일단 완성본

    아키텍처

     

    1. user가 접속하면 websocek에 연결되면서 connection Id를 부여 받는다.

    2. 유저목록 lambda는 해당 connection Id를 USERLIST에 저장한다.

    3. 유저가 접속하면 일단 기존에 있는 채팅 목록을 보여준다

       -> 채팅조회 lambda를 이용하여 MESSAGES에 있는 데이터를 보여준다

    4. 유저가 메세지를 보내게 되면 채팅 저장 및 전달 lambda를 통해 MESSAGES에 데이터를 저장한다.

    5. 유저가 접속을 끊으면 ConnectionId를 유저목록 Lambda를 통해 USERLIST에서 지운다.

     

     

    AWS Resource

     

    1. API GateWay

     

    Amazon API Gateway는 어떤 규모에서든 개발자가 API를 손쉽게 생성, 게시, 유지관리, 모니터링 및 보안 유지할 수 있도록 하는 완전 관리형 서비스 이다.

     

    * RESTful API

     -HTTP 프로토콜 기반의 REST API로 백엔드 서비스 호출

     

    * WebSocketAPI

       - WebSocket 프로토콜로 백엔드 서비스 호출

       - WebSocket 연결시 ConnectionID 부여

       - ConnectionID로 클라이언트를 구분해 메세지 전송 가능

     

     

    원하는 기능을 구현 하기 위해서는 DynamoDB에 채팅 메세지를 저장하는 Lambda함수,

    채팅 메세지를 조회하는 Lambda함수를 호출 하기 위해 REST APIGateway를 생성한다. 

     

    Websocket과 연결한 클라이언트의 connectionId를 DynamoDB에 저장하는 Lambda함수,

    연결이 끊긴 클라이언트의 connectionId를 삭제하는 Lambda함수를 호출하기 위해서는

    Websocket APIGateway를 생성한다.

     

     

    2. DynamoDB

     

    Key-Value 기반의 완전 관리형 NoSQL 데이터베이스

     

    - USERLIST : 채팅방에 접속한 유저들의 ConnectionID 저장

    - MESSAGES : 채팅 메세지 저장 

     

     

     

     

     

     

    3. Lambda

     

    이벤트에 대한 응답으로 코드를 실행하고 자동으로 기본 컴퓨팅 리소스를 관리하는 서버리스 컴퓨팅 서비스

     

     

     

    3. 채팅메세지 입력  lambda

    4.

     

     

     

    (1) websocket 연결 lambda

     - connectionId를 USERLIST DB에 저장

    const AWS = require('aws-sdk');
    exports.handler = async (event) => {
        let inputObject = event.queryStringParameters;
        var docClient = new AWS.DynamoDB.DocumentClient();
    
        //웹소켓에 접속하면 부여되는 connectionId를 DB에 저장한다.
        const item = {
            room_id: inputObject.room_id,
            connection_id: event.requestContext.connectionId,
            user_id: inputObject.user_id,
            timestamp: Date.now()
        }
        try {
            var params = {
                TableName: 'chatapp-userlist',
                Item: item
            };
            await docClient.put(params).promise();
            let response = {
                isBase64Encoded: true,
                statusCode: 200,
                headers: {
                    "Content-Type": "application/json; charset=utf-8",
                    "Access-Control-Expose-Headers": "*",
                    "Access-Control-Allow-Origin": "*",
                },
                body: "ok"
            };
            return response;
        } catch (e) {
            console.log(e);
            return "error";
        }
    
    };

     

    (2) websocket 해제 lambda

     - connectionId를  DB에서 삭제

    const AWS = require('aws-sdk');
    
    exports.handler = async event => {
      var docClient = new AWS.DynamoDB.DocumentClient();
      //웹소켓의 연결이 해제되면 Connection ID를 삭제한다.
      var params = {
        TableName: 'chatapp-userlist',
        Key: {
          connection_id: event.requestContext.connectionId
        }
      };
      await docClient.delete(params).promise();
      return "Disconnected";
    };

     

    (3) 채팅 입력 Lambda

     - 채팅 메세지를 MESSAGES 테이블에 저장

     - DB에서 해당 채팅방의 ConnectionId를 불러와 WebSocket API GateWay를 통해 메시지 전달

    const AWS = require('aws-sdk');
    exports.handler = async (event, context) => {
        var docClient = new AWS.DynamoDB.DocumentClient();
        const inputObject = JSON.parse(event.body);
    
        //우선 해당 채팅방의 접속한 모든 유저를 가져온다.
        var params = {
            TableName: 'chatapp-userlist',
            IndexName: 'room_id-user_id-index',
            KeyConditionExpression: '#HashKey = :hkey',
            ExpressionAttributeNames: { '#HashKey': 'room_id' },
            ExpressionAttributeValues: {
                ':hkey': inputObject.room_id
            }
        };
        const result = await docClient.query(params).promise();
        const now = Date.now();
    
        //채팅을 DB에 저장한다.
        const item = {
            room_id: inputObject.room_id,
            timestamp: now,
            message: inputObject.text,
            user_id: inputObject.user_id,
            name: inputObject.name,
        };
        var params = {
            TableName: 'chatapp-chat-messages',
            Item: item
        };
        await docClient.put(params).promise();
    
        //이전에 불러온 방에 접속한 유저들 모두에게 채팅을 보낸다.
        const apigwManagementApi = new AWS.ApiGatewayManagementApi({
            apiVersion: '2018-11-29',
            endpoint: `${process.env.socket_api_gateway_id}.execute-api.ap-northeast-2.amazonaws.com/dev`
        });
        
    		if (result.Items) {
            const postCalls = result.Items.map(async ({ connection_id }) => {
                const dt = { ConnectionId: connection_id, Data: JSON.stringify(item) };
                try {
                    await apigwManagementApi.postToConnection(dt).promise();
                } catch (e) {
                    console.log(e);
                    //만약 이 접속은 끊긴 접속이라면, DB에서 삭제한다.
                    if (e.statusCode === 410) {
                        console.log(`Found stale connection, deleting ${connection_id}`);
                        var params = {
                            TableName: 'chatapp-userlist',
                            Key: {
                                connection_id: connection_id
                            }
                        };
                        await docClient.delete(params).promise();
                    }
                }
            });
            try {
                await Promise.all(postCalls);
            } catch (e) {
                return { statusCode: 500, body: e.stack };
            }
        }
        let response = {
            isBase64Encoded: true,
            statusCode: 200,
            headers: {
                "Content-Type": "application/json; charset=utf-8",
                "Access-Control-Expose-Headers": "*",
                "Access-Control-Allow-Origin": "*",
            },
            body: JSON.stringify(result.Items)
        };
        return response
    };

     

    (4) 채팅메세지 조회  lambda

    - DB에서 해당 채팅방의 메세지 조회 (채팅방에 처음 입장시)

    var AWS = require("aws-sdk");
    AWS.config.update({
        region: "ap-northeast-2"
    });
    exports.handler = async function (event, context) {
        var docClient = new AWS.DynamoDB.DocumentClient();
        //채팅 메세지를 가져온다.
        var params = {
            TableName: 'chatapp-chat-messages',
            KeyConditionExpression: '#HashKey = :hkey',
            ExpressionAttributeNames: { '#HashKey': 'room_id' },
            ExpressionAttributeValues: {
                ':hkey': event.queryStringParameters.room_id
            }
        };
        try {
            const result = await docClient.query(params).promise();
            let response = {
                isBase64Encoded: true,
                statusCode: 200,
                headers: {
                    "Content-Type": "application/json; charset=utf-8",
                    "Access-Control-Expose-Headers": "*",
                    "Access-Control-Allow-Origin": "*",
                },
                body: JSON.stringify(result.Items)
            };
            return response
        }
        catch (e) {
            console.log(e)
            let response = {
                isBase64Encoded: true,
                statusCode: 500,
                headers: {
                    "Content-Type": "application/json; charset=utf-8",
                    "Access-Control-Allow-Origin": "*",
                },
                body: JSON.stringify("error")
            };
            return response;
        }
    }

     

    4. S3

     

     Simple Storage Servic

     

      React 기반의 정적 컨텐츠를 호스팅 하기 위해 사용한다.

      React에서는 Websocket/Rest API프로토콜을 사용해 API GateWay의 API를 호출

Designed by Tistory.