Created
January 18, 2017 16:07
-
-
Save floooh/b4d25d0e11ef2be91dd19cb974bcc7a6 to your computer and use it in GitHub Desktop.
NetClient.h (emscripten/osx/win)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//------------------------------------------------------------------------------ | |
// NetClient.cc | |
//------------------------------------------------------------------------------ | |
#if ORYOL_WINDOWS | |
#define _WINSOCK_DEPRECATED_NO_WARNINGS (1) | |
#include <WinSock2.h> | |
typedef int ssize_t; | |
#endif | |
#if ORYOL_POSIX | |
#include <sys/types.h> | |
#include <sys/socket.h> | |
#include <sys/select.h> | |
#include <unistd.h> | |
#include <fcntl.h> | |
#include <netinet/in.h> | |
#include <arpa/inet.h> | |
#include <netdb.h> | |
#include <errno.h> | |
#define SOCKET_ERROR (-1) | |
#endif | |
#include "NetClient.h" | |
using namespace Oryol; | |
namespace Oryol { | |
//------------------------------------------------------------------------------ | |
static bool wouldBlockOrConnected() { | |
#if ORYOL_WINDOWS | |
const int wsaError = WSAGetLastError(); | |
return ((wsaError == WSAEWOULDBLOCK) || (wsaError == WSAEALREADY) || (wsaError == WSAEISCONN)); | |
#else | |
return ((errno == EINPROGRESS) || (errno == EWOULDBLOCK) || (errno == EISCONN)); | |
#endif | |
} | |
//------------------------------------------------------------------------------ | |
void | |
NetClient::Setup(const NetClientSetup& setup) { | |
o_assert(setup.ReceiveFunc); | |
#if ORYOL_WINDOWS | |
WSADATA wsaData; | |
WSAStartup(MAKEWORD(2, 2), &wsaData); | |
#endif | |
this->setup = setup; | |
this->state = Disconnected; | |
} | |
//------------------------------------------------------------------------------ | |
void | |
NetClient::Discard() { | |
if (!this->IsDisconnected()) { | |
this->Disconnect(0); | |
} | |
} | |
//------------------------------------------------------------------------------ | |
void | |
NetClient::Connect(const URL& serverUrl) { | |
this->setup.ServerUrl = serverUrl; | |
if (this->IsConnected()) { | |
this->Disconnect(10); | |
} | |
} | |
//------------------------------------------------------------------------------ | |
void | |
NetClient::Disconnect(int waitFramesUntilReconnect) { | |
this->destroySocket(); | |
this->state = Disconnected; | |
// wait a few seconds before attempting reconnect | |
this->waitFrames = waitFramesUntilReconnect; | |
} | |
//------------------------------------------------------------------------------ | |
void | |
NetClient::Update() { | |
// if Disconnect() was called, wait a bit before reconnect | |
// attempt to not overwhelm the server | |
if (this->waitFrames > 0) { | |
o_assert(Disconnected == this->state); | |
this->waitFrames--; | |
return; | |
} | |
// normal connecting/connected loop | |
switch (this->state) { | |
case Disconnected: | |
this->doConnect(); | |
break; | |
case Connected: | |
this->onConnected(); | |
break; | |
} | |
} | |
//------------------------------------------------------------------------------ | |
bool | |
NetClient::Send(const String& msg) { | |
if ((this->sendBuffer.Size() + msg.Length()) < MaxSendBufferSize) { | |
this->sendBuffer.Add((const uint8_t*)msg.AsCStr(), msg.Length()); | |
this->sendBuffer.Add((const uint8_t*)"\r", 1); | |
return true; | |
} | |
else { | |
// send buffer is full, drop the message | |
return false; | |
} | |
} | |
//------------------------------------------------------------------------------ | |
void | |
NetClient::createSocket() { | |
// create socket | |
this->sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); | |
o_assert(SOCKET_ERROR != this->sock); | |
// switch to non-blocking | |
#if ORYOL_WINDOWS | |
u_long enabled = 1; | |
ioctlsocket(this->sock, FIONBIO, &enabled); | |
#else | |
fcntl(this->sock, F_SETFL, O_NONBLOCK); | |
#endif | |
} | |
//------------------------------------------------------------------------------ | |
void | |
NetClient::destroySocket() { | |
#if ORYOL_EMSCRIPTEN | |
close(this->sock); | |
#elif ORYOL_POSIX | |
shutdown(this->sock, SHUT_RDWR); | |
close(this->sock); | |
#else | |
shutdown(this->sock, SD_BOTH); | |
closesocket(this->sock); | |
#endif | |
this->sock = 0; | |
} | |
//------------------------------------------------------------------------------ | |
void | |
NetClient::doConnect() { | |
o_assert(0 == this->sock); | |
o_assert(this->state == Disconnected); | |
o_assert(this->setup.ServerUrl.HasHost()); | |
o_assert(this->setup.ServerUrl.HasPort()); | |
this->createSocket(); | |
String hostName = this->setup.ServerUrl.Host(); | |
const uint16_t port = atoi(this->setup.ServerUrl.Port().AsCStr()); | |
sockaddr_in addr; | |
Memory::Clear(&addr, sizeof(addr)); | |
addr.sin_family = AF_INET; | |
addr.sin_port = htons(port); | |
uint32_t ipAddr = 0; | |
if (hostName.AsCStr()[0] >= '0' && hostName.AsCStr()[0] <= '9') { | |
// an ip address | |
ipAddr = inet_addr(hostName.AsCStr()); | |
} | |
else { | |
// a host name | |
struct hostent* he = gethostbyname(hostName.AsCStr()); | |
if (he) { | |
ipAddr = *(u_long *)he->h_addr_list[0]; | |
} | |
else { | |
o_error("Couldn't resolve host name '%s'\n", hostName.AsCStr()); | |
} | |
} | |
#if ORYOL_WINDOWS | |
addr.sin_addr.S_un.S_addr = ipAddr; | |
#else | |
addr.sin_addr.s_addr = ipAddr; | |
#endif | |
Log::Info("Connecting to '%s' => '%d.%d.%d.%d', port %d\n", | |
hostName.AsCStr(), | |
ipAddr & 0xFF, | |
(ipAddr >> 8) & 0xFF, | |
(ipAddr >> 16) & 0xFF, | |
(ipAddr >> 24) & 0xFF, | |
port); | |
const int connectRes = connect(this->sock, (sockaddr*)&addr, sizeof(addr)); | |
if (SOCKET_ERROR == connectRes) { | |
if (wouldBlockOrConnected()) { | |
this->state = Connected; | |
return; | |
} | |
} | |
Log::Warn("Failed to connect to '%s:%d'\n", hostName.AsCStr(), port); | |
this->Disconnect(300); | |
} | |
//------------------------------------------------------------------------------ | |
void | |
NetClient::onConnected() { | |
o_assert(this->sock); | |
// send and receivce data | |
fd_set fdr, fdw; | |
FD_ZERO(&fdr); | |
FD_ZERO(&fdw); | |
FD_SET(this->sock, &fdr); | |
FD_SET(this->sock, &fdw); | |
// for windows, it is necessary to provide an empty timeout | |
// structure, in order for select() to not block | |
struct timeval tv = { }; | |
const int selectRes = select(int(this->sock+1), &fdr, &fdw, 0, &tv); | |
if (selectRes == SOCKET_ERROR) { | |
o_error("select() failed.\n"); | |
} | |
else if (selectRes > 0) { | |
if (FD_ISSET(this->sock, &fdr)) { | |
// recv the next chunk of data into the recv buffer | |
if (!this->recvNextChunk()) { | |
this->Disconnect(300); | |
return; | |
} | |
} | |
if (FD_ISSET(this->sock, &fdw)) { | |
// send the next chunk of data from the send buffer | |
if (!this->sendNextChunk()) { | |
this->Disconnect(300); | |
return; | |
} | |
} | |
} | |
// scan for complete received messages (separated by newline) | |
this->scanMessages(); | |
} | |
//------------------------------------------------------------------------------ | |
bool | |
NetClient::sendNextChunk() { | |
o_assert(this->sock); | |
const int maxSendSize = 4096; | |
int sendSize = this->sendBuffer.Size(); | |
if (sendSize > maxSendSize) { | |
sendSize = maxSendSize; | |
} | |
if (sendSize > 0) { | |
const uint8_t* sendData = this->sendBuffer.Data(); | |
const ssize_t sendRes = send(this->sock, (const char*)sendData, sendSize, 0); | |
if (SOCKET_ERROR == sendRes) { | |
// send error | |
Log::Warn("Failed to send '%d' bytes, disconnecting!\n", sendSize); | |
return false; | |
} | |
else { | |
// res is number of bytes sent | |
const int sentBytes = sendRes; | |
this->sendBuffer.Remove(0, sentBytes); | |
} | |
} | |
return true; | |
} | |
//------------------------------------------------------------------------------ | |
bool | |
NetClient::recvNextChunk() { | |
o_assert(this->sock); | |
const int maxRecvSize = 4096; | |
uint8_t recvBuf[maxRecvSize] = { }; | |
bool done = false; | |
while (!done) { | |
ssize_t recvRes = recv(this->sock, (char*) recvBuf, sizeof(recvBuf), 0); | |
if (0 == recvRes) { | |
Log::Warn("Error in recv() (returned 0), disconnecting!\n"); | |
return false; | |
} | |
else if (recvRes > 0) { | |
const int bytesReceived = recvRes; | |
if (bytesReceived > 0) { | |
this->recvBuffer.Add(recvBuf, bytesReceived); | |
this->lastRecvTime = Clock::Now(); | |
} | |
} | |
else { | |
if (wouldBlockOrConnected()) { | |
done = true; | |
} | |
else { | |
Log::Warn("Error in recv() (returned error), disconnecting!\n"); | |
return false; | |
} | |
} | |
} | |
return true; | |
} | |
//------------------------------------------------------------------------------ | |
void | |
NetClient::scanMessages() { | |
if (this->recvBuffer.Empty()) { | |
return; | |
} | |
uint8_t* recvData = this->recvBuffer.Data(); | |
int recvSize = this->recvBuffer.Size(); | |
for (int scanPos = 0; scanPos < recvSize; scanPos++) { | |
if ('\r' == recvData[scanPos]) { | |
// found the end of a command, extract into string, | |
// remove from recvBuffer and call handler | |
String msg((const char*)recvData, 0, scanPos); | |
this->recvBuffer.Remove(0, scanPos + 1); | |
// reset variables for next command | |
recvData = this->recvBuffer.Data(); | |
recvSize = this->recvBuffer.Size(); | |
scanPos = 0; | |
// and call the message callback | |
this->setup.ReceiveFunc(msg); | |
} | |
} | |
} | |
} // namespace Oryol |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#pragma once | |
//------------------------------------------------------------------------------ | |
/** | |
@class Oryol::NetClient | |
@brief WebSocket client wrapper | |
*/ | |
#include "Core/Types.h" | |
#include "Core/Containers/Buffer.h" | |
#include "IO/Core/URL.h" | |
#include "Core/Time/Clock.h" | |
#include <functional> | |
#if ORYOL_WINDOWS | |
typedef unsigned long long SOCKET; | |
#endif | |
namespace Oryol { | |
class NetClientSetup { | |
public: | |
Oryol::URL ServerUrl = "tcp://localhost:13254"; | |
std::function<void(Oryol::String msg)> ReceiveFunc; | |
}; | |
class NetClient { | |
public: | |
enum State { | |
Disconnected, | |
Connected, | |
}; | |
static const int MaxSendBufferSize = 16 * 1024; | |
/// setup the client, and start connecting to the server | |
void Setup(const NetClientSetup& setup); | |
/// shutdown the client, disconnect if currently connected | |
void Discard(); | |
/// connect to a different server address | |
void Connect(const Oryol::URL& url); | |
/// initiate a disconnect from server | |
void Disconnect(int waitFramesUntilReconnect); | |
/// do per-frame-update, advance state, receive and send messages as needed | |
void Update(); | |
/// asynchronously send a message, return false if send buffer was full | |
bool Send(const Oryol::String& msg); | |
/// get the server address | |
const char* ServerAddress() const { | |
return this->setup.ServerUrl.AsCStr(); | |
} | |
/// return true if currently connected | |
bool IsConnected() const { | |
return Connected == this->state; | |
} | |
/// return true if currently disconnected | |
bool IsDisconnected() const { | |
return Disconnected == this->state; | |
} | |
/// get time since last message was received | |
Oryol::Duration HeartbeatTime() const { | |
return Oryol::Clock::Since(this->lastRecvTime); | |
} | |
private: | |
/// create the client socket | |
void createSocket(); | |
/// destroy the client socket | |
void destroySocket(); | |
/// start connecting, called when currently disconnected | |
void doConnect(); | |
/// called while in connecting state, handle connection handshake | |
void onConnecting(); | |
/// called while in connected state, send and receive messages | |
void onConnected(); | |
/// send the next chunk of data from the send buffer | |
bool sendNextChunk(); | |
/// receive the next chunk of data and write to recv buffer | |
bool recvNextChunk(); | |
/// scan receive buffer for complete messages, and emit them | |
void scanMessages(); | |
NetClientSetup setup; | |
Oryol::TimePoint lastRecvTime; | |
State state = Disconnected; | |
Oryol::Buffer sendBuffer; | |
Oryol::Buffer recvBuffer; | |
#if ORYOL_WINDOWS | |
SOCKET sock = 0; | |
#else | |
int sock = 0; | |
#endif | |
int waitFrames = 0; | |
}; | |
} // namespace Oryol |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment