MQTT로 카톡서버 만들어보자 - 4/10. 내전화 받았늬?

빙고리우스·2024년 3월 27일
5
post-thumbnail

늬 내 전화 아이 받늬?

말을 하는데, 상대가 '응', '응' 하며 추임새를 넣어주면 말하는 사람도 신이 난다.

그런데 내 말엔 신경도 안 쓰고 지 할 일만 하는 놈도 있다.
'늬 내 말 듣고 있니?' 라고 답답해서 중간에 물으면 '야...다 듣고 있어!' 이러면 짜증 제대로다.

이번 글에서는 내가 talk을 날렸을 때, 상대편 대응 방식에 따라 어떻게 톡 프로그램을 코딩해야 하는 지 알아보자.

즉 대꾸하는 분과 대꾸없는 놈, 두 케이스에 대해서...

pub/sub 통신에서는 보내는 사람과 받는 사람, 즉 종단간의 연결 보다는 중간에 있는 broker 와의 대화에 집중한다.
broker와의 통화 품질을 QoS라고 하는데, 여기에는 다음 3가지가 있다.

QoS

QoS어려운 말쉬운 말ㅈㄹ 쉬운 말
0At most onceFire and Forget쏘고 잊으세요
1At least onceAcknowledged delivery최소 한 번은 보내라
2Exactly onceAssured delivery딱 한 번만 보내라

Ref: MQTT Spec v3.1.1 4.3 Quality of Service levels and protocol flows

이건 client인 나와 broker인 너와의 관계다.
pub하는 client와 sub하는 server와 관계는 제 3자인 broker가 끼여드는데, 이것도 여기에 기초한다.

다음은 QoS 별 응답 프로토콜이다. 자세한 내용은 참고를 클릭하면 되는데....
슬프게도 영어다😭😭😭

QoS응답참고
0없음
1PUBACKMQTT Spec v3.1.1 3.4 PUBACK – Publish acknowledgement
2PUBRECMQTT Spec v3.1.1 3.5 PUBREC – Publish received
PUBRELMQTT Spec v3.1.1 3.6 PUBREL – Publish release
PUBCOMPMQTT Spec v3.1.1 3.7 PUBCOMP – Publish complete

지면도 남고 하니 하나씩 알아보자.

Qos Level 0

쏘고 잊으세요.

이거 딱 그거다. 월요일 아침에 하는 교장 선생님 훈화
연식 들통나는 건가 😜
자기 말만 하고 땡이다

출처: The Importance of MQTT QoS: A Comprehensive Guide (Quality of Service)

그림 보면 알겠지만, 쏘기만 한다.
Broker도 마찬가지다.
Subscriber한테 쏘기만 한다.

요 걸 3자 관계로 그려보면 다음과 같다.

출처: Introduction to MQTT QoS (Quality of Service)

Publisher는 Broker한테 쏘고 Broker도 Subscriber한테 쏘기만 할 뿐 받았는지 관심이 없다.
말 그대로 쏘고 잊는다.
메세지 저장 이딴 것 없다.

따라서 ㅈㄹ 빠르다.

소스로는 { qos: 0} 만 추가하면 된다.

const mqtt = require("mqtt");
const client = mqtt.connect("mqtt://localhost");

const user = { id: 'client1', pwd: 'pwd' };

client.on("connect", () => {
    client.subscribe("clients/client1");
    client.publish("server/payload", 'hi', { qos: 0 }, (err, res) => {
        if (err) console.log('err', err);
        else console.dir(res);

        client.end();
    });
});

요걸 실행해보면


callback 함수 resundefined다.

이 딴 걸 어디에다 쓰냐고?
다 필요하니까 만들어 둔 것이다.
온,습도 센서가 온,습도를 pub한다고 했을 때, 받는 쪽 입장에서는 최신 데이터가 젤 중요하다.
이럴 땐 QoS 0 이 딱이다.
중간 것 몇 개 잃어 버려도 개안타~

Qos Level 1

최소 한 번은 보내라.

이건 매번 확인하는 거다. 내 말 들었는지.
'응', '응' 하는 맞장구가 없으면 한 말 또 하는거다.

출처: The Importance of MQTT QoS: A Comprehensive Guide (Quality of Service)

출처: Introduction to MQTT QoS (Quality of Service)

Publisher는 Broker한테 쏘고 Broker는 Subscriber한테 쏘고 PUBACK을 돌려준다.
PUBACK을 실패하면 Broker는 저장해두었던 걸 한 번 더 보낼 수 있다.

QoS 0 보다는 아니지만, 나름 빠른데, 1번 이상 Subscriber가 받을 수 있다.

QoS 만 0에서 1로 바꾼 소스다.

client.on("connect", () => {
    client.subscribe("clients/client1");
    client.publish("server/payload", 'hi', { qos: 1 }, (err, res) => {
        if (err) console.log('err', err);
        else console.dir(res);

        client.end();
    });
});

요걸 실행해보면

res.cmd 에 'publish', 그리고 broker가 따 준 messageId가 들어왔다.

중복 문제만 없다면 요거이 딱인데...😅

QoS Level 2

딱 한 번만 보내라.


요건 빼도 박도 못하게 도장 꽉꽉 찍어두는거다.

출처: The Importance of MQTT QoS: A Comprehensive Guide (Quality of Service)

출처: Introduction to MQTT QoS (Quality of Service)

딱 한번만 보내고/받기 위해서는 Broker가 중간에서 보낸 놈과 받는 놈을 잡고 서로 주고 받은 걸 확인 시켜주는 작업을 한다.
그러다 보니 확인하는 프로토콜(PUBREC,PUBREL,PUBCOMP)이 많고, 당연히 통신도 느려진다.
Broker도 할 일이 많다보니 부하도 많이 걸리고, 그림 보면 알겠지만 Publisher, Subscriber 도 뭔가 복잡하고 바쁘다.

Qos 를 2로 바꾼 소스다.

client.on("connect", () => {
    client.subscribe("clients/client1");
    client.publish("server/payload", 'hi', { qos: 2 }, (err, res) => {
        if (err) console.log('err', err);
        else console.dir(res);

        client.end();
    });
});

이를 실행하면

res.cmd 에 'pubrel', 그리고 broker가 따 준 messageId가 들어왔다.

요거이 우리가 원하는 바이긴 한데, 느리다는 단점이 있다.

QoS 2와 같이 중복없이 가는데, 빠른 방법은 없을까?

QoS = 1과 중복 방지

받았다는 걸 확인할 수 있는 PUBACK은 받지만, 중복을 피하고 싶다.
즉 QoS = 2의 효과를 원하지만, 속도가 느린 건 원치 않는다. 이건데...

세상에 공짜가 있나
Broker 부하를 줄이려면 누군가 그 부하를 져야할텐데

누가 그 부담을 질 것인가?

Database <- 얘 밖에 없다.

어떻게?

Insert 대신 Upsert로

쫑날 수도 있으니, Upsert로 ...
Upsert하려면 PK가 있어야 하는데.

아 그러면 client에서 talk을 쏠 때 unique한 talk_id를 만들어서 쏘면 되지 않을까?

talk_id를 unique하게 만드는 방법은 Date.now() 를 쓰던 UUID를 쓰던 방법은 많을 것이다.
그래도 쫑이 걱정된다면 prefix를 줘서 나미 따듯 쫑 안 나게 비켜가게 해야지.
이는 각자 개발 짬에 따라 코딩하시라.

client.js 수정

const { v4: uuidv4 } = require('uuid');

...

rl.on('line', function (line) {
    if (line === 'exit') rl.close();

    const talk = { data: { talk_id: uuidv4(), from: user_id, room: 'room1', msg: line } };
    client.publish("server/talk", JSON.stringify(talk));

    rl.prompt()
});

server.js 수정

    mysql_conn.sql(UPSERT_TALK, [json.data.talk_id, json.data.from, json.data.room, json.data.msg, json.data.talk_id], function(err, results, fields) {
      room.forEach(client => {
          if (talk.data.from === client) return false;
          let client_topic = 'clients/' + client + '/talk';
          server.publish(client_topic, message);
      });
	}

이건 번외지만 SQL 이 다음과 같이 바뀔 것이다.

Before

INSERT INTO talk_table (from, room, msg)
VALUES (#{from}, #{room}, #{msg});

After

INSERT INTO talk_table (id, from, room, msg)
VALUES (#{talk_id}, #{from}, #{room}, #{msg}) ON DUPLICATE KEY
UPDATE id = #{talk_id};

PUBACK: pub back 이 아니라 pub ack 이다. 개인적으론 pub back도 좋다고 생각한다. callback 처럼.
profile
다할줄아는 사람보다 뭔가 한가지 똑부러지게하는 사람이되자.

2개의 댓글

comment-user-thumbnail
2024년 3월 27일

귀에 콕콕 박히네요. 늬 내 전화 아이받늬?ㅋㅋㅋ

1개의 답글