QtGrpc 채팅

채팅방에서 모든 종류의 메시지를 공유할 수 있는 채팅 애플리케이션입니다.

Chat 예제는 QtGrpc 클라이언트 API의 고급 사용법을 보여줍니다. 서버는 사용자가 등록하고 인증하여 ChatRoom에 참여할 수 있도록 합니다. 일단 참여하면 사용자는 채팅방에서 문자 메시지, 이미지, 사용자 활동 또는 디스크의 기타 파일과 같은 다양한 유형의 메시지를 다른 모든 참가자와 공유할 수 있습니다.

이 예제에서 다루는 몇 가지 주요 주제는 다음과 같습니다:

프로토부프 스키마

Protobuf 스키마는 채팅 애플리케이션에서 사용되는 메시지 및 서비스의 구조를 정의합니다. 스키마는 두 개의 파일로 나뉩니다:

syntax = "proto3";

package chat;

import "chatmessages.proto";

service QtGrpcChat {
    // Register a user with \a Credentials.
    rpc Register(Credentials) returns (None);
    // Join as a registered user and exchange \a ChatMessage(s)
    rpc ChatRoom(stream ChatMessage) returns (stream ChatMessage) {}
}

qtgrpcchat.proto 파일은 두 개의 RPC 메서드를 제공하는 QtGrpcChat 서비스를 지정합니다:

  • Register: 제공된 Credentials 에 사용자를 등록합니다. 서버는 데이터베이스에서 일반 텍스트로 사용자를 저장하고 확인합니다.
  • ChatRoom: 연결된 모든 클라이언트 간에 ChatMessage 을 교환하기 위한 양방향 스트림을 설정합니다. 서버는 수신되는 모든 메시지를 연결된 다른 클라이언트에 브로드캐스트합니다.
syntax = "proto3";

package chat;

import "QtCore/QtCore.proto";

message ChatMessage {
    string username = 1;
    int64 timestamp = 2;
    oneof content {
        TextMessage text = 3;
        FileMessage file = 4;
        UserStatus user_status = 5;
    }
}

chatmessages.proto 파일은 태그가 지정된 유니온(합계 유형이라고도 함)인 ChatMessage 을 정의합니다. 이는 ChatRoom 스트리밍 RPC를 통해 전송할 수 있는 모든 개별 메시지를 나타냅니다. 모든 ChatMessage 에는 발신자를 식별하기 위해 usernametimestamp 이 포함되어야 합니다.

QtProtobufQtCoreTypes 모듈의 유형을 활성화하기 위해 QtCore/QtCore.proto 가져오기를 포함시켜 QtCore-특정 유형과 해당 Protobuf 등가 유형 간에 원활하게 변환할 수 있도록 합니다.

message FileMessage {
    enum Type {
        UNKNOWN = 0;
        IMAGE = 1;
        AUDIO = 2;
        VIDEO = 3;
        TEXT = 4;
    }
    Type type = 1;
    string name = 2;
    bytes content = 3;
    uint64 size = 4;

    message Continuation {
        uint64 index = 1;
        uint64 count = 2;
        QtCore.QUuid uuid = 3;
    }
    optional Continuation continuation = 5;
}

FileMessageChatMessage sum 유형에 대해 지원되는 메시지 유형 중 하나입니다. 로컬 파일을 메시지로 래핑할 수 있습니다. 선택 사항인 Continuation 필드는 대용량 파일 전송을 청크 단위로 처리하여 안정적인 전송을 보장합니다.

참고: Protobuf 스키마 및 애플리케이션 코드에서 ProtobufQtCoreTypes 모듈을 사용하는 방법에 대한 자세한 내용은 Qt Core usage.

서버

참고: 여기에 설명된 서버 애플리케이션은 gRPC 라이브러리를 사용합니다.

서버 애플리케이션은 비동기 gRPC 콜백 API를 사용합니다. 이를 통해 완료 대기열을 수동으로 관리해야 하는 복잡함 없이 비동기 API의 성능 이점을 활용할 수 있습니다.

class QtGrpcChatService final : public chat::QtGrpcChat::CallbackService

생성된 QtGrpcChat 서비스의 CallbackService 하위 클래스인 QtGrpcChatService 클래스를 선언합니다.

    grpc::ServerBidiReactor<chat::ChatMessage, chat::ChatMessage> *
    ChatRoom(grpc::CallbackServerContext *context) override
    {
        return new ChatRoomReactor(this, context);
    }

    grpc::ServerUnaryReactor *Register(grpc::CallbackServerContext *context,
                                       const chat::Credentials *request,
                                       chat::None * /*response*/) override

가상 함수를 재정의하여 서비스에서 제공하는 두 개의 gRPC 메서드에 대한 기능을 구현합니다:

  • Register 메서드는 사용자를 확인하고 일반 텍스트 데이터베이스에 저장합니다.
  • ChatRoom 메서드는 메타데이터에 제공된 자격 증명을 데이터베이스와 대조하여 확인합니다. 성공하면 통신을 위한 양방향 스트림을 설정합니다.
    // Broadcast \a message to all connected clients. Optionally \a skip a client
    void broadcast(const std::shared_ptr<chat::ChatMessage> &message, const ChatRoomReactor *skip)
    {
        for (auto *client : activeClients()) {
            assert(client);
            if (skip && client == skip)
                continue;
            client->startSharedWrite(message);
        }
    }

서비스 구현은 ChatRoom 메서드를 통해 연결하거나 연결 해제하는 모든 활성 클라이언트를 추적합니다. 이렇게 하면 연결된 모든 클라이언트와 메시지를 공유하는 broadcast 기능이 활성화됩니다. 저장 공간과 오버헤드를 줄이기 위해 ChatMessageshared_ptr 로 래핑됩니다.

    // Share \a response. It will be kept alive until the last write operation finishes.
    void startSharedWrite(std::shared_ptr<chat::ChatMessage> response)
    {
        std::scoped_lock lock(m_writeMtx);
        if (m_response) {
            m_responseQueue.emplace(std::move(response));
        } else {
            m_response = std::move(response);
            StartWrite(m_response.get());
        }
    }

startSharedWrite 메서드는 ChatRoomReactor 의 멤버 함수입니다. 리액터(즉, 클라이언트)가 현재 쓰기 중이면 메시지가 대기열에 버퍼링됩니다. 그렇지 않으면 쓰기 작업이 시작됩니다. 모든 클라이언트 간에 공유되는 고유한 단일 메시지가 있습니다. response 메시지의 각 복사본은 use_count. 모든 클라이언트가 메시지 쓰기를 완료하고 use_count 이 0으로 떨어지면 리소스가 해제됩니다.

    // Distribute the incoming message to all other clients.
    m_service->broadcast(m_request, this);
    m_request = std::make_shared<chat::ChatMessage>(); // detach
    StartRead(m_request.get());

이 스니펫은 ChatRoomReactor::OnReadDone 가상 메서드의 일부입니다. 이 메서드가 호출될 때마다 클라이언트로부터 새 메시지가 수신됩니다. 메시지는 발신자를 건너뛰고 다른 모든 클라이언트로 브로드캐스트됩니다.

    std::scoped_lock lock(m_writeMtx);

    if (!m_responseQueue.empty()) {
        m_response = std::move(m_responseQueue.front());
        m_responseQueue.pop();
        StartWrite(m_response.get());
        return;
    }

    m_response.reset();

이 코드 조각은 ChatRoomReactor::OnWriteDone 가상 메서드의 일부입니다. 이 메서드가 호출될 때마다 메시지가 클라이언트에 기록됩니다. 대기열에 버퍼링된 메시지가 있는 경우 다음 메시지가 작성됩니다. 그렇지 않으면 m_response 이 재설정되어 쓰기 작업이 진행 중이 아님을 알립니다. broadcast 메서드와의 경합을 방지하기 위해 잠금이 사용됩니다.

클라이언트

클라이언트 애플리케이션은 제공된 Protobuf 스키마를 사용하여 서버와 통신합니다. 사용자를 등록하고 ChatRoom gRPC 메서드의 수명이 긴 양방향 스트림을 처리하기 위한 프런트엔드 및 백엔드 기능을 모두 제공합니다. 이를 통해 ChatMessages의 시각화 및 통신이 가능합니다.

설정
add_library(qtgrpc_chat_client_proto STATIC)
qt_add_protobuf(qtgrpc_chat_client_proto
    QML
    QML_URI QtGrpcChat.Proto
    PROTO_FILES
        ../proto/chatmessages.proto
    PROTO_INCLUDES
        $<TARGET_PROPERTY:Qt6::ProtobufQtCoreTypes,QT_PROTO_INCLUDES>
)

qt_add_grpc(qtgrpc_chat_client_proto CLIENT
    PROTO_FILES
        ../proto/qtgrpcchat.proto
    PROTO_INCLUDES
        $<TARGET_PROPERTY:Qt6::ProtobufQtCoreTypes,QT_PROTO_INCLUDES>
)

먼저 Protobuf 스키마에서 소스 파일을 생성합니다. qtgrpcchat.proto 파일에는 message 정의가 포함되어 있지 않으므로 qtgrpcgen 생성만 필요합니다. 또한 "QtCore/QtCore.proto" 가져오기가 유효한지 확인하기 위해 ProtobufQtCoreTypes 모듈의 PROTO_INCLUDES 을 제공합니다.

target_link_libraries(qtgrpc_chat_client_proto
    PUBLIC
        Qt6::Protobuf
        Qt6::ProtobufQtCoreTypes
        Qt6::Grpc
)

독립적인 qtgrpc_chat_client_proto 타깃이 ProtobufQtCoreTypes 모듈을 포함한 종속성에 대해 공개적으로 링크되도록 합니다. 그런 다음 애플리케이션 타깃이 이 라이브러리에 대해 링크됩니다.

백엔드 로직

애플리케이션의 백엔드는 네 가지 중요한 요소를 중심으로 구축됩니다:

  • ChatEngine: 애플리케이션 로직을 관리하는 QML 지향 싱글톤.
  • ClientWorker: gRPC 클라이언트 기능을 비동기적으로 제공하는 워커 객체.
  • ChatMessageModel: ChatMessage s를 처리하고 저장하기 위한 사용자 정의 QAbstractListModel.
  • UserStatusModel: 사용자 활동을 관리하기 위한 사용자 정의 QAbstractListModel.
    explicit ChatEngine(QObject *parent = nullptr);
    ~ChatEngine() override;

    // Register operations
    Q_INVOKABLE void registerUser(const chat::Credentials &credentials);
    // ChatRoom operations
    Q_INVOKABLE void login(const chat::Credentials &credentials);
    Q_INVOKABLE void logout();
    Q_INVOKABLE void sendText(const QString &message);
    Q_INVOKABLE void sendFile(const QUrl &url);
    Q_INVOKABLE void sendFiles(const QList<QUrl> &urls);
    Q_INVOKABLE bool sendFilesFromClipboard();

위의 스니펫은 서버와 상호 작용하기 위해 QML에서 호출되는 Q_INVOKABLE 함수 중 일부를 보여줍니다.

    explicit ClientWorker(QObject *parent = nullptr);
    ~ClientWorker() override;

public Q_SLOTS:
    void registerUser(const chat::Credentials &credentials);
    void login(const chat::Credentials &credentials);
    void logout();
    void sendFile(const QUrl &url);
    void sendFiles(const QList<QUrl> &urls);
    void sendMessage(const chat::ChatMessage &message);

ClientWorker 에서 제공하는 슬롯은 ChatEngine 에서 노출되는 API를 어느 정도 반영합니다. ClientWorker 은 대용량 파일 전송 또는 수신과 같은 고비용 작업을 백그라운드에서 처리하기 위해 전용 스레드에서 작동합니다.

m_clientWorker->moveToThread(&m_clientThread);
m_clientThread.start();
connect(&m_clientThread, &QThread::finished, m_clientWorker, &QObject::deleteLater);
connect(m_clientWorker, &ClientWorker::registerFinished, this, &ChatEngine::registerFinished);
connect(m_clientWorker, &ClientWorker::chatError, this, &ChatEngine::chatError);
...

ChatEngine 생성자에서는 ClientWorker 을 전용 워커 스레드에 할당하고 신호를 계속 처리 및 전달하여 QML 측에서 사용할 수 있도록 합니다.

void ChatEngine::registerUser(const chat::Credentials &credentials)
{
    QMetaObject::invokeMethod(m_clientWorker, &ClientWorker::registerUser, credentials);
}
...
void ClientWorker::registerUser(const chat::Credentials &credentials)
{
    if (credentials.name().isEmpty() || credentials.password().isEmpty()) {
        emit chatError(tr("Invalid credentials for registration"));
        return;
    }

    if ((!m_client || m_hostUriDirty) && !initializeClient()) {
        emit chatError(tr("Failed registration: unabled to initialize client"));
        return;
    }

    auto reply = m_client->Register(credentials, QGrpcCallOptions{}.setDeadlineTimeout(5s));
    const auto *replyPtr = reply.get();
    connect(
        replyPtr, &QGrpcCallReply::finished, this,
        [this, reply = std::move(reply)](const QGrpcStatus &status) {
            emit registerFinished(status);
        },
        Qt::SingleShotConnection);
}

이는 ChatEngineClientWorker 과 상호 작용하여 사용자를 등록하는 방법을 보여줍니다. ClientWorker 은 자체 스레드에서 실행되므로 invokeMethod 을 사용하여 안전하게 멤버 함수를 호출하는 것이 중요합니다.

ClientWorker 에서 클라이언트가 초기화되지 않았는지 또는 호스트 URI가 변경되었는지 확인합니다. 두 조건 중 하나가 충족되면 initializeClient 을 호출하여 새 QGrpcHttp2Channel 을 생성합니다. 이 작업은 비용이 많이 드는 작업이므로 발생 빈도를 최소화합니다.

Register RPC를 처리하기 위해 setDeadlineTimeout 옵션을 사용하여 서버 비활성화를 방지합니다. 일반적으로 단항 RPC에는 기한을 설정하는 것이 좋습니다.

void ClientWorker::login(const chat::Credentials &credentials)
{
    if (credentials.name().isEmpty() || credentials.password().isEmpty()) {
        emit chatError(tr("Invalid credentials for login"));
        return;
    }
    ...
    QGrpcCallOptions opts;
    opts.setMetadata({
        { "user-name",     credentials.name().toUtf8()     },
        { "user-password", credentials.password().toUtf8() },
    });
    connectStream(opts);
}

ChatRoom 에 로그인할 때는 setMetadata 옵션을 사용하여 서버에서 인증에 필요한 사용자 자격 증명을 제공합니다. 실제 호출 및 연결 설정은 connectStream 메서드에서 처리됩니다.

void ClientWorker::connectStream(const QGrpcCallOptions &opts)
{
    ...
    m_chatStream = m_client->ChatRoom(*initialMessage, opts);
    ...
    connect(m_chatStream.get(), &QGrpcBidiStream::finished, this,
            [this, opts](const QGrpcStatus &status) {
                if (m_chatState == ChatState::Connected) {
                    // If we're connected retry again in 250 ms, no matter the error.
                    QTimer::singleShot(250, [this, opts]() { connectStream(opts); });
                } else {
                    setState(ChatState::Disconnected);
                    m_chatResponse = {};
                    m_userCredentials = {};
                    m_chatStream.reset();
                    emit chatStreamFinished(status);
                }
            });
    ...

연결 중 스트림이 갑자기 종료되는 경우를 대비해 기본적인 재연결 로직을 구현합니다. 재연결은 최초 호출의 QGrpcCallOptions 주소로 connectStream 을 다시 호출하기만 하면 됩니다. 이렇게 하면 필요한 모든 연결도 업데이트됩니다.

참고: Android의 잠자기/앱 대기 모드는 파일 다이얼로그를 사용하거나 다른 앱으로 전환하는 등의 방법으로 트리거할 수 있습니다. 이 모드는 네트워크 액세스를 종료하여 모든 활성 QTcpSocket 연결을 닫고 스트림이 finished 이 되게 합니다. 재연결 로직으로 이 문제를 해결합니다.

    connect(m_chatStream.get(), &.QGrpcBidiStream::messageReceived, this, [this] { ... switch (m_chatResponse.contentField()) { case chat::ChatMessage::ContentFields::UninitializedField:            qDebug("Received uninitialized message");
           return; case chat::ChatMessage::ContentFields::Text: if (m_chatResponse.text().content().isEmpty()) return; break; case chat::ChatMessage::ContentFields::File: // 파일 메시지를 다운로드하고 다운로드한 URL을 // 콘텐츠에 저장하여 모델이 거기에서 참조할 수 있게 합니다.
            m_chatResponse.file() . setContent(saveFileRequest(m_chatResponse.file()).toString().toUtf8()); break; ... emit chatStreamMessageReceived(m_chatResponse); }); setState(Backend::ChatState::Connecting); }

메시지가 수신되면 ClientWorker 에서 FileMessage 콘텐츠를 저장하는 등 일부 전처리를 수행하여 ChatEngine 에서 모델에만 집중할 수 있도록 합니다. ContentFields 열거형을 사용하여 ChatMessage 합계 유형의 oneof content 필드를 안전하게 확인합니다.

void ChatEngine::sendText(const QString &message)
{
    if (message.trimmed().isEmpty())
        return;

    if (auto request = m_clientWorker->createMessage()) {
        chat::TextMessage tmsg;
        tmsg.setContent(message.toUtf8());
        request->setText(std::move(tmsg));
        QMetaObject::invokeMethod(m_clientWorker, &ClientWorker::sendMessage, *request);
        m_chatMessageModel->appendMessage(*request);
    }
}
...
void ClientWorker::sendMessage(const chat::ChatMessage &message)
{
    if (!m_chatStream || m_chatState != ChatState::Connected) {
        emit chatError(tr("Unable to send message"));
        return;
    }
    m_chatStream->writeMessage(message);
}

메시지를 보낼 때 ChatEngine 는 적절한 형식의 요청을 만듭니다. 예를 들어 sendText 메서드는 QString 을 수락하고 createMessage 함수를 사용하여 usernametimestamp 필드가 설정된 유효한 메시지를 생성합니다. 그런 다음 클라이언트가 호출되어 메시지를 전송하고 사본이 자체 ChatMessageModel 에 큐에 대기열에 추가됩니다.

QML 프론트엔드
import QtGrpc
import QtGrpcChat
import QtGrpcChat.Proto

QML 코드에는 다음과 같은 임포트가 사용됩니다:

  • QtGrpc: StatusCode 와 같은 QtGrpc QML 기능을 제공합니다.
  • QtGrpcChat: ChatEngine 싱글톤과 같은 컴포넌트를 포함하는 애플리케이션 모듈.
  • QtGrpcChat.Proto: 생성된 프로토버프 유형에 대한 QML 액세스를 제공합니다.
    Connections {
        target: ChatEngine
        function onChatStreamFinished(status) {
            root.handleStatus(status)
            loginView.clear()
        }
        function onChatStateChanged() {
            if (ChatEngine.chatState === Backend.ChatState.Connected && mainView.depth === 1)
                mainView.push("ChatView.qml")
            else if (ChatEngine.chatState === Backend.ChatState.Disconnected && mainView.depth > 1)
                mainView.pop()
        }
        function onRegisterFinished(status) {
            root.handleStatus(status)
        }
        function onChatError(message) {
            statusDisplay.text = message
            statusDisplay.color = "yellow"
            statusDisplay.restart()
        }
    }

Main.qml 에서는 ChatEngine 에서 방출되는 핵심 신호를 처리합니다. 이러한 신호의 대부분은 전역적으로 처리되며 애플리케이션의 모든 상태에서 시각화됩니다.

Rectangle {
    id: root

    property credentials creds
    ...
    ColumnLayout {
        id: credentialsItem
        ...
        RowLayout {
            id: buttonLayout
            ...
            Button {
                id: loginButton
                ...
                enabled: nameField.text && passwordField.text
                text: qsTr("Login")
                onPressed: {
                    root.creds.name = nameField.text
                    root.creds.password = passwordField.text
                    ChatEngine.login(root.creds)
                }
            }

프로토뷔프 스키마에서 생성된 메시지 유형은 QML_VALUE_TYPEs(메시지 정의의 카멜케이스 버전)이므로 QML에서 액세스할 수 있습니다. LoginView.qmlcredentials 값 유형 속성을 사용하여 ChatEngine 에서 login 을 시작합니다.

        ListView {
            id: chatMessageView
            ...
            component DelegateBase: Item {
                id: base

                required property chatMessage display
                default property alias data: chatLayout.data
                ...
            }
            ...
            // We use the DelegateChooser and the 'whatThis' role to determine
            // the correct delegate for any ChatMessage
            delegate: DelegateChooser {
                role: "whatsThis"
                ...
                DelegateChoice {
                    roleValue: "text"
                    delegate: DelegateBase {
                        id: dbt
                        TextDelegate {
                            Layout.fillWidth: true
                            Layout.maximumWidth: root.maxMessageBoxWidth
                            Layout.preferredHeight: implicitHeight
                            Layout.bottomMargin: root.margin
                            Layout.leftMargin: root.margin
                            Layout.rightMargin: root.margin

                            message: dbt.display.text
                            selectionColor: dbt.lightColor
                            selectedTextColor: dbt.darkColor
                        }
                    }
                }

ChatView.qml 에서 ListViewChatRoom 에 메시지를 표시합니다. ChatMessage 합계 유형을 조건부로 처리해야 하므로 약간 더 복잡합니다.

이를 처리하기 위해 메시지 유형에 따라 적절한 델리게이트를 선택할 수 있는 DelegateChooser를 사용합니다. 모델에서 기본 whatThis 역할을 사용하여 각 ChatMessage 인스턴스에 대한 메시지 유형을 제공합니다. 그런 다음 DelegateBase 컴포넌트는 모델의 display 역할에 액세스하여 채팅 메시지 데이터를 렌더링할 수 있도록 합니다.

TextEdit {
    id: root

    required property textMessage message

    text: message.content
    color: "#f3f3f3"
    font.pointSize: 14
    wrapMode: TextEdit.Wrap
    readOnly: true
    selectByMouse: true
}

다음은 TextMessage 유형을 시각화하는 컴포넌트 중 하나입니다. 이 컴포넌트는 protobuf 모듈의 textMessage 값 유형을 사용하여 텍스트를 시각화합니다.

                TextArea.flickable: TextArea {
                    id: inputField

                    function sendTextMessage() : void {
                        if (text === "")
                            return
                        ChatEngine.sendText(text)
                        text = ""
                    }
                    ...
                    Keys.onPressed: (event) => {
                        if (event.key === Qt.Key_Return && event.modifiers & Qt.ControlModifier) {
                            sendTextMessage()
                            event.accepted = true
                        } else if (event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) {
                            if (ChatEngine.sendFilesFromClipboard())
                                event.accepted = true
                        }
                    }

Chat 클라이언트는 다음과 같이 메시지를 보낼 때 다양한 액세스 포인트를 제공합니다:

  • 파일 수락 애플리케이션에 끌어다 놓기.
  • <Ctrl + V>를 누르면 Q클립보드에 저장된 모든 내용을 보낼 수 있습니다.
  • <Ctrl + Enter>로 메시지를 보내려면 inputField
  • 보내기 버튼을 클릭하면 inputField
  • 파일 다이얼로그를 통해 파일 선택

SSL

서버와 클라이언트 간의 통신을 보호하기 위해 SSL/TLS 암호화가 사용됩니다. 이를 위해서는 최소한 다음이 필요합니다:

  • 개인 키: 보안 연결을 설정하는 데 사용되는 서버의 개인 키가 포함되어 있습니다. 이 키는 기밀로 유지되어야 하며 절대 공유해서는 안 됩니다.
  • 인증서: 서버의 신원을 확인하기 위해 클라이언트와 공유되는 서버의 공개 인증서를 포함합니다. 일반적으로 CA(인증 기관)에서 서명하거나 테스트 목적으로 자체 서명할 수 있습니다.
  • 선택적 루트 CA 인증서: 사용자 지정 CA(인증 기관)를 사용하여 서버 인증서에 서명하는 경우 클라이언트 측에서 서버의 인증서 체인의 유효성을 검사하기 위해 루트 CA 인증서가 필요합니다. 이렇게 하면 사용자 지정 CA의 루트 인증서가 공개 CA처럼 클라이언트의 신뢰 저장소에 사전 설치되지 않으므로 클라이언트가 서버의 인증서를 신뢰할 수 있습니다.

저희는 OpenSSL을 사용하여 이러한 파일을 만들고 gRPC 통신이 SSL/TLS를 사용하도록 설정했습니다.

        grpc::SslServerCredentialsOptions sslOpts;
        sslOpts.pem_key_cert_pairs.emplace_back(grpc::SslServerCredentialsOptions::PemKeyCertPair{
            LocalhostKey,
            LocalhostCert,
        });
        builder.AddListeningPort(QtGrpcChatService::httpsAddress(), grpc::SslServerCredentials(sslOpts));
        builder.AddListeningPort(QtGrpcChatService::httpAddress(), grpc::InsecureServerCredentials());

개인 키와 인증서를 gRPC 서버에 제공합니다. 이를 통해 서버 측에서 TLS를 사용하도록 SslServerCredentials 을 구성합니다. 보안 통신 외에도 암호화되지 않은 액세스도 허용합니다.

서버는 다음 주소에서 수신 대기합니다:

  • HTTPS : 0.0.0.0:65002
  • HTTP : 0.0.0.0:65003

서버는 0.0.0.0 에 바인딩하여 모든 네트워크 인터페이스에서 수신 대기하므로 동일한 네트워크에 있는 모든 디바이스에서 액세스할 수 있습니다.

if (m_hostUri.scheme()== "https") { if (!QSslSocket::supportsSsl()) { emit chatError(tr("장치가 SSL을 지원하지 않습니다. 'http' 체계를 사용해 주세요.")); return false; } QFile crtFile(":/res/root.crt"); if (!crtFile.open(QFile::ReadOnly)) {        qFatal("Unable to load root certificate");
       반환 거짓; } QSslConfiguration sslConfig;    QSslCertificate crt(crtFile.readAll()); sslConfig.addCaCertificate(crt); sslConfig.setProtocol(QSsl::TlsV1_2OrLater); sslConfig.setAllowedNextProtocols({ "h2" }); // HTTP/2 허용 // 호스트 이름 확인을 비활성화하여 모든 로컬 IP에서 연결을 허용합니다 . // 개발에는 허용되지만 보안을 위해 운영에서는 피하십시오.sslConfig.setPeerVerifyMode(QSslSocket::VerifyNone); opts.setSslConfiguration(sslConfig); }

클라이언트는 CA를 자체 서명했으므로 루트 CA 인증서를 로드합니다. 이 인증서는 QSslCertificate 를 생성하는 데 사용됩니다. HTTP/2를 사용하므로 "h2" 프로토콜에 setAllowedNextProtocols 를 제공하는 것이 중요합니다.

예제 실행하기

  • qtgrpc_chat_server 가 실행 중이고 수신에 성공했는지 확인합니다.
  • 서버와 동일한 기기를 사용하는 경우 qtgrpc_chat_client 을 실행할 때 기본 localhost 주소로 충분합니다. 서버를 호스팅하는 기기 이외의 기기를 사용하는 경우 설정 대화 상자에서 서버를 실행하는 호스트의 올바른 IP 주소를 지정합니다.
  • 원활한 이모티콘 환경 🚀으로 빌드하려면 클라이언트에서 GRPC_CHAT_USE_EMOJI_FONT CMake 옵션이 활성화되어 있는지 확인하세요.

에서 예제를 실행하려면 Qt Creator에서 Welcome 모드를 열고 Examples 에서 예제를 선택합니다. 자세한 내용은 Qt Creator: 튜토리얼을 참조하세요 : 빌드 및 실행을 참조하세요.

예제 프로젝트 @ code.qt.io

© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.