diff --git a/build/pomdog/CMakeLists.txt b/build/pomdog/CMakeLists.txt index e49ea2b6a..6cac764de 100644 --- a/build/pomdog/CMakeLists.txt +++ b/build/pomdog/CMakeLists.txt @@ -206,6 +206,7 @@ set(POMDOG_SOURCES_CORE ${POMDOG_DIR}/include/Pomdog/Network/ArrayView.hpp ${POMDOG_DIR}/include/Pomdog/Network/IOService.hpp ${POMDOG_DIR}/include/Pomdog/Network/TCPStream.hpp + ${POMDOG_DIR}/include/Pomdog/Network/UDPStream.hpp ${POMDOG_DIR}/include/Pomdog/Network/detail/ForwardDeclarations.hpp ${POMDOG_DIR}/include/Pomdog/Reactive/Observable.hpp ${POMDOG_DIR}/include/Pomdog/Reactive/ObservableBase.hpp @@ -334,6 +335,7 @@ set(POMDOG_SOURCES_CORE ${POMDOG_DIR}/src/Network/IOService.cpp ${POMDOG_DIR}/src/Network/SocketProtocol.hpp ${POMDOG_DIR}/src/Network/TCPStream.cpp + ${POMDOG_DIR}/src/Network/UDPStream.cpp ${POMDOG_DIR}/src/RenderSystem/BufferBindMode.hpp ${POMDOG_DIR}/src/RenderSystem/BufferHelper.cpp ${POMDOG_DIR}/src/RenderSystem/BufferHelper.hpp @@ -686,6 +688,8 @@ set(POMDOG_SOURCES_NETWORK_POSIX ${POMDOG_DIR}/src/Network.POSIX/SocketHelperPOSIX.hpp ${POMDOG_DIR}/src/Network.POSIX/TCPStreamPOSIX.cpp ${POMDOG_DIR}/src/Network.POSIX/TCPStreamPOSIX.hpp + ${POMDOG_DIR}/src/Network.POSIX/UDPStreamPOSIX.cpp + ${POMDOG_DIR}/src/Network.POSIX/UDPStreamPOSIX.hpp ) set(POMDOG_SOURCES_NETWORK_WIN32 @@ -695,6 +699,8 @@ set(POMDOG_SOURCES_NETWORK_WIN32 ${POMDOG_DIR}/src/Network.Win32/SocketHelperWin32.hpp ${POMDOG_DIR}/src/Network.Win32/TCPStreamWin32.cpp ${POMDOG_DIR}/src/Network.Win32/TCPStreamWin32.hpp + ${POMDOG_DIR}/src/Network.Win32/UDPStreamWin32.cpp + ${POMDOG_DIR}/src/Network.Win32/UDPStreamWin32.hpp ) set(SOURCE_FILES ${POMDOG_SOURCES_CORE} ${POMDOG_SOURCES_EXPERIMENTAL}) diff --git a/include/Pomdog/Network/UDPStream.hpp b/include/Pomdog/Network/UDPStream.hpp new file mode 100644 index 000000000..708db6ab1 --- /dev/null +++ b/include/Pomdog/Network/UDPStream.hpp @@ -0,0 +1,64 @@ +// Copyright (c) 2013-2019 mogemimi. Distributed under the MIT license. + +#pragma once + +#include "Pomdog/Application/Duration.hpp" +#include "Pomdog/Basic/Export.hpp" +#include "Pomdog/Network/detail/ForwardDeclarations.hpp" +#include "Pomdog/Signals/detail/ForwardDeclarations.hpp" +#include "Pomdog/Utility/Errors.hpp" +#include +#include +#include +#include + +namespace Pomdog { + +class POMDOG_EXPORT UDPStream final { +public: + UDPStream(); + explicit UDPStream(IOService* service); + + UDPStream(const UDPStream&) = delete; + UDPStream& operator=(const UDPStream&) = delete; + + UDPStream(UDPStream&& other); + UDPStream& operator=(UDPStream&& other); + + ~UDPStream(); + + /// Opens a UDP connection to a remote host. + static std::tuple> + Connect(IOService* service, const std::string_view& address); + + /// Starts listening for incoming datagrams. + static std::tuple> + Listen(IOService* service, const std::string_view& address); + + /// Closes the connection. + void Disconnect(); + + /// Writes data to the connection. + std::shared_ptr Write(const ArrayView& data); + + /// Writes data to address. + std::shared_ptr + WriteTo(const ArrayView& data, const std::string_view& address); + + /// Sets a callback function that is called when a connection is successfully established. + [[nodiscard]] Connection + OnConnected(std::function&)>&& callback); + + /// Sets a callback function that is called when a data packet is received. + [[nodiscard]] Connection + OnRead(std::function&, const std::shared_ptr&)>&& callback); + + /// Sets a callback function that is called when a data packet is received from the connection. + [[nodiscard]] Connection + OnReadFrom(std::function&, const std::string_view& address, const std::shared_ptr&)>&& callback); + +private: + std::unique_ptr nativeStream; +}; + +} // namespace Pomdog diff --git a/src/Network.POSIX/UDPStreamPOSIX.cpp b/src/Network.POSIX/UDPStreamPOSIX.cpp new file mode 100644 index 000000000..4e829c79c --- /dev/null +++ b/src/Network.POSIX/UDPStreamPOSIX.cpp @@ -0,0 +1,267 @@ +// Copyright (c) 2013-2019 mogemimi. Distributed under the MIT license. + +#include "UDPStreamPOSIX.hpp" +#include "SocketHelperPOSIX.hpp" +#include "../Network/AddressParser.hpp" +#include "../Network/EndPoint.hpp" +#include "../Network/ErrorHelper.hpp" +#include "Pomdog/Network/ArrayView.hpp" +#include "Pomdog/Network/IOService.hpp" +#include "Pomdog/Utility/Assert.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Pomdog::Detail { +namespace { + +constexpr int InvalidSocket = -1; + +bool isSocketValid(int descriptor) noexcept +{ + return descriptor >= 0; +} + +#if defined(POMDOG_PLATFORM_LINUX) +constexpr int flags = MSG_NOSIGNAL; +#else +constexpr int flags = 0; +#endif + +} // namespace + +UDPStreamPOSIX::UDPStreamPOSIX(IOService* serviceIn) + : service(serviceIn) +{ +} + +UDPStreamPOSIX::~UDPStreamPOSIX() +{ + if (blockingThread.joinable()) { + blockingThread.join(); + } + + if (isSocketValid(this->descriptor)) { + ::close(this->descriptor); + this->descriptor = InvalidSocket; + } +} + +std::shared_ptr +UDPStreamPOSIX::Connect(const std::string_view& host, const std::string_view& port, const Duration& connectTimeout) +{ + POMDOG_ASSERT(service != nullptr); + + // NOTE: A std::string_view doesn't provide a conversion to a const char* because it doesn't store a null-terminated string. + const auto hostBuf = std::string{host}; + const auto portBuf = std::string{port}; + + std::thread connectThread([this, hostBuf = std::move(hostBuf), portBuf = std::move(portBuf), connectTimeout = connectTimeout] { + auto[fd, err] = Detail::ConnectSocketPOSIX(hostBuf, portBuf, SocketProtocol::UDP, connectTimeout); + + if (err != nullptr) { + auto wrapped = Errors::Wrap(std::move(err), "couldn't connect to UDP socket on " + hostBuf + ":" + portBuf); + errorConn = service->ScheduleTask([this, err = std::move(wrapped)] { + this->OnConnected(std::move(err)); + this->errorConn.Disconnect(); + }); + return; + } + this->descriptor = fd; + + eventLoopConn = service->ScheduleTask([this] { + this->OnConnected(nullptr); + eventLoopConn.Disconnect(); + eventLoopConn = service->ScheduleTask([this] { this->ReadEventLoop(); }); + }); + }); + + this->blockingThread = std::move(connectThread); + + return nullptr; +} + +std::shared_ptr +UDPStreamPOSIX::Listen(const std::string_view& host, const std::string_view& port) +{ + POMDOG_ASSERT(service != nullptr); + + // NOTE: A std::string_view doesn't provide a conversion to a const char* because it doesn't store a null-terminated string. + const auto hostBuf = std::string{host}; + const auto portBuf = std::string{port}; + + auto[fd, err] = Detail::BindSocketPOSIX(hostBuf, portBuf, SocketProtocol::UDP); + + if (err != nullptr) { + errorConn = service->ScheduleTask([this, err = std::move(err)] { + this->OnConnected(err); + this->errorConn.Disconnect(); + }); + return err; + } + this->descriptor = fd; + + eventLoopConn = service->ScheduleTask([this] { + this->OnConnected(nullptr); + eventLoopConn.Disconnect(); + eventLoopConn = service->ScheduleTask([this] { this->ReadFromEventLoop(); }); + }); + + return nullptr; +} + +void UDPStreamPOSIX::Close() +{ + this->eventLoopConn.Disconnect(); + this->errorConn.Disconnect(); + + if (isSocketValid(this->descriptor)) { + ::close(this->descriptor); + this->descriptor = InvalidSocket; + } +} + +std::shared_ptr +UDPStreamPOSIX::Write(const ArrayView& data) +{ + POMDOG_ASSERT(isSocketValid(descriptor)); + POMDOG_ASSERT(data.GetData() != nullptr); + POMDOG_ASSERT(data.GetSize() > 0); + + auto result = ::send(this->descriptor, data.GetData(), data.GetSize(), flags); + + if (result == -1) { + auto errorCode = Detail::ToErrc(errno); + return Errors::New(errorCode, "send failed with error"); + } + + return nullptr; +} + +std::shared_ptr +UDPStreamPOSIX::WriteTo(const ArrayView& data, const std::string_view& address) +{ + POMDOG_ASSERT(isSocketValid(this->descriptor)); + POMDOG_ASSERT(data.GetData() != nullptr); + POMDOG_ASSERT(data.GetSize() > 0); + + const auto[family, hostView, portView] = Detail::AddressParser::TransformAddress(address); + auto host = std::string{hostView}; + auto port = std::string{portView}; + + struct ::addrinfo hints; + std::memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_protocol = IPPROTO_UDP; + + struct ::addrinfo* addrListRaw = nullptr; + + auto res = ::getaddrinfo(host.data(), port.data(), &hints, &addrListRaw); + if (res != 0) { + return Errors::New("getaddrinfo failed with error " + std::to_string(res)); + } + + auto addrList = std::unique_ptr{addrListRaw, ::freeaddrinfo}; + addrListRaw = nullptr; + + std::optional lastError; + + for (auto info = addrList.get(); info != nullptr; info = info->ai_next) { + auto result = ::sendto( + this->descriptor, data.GetData(), data.GetSize(), flags, + info->ai_addr, static_cast(info->ai_addrlen)); + + if (result == -1) { + lastError = Detail::ToErrc(errno); + continue; + } + lastError = std::nullopt; + break; + } + + if (lastError != std::nullopt) { + return Errors::New(*lastError, "sendto failed with error"); + } + + return nullptr; +} + +int UDPStreamPOSIX::GetHandle() const noexcept +{ + return descriptor; +} + +void UDPStreamPOSIX::ReadEventLoop() +{ + POMDOG_ASSERT(isSocketValid(descriptor)); + + // NOTE: Read per 1 frame (= 1/60 seconds) for a packet up to 1024 bytes. + std::array buffer; + + const auto readSize = ::recv(this->descriptor, buffer.data(), buffer.size(), flags); + if (readSize == -1) { + const auto errorCode = Detail::ToErrc(errno); + if (errorCode == std::errc::resource_unavailable_try_again || errorCode == std::errc::operation_would_block) { + // NOTE: There is no data to be read yet + return; + } + + this->OnRead({}, Errors::New(errorCode, "read failed with error")); + return; + } + + if (readSize < 0) { + return; + } + + // NOTE: When the peer socket has performed orderly shutdown, + // the read size will be 0 (meaning the "end-of-file"). + POMDOG_ASSERT(readSize >= 0); + auto view = ArrayView{buffer.data(), static_cast(readSize)}; + this->OnRead(std::move(view), nullptr); +} + +void UDPStreamPOSIX::ReadFromEventLoop() +{ + POMDOG_ASSERT(isSocketValid(descriptor)); + + // NOTE: Read per 1 frame (= 1/60 seconds) for a packet up to 1024 bytes. + std::array buffer; + + struct ::sockaddr_storage addrInfo; + socklen_t addrLen = sizeof(addrInfo); + + const auto readSize = ::recvfrom( + this->descriptor, buffer.data(), buffer.size(), flags, + reinterpret_cast(&addrInfo), &addrLen); + + if (readSize == -1) { + const auto errorCode = Detail::ToErrc(errno); + if (errorCode == std::errc::resource_unavailable_try_again || errorCode == std::errc::operation_would_block) { + // NOTE: There is no data to be read yet + return; + } + + this->OnReadFrom({}, "", Errors::New(errorCode, "read failed with error")); + return; + } + + if (readSize < 0) { + return; + } + + // NOTE: When the peer socket has performed orderly shutdown, + // the read size will be 0 (meaning the "end-of-file"). + POMDOG_ASSERT(readSize >= 0); + auto addr = EndPoint::CreateFromAddressStorage(addrInfo); + auto view = ArrayView{buffer.data(), static_cast(readSize)}; + this->OnReadFrom(std::move(view), addr.ToString(), nullptr); +} + +} // namespace Pomdog::Detail diff --git a/src/Network.POSIX/UDPStreamPOSIX.hpp b/src/Network.POSIX/UDPStreamPOSIX.hpp new file mode 100644 index 000000000..6a8acbaa1 --- /dev/null +++ b/src/Network.POSIX/UDPStreamPOSIX.hpp @@ -0,0 +1,76 @@ +// Copyright (c) 2013-2019 mogemimi. Distributed under the MIT license. + +#pragma once + +#include "Pomdog/Application/Duration.hpp" +#include "Pomdog/Network/detail/ForwardDeclarations.hpp" +#include "Pomdog/Signals/Delegate.hpp" +#include "Pomdog/Signals/ScopedConnection.hpp" +#include "Pomdog/Utility/Errors.hpp" +#include +#include +#include +#include +#include + +namespace Pomdog::Detail { + +class UDPStreamPOSIX final { +public: + UDPStreamPOSIX() = delete; + + explicit UDPStreamPOSIX(IOService* service); + + ~UDPStreamPOSIX(); + + UDPStreamPOSIX(const UDPStreamPOSIX&) = delete; + UDPStreamPOSIX& operator=(const UDPStreamPOSIX&) = delete; + + UDPStreamPOSIX(UDPStreamPOSIX&&) = delete; + UDPStreamPOSIX& operator=(UDPStreamPOSIX&&) = delete; + + /// Opens a UDP connection over UDP to a remote host. + [[nodiscard]] std::shared_ptr + Connect(const std::string_view& host, const std::string_view& port, const Duration& timeout); + + /// Starts listening for incoming datagrams. + [[nodiscard]] std::shared_ptr + Listen(const std::string_view& host, const std::string_view& port); + + /// Closes the connection. + void Close(); + + /// Writes data to the connection. + [[nodiscard]] std::shared_ptr + Write(const ArrayView& data); + + /// Writes data to address. + [[nodiscard]] std::shared_ptr + WriteTo(const ArrayView& data, const std::string_view& address); + + /// Returns the native socket handle. + [[nodiscard]] int GetHandle() const noexcept; + + /// Delegate that fires when a connection is successfully established. + Delegate&)> OnConnected; + + /// Delegate that fires when a data packet is received. + Delegate&, const std::shared_ptr&)> OnRead; + + /// Delegate that fires when a data packet is received from the connection. + Delegate& view, const std::string_view& address, const std::shared_ptr&)> OnReadFrom; + +private: + void ReadEventLoop(); + + void ReadFromEventLoop(); + +private: + std::thread blockingThread; + IOService* service = nullptr; + ScopedConnection eventLoopConn; + ScopedConnection errorConn; + int descriptor = -1; +}; + +} // namespace Pomdog::Detail diff --git a/src/Network.Win32/UDPStreamWin32.cpp b/src/Network.Win32/UDPStreamWin32.cpp new file mode 100644 index 000000000..9d747d0a9 --- /dev/null +++ b/src/Network.Win32/UDPStreamWin32.cpp @@ -0,0 +1,248 @@ +// Copyright (c) 2013-2019 mogemimi. Distributed under the MIT license. + +#include "UDPStreamWin32.hpp" +#include "SocketHelperWin32.hpp" +#include "../Network/AddressParser.hpp" +#include "../Network/EndPoint.hpp" +#include "../Network/ErrorHelper.hpp" +#include "Pomdog/Network/ArrayView.hpp" +#include "Pomdog/Network/IOService.hpp" +#include "Pomdog/Utility/Assert.hpp" +#include +#include +#include + +namespace Pomdog::Detail { +namespace { + +bool isSocketValid(::SOCKET descriptor) noexcept +{ + return descriptor != INVALID_SOCKET; +} + +constexpr int flags = 0; + +} // namespace + +UDPStreamWin32::UDPStreamWin32(IOService* serviceIn) + : service(serviceIn) +{ +} + +UDPStreamWin32::~UDPStreamWin32() +{ + if (blockingThread.joinable()) { + blockingThread.join(); + } + + if (isSocketValid(this->descriptor)) { + ::closesocket(this->descriptor); + this->descriptor = INVALID_SOCKET; + } +} + +std::shared_ptr +UDPStreamWin32::Connect(const std::string_view& host, const std::string_view& port, const Duration& connectTimeout) +{ + POMDOG_ASSERT(service != nullptr); + + // NOTE: A std::string_view doesn't provide a conversion to a const char* because it doesn't store a null-terminated string. + const auto hostBuf = std::string{host}; + const auto portBuf = std::string{port}; + + std::thread connectThread([this, hostBuf = std::move(hostBuf), portBuf = std::move(portBuf), connectTimeout = connectTimeout] { + auto[fd, err] = Detail::ConnectSocketWin32(hostBuf, portBuf, SocketProtocol::UDP, connectTimeout); + + if (err != nullptr) { + auto wrapped = Errors::Wrap(std::move(err), "couldn't connect to UDP socket on " + hostBuf + ":" + portBuf); + errorConn = service->ScheduleTask([this, err = std::move(wrapped)] { + this->OnConnected(std::move(err)); + this->errorConn.Disconnect(); + }); + return; + } + this->descriptor = fd; + + eventLoopConn = service->ScheduleTask([this] { + this->OnConnected(nullptr); + eventLoopConn.Disconnect(); + eventLoopConn = service->ScheduleTask([this] { this->ReadEventLoop(); }); + }); + }); + + this->blockingThread = std::move(connectThread); + + return nullptr; +} + +std::shared_ptr +UDPStreamWin32::Listen(const std::string_view& host, const std::string_view& port) +{ + POMDOG_ASSERT(service != nullptr); + + // NOTE: A std::string_view doesn't provide a conversion to a const char* because it doesn't store a null-terminated string. + const auto hostBuf = std::string{host}; + const auto portBuf = std::string{port}; + + auto[fd, err] = Detail::BindSocketWin32(hostBuf, portBuf, SocketProtocol::UDP); + + if (err != nullptr) { + auto wrapped = Errors::Wrap(std::move(err), "couldn't listen to UDP socket on " + hostBuf + ":" + portBuf); + errorConn = service->ScheduleTask([this, err = std::move(wrapped)] { + this->OnConnected(std::move(err)); + this->errorConn.Disconnect(); + }); + return err; + } + this->descriptor = fd; + + eventLoopConn = service->ScheduleTask([this] { + this->OnConnected(nullptr); + eventLoopConn.Disconnect(); + eventLoopConn = service->ScheduleTask([this] { this->ReadFromEventLoop(); }); + }); + + return nullptr; +} + +void UDPStreamWin32::Close() +{ + this->eventLoopConn.Disconnect(); + this->errorConn.Disconnect(); + + if (isSocketValid(this->descriptor)) { + ::closesocket(this->descriptor); + this->descriptor = INVALID_SOCKET; + } +} + +std::shared_ptr +UDPStreamWin32::Write(const ArrayView& data) +{ + POMDOG_ASSERT(isSocketValid(descriptor)); + POMDOG_ASSERT(data.GetData() != nullptr); + POMDOG_ASSERT(data.GetSize() > 0); + + auto result = ::send(this->descriptor, reinterpret_cast(data.GetData()), static_cast(data.GetSize()), flags); + + if (result == SOCKET_ERROR) { + return Errors::New("send failed with error: " + std::to_string(::WSAGetLastError())); + } + + return nullptr; +} + +std::shared_ptr +UDPStreamWin32::WriteTo(const ArrayView& data, const std::string_view& address) +{ + POMDOG_ASSERT(isSocketValid(this->descriptor)); + POMDOG_ASSERT(data.GetData() != nullptr); + POMDOG_ASSERT(data.GetSize() > 0); + + const auto[family, hostView, portView] = Detail::AddressParser::TransformAddress(address); + auto host = std::string{hostView}; + auto port = std::string{portView}; + + struct ::addrinfo hints; + std::memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_protocol = IPPROTO_UDP; + + struct ::addrinfo* addrListRaw = nullptr; + + auto res = ::getaddrinfo(host.data(), port.data(), &hints, &addrListRaw); + if (res != 0) { + return Errors::New("getaddrinfo failed with error " + std::to_string(res)); + } + + auto addrList = std::unique_ptr{addrListRaw, ::freeaddrinfo}; + addrListRaw = nullptr; + + std::optional lastError; + + for (auto info = addrList.get(); info != nullptr; info = info->ai_next) { + auto result = ::sendto( + this->descriptor, reinterpret_cast(data.GetData()), static_cast(data.GetSize()), flags, + info->ai_addr, static_cast(info->ai_addrlen)); + + if (result == SOCKET_ERROR) { + lastError = ::WSAGetLastError(); + continue; + } + lastError = std::nullopt; + break; + } + + if (lastError != std::nullopt) { + return Errors::New("sendto failed with error: " + std::to_string(*lastError)); + } + + return nullptr; +} + +::SOCKET UDPStreamWin32::GetHandle() const noexcept +{ + return descriptor; +} + +void UDPStreamWin32::ReadEventLoop() +{ + POMDOG_ASSERT(isSocketValid(descriptor)); + + // NOTE: Read per 1 frame (= 1/60 seconds) for a packet up to 1024 bytes. + std::array buffer; + + const auto readSize = ::recv(this->descriptor, reinterpret_cast(buffer.data()), static_cast(buffer.size()), flags); + if (readSize < 0) { + const auto errorCode = ::WSAGetLastError(); + if (errorCode == WSAEWOULDBLOCK) { + // NOTE: There is no data to be read yet + return; + } + + this->OnRead({}, Errors::New("read failed with error: " + std::to_string(errorCode))); + return; + } + + // NOTE: When the peer socket has performed orderly shutdown, + // the read size will be 0 (meaning the "end-of-file"). + POMDOG_ASSERT(readSize >= 0); + auto view = ArrayView{buffer.data(), static_cast(readSize)}; + this->OnRead(std::move(view), nullptr); +} + +void UDPStreamWin32::ReadFromEventLoop() +{ + POMDOG_ASSERT(isSocketValid(descriptor)); + + // NOTE: Read per 1 frame (= 1/60 seconds) for a packet up to 1024 bytes. + std::array buffer; + + struct ::sockaddr_storage addrInfo; + socklen_t addrLen = sizeof(addrInfo); + + const auto readSize = ::recvfrom( + this->descriptor, reinterpret_cast(buffer.data()), static_cast(buffer.size()), flags, + reinterpret_cast(&addrInfo), &addrLen); + + if (readSize < 0) { + const auto errorCode = ::WSAGetLastError(); + if (errorCode == WSAEWOULDBLOCK) { + // NOTE: There is no data to be read yet + return; + } + + this->OnReadFrom({}, "", Errors::New("read failed with error: " + std::to_string(errorCode))); + return; + } + + // NOTE: When the peer socket has performed orderly shutdown, + // the read size will be 0 (meaning the "end-of-file"). + POMDOG_ASSERT(readSize >= 0); + auto addr = EndPoint::CreateFromAddressStorage(addrInfo); + auto view = ArrayView{buffer.data(), static_cast(readSize)}; + this->OnReadFrom(std::move(view), addr.ToString(), nullptr); +} + +} // namespace Pomdog::Detail diff --git a/src/Network.Win32/UDPStreamWin32.hpp b/src/Network.Win32/UDPStreamWin32.hpp new file mode 100644 index 000000000..2e703bd54 --- /dev/null +++ b/src/Network.Win32/UDPStreamWin32.hpp @@ -0,0 +1,77 @@ +// Copyright (c) 2013-2019 mogemimi. Distributed under the MIT license. + +#pragma once + +#include "Pomdog/Application/Duration.hpp" +#include "Pomdog/Network/detail/ForwardDeclarations.hpp" +#include "Pomdog/Signals/Delegate.hpp" +#include "Pomdog/Signals/ScopedConnection.hpp" +#include "Pomdog/Utility/Errors.hpp" +#include +#include +#include +#include +#include +#include + +namespace Pomdog::Detail { + +class UDPStreamWin32 final { +public: + UDPStreamWin32() = delete; + + explicit UDPStreamWin32(IOService* service); + + ~UDPStreamWin32(); + + UDPStreamWin32(const UDPStreamWin32&) = delete; + UDPStreamWin32& operator=(const UDPStreamWin32&) = delete; + + UDPStreamWin32(UDPStreamWin32&&) = delete; + UDPStreamWin32& operator=(UDPStreamWin32&&) = delete; + + /// Opens a UDP connection over UDP to a remote host. + [[nodiscard]] std::shared_ptr + Connect(const std::string_view& host, const std::string_view& port, const Duration& timeout); + + /// Starts listening for incoming datagrams. + [[nodiscard]] std::shared_ptr + Listen(const std::string_view& host, const std::string_view& port); + + /// Closes the connection. + void Close(); + + /// Writes data to the connection. + [[nodiscard]] std::shared_ptr + Write(const ArrayView& data); + + /// Writes data to address. + [[nodiscard]] std::shared_ptr + WriteTo(const ArrayView& data, const std::string_view& address); + + /// Returns the native socket handle. + [[nodiscard]] ::SOCKET GetHandle() const noexcept; + + /// Delegate that fires when a connection is successfully established. + Delegate&)> OnConnected; + + /// Delegate that fires when a data packet is received. + Delegate&, const std::shared_ptr&)> OnRead; + + /// Delegate that fires when a data packet is received from the connection. + Delegate& view, const std::string_view& address, const std::shared_ptr&)> OnReadFrom; + +private: + void ReadEventLoop(); + + void ReadFromEventLoop(); + +private: + std::thread blockingThread; + IOService* service = nullptr; + ScopedConnection eventLoopConn; + ScopedConnection errorConn; + ::SOCKET descriptor = INVALID_SOCKET; +}; + +} // namespace Pomdog::Detail diff --git a/src/Network/UDPStream.cpp b/src/Network/UDPStream.cpp new file mode 100644 index 000000000..316d78edd --- /dev/null +++ b/src/Network/UDPStream.cpp @@ -0,0 +1,101 @@ +// Copyright (c) 2013-2019 mogemimi. Distributed under the MIT license. + +#include "Pomdog/Network/UDPStream.hpp" +#if defined(POMDOG_PLATFORM_MACOSX) \ + || defined(POMDOG_PLATFORM_APPLE_IOS) \ + || defined(POMDOG_PLATFORM_LINUX) +#include "../Network.POSIX/UDPStreamPOSIX.hpp" +#elif defined(POMDOG_PLATFORM_WIN32) || defined(POMDOG_PLATFORM_XBOX_ONE) +#include "../Network.Win32/UDPStreamWin32.hpp" +#endif +#include "AddressParser.hpp" +#include "Pomdog/Utility/Assert.hpp" +#include + +namespace Pomdog { + +UDPStream::UDPStream() = default; + +UDPStream::UDPStream(IOService* service) + : nativeStream(std::make_unique(service)) +{ +} + +UDPStream::~UDPStream() +{ +} + +UDPStream::UDPStream(UDPStream&& other) = default; +UDPStream& UDPStream::operator=(UDPStream&& other) = default; + +std::tuple> +UDPStream::Connect(IOService* service, const std::string_view& address) +{ + POMDOG_ASSERT(service != nullptr); + + UDPStream stream{service}; + POMDOG_ASSERT(stream.nativeStream != nullptr); + + const auto[family, host, port] = Detail::AddressParser::TransformAddress(address); + + if (auto err = stream.nativeStream->Connect(host, port, std::chrono::seconds{5}); err != nullptr) { + return std::make_tuple(std::move(stream), std::move(err)); + } + return std::make_tuple(std::move(stream), nullptr); +} + +std::tuple> +UDPStream::Listen(IOService* service, const std::string_view& address) +{ + POMDOG_ASSERT(service != nullptr); + + UDPStream stream{service}; + POMDOG_ASSERT(stream.nativeStream != nullptr); + + const auto[family, host, port] = Detail::AddressParser::TransformAddress(address); + + if (auto err = stream.nativeStream->Listen(host, port); err != nullptr) { + return std::make_tuple(std::move(stream), std::move(err)); + } + return std::make_tuple(std::move(stream), nullptr); +} + +void UDPStream::Disconnect() +{ + POMDOG_ASSERT(nativeStream != nullptr); + nativeStream->Close(); +} + +std::shared_ptr +UDPStream::Write(const ArrayView& data) +{ + POMDOG_ASSERT(nativeStream != nullptr); + return nativeStream->Write(data); +} + +std::shared_ptr +UDPStream::WriteTo(const ArrayView& data, const std::string_view& address) +{ + POMDOG_ASSERT(nativeStream != nullptr); + return nativeStream->WriteTo(data, address); +} + +Connection UDPStream::OnConnected(std::function&)>&& callback) +{ + POMDOG_ASSERT(nativeStream != nullptr); + return nativeStream->OnConnected.Connect(std::move(callback)); +} + +Connection UDPStream::OnRead(std::function&, const std::shared_ptr&)>&& callback) +{ + POMDOG_ASSERT(nativeStream != nullptr); + return nativeStream->OnRead.Connect(std::move(callback)); +} + +Connection UDPStream::OnReadFrom(std::function&, const std::string_view& address, const std::shared_ptr&)>&& callback) +{ + POMDOG_ASSERT(nativeStream != nullptr); + return nativeStream->OnReadFrom.Connect(std::move(callback)); +} + +} // namespace Pomdog diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 76fc26196..6e4acda0f 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -65,6 +65,7 @@ add_executable(PomdogTest ${POMDOG_TEST_DIR}/Network/ArrayViewTest.cpp ${POMDOG_TEST_DIR}/Network/Executor.hpp ${POMDOG_TEST_DIR}/Network/TCPStreamTest.cpp + ${POMDOG_TEST_DIR}/Network/UDPStreamTest.cpp ${POMDOG_TEST_DIR}/Signals/ConnectionTest.cpp ${POMDOG_TEST_DIR}/Signals/ConnectionListTest.cpp ${POMDOG_TEST_DIR}/Signals/DelegateTest.cpp diff --git a/test/Network/UDPStreamTest.cpp b/test/Network/UDPStreamTest.cpp new file mode 100644 index 000000000..7f76a3d9a --- /dev/null +++ b/test/Network/UDPStreamTest.cpp @@ -0,0 +1,114 @@ +// Copyright (c) 2013-2019 mogemimi. Distributed under the MIT license. + +#include "Executor.hpp" +#include "Pomdog/Application/GameClock.hpp" +#include "Pomdog/Network/ArrayView.hpp" +#include "Pomdog/Network/IOService.hpp" +#include "Pomdog/Network/UDPStream.hpp" +#include "Pomdog/Signals/ConnectionList.hpp" +#include "Pomdog/Utility/Errors.hpp" +#include "Pomdog/Utility/StringHelper.hpp" +#include "catch.hpp" +#include +#include +#include +#include + +using namespace Pomdog; + +TEST_CASE("Ping Pong Server using UDP Connection", "[Network]") +{ + Executor executor; + ConnectionList conn; + + std::vector serverLogs; + std::vector clientLogs; + + auto[serverStream, serverErr] = UDPStream::Listen(executor.GetService(), "localhost:30088"); + REQUIRE(serverErr == nullptr); + auto server = std::move(serverStream); + + conn += server.OnConnected([&](const std::shared_ptr& err) { + if (err != nullptr) { + WARN("Unable to listen client"); + serverLogs.push_back(err->ToString()); + executor.ExitLoop(); + return; + } + + serverLogs.push_back("server connected"); + }); + conn += server.OnReadFrom([&](const ArrayView& view, const std::string_view& address, const std::shared_ptr& err) { + if (err != nullptr) { + WARN("Unable to read message"); + serverLogs.push_back(err->ToString()); + executor.ExitLoop(); + return; + } + + serverLogs.push_back("server read"); + auto text = std::string_view{reinterpret_cast(view.GetData()), view.GetSize()}; + + if (text != "ping") { + executor.ExitLoop(); + } + REQUIRE(text == "ping"); + REQUIRE(!address.empty()); + + std::string_view s = "pong"; + auto buf = ArrayView{s.data(), s.size()}.ViewAs(); + server.WriteTo(buf, address); + + server.Disconnect(); + serverLogs.push_back("server disconnected"); + }); + + auto[clientStream, clientErr] = UDPStream::Connect(executor.GetService(), "localhost:30088"); + REQUIRE(clientErr == nullptr); + auto client = std::move(clientStream); + + conn += client.OnConnected([&](const std::shared_ptr& err) { + if (err != nullptr) { + WARN("Unable to connect server"); + clientLogs.push_back(err->ToString()); + executor.ExitLoop(); + return; + } + + clientLogs.push_back("client connected"); + std::string_view s = "ping"; + client.Write(ArrayView{s.data(), s.size()}.ViewAs()); + }); + conn += client.OnRead([&](const ArrayView& view, const std::shared_ptr& err) { + if (err != nullptr) { + WARN("Unable to read message"); + clientLogs.push_back(err->ToString()); + executor.ExitLoop(); + return; + } + + clientLogs.push_back("client read"); + auto text = std::string_view{reinterpret_cast(view.GetData()), view.GetSize()}; + + if (text != "pong") { + executor.ExitLoop(); + } + REQUIRE(text == "pong"); + + client.Disconnect(); + clientLogs.push_back("client disconnected"); + executor.ExitLoop(); + }); + + executor.RunLoop(); + + REQUIRE(serverLogs.size() == 3); + REQUIRE(serverLogs[0] == "server connected"); + REQUIRE(serverLogs[1] == "server read"); + REQUIRE(serverLogs[2] == "server disconnected"); + + REQUIRE(clientLogs.size() == 3); + REQUIRE(clientLogs[0] == "client connected"); + REQUIRE(clientLogs[1] == "client read"); + REQUIRE(clientLogs[2] == "client disconnected"); +}