"Linux Gazette...making Linux just a little more fun!"
Linux Socket Programming In C++
By Rob Tougher
Contents
- 1. Introduction
- 2. Overview of Client-Server Communications
- 3. Implementing a Simple Server and Client
- 3.1 Server - establishing a listening socket
- 3.2 Client - connecting to the server
- 3.3 Server - Accepting the client's connection attempt
- 3.4 Client and Server - sending and receiving data
- 4 Compiling and Testing Our Client and Server
- 4.1 File list
- 4.2 Compile and test
- 5. Conclusion
1. Introduction
Sockets are a mechanism for exchanging data between processes. These processes can either be on the same machine, or on different machines connected via a network. Once a socket connection is established, data can be sent in both directions until one of the endpoints closes the connection.I needed to use sockets for a project I was working on, so I developed and refined a few C++ classes to encapsulate the raw socket API calls. Generally, the application requesting the data is called the client, and the application servicing the request is called the server. I created two primary classes, ClientSocket and ServerSocket, that the client and server could use to exchange data.
The goal of this article is to teach you how to use the ClientSocket and ServerSocket classes in your own applications. We will first briefly discuss client-server communications, and then we will develop a simple example server and client that utilize these two classes.
2. Overview of Client-Server Communications
Before we go jumping into code, we should briefly go over the set of steps in a typical client-server connection. The following table outlines these steps:Server | Client |
1. Establish a listening socket and wait for connections from clients. | |
2. Create a client socket and attempt to connect to server. | |
3. Accept the client's connection attempt. | |
4. Send and receive data. | 4. Send and receive data. |
5. Close the connection. | 5. Close the connection. |
3. Implementing a Simple Server and Client
Now its time to dig into the code. In the following section we will create both a client and a server that perform all of the steps outlined above in the overview. We will implement these operations in the order they typically happen - i.e. first we'll create the server portion that listens to the socket, next we'll create the client portion that connects to the server, and so on. All of the code in this section can be found in simple_server_main.cpp and simple_client_main.cpp.If you would rather just examine and experiment with the source code yourself, jump to this section. It lists the files in the project, and discusses how to compile and test them.
3.1 Server - establishing a listening socket
The first thing we need to do is create a simple server that listens for incoming requests from clients. Here is the code required to establish a server socket:listing 1 : creating a server socket ( part of simple_server_main.cpp )
#include "ServerSocket.h" #include "SocketException.h" #includeint main ( int argc, int argv[] ) { try { // Create the server socket ServerSocket server ( 30000 ); // rest of code - // accept connection, handle request, etc... } catch ( SocketException& e ) { std::cout << "Exception was caught:" << e.description() << "\nExiting.\n"; } return 0; }
Notice the try/catch block. The ServerSocket and ClientSocket classes use the exception-handling feature of C++. If a class method fails for any reason, it throws an exception of type SocketException, which is defined in SocketException.h. Not handling this exception results in program termination, so it is best to handle it. You can get the text of the error by calling SocketException's description() method as shown above.
3.2 Client - connecting to the server
The second step in a typical client-server connection is the client's responsibility - to attempt to connect to the server. This code is similar to the server code you just saw:listing 2 : creating a client socket ( part of simple_client_main.cpp )
#include "ClientSocket.h" #include "SocketException.h" #include#include int main ( int argc, int argv[] ) { try { // Create the client socket ClientSocket client_socket ( "localhost", 30000 ); // rest of code - // send request, retrieve reply, etc... } catch ( SocketException& e ) { std::cout << "Exception was caught:" << e.description() << "\n"; } return 0; }
3.3 Server - accepting the client's connection attempt
The next step of the client-server connection occurs within the server. It is the responsibility of the server to accept the client's connection attempt, which opens up a channel of communication between the two socket endpoints.We have to add this functionality to our simple server. Here is the updated version:
listing 3 : accepting a client connection ( part of simple_server_main.cpp )
#include "ServerSocket.h" #include "SocketException.h" #includeint main ( int argc, int argv[] ) { try { // Create the socket ServerSocket server ( 30000 ); while ( true ) { ServerSocket new_sock; server.accept ( new_sock ); // rest of code - // read request, send reply, etc... } } catch ( SocketException& e ) { std::cout << "Exception was caught:" << e.description() << "\nExiting.\n"; } return 0; }
3.4 Client and Server - sending and receiving data
Now that the server has accepted the client's connection request, it is time to send data back and forth over the socket connection.An advanced feature of C++ is the ability to overload operators - or simply, to make an operator perform a certain operation. In the ClientSocket and ServerSocket classes I overloaded the << and >> operators, so that when used, they wrote data to and read data from the socket. Here is the updated version of the simple server:
listing 4 : a simple implementation of a server ( simple_server_main.cpp )
#include "ServerSocket.h" #include "SocketException.h" #includeint main ( int argc, int argv[] ) { try { // Create the socket ServerSocket server ( 30000 ); while ( true ) { ServerSocket new_sock; server.accept ( new_sock ); try { while ( true ) { std::string data; new_sock >> data; new_sock << data; } } catch ( SocketException& ) {} } } catch ( SocketException& e ) { std::cout << "Exception was caught:" << e.description() << "\nExiting.\n"; } return 0; }
If you're paying attention, you'll notice that what we've created here is an echo server. Every piece of data that gets sent from the client to the server gets sent back to the client as is. We can write the client so that it sends a piece of data, and then prints out the server's response:
listing 5 : a simple implementation of a client ( simple_client_main.cpp )
#include "ClientSocket.h" #include "SocketException.h" #include#include int main ( int argc, int argv[] ) { try { ClientSocket client_socket ( "localhost", 30000 ); std::string reply; try { client_socket << "Test message."; client_socket >> reply; } catch ( SocketException& ) {} std::cout << "We received this response from the server:\n\"" << reply << "\"\n";; } catch ( SocketException& e ) { std::cout << "Exception was caught:" << e.description() << "\n"; } return 0; }
4. Compiling and Testing Our Client And Server
Now that we've gone over the basic usage of the ClientSocket and ServerSocket classes, we can build the whole project and test it.4.1 File list
The following files make up our example:- Miscellaneous:
- Makefile - the Makefile for this project
- Socket.h, Socket.cpp - the Socket class, which implements the raw socket API calls.
- SocketException.h - the SocketException class
- Server:
- simple_server_main.cpp - main file
- ServerSocket.h, ServerSocket.cpp - the ServerSocket class
- Client:
- simple_client_main.cpp - main file
- ClientSocket.h, ClientSocket.cpp - the ClientSocket class
4.2 Compile and Test
Compiling is simple. First save all of the project files into one subdirectory, then type the following at your command prompt:prompt$ cd directory_you_just_created prompt$ make
first prompt: prompt$ ./simple_server running.... second prompt: prompt$ ./simple_client We received this response from the server: "Test message." prompt$