Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++

A Universal TCP Socket Class for Non-blocking Server/Clients

4.66/5 (121 votes)
26 Jun 2012CPOL13 min read 903.2K   20.5K  
A universal class for bidirectional TCP communication
Image 1

Introduction

Event driven (non blocking, asynchronous) Winsock programming is a very complex topic. It is definitely NOT for beginners!
I needed such code and searched the internet for a TCP communication class which is ready to use.

But all I found (even here on CodeProject) was either buggy, immature beginner code or much too complex for my needs.
So I ended up - investing many hours - to write my own decent class.
I offer it here on CodeProject for all those who need a reliable and robust TCP socket class.

What you can download here is a very clean code written by an experienced programmer, with proper error handling and plenty of comments.
The universal generic C++ class cSocket, written in plain C++, comes with a MFC demo application.

The cSocket class is so easy to use that even a beginner will have no problems with it.

Features

  • Very easy to use professional class
  • All the code is in one single C++ file.
  • Very clean code  
  • Can be used in production
  • TCP Socket with functionality for Client and Server
  • The server allows up to 62 clients to connect at the same time
  • Support of multiple network adapters
  • The class cSocket is thread safe:
    1. It is possible to create one extra thread which processes the events (FD_READ, etc.).
    2. It is also possible to run the entire server or client in ONE single thread. The whole application can run only within the GUI thread!
  • All functions of the class are non-blocking (asynchronous) and event driven.
  • You receive an event on the server when a client has connected or disconnected, and on the client when the server has been shut down (and obviously when data was received).
  • Event handling is optimized for servers under stress (near 100% CPU load).
  • You don't have to worry about any limited size buffers:
    • SEND: You can send data of any size.
    • RECEIVE: Whenever a new data block is received, it is automatically appended to a dynamic receive memory where data accumulates until you remove it (dynamic FIFO buffer).
  • If a client is idle for longer than a user defined maximum time, it gets disconnected from the server.
  • The code compiles as UNICODE and MBCS. (Tested on Visual Studio 6, 2003, 2005)
  • In the ZIP file, you can find additionally a Winsock FAQ which I compiled into a CHM file. This answers many questions concerning Winsock programing.
  • Can be used on Windows CE.
  • Contains a 32 Bit and 64 Bit Solution. 

Non-Features

You can find a lot of ugly things in other TCP projects on the internet, which you will NOT find in cSocket:

  • You do NOT have to derive a class from cSocket to receive events.
  • You do NOT have to write a callback function for each network event.
  • You will NOT have 40 threads running on a server when 40 clients are connected.
  • It will NOT happen that a network error occurs and the application does not know what's wrong because of crappy error handling.
  • You do not have to care about managing fixed size buffers.
  • This project does NOT use WSAAsyncSelect which requires a window handle for notifications: cSocket runs completely window-less.
  • This class does NOT create any new dependency to an additional MSVCPxx.DLL because it is STL free.

Versions

  • Version 1.0: Initial version
  • Version 1.1: Event handling optimized for servers under stress
  • Version 1.2: Added support for multiple network adapters
  • Version 1.3: Documentation extended by 2 new chapters
  • Version 1.4: Added cSocket::DisconnectClient(), a maximum idle time after which an idle socket is disconnected, dynamic receive memory and 3 demo modes.
  • Version 1.5: Workaround for undocumented Windows CE bug.

Running the Demo Application

Start the SocketDemo.exe three times on the same computer!
The 3 applications will appear at different locations on the screen.
Use the left one as server by hitting the button "Listen" and the others as clients by hitting the button "Connect".
Then you can transfer messages with the button "Send" and see all occurring WSA events.

After understanding how it works, you can try the same thing on different computers via network.
Turn off the Windows Firewall before!

Multiple Network Adapters

Let's suppose that your PC has two network adapters, then it will also have two local IP addresses.
When starting the server, you can choose on which local IP it should listen or it can listen on both IPs at once.

You can even run two servers on the same machine which listen on the same port if both are bound to different local IPs!
So one server can respond to clients in one subnet and the other server can respond to clients in the other subnet (e.g. LAN y WAN).

Winsock Programming

The class cSocket demonstrates the correct usage of the following Winsock commands and events:

  • socket, bind, listen, connect, shutdown, closesocket, gethostbyname, gethostname, getpeername
  • WSAAccept, WSAStartup, WSACleanup, WSARecv, WSASend, WSACreateEvent, WSACloseEvent, WSAEventSelect, WSAWaitForMultipleEvents, WSAEnumNetworkEvents, WSAGetLastError
  • FD_READ, FD_WRITE, FD_ACCEPT, FD_CONNECT, FD_CLOSE

The Winsock library is quite complex and offers multiple ways of socket programming.
The synchronous = blocking technique has the disadvantage that the server must run 40 threads when 40 clients are connected.
The asynchronous, event driven technique, which is used in this project, can serve 62 connected clients at once in a single thread.
For further details, have a look into the Winsock FAQ in the ZIP file!

Setup Server / Client

Using cSocket is really easy:

Server

C++
OnButtonListen()
{
    mi_Socket.Listen(0, ms32_Port,...);
    ProcessEvents();
}

OnButtonSend()
{
    mi_Socket.SendTo(h_Socket, "Hello World", 11);
}

ATTENTION

Do not send data to the socket before you have received the FD_ACCEPT event!
With this event, you also get the socket handle which you need later for SendTo().

Client

C++
OnButtonConnect()
{
    mi_Socket.ConnectTo(mu32_ServerIP, ms32_Port,...);
    ProcessEvents();
}

OnButtonSend()
{
    mi_Socket.SendTo(h_Socket, "Hello World", 11));
} 

ATTENTION

Do not send data to the socket before you have received the FD_CONNECT event!
With this event, you also get the socket handle which you need later for SendTo().

Every TCP connection between a server and a client is bidirectional and allows reading and writing.

Threading

You can use the cSocket class single-threaded or multi-threaded.
The demo application demonstrates both: In SocketDemoDlg.h, you can switch with PROCESS_EVENTS_IN_GUI_THREAD between both modes.

Multi Threaded

The events may be processed in an extra thread:

C++
// Set Timeout OFF
mi_Socket.Listen(0, ms32_Port, INFINITE);

while (...) // This loop runs in an additional thread for TCP events
{
    DWORD u32_Event;
    SOCKET  h_Socket;
    // ProcessEvents() blocks until an event is received

    DWORD u32_Error = mi_Socket.ProcessEvents(&u32_Event, &h_Socket, ....);
    if (u32_Event & FD_ACCEPT) { /* A new client has connected */ }
    if (u32_Event & FD_READ)   { /* Data has been received */ } 
    if (u32_Event & FD_CLOSE)  { /* A socket has closed */ }

    if (u32_Error) { /* handle error */ }
};

ATTENTION

If you use Multithreading, you must take care not to output into the GUI from within the ProcessEvents thread!
For example GetWindowText() will internally call SendMessage(WM_GETTEXT).
But SendMessage() will block the calling thread, switch to the GUI thread, wait for the return value of SendMessage() and then return control to the calling thread.
This can deadlock the application if at this moment the GUI thread has entered into cSocket::SendTo() where the Mutex blocks the GUI thread.
The demo application uses a timer to avoid this deadlock. This approach also makes the output into the edit box much faster.

Single Threaded

The function PumpMessages() processes all Windows messages which arrive in the application so the GUI stays responsive although the code runs an endless loop:

C++
// Set Timeout 50 ms
mi_Socket.Listen(0, ms32_Port, 50);

while (...) // This loop runs in the GUI thread
{
    PumpMessages(); // process Windows messages
       
    DWORD u32_Event;
    SOCKET  h_Socket;
    // ProcessEvents() returns after 50 ms or after an event was received
    DWORD u32_Error = mi_Socket.ProcessEvents(&u32_Event, &h_Socket, ....);

    if (u32_Error == ERROR_TIMEOUT)
        continue;

    if (u32_Event & FD_ACCEPT) { /* A new client has connected */ }
    if (u32_Event & FD_READ)   { /* Data has been received */ }
    if (u32_Event & FD_CLOSE)  { /* A socket has closed */ }

    if (u32_Error) { /* handle error */ }
};

The SocketList

cSocket uses an internal SocketList which stores events, socket handles and their (peer) IP addresses, and a write buffer.
The event array is passed to WSAWaitForMultipleEvents() which waits for any event to be signalled.
WSAWaitForMultipleEvents() allows a maximum of 64 events(WSA_MAXIMUM_WAIT_EVENTS).

The following diagram will make it easier to understand the code:

Image 2

Server + Client

The Event 0 is not associated with a socket. It is used for the Lock which synchronizes the threads.
The lock is realized by a waitable timer which releases a blocking Wait function.
The same timer is also used to shut down idle clients.

Server

Socket 0 is never connected with a Client, it only waits for incoming FD_ACCEPT events.
So only the Sockets 1...62 are available for Client connections => maximum 62 Clients.

Client

The Client never uses more than one socket.

How to Transfer Data

You want to write an application that transfers blocks of data to another computer. How to do this in detail?

Option 1 (version HTTP)

You can do it as the HTTP protocol does: The sender sends data and when he has finished, he closes the connection. The receiver recognizes the end of the datablock when the connection was closed. The disadvantages are:

  1. The receiver has no chance to distinguish if the connection was closed intentionally by the sender or due to a network problem. This is not reliable.
  2. For each datablock (even to transfer only 20 bytes), you must open a new connection to the other side.

Option 2 (version Telnet)

If you want to transfer only string messages, the sender puts a terminating zero (or linefeed) at the end of the message and the receiver recognizes the end of the datablock when he receives this Byte.

Option 3 (version Serialized Data)

Have a look at the PHP command serialize() which elegantly serializes complex data (even nested arrays) into strings:

C++
Array
(
    0 => "Red",
    "Key" => "Value",
    5 => Array(0 => "Sub1", 1 => "Sub2"),
    7 => 77777
)

Results serialized:

a:4:{i:0;s:3:"Red";s:3:"Key";s:5:"Value";i:5;a:2:{i:0;s:4:"Sub1";i:1;s:4:"Sub2";}i:7;i:77777;}

Another option would be to serialize to XML, but this results in much longer strings and much more complex parsing.

Option 4 (version Binary Structure)

If you want to transfer binary data, I recommend to prefix each datablock with a DWORD that tells the receiver the count of Bytes that will follow. You can put several fixed length fields into a structure followed by a variable length binary data block.

C++
struct kTransfer
{
   DWORD u32_TotalLength;
   char   s8_Command[20];
   char   s8_Param_1[50];
   char   s8_Param_2[50];
   // followed by the variable length binary data
};

Option 5 (version Binary BLOB)

If a structure is too static for your needs, you can send BLOBs instead which are prefixed with the length and the type of the data:

C++
struct kBlobHeader
{
   DWORD u32_Length;
   char   s8_Type;
   // followed by the variable length binary data
};

The Type can be "S" for strings, "I" for integers, "F" for floats, "A" for arrays, "B" for binary data, etc... With BLOB's you can even send structures and classes (in this case s8_Type must be a string e.g. "User::cClientData"). Multiple BLOBs can be concatenated to one message.

Option 6 (version FTP)

If during your data transmission it may be desired to abort a running transfer, then you need two connections. Like FTP, you need one control port and one data port. If the control port says "Abort", the data port must stop sending data.

Endian-ness

If you have to transfer data between low-endian machines and high-endian machines, you can prefix the data blocks with a verification DWORD like 0x001188FF. If the receiver reads 0xFF881100 instead, he must transform the entire receive buffer before processing it.

Dynamic Receive Buffer

When you receive data, it will happen that not all data arrives at once.
If your program needs the entire datablock to be complete before it starts processing it, the received snippets must be accumulated in the dynamic receive memory which functions like a FIFO buffer (First In First Out).

There are 3 demo modes which demonstrate how to manage received data.

  1. Normal Mode:
    In normal mode the demo application prints received data as it comes from Winsock. If WSARecv() has received 3214 Bytes, then these 3214 Bytes will be printed immediately to the screen.
  2. Prefixed Mode:
    In length-prefixed mode the sender always first sends a DWORD which contains the length of the entire datablock that will follow. The receiver accumulates the received snippets in a dynamic memory until all bytes of the block are entirely received and then prints them at once to the screen. Then all these Bytes are removed from the receive memory.
  3. Telnet Mode:
    The characters from a Telnet terminal are received one by one with the speed as you type them. They are accumulated in a dynamic memory until a linefeed has been received. Then the entire line is printed to the screen and removed from the receive memory.

Because cSocket does all the dirty work, your code to manage the received data will be extremely simple as you can see in the 3 demo functions:

  • ProcessReceivedDataNormal()
  • ProcessReceivedDataPrefix()
  • ProcessReceivedDataTelnet()

It should be obvious that you must switch client and server always to the same mode. (E.g. you cannot send in Prefixed mode and receive in Normal mode) Additionally it should be obvious that the demo compiled as Unicode cannot communicate with the MBCS compiled version.

Idle Timeout

You can specify after how many time a server automatically disconnects idle clients. For a client, you can also specify an idle timeout.
When the timeout elapses, the connected socket is shut down gracefully and an event FD_CLOSE + FD_TIMEOUT is signaled.

The Code

For more details about the functions in cSocket, please read the plenty detailed source code comments!

A Telnet Test

Start SocketDemo on Port 23 as Server. (Button Listen)
Then at the command prompt, enter "telnet" and then "open localhost".
Then you will see that all characters you type into the Telnet Client appear in Socketdemo. (example below "Hello" + ENTER)
In Normal Mode, the characters are printed immediately.
In Telnet Mode, they are accumulated in the dynamic receive memory until a linefeed has been received.

Image 3

ATTENTION

The Unicode compiled version can NOT be used as a Telnet server!

Connecting a HTTP Server

If you want to play around a little more with SocketDemo, you can connect to a HTTP server.
Example: www.gmx.net, Port 80.
To get the startpage, you must send "GET" (uppercase!) followed by a linefeed.

Image 4

ATTENTION

  • The Unicode compiled version can NOT connect a HTTP server!
  • It is normal that the server closes the connection after each request!

Connecting a FTP Server

You can also connect to an FTP server. Example: home.arcor.de, Port 21.
All FTP commands like "USER" or "PASS" must be followed by a linefeed.

Image 5

ATTENTION

  • The Unicode compiled version can NOT connect a FTP server!
  • Please don't expect to ever see the FTP directory listing that way!
    (FTP needs two ports to be connected at the same time to work correctly:
    one that transfers the FTP commands (e.g. LIST) and one that transfers the data.
    This is just a little test which will not be able to do more than a login.)

Wininet.dll

If you need support for HTTPS, an FTP client or download files from the internet, you should not use cSocket.

cSocket makes sense when you have to exchange data directly between two or more computers which run your application.
cSocket is a low level Socket class to DIRECTLY pass raw data from one computer to another.
An advantage of cSocket is that you can very easily use strong encryption to transfer data more securely than via HTTPS.

But to use complex internet protocols like HTTPS or FTP, it is not necessary to re-invent the wheel.
In this case, have a look at the functionality in Wininet.dll.
Wininet.dll is part of Internet Explorer and so it is available on every Windows computer: it has HTTP, HTTPS and FTP built in.

An additional advantage of using Wininet.dll is that you can easily pass through Proxy servers.
You can choose to use the Proxy settings of Internet Explorer which are stored in the Registry.
So your application can download a file through the Proxy server of the company without bothering the user with the Proxy user name and password if these are already stored in Internet Explorer.

ATTENTION

But Wininet.dll has some ugly bugs!

I wrote a complex FTP downloader in C#.
In this article, you can learn how to use Wininet.dll and you see the 4 workarounds which I developed to use Wininet.dll without complications.

If you want a Wininet.dll implementation in C++, have a look into my CabLib project which can download files via HTTP, HTTPS and FTP.

Elmu

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)