Skip to content

Instantly share code, notes, and snippets.

@floooh
Created January 18, 2017 16:07
Show Gist options
  • Save floooh/b4d25d0e11ef2be91dd19cb974bcc7a6 to your computer and use it in GitHub Desktop.
Save floooh/b4d25d0e11ef2be91dd19cb974bcc7a6 to your computer and use it in GitHub Desktop.
NetClient.h (emscripten/osx/win)
//------------------------------------------------------------------------------
// 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
#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