Saturday, July 26, 2014

Winsock TCP Client in C++

Recently I've been looking at a server-client software architecture for machine learning.  A lot of really great machine learning and mathematical modeling toolboxes are now written in Python. However, if you're working directly with a sensor-based system, your sensor capture code is often written in C/C++.

Yes, you could write a wrapper for that code, but it can be a slow and redundant process.

Instead, you can set up a Python TCP server like so , even on the localhost on the same machine, send a JSON structure with your feature vector, do your heavy number-crunching in Python, and send the result back to the C++ client.

There's a lot of documentation out there for TCP servers and clients on *nix in C and C++, but the documentation for Windows is less available, written in pure C, or cryptic to understand. I wanted a class that would abstract away most of the nuts and bolts.

Using the Windows Winsock documentation example, I've written a simple Winsock TCP client class in C++. This code will build with any semi-recent version of Visual Studio Express or Professional. It's really simple stuff, just easier to read.

Here's the header file, which I called TCPClient.h

#ifndef TCP_CLIENT_H
#define TCP_CLIENT_H
#define WIN32_LEAN_AND_MEAN

#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdlib.h>
#include <stdio.h>
#include <string>
#include <iostream>
#include <sstream>

#pragma comment (lib, "Ws2_32.lib")
#pragma comment (lib, "Mswsock.lib")
#pragma comment (lib, "AdvApi32.lib")


class TCPClient{
public:
    TCPClient(); //Default constructor
    TCPClient(const char* host, const char* port); //Constructor with host-ipaddress, port number
    TCPClient(std::string host, std::string port); //Constructor with host-ipaddress, port number
    TCPClient(const char* host, const char* port, const int buffer_size); //Constructor with host-ipaddress, port number, buffersize [rarely used]
    TCPClient(std::string host, std::string port, const int buffer_size); //Constructor with host-ipaddress, port number, buffersize [rarely used]
    int init(); //initialization, called by constructors
    ~TCPClient(); //clean up winsock environment
    bool open(); //open connection
    bool open(const char* host, const char* port); //open connection with host-ipaddress, port number
    bool open(std::string host, std::string port); //open connection with host-ipaddress, port number
    int send_buf(const char* sendbuf);    //send a message. Returns the number of bytes successfully sent. Returns -1 if failed.
    int send_buf(std::string sendbuffer); //send a message. Returns the number of bytes successfully sent. Returns -1 if failed.
    int receive(std::string& recvbuffer); //receive a message. Returns the number of bytes received.
    bool shutdown_send(); //shutdown the outgoing connection
    bool close(); //close the socket    
private:
    int resolve(); //used internally, resolving host. Returns -1 if failed.
    int connect_to(); //used internally, actually connect. Returns -1 if failed.

    int _buffer_size;    //buffer size in bytes
    int iResult;        //errorcodes from Winsock

    WSADATA wsaData;   //Winsock stuff
    SOCKET ConnectSocket; //Socket
    struct addrinfo *result, *ptr, hints; //Winsock stuff

    std::string host; //host ipaddress
    std::string port; //port number
    bool _connection_open; // Is the connection open?
    bool _ipset; //Did we initialize with host and port number?
    bool _shutdown_sent; //Have we sent a shutdown message to the host?
};

#endif

Here's the class implementation. Again, really simple stuff.

#include "TCPClient.h"

int TCPClient::init(){
    ConnectSocket = INVALID_SOCKET;
    result=NULL;
    ptr=NULL;

    iResult = WSAStartup(MAKEWORD(2,2), &wsaData);
    if (iResult != 0) {
        std::cerr<<"WSAStartup failed with error: "<<iResult<<"\n";
        return -1;
    }
    ZeroMemory( &hints, sizeof(hints) );
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;
    this->_buffer_size=4096;
    this->_shutdown_sent=false;
    return 0;
}
TCPClient::TCPClient(){
    this->_ipset=false;
    if (init()<0){
        std::cerr<<"Error initializing WinSock\n";
        exit(1);
    }
}
TCPClient::TCPClient(const char* host, const char* port){
    this->host=host;
    this->port=port;
    this->_ipset=true;
    if (init()<0){
        std::cerr<<"Error initializing WinSock\n";
        exit(1);
    }
}
TCPClient::TCPClient(std::string host, std::string port){
    this->host=host;
    this->port=port;
    this->_ipset=true;
    if (init()<0){
        std::cerr<<"Error initializing WinSock\n";
        exit(1);
    }
}
TCPClient::TCPClient(const char* host, const char* port, const int buffer_size){
    this->host=host;
    this->port=port;
    this->_ipset=true;
    this->_buffer_size=buffer_size;
    if (init()<0){
        std::cerr<<"Error initializing WinSock\n";
        exit(1);
    }
}
TCPClient::TCPClient(std::string host, std::string port, const int buffer_size){
    this->host=host;
    this->port=port;
    this->_ipset=true;
    this->_buffer_size=buffer_size;
    if (init()<0){
        std::cerr<<"Error initializing WinSock\n";
        exit(1);
    }
}
TCPClient::~TCPClient(){
    WSACleanup();
}

int TCPClient::resolve(){
    // Resolve the server address and port
    iResult = getaddrinfo(host.c_str(), port.c_str(), &hints, &result);
    if ( iResult != 0 ) {
        std::cerr<<"getaddrinfo failed with error: "<<iResult<<"\n";
        WSACleanup();
        return -1;
    }
    return 0;
}
int TCPClient::connect_to(){
    // Attempt to connect to an address until one succeeds
    for(ptr=result; ptr != NULL ;ptr=ptr->ai_next) {
        // Create a SOCKET for connecting to server
        ConnectSocket = socket(ptr->ai_family, ptr->ai_socktype, 
            ptr->ai_protocol);
        if (ConnectSocket == INVALID_SOCKET) {           
            std::cerr<<"socket failed with error: "<<WSAGetLastError()<<"\n";
            WSACleanup();
            return -1;
        }
        // Connect to server.
        iResult = connect( ConnectSocket, ptr->ai_addr, (int)ptr->ai_addrlen);
        if (iResult == SOCKET_ERROR) {
            closesocket(ConnectSocket);
            ConnectSocket = INVALID_SOCKET;
            continue;
        }
        break;
    }
    freeaddrinfo(result);
    if (ConnectSocket == INVALID_SOCKET) {
        std::cerr<<"Unable to connect to server!\n";
        WSACleanup();
        return -1;
    }
    return 0;
}

bool TCPClient::open(){
    if (this->_ipset==false)
        return false;
    if (resolve()<0)
        return false;
    if (connect_to()<0)
        return false;
    return true;
}
bool TCPClient::open(const char* host, const char* port){
    this->host=host;
    this->port=port;
    this->_ipset=true;
    this->_connection_open=this->open();
    return _connection_open;
}
bool TCPClient::open(std::string host, std::string port){
    this->host=host;
    this->port=port;
    this->_ipset=true;
    this->_connection_open=this->open();
    return _connection_open;
}
int TCPClient::send_buf(const char* sendbuf){
    if (_connection_open==false)
        return -1;
    // Send an initial buffer

    iResult = send( ConnectSocket, sendbuf, (int)strlen(sendbuf), 0 );
    if (iResult == SOCKET_ERROR) {
        std::cerr<<"socket failed with error: "<<WSAGetLastError()<<"\n";    
        WSACleanup();
        return -1;
    }   
    return iResult;
}
int TCPClient::send_buf(std::string sendbuffer){
    
    int length=sendbuffer.length();
    if (_connection_open==false)
        return -1;
    // Send an initial buffer
    iResult = send( ConnectSocket, sendbuffer.c_str(), length, 0 );

    if (iResult == SOCKET_ERROR) {
        std::cerr<<"socket failed with error: "<<WSAGetLastError()<<"\n";
        this->close();
        WSACleanup();
        return -1;
    }   
    return iResult;
}
int TCPClient::receive(std::string& recvbuffer){
    // Receive up to _buffer_size bytes
    recvbuffer.clear();
    int total=0;
    char* recvbuf=new char[this->_buffer_size];    
    iResult = recv(ConnectSocket, recvbuf, _buffer_size, 0);
        if ( iResult > 0 ){            
            std::string received_str=recvbuf;
            received_str.resize(iResult);
            recvbuffer+=received_str;
            total+=iResult;            
        }
        else if ( iResult == 0 ){
            std::cout<<"\nConnection closed\n";            
        }
        else            
            std::cerr<<"recv failed with error: "<<WSAGetLastError()<<"\n";
   
    delete [] recvbuf;
    return total;
}
bool TCPClient::shutdown_send(){
     // shutdown the connection since no more data will be sent
    iResult = shutdown(ConnectSocket, SD_SEND);    
    if (iResult == SOCKET_ERROR) {
       std::cerr<<"shutdown failed with error: "<<WSAGetLastError()<<"\n";       
        this->close();
        WSACleanup();
        return false;
    }
    this->_shutdown_sent=true;
    return true;
}
bool TCPClient::close(){
    if (!_shutdown_sent)
        shutdown_send();        
     // cleanup
    closesocket(ConnectSocket);
    return true;
}

And finally, this is the main function, which shows how to invoke the class. I had a very simple TCP server written in Python, running on a different machine on the same local network.

The main program simple gets your keyboard input and sends it to the server. If you input the string "exit", the program exits cleanly.


int main (int argc, char* argv[]){
    TCPClient myClient("192.168.1.2","5000");    
    if (!myClient.open()){
        char a;
        std::cout<<"Enter any key to exit\n";
        std::cin>>a;
        return 0;
    }

    while (true){        
        std::string input; 
        std::cout<<"Enter something to send to the server\n";
        getline(std::cin,input);
        if (!input.compare("exit")){
            myClient.close();
            break;
        }
        if (myClient.send_buf(input)<0)    
            break;    
        std::string received;
        if    (myClient.receive(received)<0)
            break;    
        std::cout<<"Received:\n"<<received<<"\n\n";            
    }
    return 0;
}

Much easier to read and understand, than the original code on the Microsoft documentation site. Cheers!

1 comment:

  1. Hello.
    Maybe I'm wrong but the following code doesn't look right to me:

    bool TCPClient::shutdown_send(){
    // shutdown the connection since no more data will be sent
    iResult = shutdown(ConnectSocket, SD_SEND);
    if (iResult == SOCKET_ERROR) {
    std::cerr<<"shutdown failed with error: "<close();
    WSACleanup();
    return false;
    }
    this->_shutdown_sent=true;
    return true;
    }
    bool TCPClient::close(){
    if (!_shutdown_sent)
    shutdown_send();
    // cleanup
    closesocket(ConnectSocket);
    return true;
    }

    To put it in perspective, what happens when the shutdown() method fails? You are calling the close() method of your class again which in turn is calling the shutdown_send() again but at the same time it's also calling the closesocket() twice (or more if the shutdown() keeps failing. Apart from this, in the shutdown_send() you are calling WSACleanup() but you also have it in the ~TCPClient() destructor.. Can you please help me understand if I am missing something?

    ReplyDelete