Question

I'm trying to improve my knowledge of OOP and decided to create a simple class to simplify sockets programming. This is a learning experiment so I do not want to use boost, or other libraries.

I want to implement an event-driven recv(). Meaning, everytime there is new data coming in, it should call my function.

I think I need to create a thread to run a recv() loop and then call my function everytime there is new data. Is there other way around using threads? I want my code to be portable.

Here is my simple Class and example code:

class.h:

#ifndef _SOCKETSCLASS_H
#define _SOCKETSCLASS_H

#if defined(WIN32) || defined(_WIN32) || defined(__WIN32) && !defined(__CYGWIN__)
    #define W32
    #include <WinSock2.h>
    #pragma comment(lib, "ws2_32.lib")
#else
    #include <sys/socket.h>
    #include <arpa/inet.h>
    #include <netdb.h>

    #define SOCKET int
#endif
#include <string>
#include<ctime>
#include <stdio.h>
#include <stdarg.h>
#include <varargs.h>
#include <tchar.h>

using namespace std;

#ifdef _DEBUG
    #define DEBUG(msg) XTrace(msg)
#else
    #define DEBUG(msg, params)
#endif

struct TCP_Client_opts
{
    BOOL    UseSCprotocol;
    BOOL    UseEncryption;
    BOOL    UseCompression;
    int     CompressionLevel;
    void    *Callback;
    BOOL    async;
};

struct TCP_Stats
{
    unsigned long int   upload; //bytes
    unsigned long int   download;//bytes
    time_t              uptime; //seconds
};

class TCP_Client
{
    public:
        TCP_Client();
        TCP_Client(TCP_Client_opts opts_set);
        ~TCP_Client();
        SOCKET          GetSocket();
        void            SetOptions(TCP_Client_opts opts_set);
        TCP_Client_opts GetOptions();
        BOOL            Connect(string server, int port);
        int             Send(string data);
        int             Recv(string *data);
        BOOL            IsConnected();
        int             Disconnect();
        TCP_Stats       GetStats();
    private:
        SOCKET          s = SOCKET_ERROR;
        TCP_Client_opts opts;
        TCP_Stats       stats;
        BOOL            connected = FALSE;
        time_t          starttime;
};
#endif

class.cpp:

#include "SocketsClass.h"

void XTrace(LPCTSTR lpszFormat, ...)
{
    va_list args;
    va_start(args, lpszFormat);
    int nBuf;
    TCHAR szBuffer[512]; // get rid of this hard-coded buffer
    nBuf = _vsnwprintf_s(szBuffer, 511, lpszFormat, args);
    ::OutputDebugString(szBuffer);
    va_end(args);
}


TCP_Client::TCP_Client(TCP_Client_opts opts_set)
{
    SetOptions(opts_set);
}

TCP_Client::~TCP_Client()
{
    Disconnect();
}

TCP_Client::TCP_Client()
{
}

void TCP_Client::SetOptions(TCP_Client_opts opts_set)
{
    opts = opts_set;
}

TCP_Client_opts TCP_Client::GetOptions()
{
    return opts;
}

SOCKET TCP_Client::GetSocket()
{
    return s;
}

BOOL TCP_Client::IsConnected()
{
    return connected;
}

int TCP_Client::Disconnect()
{
    connected = FALSE;
    stats.uptime = time(0) - starttime;
    return shutdown(s, 2);
}

BOOL TCP_Client::Connect(string server, int port)
{
    struct sockaddr_in RemoteHost;

#ifdef W32
    WSADATA       wsd;
    if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0)
    {
        DEBUG(L"Failed to load Winsock!\n");
        return FALSE;
    }
#endif

    //create socket if it is not already created
    if (s == SOCKET_ERROR)
    {
        //Create socket
        s = socket(AF_INET, SOCK_STREAM, 0);
        if (s == SOCKET_ERROR)
        {
            DEBUG(L"Could not create socket");
            return FALSE;
        }
    }

    //setup address structure
    if (inet_addr(server.c_str()) == INADDR_NONE)
    {
        struct hostent *he;

        //resolve the hostname, its not an ip address
        if ((he = gethostbyname(server.c_str())) == NULL)
        {
            //gethostbyname failed
            DEBUG(L"gethostbyname() - Failed to resolve hostname\n");
            return FALSE;
        }
    }   
    else//plain ip address
    {
        RemoteHost.sin_addr.s_addr = inet_addr(server.c_str());
    }

    RemoteHost.sin_family = AF_INET;
    RemoteHost.sin_port = htons(port);

    //Connect to remote server
    if (connect(s, (struct sockaddr *)&RemoteHost, sizeof(RemoteHost)) < 0)
    {
        DEBUG(L"connect() failed");
        return FALSE;
    }

    connected = TRUE;
    starttime = time(0);
    stats.download = 0;
    stats.upload = 0;
    return TRUE;
}

TCP_Stats TCP_Client::GetStats()
{
    if (connected==TRUE)
        stats.uptime = time(0)-starttime;
    return stats;
}

int TCP_Client::Send(string data)
{
    stats.upload += data.length();
    return send(s, data.c_str(), data.length(), 0);

}

int TCP_Client::Recv(string *data)
{
    int ret = 0;
    char buffer[512];

    ret = recv(s, buffer, sizeof(buffer), 0);
    data->assign(buffer);
    data->resize(ret);
    stats.download += data->length();

    return ret;
}

main.cpp:

#include <stdio.h>
#include <string.h>
#include "SocketsClass.h"

using namespace std;


int main(int argc, char *argv)
{
    TCP_Client tc;
    tc.Connect("127.0.0.1", 9999);
    tc.Send("HEllo");
    string data;
    tc.Recv(&data);
    puts(data.c_str());
    tc.Disconnect();
    printf("\n\nDL: %i\nUP: %i\nUptime: %u\n", tc.GetStats().download, tc.GetStats().upload, tc.GetStats().uptime);
    return 0;
}

Some extra questions:

  1. Imagine I'm sending a file. How would my function know that the current data is related to the previous message?
  2. How is my class design and implementation? SHould I change anything?

Thank you

Was it helpful?

Solution

If by "portable" you mean runs on other platforms besides Windows then a recv() loop in a worker thread is your only portable option. On Windows specifically, you have some additional choices:

  1. Allocate a hidden window and then use WSAAsyncSelect() to receive FD_READ notifications. This requires a message loop, which you can put in a worker thread.

  2. Use WSAEventSelect() to register a waitable event for FD_READ notifications and then wait for those events via WSAWaitForMultipleEvents() in a thread.

  3. use WSARecv() with an I/O Completion Port. Poll the IOCP via GetQueuedCompletionResult() in a thread.

As for your question regarding messaging, TCP is a byte stream, it has no concept of messages. You have to frame your messages yourself. You can either:

  1. give each message a fixed header that contains the message length. Read the header first, then read however many bytes it says, then read the next header, and so on.

  2. separate each message with a unique delimiter that does not appear in the message data. Read until you encounter that delimiter, then read until the next delimiter, and so on.

OTHER TIPS

Have your event loop call either poll or select to determine if there is data that can be read on the socket(s). Then read it, and call the appropriate callback function.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top