// Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include "internal/platform/implementation/windows/nearby_client_socket.h" #include #include #include #include #include #include #include "absl/strings/string_view.h" #include "absl/time/time.h" #include "internal/flags/nearby_flags.h" #include "internal/platform/byte_array.h" #include "internal/platform/exception.h" #include "internal/platform/flags/nearby_platform_feature_flags.h" #include "internal/platform/implementation/windows/socket_address.h" #include "internal/platform/logging.h" namespace nearby::windows { NearbyClientSocket::NearbyClientSocket() : NearbyClientSocket(INVALID_SOCKET) { } NearbyClientSocket::NearbyClientSocket(SOCKET socket) : socket_(socket) { WSADATA wsa_data; int result = WSAStartup(MAKEWORD(2, 2), &wsa_data); if (result != 0) { LOG(WARNING) << "WSAStartup failed with error " << result; } is_socket_initiated_ = (result == 0); } NearbyClientSocket::~NearbyClientSocket() { Close(); if (is_socket_initiated_) { WSACleanup(); } } bool NearbyClientSocket ::Connect(const SocketAddress& server_address, absl::Duration timeout) { if (!is_socket_initiated_) { LOG(WARNING) << "Windows socket is not initiated."; return false; } if (socket_ != INVALID_SOCKET) { LOG(ERROR) << "Socket is already connected."; return false; } socket_ = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP); if (socket_ == INVALID_SOCKET) { LOG(ERROR) << "Failed to get socket with error " << WSAGetLastError(); return false; } // On Windows dual stack is not the default. // https://learn.microsoft.com/en-us/windows/win32/winsock/dual-stack-sockets#creating-a-dual-stack-socket DWORD v6_only = 0; if (setsockopt(socket_, IPPROTO_IPV6, IPV6_V6ONLY, reinterpret_cast(&v6_only), sizeof(v6_only)) == SOCKET_ERROR) { LOG(WARNING) << "Failed to set IPV6_V6ONLY with error " << WSAGetLastError(); } BOOL flag = TRUE; if (setsockopt(socket_, SOL_SOCKET, SO_KEEPALIVE, reinterpret_cast(&flag), sizeof(flag)) == SOCKET_ERROR) { LOG(WARNING) << "Failed to set SO_KEEPALIVE with error " << WSAGetLastError(); } int send_buffer_size = static_cast(NearbyFlags::GetInstance().GetInt64Flag( nearby::platform::config_package_nearby::nearby_platform_feature:: kSocketSendBufferSize)); if (setsockopt(socket_, SOL_SOCKET, SO_SNDBUF, reinterpret_cast(&send_buffer_size), sizeof(send_buffer_size)) == SOCKET_ERROR) { LOG(WARNING) << "Failed to set SO_SNDBUF with error " << WSAGetLastError(); } flag = TRUE; setsockopt(socket_, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast(&flag), sizeof(flag)); bool has_timeout = (timeout != absl::InfiniteDuration()); if (has_timeout) { unsigned long non_blocking = 1; // NOLINT if (ioctlsocket(socket_, FIONBIO, &non_blocking) == SOCKET_ERROR) { LOG(WARNING) << "Failed to set socket to non-blocking, error: " << WSAGetLastError(); // turn off timeout if we can't set the socket to non-blocking. has_timeout = false; } } SocketAddress dual_stack_address = server_address.ToMappedIPv6(); if (connect(socket_, dual_stack_address.address(), sizeof(sockaddr_storage)) == SOCKET_ERROR) { bool connected = false; if (has_timeout && WSAGetLastError() == WSAEWOULDBLOCK) { // Wait until timeout or socket is connected. timeval tm = absl::ToTimeval(timeout); fd_set set; FD_ZERO(&set); FD_SET(socket_, &set); if (select(/*nfds=*/0, /*readfds=*/nullptr, &set, /*exceptfds=*/nullptr, &tm) > 0) { int error = -1; int size = sizeof(int); getsockopt(socket_, SOL_SOCKET, SO_ERROR, (char*)&error, /*(socklen_t *)*/ &size); connected = (error == 0); } } if (!connected) { LOG(ERROR) << "Failed to connect socket with error: " << WSAGetLastError(); closesocket(socket_); socket_ = INVALID_SOCKET; return false; } } if (has_timeout) { unsigned long non_blocking = 0; // NOLINT if (ioctlsocket(socket_, FIONBIO, /*argp=*/&non_blocking) == SOCKET_ERROR) { LOG(ERROR) << "Failed to set socket to blocking, error: " << WSAGetLastError(); } } LOG(INFO) << "Client socket connected successfully"; if (VLOG_IS_ON(1)) { SocketAddress local_address; int address_length = sizeof(sockaddr_storage); if (getsockname(socket_, local_address.address(), &address_length) != SOCKET_ERROR) { VLOG(1) << "Connected to " << server_address.ToString() << " from " << local_address.ToString(); } } return true; } ExceptionOr NearbyClientSocket::Read(std::int64_t size) { if (socket_ == INVALID_SOCKET) { LOG(WARNING) << "Trying to read from an invalid socket."; return {Exception::kIo}; } std::string buffer; buffer.resize(size); char* buffer_ptr = buffer.data(); int64_t total_bytes_read = 0; // Loop until all requested bytes have been received. while (total_bytes_read < size) { int bytes_read = recv( /*s=*/socket_, /*buf=*/buffer_ptr + total_bytes_read, /*len=*/static_cast(size - total_bytes_read), /*flags=*/0 // No special flags, just a standard receive. ); if (bytes_read > 0) { // Successfully read some bytes. total_bytes_read += bytes_read; } else if (bytes_read == 0) { // The peer has performed a graceful shutdown. Return any data already // read. buffer.resize(total_bytes_read); LOG(INFO) << "Socket closed by peer, data size: " << buffer.size(); return ExceptionOr(ByteArray(std::move(buffer))); } else { // bytes_read == SOCKET_ERROR if (WSAGetLastError() == WSAEINTR) { VLOG(1) << "Interrupted while reading from socket."; continue; } // An error occurred during the receive operation. LOG(ERROR) << "Failed to receive data; recv failed with error: " << WSAGetLastError(); return {Exception::kIo}; } } return ExceptionOr(ByteArray(std::move(buffer))); } ExceptionOr NearbyClientSocket::Skip(size_t offset) { if (socket_ == INVALID_SOCKET) { LOG(WARNING) << "Trying to skip to an invalid socket."; return {Exception::kIo}; } ExceptionOr data = Read(offset); if (data.ok()) { return ExceptionOr(data.result().size()); } LOG(ERROR) << "Failed to skip data " << WSAGetLastError(); return {Exception::kIo}; } Exception NearbyClientSocket::Write(absl::string_view data) { if (socket_ == INVALID_SOCKET) { LOG(WARNING) << "Trying to write to an invalid socket."; return {Exception::kIo}; } int count = 0; while (count < data.size()) { int result = send(/*s=*/socket_, /*buf=*/data.data() + count, /*len=*/data.size() - count, /*flags*/ 0); if (result == SOCKET_ERROR) { LOG(ERROR) << "Failed to send data " << WSAGetLastError(); return {Exception::kIo}; } count += result; } return {Exception::kSuccess}; } Exception NearbyClientSocket::Flush() { // Socket doesn't support flush. return {Exception::kSuccess}; } Exception NearbyClientSocket::Close() { if (socket_ == INVALID_SOCKET) { VLOG(1) << "Socket already closed."; return {Exception::kSuccess}; } shutdown(socket_, SD_BOTH); closesocket(socket_); socket_ = INVALID_SOCKET; LOG(INFO) << "Socket closed."; return {Exception::kSuccess}; } } // namespace nearby::windows