Lambda + Websocket + Dynamodb를 이용한 간단 채팅 실습
2019-07-29 21:39:45

IAM 역할 만들기

아래와 같은 권한을 추가하여 생성을 한다.

auth

Dynamodb 생성

  • 테이블 이름은 my_chat
  • 기본 파티션 키는 connection_id (문자열)
  • 글로벌 인덱스 키는 user_id-index

dynamodb-info

gsi

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';

/**
* TODO: 커넥션
* @description Dynamodb에 해당 유저 ID, 커넥션 ID 담아둔다.
*/
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) { // NOTE: 만약에 중복된 유저가 접속이 되는 경우..?
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'});

/**
* TODO: 연결 해제
* @description dynamodb에서 삭제함.
*/
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';

/**
* TODO: 메세지 전송
*/
exports.handler = async event => {
const result = {};
let connectionData;
try {
connectionData = await ddb.scan({ TableName: TABLE_NAME }).promise(); // NOTE: 모든 커넥션 ID를 가지고 온다.
} 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(); // NOTE: 메세지 전송
} catch (e) {
if (e.statusCode === 410) { // NOTE: 연결 끊긴 커넥션 ID는 삭제
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

gateway

경로 표현식은 쉽게 생각하면 웹소켓의 접근할 url 이라고 생각하면 될듯. 링크 참고.

connect

연결

disconnect

연결해제

message

message

배포

배포

배포-2

배포-3

클라이언트 테스트

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

Prev
2019-07-29 21:39:45
Next