실시간 채팅 스트리밍

liveChatMessages.streamList 메서드를 사용하여 장기 연결을 통해 실시간 채팅 메시지를 클라이언트에 푸시하는 GRPC 클라이언트를 만드는 방법을 알아봅니다. 이 페이지에서는 실시간 채팅 메시지를 위한 데모 Python 스트리밍 클라이언트를 설명합니다.

시작하기 전에

데모를 실행하려면 다음 섹션의 단계를 완료하세요.

GRPC 도구 및 클라이언트 설치

gRPC 프로토콜에는 서비스, 요청, 응답 메시지의 프로토콜 버퍼 정의가 필요합니다. 여기에서 다양한 프로그래밍 언어로 클라이언트 라이브러리를 생성할 수 있습니다.

스트리밍 클라이언트에서 사용하는 서비스 인터페이스를 정의하는 stream_list.proto를 사용합니다.

Python용 gRPC 클라이언트 및 도구를 다운로드합니다.

pip install grpcio
pip install grpcio-tools

언어별 클라이언트 라이브러리 생성에 대해 자세히 알아보세요.

인증 설정

프로젝트의 인증을 설정합니다. OAuth 2.0 액세스 토큰 또는 API 키를 사용하여 데모를 실행할 수 있습니다.

실시간 채팅 ID 가져오기

videos.list 메서드를 사용하여 라이브 스트림의 실시간 채팅 ID를 찾습니다. ID는 응답의 liveStreamingDetails.activeLiveChatId 필드에 있습니다.

데모

데모 Python 스트리밍 클라이언트를 빌드하려면 다음 단계를 따르세요.

  1. stream_list_demo.py 파일을 로컬로 복사합니다. 파일을 수정하여 인증 옵션 중 하나의 주석을 해제합니다.

  2. 클라이언트를 빌드합니다.

    python3 -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. stream_list.proto
    
  3. 데모를 실행합니다.

    • API_KEY_OR_OAUTH_TOKEN을 API 키 또는 OAuth 2.0 액세스 토큰으로 바꿉니다.
    • LIVE_CHAT_IDvideos.list에서 반환된 liveStreamingDetails.activeLiveChatId 값으로 바꿉니다.
    python3 stream_list_demo.py API_KEY_OR_OAUTH_TOKEN LIVE_CHAT_ID
    

stream_list_demo.py

Python 바이너리는 API 키 또는 OAuth 2.0 토큰과 live_chat_id을 문자열 인수로 허용합니다. 바이너리를 실행하기 전에 인증 옵션 중 하나를 주석 해제해야 합니다.

"""Demo of streaming list."""

import sys
import grpc
import stream_list_pb2
import stream_list_pb2_grpc


def main() -> None:
  if len(sys.argv) > 2:
    raise app.UsageError("Too many command-line arguments.")

  creds = grpc.ssl_channel_credentials()
  with grpc.secure_channel(
      "dns:///youtube.googleapis.com:443", creds
  ) as channel:
    stub = stream_list_pb2_grpc.V3DataLiveChatMessageServiceStub(channel)
    # Uncomment one of the following authentication options:
    #
    # Using an API key
    #metadata = (("x-goog-api-key", sys.argv[1]),)
    # Using an OAuth 2.0 access token
    # metadata = (("authorization", "Bearer " + sys.argv[1]),)
    next_page_token = None
    while True:
      request = stream_list_pb2.LiveChatMessageListRequest(
          part=["snippet"],
          live_chat_id=sys.argv[2],
          max_results=20,
          page_token=next_page_token,
      )
      for response in stub.StreamList(request, metadata=metadata):
        print(response)
        next_page_token = response.next_page_token
        if not next_page_token:
          break


if __name__ == "__main__":
  main()

stream_list.proto

이 프로토콜 버퍼는 스트리밍 클라이언트에서 사용하는 서비스 인터페이스를 정의합니다.

syntax = "proto2";

package youtube.api.v3;

service V3DataLiveChatMessageService {
  // Allows a user to load live chat through a server-streamed RPC.
  rpc StreamList(LiveChatMessageListRequest)
      returns (stream LiveChatMessageListResponse) {}
}

message LiveChatMessageListRequest {
  // The ID of the live chat for which comments should be returned.
  optional string live_chat_id = 1;

  // Specifies the localization language in which the system messages
  // should be returned.
  optional string hl = 2;

  // Specifies the size of the profile image that should be
  // returned for each user.
  optional uint32 profile_image_size = 3;

  // The <code><strong>maxResults</strong></code> parameter specifies the
  // maximum number of items that should be returned in the result set.
  // Not used in the streaming RPC.
  optional uint32 max_results = 98;

  // The <code><strong>pageToken</strong></code> parameter identifies a specific
  // page in the result set that should be returned. In an API response, the
  // <code>nextPageToken</code> property identify other pages that could be
  // retrieved.
  optional string page_token = 99;

  // The <code><strong>part</strong></code> parameter specifies the
  // <code>liveChatComment</code> resource parts that the API response will
  // include. Supported values are <code>id</code>, <code>snippet</code>, and
  // <code>authorDetails</code>.
  repeated string part = 100;
}

message LiveChatMessageListResponse {
  // Identifies what kind of resource this is. Value: the fixed string
  // <code>"youtube#liveChatMessageListResponse"</code>.
  optional string kind = 200;

  // Etag of this resource.
  optional string etag = 201;

  // The date and time when the underlying stream went offline. The value is
  // specified in <a href="//www.w3.org/TR/NOTE-datetime">ISO 8601</a>
  // format.
  optional string offline_at = 2;

  // General pagination information.
  optional PageInfo page_info = 1004;

  optional string next_page_token = 100602;

  repeated LiveChatMessage items = 1007;

  // Set when there is an active poll.
  optional LiveChatMessage active_poll_item = 1008;
}

message LiveChatMessage {
  // Identifies what kind of resource this is. Value: the fixed string
  // <code>"youtube#liveChatMessage"</code>.
  optional string kind = 200;

  // Etag of this resource.
  optional string etag = 201;

  // The ID that YouTube assigns to uniquely identify the message.
  optional string id = 101;

  // The <code>snippet</code> object contains basic details about the message.
  optional LiveChatMessageSnippet snippet = 2;

  // The <code>authorDetails</code> object contains basic details about the
  // user that posted this message.
  optional LiveChatMessageAuthorDetails author_details = 3;
}

message LiveChatMessageAuthorDetails {
  // The YouTube channel ID.
  optional string channel_id = 10101;
  // The channel's URL.
  optional string channel_url = 102;
  // The channel's display name.
  optional string display_name = 103;
  // The channels's avatar URL.
  optional string profile_image_url = 104;
  // Whether the author's identity has been verified by YouTube.
  optional bool is_verified = 4;
  // Whether the author is the owner of the live chat.
  optional bool is_chat_owner = 5;
  // Whether the author is a sponsor of the live chat.
  optional bool is_chat_sponsor = 6;
  // Whether the author is a moderator of the live chat.
  optional bool is_chat_moderator = 7;
}

message LiveChatMessageSnippet {
  message TypeWrapper {
    enum Type {
      INVALID_TYPE = 0;
      TEXT_MESSAGE_EVENT = 1;
      TOMBSTONE = 2;
      FAN_FUNDING_EVENT = 3;
      CHAT_ENDED_EVENT = 4;
      SPONSOR_ONLY_MODE_STARTED_EVENT = 5;
      SPONSOR_ONLY_MODE_ENDED_EVENT = 6;
      NEW_SPONSOR_EVENT = 7;
      MEMBER_MILESTONE_CHAT_EVENT = 17;
      MEMBERSHIP_GIFTING_EVENT = 18;
      GIFT_MEMBERSHIP_RECEIVED_EVENT = 19;
      MESSAGE_DELETED_EVENT = 8;
      MESSAGE_RETRACTED_EVENT = 9;
      USER_BANNED_EVENT = 10;
      SUPER_CHAT_EVENT = 15;
      SUPER_STICKER_EVENT = 16;
      POLL_EVENT = 20;
    }
  }
  // The type of message, this will always be present, it determines the
  // contents of the message as well as which fields will be present.
  optional TypeWrapper.Type type = 1;

  optional string live_chat_id = 201;

  // The ID of the user that authored this message, this field is not always
  // filled.
  // textMessageEvent - the user that wrote the message
  // fanFundingEvent - the user that funded the broadcast
  // newSponsorEvent - the user that just became a sponsor
  // memberMilestoneChatEvent - the member that sent the message
  // membershipGiftingEvent - the user that made the purchase
  // giftMembershipReceivedEvent - the user that received the gift membership
  // messageDeletedEvent - the moderator that took the action
  // messageRetractedEvent - the author that retracted their message
  // userBannedEvent - the moderator that took the action
  // superChatEvent - the user that made the purchase
  // superStickerEvent - the user that made the purchase
  // pollEvent - the user that created the poll
  optional string author_channel_id = 301;

  // The date and time when the message was orignally published. The value is
  // specified in <a href="//www.w3.org/TR/NOTE-datetime">ISO 8601</a>
  // format.
  optional string published_at = 4;

  // Whether the message has display content that should be displayed to users.
  optional bool has_display_content = 17;

  // Contains a string that can be displayed to the user.
  // If this field is not present the message is silent, at the moment only
  // messages of type TOMBSTONE and CHAT_ENDED_EVENT are silent.
  optional string display_message = 16;

  oneof displayed_content {
    // Details about the text message, this is only set if the type is
    // 'textMessageEvent'.
    LiveChatTextMessageDetails text_message_details = 19;
    LiveChatMessageDeletedDetails message_deleted_details = 20;
    LiveChatMessageRetractedDetails message_retracted_details = 21;
    LiveChatUserBannedMessageDetails user_banned_details = 22;
    // Details about the Super Chat event, this is only set if the type is
    // 'superChatEvent'.
    LiveChatSuperChatDetails super_chat_details = 27;
    // Details about the Super Sticker event, this is only set if the type is
    // 'superStickerEvent'.
    LiveChatSuperStickerDetails super_sticker_details = 28;
    // Details about the New Member Announcement event, this is only set if
    // the type is 'newSponsorEvent'. Note that "member" is the new term for
    // "sponsor".
    LiveChatNewSponsorDetails new_sponsor_details = 29;
    // Details about the Member Milestone Chat event, this is only set if
    // the type is 'memberMilestoneChatEvent'.
    LiveChatMemberMilestoneChatDetails member_milestone_chat_details = 30;
    // Details about the Membership Gifting event, this is only set if the type
    // is 'membershipGiftingEvent'.
    LiveChatMembershipGiftingDetails membership_gifting_details = 31;
    // Details about the Gift Membership Received event, this is only set if the
    // type is 'giftMembershipReceivedEvent'.
    LiveChatGiftMembershipReceivedDetails gift_membership_received_details = 32;
    // Details about the poll event, this is only set if the type is
    // 'pollEvent'.
    LiveChatPollDetails poll_details = 33;
  }
}

message LiveChatTextMessageDetails {
  // The user's message.
  optional string message_text = 1;
}

message LiveChatMessageDeletedDetails {
  optional string deleted_message_id = 101;
}

message LiveChatMessageRetractedDetails {
  optional string retracted_message_id = 201;
}

message LiveChatUserBannedMessageDetails {
  message BanTypeWrapper {
    enum BanType {
      PERMANENT = 1;
      TEMPORARY = 2;
    }
  }
  // The details of the user that was banned.
  optional ChannelProfileDetails banned_user_details = 1;

  // The type of ban.
  optional BanTypeWrapper.BanType ban_type = 2;

  // The duration of the ban. This property is only present if the
  // <code>banType</code> is <code>temporary</code>.
  optional uint64 ban_duration_seconds = 4;
}

message LiveChatSuperChatDetails {
  // The amount purchased by the user, in micros (1,750,000 micros = 1.75).
  optional uint64 amount_micros = 1;
  // The currency in which the purchase was made.
  optional string currency = 2;
  // A rendered string that displays the fund amount and currency to the user.
  optional string amount_display_string = 3;
  // The comment added by the user to this Super Chat event.
  optional string user_comment = 4;
  // The tier in which the amount belongs. Lower amounts belong to lower
  // tiers. The lowest tier is <code>1</code>.
  optional uint32 tier = 5;
}

message LiveChatSuperStickerDetails {
  // The amount purchased by the user, in micros (1,750,000 micros = 1.75).
  optional uint64 amount_micros = 1;
  // The currency in which the purchase was made.
  optional string currency = 2;
  // A rendered string that displays the fund amount and currency to the user.
  optional string amount_display_string = 3;
  // The tier in which the amount belongs. Lower amounts belong to lower
  // tiers. The lowest tier is <code>1</code>.
  optional uint32 tier = 4;
  // Information about the Super Sticker.
  optional SuperStickerMetadata super_sticker_metadata = 5;
}

message LiveChatFanFundingEventDetails {
  // The amount of the fund.
  optional uint64 amount_micros = 1;

  // The currency in which the fund was made.
  optional string currency = 2;

  // A rendered string that displays the fund amount and currency to the user.
  optional string amount_display_string = 3;

  // The comment added by the user to this fan funding event.
  optional string user_comment = 4;
}

message LiveChatNewSponsorDetails {
  // The name of the Level that the viewer just had joined. The Level names
  // are defined by the YouTube channel offering the Membership.
  //
  // In some situations this field isn't filled.
  optional string member_level_name = 1;

  // If the viewer just had upgraded from a lower level. For viewers that
  // were not members at the time of purchase, this field is false.
  optional bool is_upgrade = 2;
}

message LiveChatMemberMilestoneChatDetails {
  // The name of the Level at which the viever is a member. The Level names
  // are defined by the YouTube channel offering the Membership.
  //
  // In some situations this field isn't filled.
  optional string member_level_name = 1;

  // The total amount of months (rounded up) the viewer has been a member
  // that granted them this Member Milestone Chat. This is the same
  // number of months as is being displayed to YouTube users.
  optional uint32 member_month = 2;

  // The comment added by the member to this Member Milestone Chat.
  //
  // This field is empty for messages without a comment from the member.
  optional string user_comment = 3;
}

message LiveChatMembershipGiftingDetails {
  // The number of gift memberships purchased by the user.
  optional int32 gift_memberships_count = 1;

  // The name of the level of the gift memberships purchased by the user. The
  // Level names are defined by the YouTube channel offering the Membership.
  //
  // In some situations this field isn't filled.
  optional string gift_memberships_level_name = 2;
}

message LiveChatGiftMembershipReceivedDetails {
  // The name of the Level at which the viewer is a member. This matches the
  // `snippet.membershipGiftingDetails.giftMembershipsLevelName` of the
  // associated membership gifting message. The Level names are defined by the
  // YouTube channel offering the Membership.
  //
  // In some situations this field isn't filled.
  optional string member_level_name = 1;

  // The ID of the user that made the membership gifting purchase. This matches
  // the `snippet.authorChannelId` of the associated membership gifting message.
  optional string gifter_channel_id = 2;

  // The ID of the membership gifting message that is related to this gift
  // membership. This ID will always refer to a message whose type is
  // 'membershipGiftingEvent'.
  optional string associated_membership_gifting_message_id = 3;
}

message LiveChatPollDetails {
  message PollMetadata {
    message PollOption {
      optional string option_text = 1;
      optional int64 tally = 2;
    }
    optional string question_text = 1;
    // The options will be returned in the order that is displayed in 1P
    repeated PollOption options = 2;
  }

  // Current point in the polls lifecycle.
  message PollStatusWrapper {
    enum PollStatus {
      UNKNOWN = 0;
      ACTIVE = 1;
      CLOSED = 2;
    }
  }

  optional PollMetadata metadata = 1;
  optional PollStatusWrapper.PollStatus status = 2;
}

message SuperChatEventSnippet {
  // Channel ID where the event occurred.
  optional string channel_id = 101;

  // Details about the supporter.
  optional ChannelProfileDetails supporter_details = 2;

  // The text contents of the comment left by the user.
  optional string comment_text = 3;

  // The date and time when the event occurred. The value is
  // specified in <a href="//www.w3.org/TR/NOTE-datetime">ISO 8601</a>
  // format.
  optional string created_at = 4;

  // The purchase amount, in micros of the purchase currency.  For example, 1 is
  // represented as 1000000.
  optional uint64 amount_micros = 5;

  // The currency in which the purchase was made.  ISO 4217.
  optional string currency = 6;

  // A rendered string that displays the purchase amount and currency
  // (e.g., "$1.00").  The string is rendered for the given language.
  optional string display_string = 7;

  // The tier for the paid message, which is based on the amount of money spent
  // to purchase the message.
  optional uint32 message_type = 8;

  // True if this event is a Super Sticker event.
  optional bool is_super_sticker_event = 11;

  // If this event is a Super Sticker event, this field will contain metadata
  // about the Super Sticker.
  optional SuperStickerMetadata super_sticker_metadata = 12;
}

message SuperStickerMetadata {
  // Unique identifier of the Super Sticker. This is a shorter form of the
  // alt_text that includes pack name and a recognizable characteristic of the
  // sticker.
  optional string sticker_id = 1;

  // Internationalized alt text that describes the sticker image and any
  // animation associated with it.
  optional string alt_text = 2;

  // Specifies the localization language in which the alt text is returned.
  optional string alt_text_language = 3;
}

message ChannelProfileDetails {
  // The YouTube channel ID.
  optional string channel_id = 101;

  // The channel's URL.
  optional string channel_url = 2;

  // The channel's display name.
  optional string display_name = 3;

  // The channels's avatar URL.
  optional string profile_image_url = 4;
}

// Paging details for lists of resources, including total number of items
// available and number of resources returned in a single page.
message PageInfo {
  // The total number of results in the result set.
  optional int32 total_results = 1;

  // The number of results included in the API response.
  optional int32 results_per_page = 2;
}

부하가 높은 스트리밍을 위한 연결 관리

스트리밍 API를 사용할 때, 특히 동시 스트림이 많은 시나리오 (예: 여러 라이브 채팅 모니터링)에서는 gRPC가 연결을 관리하는 방식을 이해하는 것이 중요합니다.

gRPC 스트림은 HTTP/2 연결을 통해 실행됩니다. HTTP/2 프로토콜을 사용하면 서버가 연결당 최대 동시 스트림 수 (SETTINGS_MAX_CONCURRENT_STREAMS)에 제한을 지정할 수 있습니다. 단일 gRPC 채널에서 너무 많은 스트림을 열려고 하면 새 스트림 요청이 대기열에 추가되거나 거부되어 지연 시간이 늘어나거나 오류가 발생할 수 있습니다. 기본 한도와 동작은 언어가 다른 gRPC 클라이언트 라이브러리마다 다를 수 있습니다.

권장사항: gRPC 채널 풀링 구현

단일 연결의 스트림 제한과 관련된 잠재적인 병목 현상을 방지하려면 동시 스트림을 많이 관리할 것으로 예상되는 경우 gRPC 채널 풀링을 구현하는 것이 좋습니다. 모든 스트림에 단일 gRPC 클라이언트/채널 인스턴스를 사용하는 대신 클라이언트/채널 인스턴스 풀을 만들어 관리합니다. 이 풀에 스트림을 분산하면 기본 HTTP/2 연결 수가 증가하므로 애플리케이션에서 처리할 수 있는 동시 스트림의 총 유효 한도가 높아집니다.

최적의 풀 크기와 관리 전략은 애플리케이션의 부하, 특정 gRPC 클라이언트 라이브러리 구현, 서버의 동작에 따라 다를 수 있습니다. 개발자는 지연 시간과 오류 발생률을 모니터링하여 풀 설정을 조정해야 합니다.

추가 정보:

gRPC에 관한 일반적인 정보는 grpc.io의 공식 문서를 참고하세요. 스트림 동시성 관리 및 채널 풀링과 같은 잠재적인 클라이언트 측 솔루션의 근본적인 문제는 gRPC 커뮤니티 내에서 알려진 논의 영역입니다. 향후 gRPC에서 이 문제를 해결하는 방법에 관한 기술적 맥락은 GitHub의 grpc/grpc#21386과 같은 토론을 참고하세요.