Lambda + Websocket + Dynamodb를 이용한 간단 채팅 실습
2019-07-29 21:39:45
# AWS
IAM 역할 만들기
아래와 같은 권한을 추가하여 생성을 한다.
Dynamodb 생성
- 테이블 이름은 my_chat
- 기본 파티션 키는 connection_id (문자열)
- 글로벌 인덱스 키는 user_id-index
lambda 함수 생성
lambda는 위의 만든 IAM role로 생성을 한다.
연결 함수
연결된 유저 아이디(클라이언트에서 전송), 커넥션 아이디(api gateway쪽에서 자체 발급)를
dynamodb에 저장을 해서 관리를 하게끔 한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| const AWS = require('aws-sdk');
const ddb = new AWS.DynamoDB.DocumentClient({ region: 'ap-northeast-2' });
const TABLE_NAME = 'my_chat';
exports.handler = async (event) => { const result = {}; const connectionId = event.requestContext.connectionId; if (typeof event.queryStringParameters === 'undefined' || typeof event.queryStringParameters.user_id === 'undefined') { console.log('연결 실패', '유저 아이디가 없음'); result.statusCode = 500; result.body = `연결 실패: ${connectionId}`; return result; } const userId = event.queryStringParameters.user_id; const checkParams = { TableName: TABLE_NAME, IndexName: 'user_id-index', KeyConditionExpression: '#user_id = :user_id', ExpressionAttributeNames: { '#user_id': 'user_id', }, ExpressionAttributeValues: { ':user_id': userId, }, }; const putParams = { TableName: TABLE_NAME, Item: { connection_id: connectionId, user_id: userId, }, }; try { const duplicateCheck = await ddb.query(checkParams).promise(); if (duplicateCheck.Items.length > 0) { const del = []; duplicateCheck.Items.forEach((key) => { const delParams = { TableName: TABLE_NAME, Key: { connection_id: key.connection_id, } }; del.push(ddb.delete(delParams).promise()); Promise.all(del); }); } await ddb.put(putParams).promise(); result.statusCode = 200; result.body = `연결 성공: ${connectionId}`; console.log('연결 완료', JSON.stringify(result)); } catch (e) { result.statusCode = 500; result.body = `연결 실패: ${connectionId}`; console.log('연결 실패', JSON.stringify(result)); console.log('err', JSON.stringify(e)); } return result; };
|
연결 해제 함수
클라이언트에서 연결을 해제 시 자동 호출 되어 dynamodb에서 삭제를 한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| const AWS = require('aws-sdk');
const ddb = new AWS.DynamoDB.DocumentClient({region: 'ap-northeast-2'});
exports.handler = async event => { const connectionId = event.requestContext.connectionId; const params = { TableName: 'my_chat', Key: { connection_id: connectionId, } }; const result = {}; try { await ddb.delete(params).promise(); result.statusCode = 200; result.body = `연결 해제: ${connectionId}`; console.log('연결 해제', JSON.stringify(result)); } catch (e) { result.statusCode = 500; result.body = `해제 실패: ${connectionId}`; console.log('해제 실패', JSON.stringify(result)); console.log('err', JSON.stringify(e)); } return result; };
|
메세지 전송
클라이언트에서 메세지를 전송 시 dynamodb에 담아둔 연결된 모든 유저에게 일괄 전송 한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| const AWS = require('aws-sdk');
const ddb = new AWS.DynamoDB.DocumentClient({ region: 'ap-northeast-2' });
const TABLE_NAME = 'my_chat';
exports.handler = async event => { const result = {}; let connectionData; try { connectionData = await ddb.scan({ TableName: TABLE_NAME }).promise(); } catch (e) { result.statusCode = 500; result.body = '연결된 아이디를 가지고 올 수가 없다'; return result; } const apigwManagementApi = new AWS.ApiGatewayManagementApi({ apiVersion: '2018-11-29', endpoint: event.requestContext.domainName + '/' + event.requestContext.stage, region: 'ap-northeast-2' }); const message = JSON.parse(event.body).message; const postCalls = connectionData.Items.map(async ({ connection_id, user_id }) => { try { const post = { connection_id, user_id, message, }; console.log('post', JSON.stringify(post)); await apigwManagementApi.postToConnection({ ConnectionId: connection_id, Data: JSON.stringify(post) }).promise(); } catch (e) { if (e.statusCode === 410) { console.log(`deleting ${user_id}`); await ddb.delete({ TableName: TABLE_NAME, Key: { connection_id } }).promise(); } console.log('e', e.stack); } }); try { await Promise.all(postCalls); result.statusCode = 200; result.body = '메세지 전송 성공'; } catch (e) { result.statusCode = 500; result.body = '메세지 전송 실패'; } return result; };
|
API GATEWAY
경로 표현식은 쉽게 생각하면 웹소켓의 접근할 url 이라고 생각하면 될듯. 링크 참고.
connect
disconnect
message
배포
클라이언트 테스트
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
| <!DOCTYPE html> <html>
<head> <meta charset="utf-8" /> <title>WebSocket test</title> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script type="text/javascript">
var wSocket;
function init() { wSocket = new WebSocket("wss://evfxor8p6a.execute-api.ap-northeast-2.amazonaws.com/dev"); wSocket.onopen = function (e) { onOpen(e) }; wSocket.onclose = function (e) { onClose(e) }; wSocket.onmessage = function (e) { onMessage(e) }; wSocket.onerror = function (e) { onError(e) }; }
function onOpen(e) { console.log("WebSocket opened!"); console.log('connect: ', e);
$("#btn_open").attr("disabled", "disabled"); $("#btn_close").removeAttr("disabled"); $("#btn_send").removeAttr("disabled"); $("#message").removeAttr("disabled"); }
function onClose(e) { console.log("WebSocket closed!");
$("#btn_open").removeAttr("disabled"); $("#btn_close").attr("disabled", "disabled"); $("#btn_send").attr("disabled", "disabled"); $("#message").attr("disabled", "disabled"); }
function onMessage(e) { alert("메시지 수신 : " + e.data); }
function onError(e) { alert("오류발생 : " + e.data); }
function doOpen() { init(); }
function doClose() { wSocket.close(); }
function doSend() { var ms = { "action": "message", "message": $('#message').val() }; ms = JSON.stringify(ms); console.log(typeof ms); wSocket.send(ms); }
$(function () { $("#btn_open").removeAttr("disabled"); $("#btn_close").attr("disabled", "disabled"); $("#btn_send").attr("disabled", "disabled"); $("#message").attr("disabled", "disabled"); init(); });
</script> </head>
<body> <input type="button" onclick="doOpen();" value="Open" id="btn_open" /> <input type="button" onclick="doClose();" value="Close" id="btn_close" /> <label for="message">Message: </label><input type="text" placeholder="Message" id="message" /> <input type="button" onclick="doSend();" value="Send" id="btn_send" /> </body>
</html>
|
1
| var ms = { "action": "message", "message": $('#message').val() };
|
클라이언트에서 보낼 때 action이 api 게이트웨이에서 정한 경로이고
message는 보낼 파라미터.
참고
aws-samples/simple-websockets-chat-app
2019-07-29 21:39:45
# AWS