Socket Programming In C++ Using boost.asio - TCP Server And Client

Socket programming is nothing of a new concept for programmers. Ever since the internet came into existence, it shifted the paradigm to internet-enabled applications. That’s where network programming models start to appear. A socket is fundamentally the most basic technology of this network programming. Leave alone the technology for the moment. In most cases, it’s literally just a client-server model working. The server is supposed to serve the information requested or the required services by the client. The following analogy will help you understand the model. 

 

But how does that transfer of information take place? Well, that involves networking services from the transport layer, often referred to as TCP/IP (Transport Control Protocol/Internet Protocol). For example, when you open your browser and search for something, you’re merely requesting a server for some information over HTTP (not to forget, HTTP is nothing but an application using TCP/IP service). But where are the sockets? Let me refer to you back to the line where I said sockets are the “base” and so, they provide the programming interface for these protocols to work on. Generally speaking, sockets are providing a way for two processes or programs to communicate over the network. Sockets provide sufficiency and transparency while causing almost no communication overhead.

Why C++ though?

As I mentioned earlier, sockets are merely providing an interface for network programming and have nothing to do with programming language used for implementation. C++ might be the best choice in this regard because of the speed and efficiency that it brings to the table. Some might not agree with me at this statement because of implied complexity by the language including, but not restricted to, manual memory management, template syntax, library incompatibility, compiler etc. But I think differently. C++ will let you dig deep and see the insights of what actually is going on at the lower level although good concept and knowledge in computer networks is not debatable. This article will help you give a soft start with socket programming in C++ using boost library. But before digging into the code, let’s clarify a few points a bit more.

What socket programming is all about?

Let’s talk about what a socket actually is and how it plays its role in communication.

A socket is merely one endpoint of a two-way communication link. It represents a single connection between two entities that are trying to communicate over the network most of the time, which are server and client. More than two entities can also be set to communicate but by using multiple sockets.

This socket-based communication is done over the network; one end of which could be your computer while other could be at the other end of the world (considering again the browsing example) or over the same machine (localhost). Now, the question arises how the server knows that a client is requesting for connection and also which service is being requested? This all is the game of IP address and port number. Every computer has a specific IP address which will be used to identify it. (If you’re accessing a website, then the name will eventually be translated into IP address.). Which service is distinguished by port number?

Now, to sum it all up when a client is requesting a server for services, it opens a socket and passes the request to the server by specifying its IP address and port number (to let the server know which service is meant to be provided). The server will accept the connection request and transfer the data or provide any other service requested. Once the request is served, the connection will be closed. Observe the workflow in the following diagram.

 

Why Boost.Asio?

Writing networking code that is portable is easy to maintain has been an issue since long. C++ took a step to resolve this issue by introducing boost.asio. It is a cross-platform C++ library for network and low-level I/O programming that provides developers with a consistent asynchronous model using a modern C++ approach. Here’s a list of what it offers.

  • Cross-platform networking code (code would work on Windows, Linux etc.)
  • IPv4 and IPv6 support
  • Asynchronous event support
  • Timer support
  • iostream compatibility

We are not going to dig deep into networking but rather will develop a simple client-server model and see how things work. So without any further delay, let’s get started.

Environment setup

I’m currently in Linux (18.04 LTS) so would be covering environment setup for the same.

We just need the following to get started.

  • boost.asio
  • C++ compiler (preferably g++)
  • Text-editor

The simplest way to get asio on Linux is by executing the following command.

  1. $ sudo apt-get install libboost-all-dev  

If you’re using some other platform or the above doesn’t seem a good fit for you, follow the document here to get asio on your system.

The next step is to make sure you have C++ compiler on your system. I’m using g++. You can get the same with the following command in Linux.

  1. $ sudo apt-get install g++  

Once you have the compiler, you’re good to follow along. I don’t have any text-editor preference. You can choose one of your choices.

Now that we have everything, we are in a position to start coding for our TCP server-client model.

TCP server

As we talked earlier in the article, the server specifies an address for the client at which it makes a request to the Server. The server listens to the new connection and responds accordingly. Now, here are the steps for our server development.

Of course, we need to import our libraries before anything else. So, here we go.

  1. #include <iostream>  
  2. #include <boost/asio.hpp>  
  3.   
  4. using namespace boost::asio;  
  5. using ip::tcp;  
  6. using std::string;  
  7. using std::cout;  
  8. using std::endl;  

Using namespace std is considered a bad practice for the reason that it imports all sorts of names globally and can cause ambiguities. As we just need three names that belong to std namespace, it's better to import them separately or else suit yourself.

We want our server to receive a message from the client and then respond back. For that, we need two functions, for read and write operations respectively.

  1. string read_(tcp::socket & socket) {  
  2.        boost::asio::streambuf buf;  
  3.        boost::asio::read_until( socket, buf, "\n" );  
  4.        string data = boost::asio::buffer_cast<const char*>(buf.data());  
  5.        return data;  
  6. }  
  7.   
  8. void send_(tcp::socket & socket, const string& message) {  
  9.        const string msg = message + "\n";  
  10.        boost::asio::write( socket, boost::asio::buffer(message) );  
  11. }  

Let’s break things down a little bit. Here, we are using TCP Socket for communication. The read_until and write functions from boost::asio have been used to perform the desired function. The boost::asio::buffer function creates a buffer of the data that is being communicated.

Now that we have our functions, let’s kick the server in.

  1. int main() {  
  2.       boost::asio::io_service io_service;  
  3.   
  4. //listen for new connection  
  5.       tcp::acceptor acceptor_(io_service, tcp::endpoint(tcp::v4(), 1234 ));  
  6.   
  7. //socket creation  
  8.       tcp::socket socket_(io_service);  
  9.   
  10. //waiting for the connection  
  11.       acceptor_.accept(socket_);  
  12.   
  13. //read operation  
  14.       string message = read_(socket_);  
  15.       cout << message << endl;  
  16.   
  17. //write operation  
  18.       send_(socket_, "Hello From Server!");  
  19.       cout << "Servent sent Hello message to Client!" << endl;  
  20.       return 0;  
  21. }  

The io_service object is needed whenever a program is using asio. tcp::acceptor is used to listening to the connection requested by the client. We are passing two arguments to the function; one is the same io_service object we declared previously and the next is the end point of connection being initialised to ipv4 and 1234 port. Next, the server will create a socket and wait for a connection from the client. As soon as the connection is built, our read and write operations will be executed and the connection will be closed.

TCP client

We need the other end for our communication as well, i.e., a client that request server. Basic structure is same as we did for server.

  1. #include <iostream>  
  2. #include <boost/asio.hpp>  
  3.   
  4. using namespace boost::asio;  
  5. using ip::tcp;  
  6. using std::string;  
  7. using std::cout;  
  8. using std::endl;  

Those are the same imports as we did for server. Nothing is new.

  1. int main() {  
  2.      boost::asio::io_service io_service;  
  3.   
  4. //socket creation  
  5.      tcp::socket socket(io_service);  
  6.   
  7. //connection  
  8.      socket.connect( tcp::endpoint( boost::asio::ip::address::from_string("127.0.0.1"), 1234 ));  
  9.   
  10. // request/message from client  
  11.      const string msg = "Hello from Client!\n";  
  12.      boost::system::error_code error;  
  13.      boost::asio::write( socket, boost::asio::buffer(msg), error );  
  14.      if( !error ) {  
  15.           cout << "Client sent hello message!" << endl;  
  16.      }  
  17.      else {  
  18.           cout << "send failed: " << error.message() << endl;  
  19.      }  
  20.   
  21. // getting a response from the server  
  22.      boost::asio::streambuf receive_buffer;  
  23.      boost::asio::read(socket, receive_buffer, boost::asio::transfer_all(), error);  
  24.      if( error && error != boost::asio::error::eof ) {  
  25.           cout << "receive failed: " << error.message() << endl;  
  26.      }  
  27.      else {  
  28.           const char* data = boost::asio::buffer_cast<const char*>(receive_buffer.data());  
  29.           cout << data << endl;  
  30.      }  
  31.      return 0;  
  32. }  

We again started by creating the io_service object and creating the socket. We need to connect to the server using localhost (IP 127.0.0.1) and specifying the same port as we did for the server to establish the connection successfully. Once the connection is established, we’re sending a hello message to the server using boost::asio::write. If the message is transferred, then the server will send back the response. For this purpose, we have boost::asio::read function to read back the response. Now, let‘s run our program to see things in action.

Compile and run the server by executing the following command.

  1. $ g++ server.cpp -o server –lboost_system  
  2.   
  3. $ ./server  

 Move to the other terminal window to run client.

  1. $ g++ client.cpp -o client –lboost_system    
  2.   
  3. $ ./client    

 
 
Observe the workflow from the above output. The client sent its request by saying hello to the server after which the server responded back with hello. Once the data transfer is complete, the connection is closed.

TCP asynchronous Server

The above programs explain our simple synchronous TCP server and client in which we did the operations in sequential order, i.e. reading from socket then write. Each operation is blocking which means read operation should finish first and then we can do the write operation. But what if there are more than one clients trying to connect to the server? In case we don’t want our main program to be interrupted while we're reading from or writing to a socket, a multi-threaded TCP client-server is required to handle the situation. The other option is having an asynchronous server. We can start the operation, but we don’t know when it will end, we don’t need to know pre-hand either because it is not blocking. The other operations can be performed side by side. Now that we know the basics, let‘s dive in and try to create an asynchronous server.

  1. //importing libraries  
  2. #include <iostream>  
  3. #include <boost/asio.hpp>  
  4. #include <boost/bind.hpp>  
  5. #include <boost/enable_shared_from_this.hpp>  
  6.   
  7. using namespace boost::asio;  
  8. using ip::tcp;  
  9. using std::cout;  
  10. using std::endl;  

 We have two new imports bind and enable_shared_from_this. We’ll be using the former to bind an argument to a specific value and route input arguments into arbitrary positions. The latter is to get a valid shared_ptr instance.

Let’s define a class to handle the connection as follow:

  1. class con_handler : public boost::enable_shared_from_this<con_handler>  
  2. {  
  3.      private:  
  4.           tcp::socket sock;  
  5.           std::string message="Hello From Server!";  
  6.           enum {length = 1024};  
  7.           char data[length];  
  8.      public:  
  9.           typedef boost::shared_ptr<con_handler> pointer;  
  10.           con_handler(boost::asio::io_service& io_service): sock(io_service){}  
  11.   
  12. // creating the pointer  
  13.      static pointer create(boost::asio::io_service& io_service){  
  14.           return pointer(new con_handler(io_service));  
  15.      }  
  16.   
  17. //socket creation  
  18.      tcp::socket& socket(){  
  19.           return sock;  
  20.      }  
  21.   
  22.      void start(){  
  23.           sock.async_read_some(boost::asio::buffer(data, length),  
  24.                boost::bind(&con_handler::handle_read,  
  25.                shared_from_this(),  
  26.                boost::asio::placeholders::error,  
  27.                boost::asio::placeholders::bytes_transferred));  
  28.   
  29.           sock.async_write_some(boost::asio::buffer(message, length),  
  30.                boost::bind(&con_handler::handle_write,  
  31.                shared_from_this(),  
  32.                boost::asio::placeholders::error,  
  33.                boost::asio::placeholders::bytes_transferred));  
  34.           }  
  35.   
  36.      void handle_read(const boost::system::error_code& err,size_t bytes_transferred){  
  37.           if (!err) {  
  38.                cout << data << endl;  
  39.             }   
  40.           else {  
  41.                std::cerr << "error: " << err.message() << std::endl;  
  42.                sock.close();  
  43.             }  
  44.       }  
  45.   
  46.      void handle_write(const boost::system::error_code& err,size_t bytes_transferred){  
  47.           if (!err) {  
  48.           cout << "Server sent Hello message!"<< endl;  
  49.           }   
  50.           else {  
  51.           std::cerr << "error: " << err.message() << endl;  
  52.           sock.close();  
  53.           }  
  54.      }  
  55. };  

 The shared_ptr and enabled_shared_from_this is to keep our object alive for any operation that refers to it. Then we created the socket pretty much the same way as we did in case of synchronous server. Now is the time to specify the functions we want to perform using that socket. We are using async_read_some and async_write_some functions to achieve the same functionality as that of our previously developed server but now asynchronously. boost::bind is being used to bind and route the arguments to handle_read/write. The handle_read/write function will now be responsible for any further action. If you look into the handle_read/write definition, it has the same arguments as last two of boot::bind and is performing some action in case the data is successfully transferred between client and server or not.

  1. class Server {  
  2.      private:  
  3.           tcp::acceptor acceptor_;  
  4.           void start_accept(){  
  5.      // socket  
  6.           con_handler::pointer connection =  
  7.           con_handler::create(acceptor_.get_io_service());  
  8.      // asynchronous accept operation and wait for a new connection.  
  9.           acceptor_.async_accept(connection->socket(),  
  10.                boost::bind(&Server::handle_accept, this, connection,  
  11.                boost::asio::placeholders::error));  
  12.           }  
  13.   
  14.      public:  
  15.      // constructor for accepting connection from client  
  16.      Server(boost::asio::io_service& io_service): acceptor_(io_service, tcp::endpoint(tcp::v4(), 1234)){  
  17.      start_accept();  
  18.      }  
  19.        
  20.      void handle_accept(con_handler::pointer connection,const boost::system::error_code& err){  
  21.           if (!err) {  
  22.           connection->start();  
  23.           }  
  24.      start_accept();  
  25.      }  
  26. };  

 Our server class will create a socket and will start the accept operation to wait for connection asynchronously. We also defined a constructor for our server to start listening for connection on the specified port. If no error occurs, connection with the client will be established. Here’s our main function.

  1. int main(int argc, char *argv[])  
  2. {  
  3.      try{  
  4.           boost::asio::io_service io_service;  
  5.           Server server(io_service);  
  6.           io_service.run();  
  7.      }  
  8.   
  9.      catch(std::exception& e){  
  10.           std::cerr << e.what() << endl;  
  11.      }  
  12.      return 0;  
  13. }  

 Again, we need our io_service object along with an instance of Server class. The run() function will block until all the work is done and until the io_service is stopped.

It’s time to let our monster out. Compile and run the above code with:

  1. $ g++ async_server.cpp -o async_server –lboost_system  
  2.   
  3. $ ./async_server  

 Run your client again.

  1. $ ./client  

 

 
 
Note that the client closed the connection after exchanging the data but the server is still up and running. New connections can be made to the server or else it will continue to run until explicitly asked to stop. If we want it to stop then we can do the following.
  1. boost::optional<boost::asio::io_service::work> work = boost::in_place(boost::ref(io_service));  
  2. work = boost::none;  

This will tell run() that all work is done and not to block anymore.

End words

This article is not meant to show you the best practices or making your pro in network programming rather focused to give you an easy start with socket programming in boost.asio. It is a pretty handy library so if you’re interested in some high-end network programming, then I would encourage you to take a deep dive and play around it more. Also, the source code of both server and client is attached. Feel free to make some changes and let me know if you have some good ideas.


Similar Articles