前回 P2PNAT サンプルの初期化部分を中心にみていきました。
今回は以下の EOS のドキュメントを追いながらコードも見ていきたいと思います。
NAT P2P インターフェース | Epic Online Services ドキュメンテーション
接続をリクエストする
ローカル ユーザーが EOS_P2P_SendPacket 関数などでリモート ユーザーに情報を送信する場合、P2P インターフェースはこの 2 人のユーザー間の接続開始リクエストを自動で行い、その際に EOS_P2P_SocketId はこの接続の識別子として機能します。情報を送信するユーザーは SocketId に対して自分のリクエストを自動で承認しますが、情報を受信するユーザーはそれを承認する必要があり、通常は受信接続リクエストをリッスンして EOS_P2P_AcceptConnection 関数を使用します。
EOS_P2P_SendPacket
パケットの送信は、サンプルでは Chat メッセージの送信と同時に行われます。以下は、サンプルの Chat ウィンドウである FP2PNATDialog
において、Chat の文字列を送信 (Enter を押した) 場合のイベントハンドラです。
FP2PNAT::SendMessage
を呼び出しているのが分かります。 CurrentChat
は前回の最後で設定している部分を見ましたが、Chat 相手の FProductUserId
です。これは実態としては EOS_ProductUserId
なのですが、ドキュメントにも以下の記述があります。
通常、有効な
EOS_ProductUserId
は両方のユーザーに必要で、データの送信で自分が誰かを示し、またデータの送信先のユーザーを指定します。
void FP2PNATDialog::OnSendMessage(const std::wstring& Value) { if (CurrentChat.IsValid()) { FChatWithFriendData& Chat = GetChat(CurrentChat); std::vector<std::wstring> NewLines; SplitMessageIntoLines(Value, NewLines); for (const std::wstring& NextLine : NewLines) { if (!NextLine.empty()) { Chat.ChatLines.push_back(std::make_pair(true, NextLine)); } } bChatViewDirty = true; FGame::Get().GetP2PNAT()->SendMessage(CurrentChat, Value); } ChatInputField->Clear(); }
続いて呼び出し先の FP2PNAT::SendMessage
です。
void FP2PNAT::SendMessage(FProductUserId FriendId, const std::wstring& Message) { if (!FriendId.IsValid() || Message.empty()) { FDebugLog::LogError(L"EOS P2PNAT SendMessage: bad input data (account id is wrong or message is empty)."); return; } PlayerPtr Player = FPlayerManager::Get().GetPlayer(FPlayerManager::Get().GetCurrentUser()); if (Player == nullptr) { FDebugLog::LogError(L"EOS P2PNAT SendMessage: error user not logged in."); return; } EOS_HP2P P2PHandle = EOS_Platform_GetP2PInterface(FPlatform::GetPlatformHandle()); EOS_P2P_SocketId SocketId; SocketId.ApiVersion = EOS_P2P_SOCKETID_API_LATEST; strncpy_s(SocketId.SocketName, "CHAT", 5); EOS_P2P_SendPacketOptions Options; Options.ApiVersion = EOS_P2P_SENDPACKET_API_LATEST; Options.LocalUserId = Player->GetProductUserID(); Options.RemoteUserId = FriendId; Options.SocketId = &SocketId; Options.bAllowDelayedDelivery = EOS_TRUE; Options.Channel = 0; Options.Reliability = EOS_EPacketReliability::EOS_PR_ReliableOrdered; std::string MessageNarrow = FStringUtils::Narrow(Message); Options.DataLengthBytes = static_cast<uint32_t>(MessageNarrow.size()); Options.Data = MessageNarrow.data(); EOS_EResult Result = EOS_P2P_SendPacket(P2PHandle, &Options); if (Result != EOS_EResult::EOS_Success) { FDebugLog::LogError(L"EOS P2PNAT SendMessage: error while sending data, code: %ls.", FStringUtils::Widen(EOS_EResult_ToString(Result)).c_str()); } }
FPlayerManager
に関しては、ここまでで見ていないので飛ばします。ローカルのユーザーの情報を取得しているのだと思います。
- EOS_P2P_SocketId | Epic Online Services ドキュメンテーション
P2P Socket ID The Socket ID contains an application-defined name for the connection between a local person and another peer. When a remote user receives a connection request from you, they will receive this information.
It can be important to only accept connections with a known socket-name and/or from a known user, to prevent leaking of private information, such as a user's IP address. Using the socket name as a secret key can help prevent such leaks. Shared private data, like a private match's Session ID are good candidates for a socket name.
- EOS_P2P_SendPacketOptions | Epic Online Services ドキュメンテーション
- EOS_P2P_SendPacket | Epic Online Services ドキュメンテーション
Send a packet to a peer at the specified address. If there is already an open connection to this peer, it will be sent immediately. If there is no open connection, an attempt to connect to the peer will be made.
まず EOS_P2P_SocketId
に関してですが、socket name は application-defined
となっているので、このサンプルのように CHAT
のようなシンプルな名前をつけることは通常ないのかもしれません。そもそも設定用のプロパティも公開されていません。この場合だと、SocketId
を保持していないからでしょうか。
続いて EOS_P2P_SendPacketOptions
ですが、LocalUserId
と RemoteUserId
に Chat 相手と自身の FProductUserId
を指定しています。Channel
に関してはわかりませんでした。
接続リクエスト通知を受信する
EOS_P2P_AddNotifyPeerConnectionRequest
は FP2PNAT::OnGameEvent
で UserConnectLoggedIn
をハンドルする際に使われます。
この UserConnectLoggedIn
は UserLoggedIn
とは異なり、Connect インターフェース を利用した場合のイベントですが、サンプルアプリケーションは UserLoggedIn
の場合に必ず Connect インターフェースでのログインも行われています。(ちょっとこの部分理解が浅いので後日調べたいと思います)
void FP2PNAT::OnGameEvent(const FGameEvent& Event) { if (Event.GetType() == EGameEventType::UserConnectLoggedIn) { FProductUserId UserId = Event.GetProductUserId(); OnUserConnectLoggedIn(UserId); } else if (Event.GetType() == EGameEventType::UserLoggedOut) { // 略 } } void FP2PNAT::OnUserConnectLoggedIn(FProductUserId UserId) { RefreshNATType(); //subscribe to connection requests SubscribeToConnectionRequests(); } void FP2PNAT::SubscribeToConnectionRequests() { PlayerPtr Player = FPlayerManager::Get().GetPlayer(FPlayerManager::Get().GetCurrentUser()); if (Player == nullptr || !Player->GetProductUserID().IsValid()) { FDebugLog::LogError(L"EOS P2PNAT SubscribeToConnectionRequests: error user not logged in."); return; } if (ConnectionNotificationId == EOS_INVALID_NOTIFICATIONID) { EOS_HP2P P2PHandle = EOS_Platform_GetP2PInterface(FPlatform::GetPlatformHandle()); EOS_P2P_SocketId SocketId; SocketId.ApiVersion = EOS_P2P_SOCKETID_API_LATEST; strncpy_s(SocketId.SocketName, "CHAT", 5); EOS_P2P_AddNotifyPeerConnectionRequestOptions Options; Options.ApiVersion = EOS_P2P_ADDNOTIFYPEERCONNECTIONREQUEST_API_LATEST; Options.LocalUserId = Player->GetProductUserID(); Options.SocketId = &SocketId; ConnectionNotificationId = EOS_P2P_AddNotifyPeerConnectionRequest(P2PHandle, &Options, nullptr, OnIncomingConnectionRequest); if (ConnectionNotificationId == EOS_INVALID_NOTIFICATIONID) { FDebugLog::LogError(L"EOS P2PNAT SubscribeToConnectionRequests: could not subscribe, bad notification id returned."); } } } void FP2PNAT::UnsubscribeFromConnectionRequests() { EOS_HP2P P2PHandle = EOS_Platform_GetP2PInterface(FPlatform::GetPlatformHandle()); EOS_P2P_RemoveNotifyPeerConnectionRequest(P2PHandle, ConnectionNotificationId); ConnectionNotificationId = EOS_INVALID_NOTIFICATIONID; }
- EOS_P2P_AddNotifyPeerConnectionRequestOptions | Epic Online Services ドキュメンテーション
- EOS_P2P_AddNotifyPeerConnectionRequest | Epic Online Services ドキュメンテーション
A valid notification ID if successfully bound, or
EOS_INVALID_NOTIFICATIONID
otherwise
ConnectionNotificationId
は NAT P2P インターフェースのページに以下がある通り、通知受信の解除にも使われるため保持されています。
以下のパラメータを渡して EOS_P2P_RemoveNotifyPeerConnectionRequest 関数を使用し、ピア接続リクエスト ハンドラを削除できます。
接続を承認する
EOS_P2P_AcceptConnection
は上述の EOS_P2P_AddNotifyPeerConnectionRequest
の呼び出しに渡した callback 関数である OnIncomingConnectionRequest
で使われます。つまり、通知を受信した際に承認するという流れのようです。
void EOS_CALL FP2PNAT::OnIncomingConnectionRequest(const EOS_P2P_OnIncomingConnectionRequestInfo* Data) { if (Data) { std::string SocketName = Data->SocketId->SocketName; if (SocketName != "CHAT") { FDebugLog::LogError(L"EOS P2PNAT OnIncomingConnectionRequest: bad socket id."); return; } PlayerPtr Player = FPlayerManager::Get().GetPlayer(FPlayerManager::Get().GetCurrentUser()); if (Player == nullptr || !Player->GetProductUserID().IsValid()) { FDebugLog::LogError(L"EOS P2PNAT OnIncomingConnectionRequest: error user not logged in."); return; } EOS_HP2P P2PHandle = EOS_Platform_GetP2PInterface(FPlatform::GetPlatformHandle()); EOS_P2P_AcceptConnectionOptions Options; Options.ApiVersion = EOS_P2P_ACCEPTCONNECTION_API_LATEST; Options.LocalUserId = Player->GetProductUserID(); Options.RemoteUserId = Data->RemoteUserId; EOS_P2P_SocketId SocketId; SocketId.ApiVersion = EOS_P2P_SOCKETID_API_LATEST; strncpy_s(SocketId.SocketName, "CHAT", 5); Options.SocketId = &SocketId; EOS_EResult Result = EOS_P2P_AcceptConnection(P2PHandle, &Options); if (Result != EOS_EResult::EOS_Success) { FDebugLog::LogError(L"EOS P2PNAT OnIncomingConnectionRequest: error while accepting connection, code: %ls.", FStringUtils::Widen(EOS_EResult_ToString(Result)).c_str()); } } else { FDebugLog::LogError(L"EOS P2PNAT SubscribeToConnectionRequests: EOS_P2P_OnIncomingConnectionRequestInfo is NULL."); } }
- EOS_P2P_AcceptConnectionOptions | Epic Online Services ドキュメンテーション
- EOS_P2P_AcceptConnection | Epic Online Services ドキュメンテーション
EOS_P2P_SendPacket
と似たインターフェースになっています。
接続を終了する
ここに関しては、サンプルでは利用されていませんでした。
データを受信する
void FP2PNAT::Update() { HandleReceivedMessages(); } void FP2PNAT::HandleReceivedMessages() { PlayerPtr Player = FPlayerManager::Get().GetPlayer(FPlayerManager::Get().GetCurrentUser()); if (Player == nullptr || !Player->GetProductUserID().IsValid()) return; EOS_HP2P P2PHandle = EOS_Platform_GetP2PInterface(FPlatform::GetPlatformHandle()); EOS_P2P_ReceivePacketOptions Options; Options.ApiVersion = EOS_P2P_RECEIVEPACKET_API_LATEST; Options.LocalUserId = Player->GetProductUserID(); Options.MaxDataSizeBytes = 4096; Options.RequestedChannel = nullptr; //Packet params FProductUserId FriendId; EOS_P2P_SocketId SocketId; SocketId.ApiVersion = EOS_P2P_SOCKETID_API_LATEST; uint8_t Channel = 0; std::vector<char> MessageData; MessageData.resize(Options.MaxDataSizeBytes); uint32_t BytesWritten = 0; EOS_EResult Result = EOS_P2P_ReceivePacket(P2PHandle, &Options, &FriendId.AccountId, &SocketId, &Channel, MessageData.data(), &BytesWritten); if (Result == EOS_EResult::EOS_NotFound) { //no more packets, just end return; } else if (Result == EOS_EResult::EOS_Success) { auto P2PDialog = static_cast<FMenu&>(*FGame::Get().GetMenu()).GetP2PNATDialog(); if (P2PDialog) { std::wstring MessageWide; std::string MessageNarrow(MessageData.data(), BytesWritten); MessageWide = FStringUtils::Widen(MessageNarrow); P2PDialog->OnMessageReceived(MessageWide, FriendId); } } else { FDebugLog::LogError(L"EOS P2PNAT HandleReceivedMessages: error while reading data, code: %ls.", FStringUtils::Widen(EOS_EResult_ToString(Result)).c_str()); } }
- EOS_P2P_ReceivePacketOptions | Epic Online Services ドキュメンテーション
- EOS_P2P_ReceivePacket | Epic Online Services ドキュメンテーション
引き続き Channel
の用途は分かっていないのですが、Chat に利用する SocketId
と、相手 Friend の id を使って受信をしています。
ただ、ここまでは ProductUserId
であったのが、ここでは EOS_EpicAccountId
に相当する Id が使われているようです。(ドキュメントでは EOS_ProductUserId
になっているので、少し謎です)
その後 P2PDialog
の OnMessageReceived
に受信したデータを渡しています。
まとめ
今回は公式の NAT P2P インターフェースのドキュメントをなぞる形でサンプルコードを追っていきました。
今回以下の疑問が出ました。
- EOS Connect インターフェースでのログインはどういうことか
EOS_EpicAccountId
とEOS_ProductUserId
はどのように違うのか
おそらくこの 2 つは関連すると考えられるので、引き続きここを理解できればと思います。