Click here to Skip to main content
15,121,790 members
Articles / Programming Languages / C++
Article
Posted 10 Jun 2020

Tagged as

Stats

8.5K views
332 downloads
9 bookmarked

Windows Sockets Streams Part II - Multi-Threaded TCP Servers

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
10 Jun 2020MIT6 min read
Make a multi-threaded server in 20 lines or less
This article continues the series on Windows sockets programming in C++ showing the architecture of a simple multi-threaded TCP server.

Introduction

Some time ago, I wrote an article that describes a collection of classes designed to smooth out the rough edges one encounters while working with sockets in Windows. It is now time to expand upon those elements and put them to good use. The next chapter of this series is probably going to be more interesting as it shows how to build an embeddable HTTP server but before getting to it, we have first to see how to create a simple TCP server.

Background

First, a quick recap of the previous article: it introduced the sock class, a handy encapsulation for a Windows socket. It has methods for most functions that you need to invoke when working with sockets. Also, it is a good C++ citizen with copy constructors, assignment operators and so on. This is going to be the basic building block of our TCP server object.

A TCP server listens on a socket by invoking the listen function. When a client connects to the server, the accept function returns another socket and the server can communicate with the client over this newly created socket. A well-behaved server will probably continue to somehow service the original socket. If not, other clients that try to connect to it will not be serviced promptly. A popular architecture (by no means the only one) uses a thread to listen on the primary socket and dispatches other threads to take care of communication with each individual client. Our tcpserver class uses this architecture.

The tcpserver Object

Here is the beginning of the class declaration for tcpserver:

C++
class tcpserver : public sock, public thread
{
public:
  tcpserver (unsigned int max_conn=0, DWORD idle_time = INFINITE, const char *name = 0);
  ~tcpserver ();
...

It is derived from sock, our C++ socket wrapper, and from thread, an encapsulation for Windows threads. The thread class will be discussed in more detail latter in this article. Because it is derived from sock, you can use all the functions available to control this socket's behavior. In particular, you will probably have to bind it to an interface and a port number.

Being derived from thread, this object, when started, creates another thread of execution that keeps waiting for new connections. The other constructor parameters, max_conn and idle_time, specify the maximum number of connections permitted (0 is for unlimited connections) and the maximum time the server thread will wait for a new connection (INFINITE means the server will wait forever).

Included with the code of this article is a small sample that implements an echo server. This server simply waits for lines sent by the client and echoes them back complete with newline. From this sample code, here is how the echo server is constructed:

C++
  tcpserver srv;

  srv.bind (inaddr (INADDR_LOOPBACK, 12321));
...
  srv.start ();
...

We have to look a bit inside the tcpserv implementation (in file tcpserver.cpp) to see what happens when a client connects to the port where the main server thread is listening. As you might imagine, every thread object, and tcpserver is a thread, has a run() function that does the bulk of the work. Taking out the tedious bits, a fragment of the run() function for the tcpserver looks like this:

C++
if (is_readready(0))
{
  /// - check if there is space in connections table
  if (limit && count >= limit)
  {
    //too many connections
    s.close ();
    continue;
  }
  contab_lock.enter ();
  /// - find an empty slot in connections table
  ...
  inaddr peer;
  contab[i] = new conndata;
  contab[i]->socket = accept (peer);
  ...
  /// - invoke make_thread to get a servicing thread
  contab[i]->thread = make_thread (contab[i]->socket);
  ...
  /// - invoke initconn function
  initconn (contab[i]->socket, contab[i]->thread);
  contab_lock.leave ();

Translated into English, it says that the server maintains a table of active connections and, when a new client connects, it invokes a virtual function make_thread to create a new thread to service the connection. It is up to this thread to do whatever it feels appropriate. A simple echo server will just return the lines it received from the client while a HTTP server will implement the HTTP protocol.

This design implies that you will have to derive your own class from tcpserver, with its own make_thread function, to implement a specific server. For simple servers however, like our echo server, there is a shortcut: you can use the set_connfunc() function to pass a function pointer or a lambda expression and the server will create a thread that has this function as body. The signature of the set_connfunc method is:

C++
void set_connfunc (std::function<int (sock&)>f);

It receives a reference to the socket created for the client (as a result of accept() function) and is supposed to carry out the whole conversation with the client.

Here is the whole implementation of our echo server:

C++
int main (int argc, char** argv)
{
  tcpserver srv;

  srv.bind (inaddr (INADDR_LOOPBACK, 12321));

  srv.set_connfunc (
    [](sock& conn)->int {
      sockstream strm (conn);
      std::string line;

      //echo each line
      while (getline (strm, line))
        strm << line << endl;

      return 0;
    }
  );

  srv.start ();
  
  while (_kbhit ())
    ;
  _getch ();
  
  srv.terminate ();
  return 0;
}

This is the whole shebang:

  • We create the server and bind the main socket to a port.
  • The connection function is a lambda expression that receives the connection socket as parameter.
  • It creates a socket stream on that socket and keeps reading lines (using getline function).
  • Each line is sent back to the client.
  • When the client closes connection, the loop breaks and the thread terminates.
  • After starting the server thread, the main thread waits for a key press.
  • When a key is pressed, the server is unceremoniously shut down and whole show stops.

In less than 20 lines of code, we have implemented a fully functional multi-threaded echo server. At Rosetta Code, there is a page with implementation of this server in different programming languages. Compared with the other languages, ours doesn't look that bad.

The Thread Class

This class is part a collection of wrappers for basic Windows synchronization objects. I'm well aware that there are now many synchronization objects (including threads) as part of the standard C++ library. Back in the day when I wrote mine, there were no such niceties. Even today, my wrappers still offer the advantage of closely mimicking the Windows API functions. All these classes are derived from an abstract base class syncbase that encapsulates a Windows handle, be it a thread handle, an event handle, semaphore, mutex, etc.

While most other classes are really thin wrappers over the corresponding Windows API object, thread class is a bit more complicated. Below are the most significant methods of the thread object:

C++
public:
  thread (std::function<int ()> func, const char *name=0);
  virtual       ~thread   ();
  virtual void  start     ();
  ...
protected:
       thread (const char *name=0, bool inherit=false, DWORD stack_size=0, 
               PSECURITY_DESCRIPTOR sd=NULL);

  /// Initialization function called before run
  virtual bool  init      ();

  /// Finalization function called after run
  virtual bool  term      ();;

  /// Thread's body
  virtual void  run       ();

The protected constructor allows you to create an object derived from thread that presumably overrides the run() function to implement thread's behavior.

The public constructor accepts a function pointer or a lambda expression that becomes thread's body. In many cases, it is easier to use this public constructor instead of deriving another object. The draw-back is that you don't have such a finer control like the one provided by methods like init() and term().

Threads are created in a state of "suspended animation". To let them run, you have to call the start() function (do not confuse the two functions: run() represents the body of the thread, start() begins execution of the thread).

We've seen that tcpserver is derived from thread and has its own implementation of the run() function. For an example of thread created from a lambda expression, look at the implementation of tcpserver::make_thread() function:

C++
thread* tcpserver::make_thread (sock& connection)
{
  if (connfunc)
  {
    auto f =
      [&]()->int {
      int ret = connfunc (connection);
      close_connection (connection);
      return ret;
    };
    return new thread (f);
  }
  return NULL;
}

If you remember, we said that the signature of the connection function is:

C++
int f (sock& socket);

Meanwhile, the thread constructor needs a function with the signature:

C++
int f ();

The lambda expression f takes care of invoking the connfunc with the appropriate connection parameter. It also has the proper signature to be passed as argument to thread constructor.

Final Thoughts

  • The code included with this article is cut out from my mlib project. While it can be used as it is, I strongly recommend getting the whole library from GitHub.
  • There is a third article coming up in this series that is going to describe the embeddable HTTP server. Stay tuned!

History

  • 10th June, 2020 - Initial version

License

This article, along with any associated source code and files, is licensed under The MIT License

Share

About the Author

Mircea Neacsu
Canada Canada
No Biography provided

Comments and Discussions

 
-- There are no messages in this forum --