Building a simple client/server application is the standard first internet-based application developers attempt. These applications are built on top of the socket communication library, but socket programming in C++ is not obvious as there are no standard libraries, and thus, you have to fall back to the C API. The closest "standardish" sort of thing we have is Boost.asio, which is at the other end of the spectrum in terms of API and involves a cognitive leap to understand what is happening underneath (or you can trust the library maintainers). The other alternative is libcurl; the "easy curl" layer is an abstraction of the socket()
API, while the "multi curl" layer is an abstraction of the pselect()
API that allows multiple sockets to be handled in a single thread.
I am writing a series of articles, starting with a basic C++ client/server application and walking through the process of building a C++ communication library. During this process, I will be using examples from codereview.stackexchange.com to illustrate common mistakes and try to show how to write the code correctly (This will also be a learning exercise for me, so please let me know if you spot a mistake).
Currently, the plan is to write the following articles:
- Client/Server C
- Client/Server C Read/Write
- Client/Server C++ Wrapper
- Mult-Threaded Server
- Non-Blocking Socket
- Co-Routines
Client/Server C++ Basic Version
The minimum example of a working Client/Server application in C++:
The whole working version is here
#include <netinet/in.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define SERVER_BUFFER_SIZE 1024
int main()
{
int socketId = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in serverAddr;
bzero((char*)&serverAddr, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(8080);
serverAddr.sin_addr.s_addr = INADDR_ANY;
bind(socketId, (struct sockaddr *) &serverAddr, sizeof(serverAddr));
listen(socketId, 5);
int finished = 0;
while(!finished)
{
struct sockaddr_storage serverStorage;
socklen_t addr_size = sizeof serverStorage;
int newSocket = accept(socketId, (struct sockaddr*)&serverStorage, &addr_size);
char buffer[SERVER_BUFFER_SIZE];
int get = read(newSocket, buffer, SERVER_BUFFER_SIZE - 1);
buffer[get] = '\0';
fprintf(stdout, "%s\n", buffer);
write(newSocket, "OK", 2);
fprintf(stdout, "Message Complete\n");
close(newSocket);
}
close(socketId);
}
#include <arpa/inet.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define CLIENT_BUFFER_SIZE 1024
int main(int argc, char* argv[])
{
if (argc != 3)
{
fprintf(stderr, "Usage: client <host> <Message>\n");
exit(1);
}
int socketId = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in serverAddr;
socklen_t addrSize = sizeof(serverAddr);
bzero((char*)&serverAddr, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(8080);
serverAddr.sin_addr.s_addr = inet_addr(argv[1]);
connect(socketId, (struct sockaddr*)&serverAddr, addrSize);
write(socketId, argv[2], strlen(argv[2]));
shutdown(socketId, SHUT_WR);
char buffer[CLIENT_BUFFER_SIZE];
size_t get = read(socketId, buffer, CLIENT_BUFFER_SIZE - 1);
buffer[get] = '\0';
fprintf(stdout, "%s %s\n", "Response from server", buffer);
close(socketId);
}
This version of the Client/Server works (a lot of the time) but has a couple of significant issues.
Checking Error Codes
If the calls to socket()
, bind()
, listen()
, or connect()
fail, we have a catastrophic error; any further actions will also fail. A few of the error codes generated by these functions can potentially be recovered from, but most are programming errors or permission failures. As a result, a human-readable message with application termination is an acceptable solution (at this point).
Note: When these functions fail, they set the global variable errno
, which can be translated into a human-readable string with strerror()
. Thus, the simplest solution is to generate an appropriate error message for the user and terminate the application.
Socket Validation
int socketId = socket(PF_INET, SOCK_STREAM, 0);
if (socketId == -1)
{
fprintf(stderr, "Failed: socket()\n%s\n", strerror());
exit(1);
}
Bind Validation
if (bind(socketId, (struct sockaddr *) &serverAddr, sizeof(serverAddr)) == -1)
{
fprintf(stderr, "Failed: bind()\n%s\n", strerror());
close(socketId);
exit(1);
}
Listen Validation
if (listen(socketId, 5) == -1)
{
fprintf(stderr, "Failed: connect()\n%s\n", strerror());
close(socketId);
exit(1);
}
Connect Validation
if (connect(socketId, (struct sockaddr*)&serverAddr, addrSize) == -1)
{
fprintf(stderr, "Failed: connect()\n%s\n", strerror());
close(socketId);
exit(1);
}
Summary
The basic socket programs are relatively trivial. However, version 1 has some obvious flaws, the foremost of which is checking error states (which many beginners forget in their first version). The following article will provide more details about read and write operations on the socket.
Inspiration for Article