You are on page 1of 15

TCP/IP Network Programming Design

Patterns in C++

Author: Vic Hargrave


Published: February 8, 2013

Updated: September 17, 2015

Category: Programming

Tags: Design Patterns, Network, Sample Code, Sockets, TCP/IP

Network programming with the BSD Sockets API involves


making a series of boilerplate calls to several operating system level functions every time you
want to create connections and transfer data over TCP/IP networks. This process can be both
cumbersome and error prone.
Fortunately there is an easier way to develop network applications. By thinking in terms
of design patterns, we can devise abstractions for creating connections and transferring data
between network peers that encapsulate socket calls in easy to use C++ classes.

Contents [show]

Network Programming Basics


Internet Model
Before launching into the design patterns, lets go over some basics of network programming
with BSD Sockets.
The Internet model is a subset of the Open Systems Interconect (OSI) model that describes how
network protocols and equipment should interoperate. The mapping of the Internet stack layers
to the OSI model is illustrated below.

The Internet application layer combines the application, presentation and session layers of the
OSI model. Its in this layer where the Internet protocols HTTP, SSH, DNS, etc. are
implemented that directly interact with Internet applications.
At the bottom of the OSI stack is the datalink and physical layers which map to a single Network
Link layer in the Internet model. Network drivers are implemented here that provide the Network
layer with the means to send packets over physical network media such as Ethernet, PPP and
ADSL.
The Network and Transport layers are the same across both models. The Network layer in the
Internet model provides connectionless Internet protocol packet delivery, host IP addresses and
routing hosts and other networks. The ICMP, ARP and DHCP are implemented in the Network
layer on top of IP.
Both TCP and UDP protocols live in the transport layer and add the concept of ports to
differentiate applications running on a given host. TCP provides connection based, reliable

network communication and stream based data delivery services. Reliability is achieved through
retransmission of dropped packets. UDP provides connectionless and packet based delivery
where the data is delivered in datagrams packets with port numbers. UDP, like IP, gives only
best effort data delivery without retransmissions of dropped packets.
BSD Sockets is an API to the transport layer of the Internet Protocol Stack. It supports creating
both TCP and UDP network I/O.

Socket Workflow
To establish TCP connections the server host calls socket() to create a listening socket then
specifies the IP address and TCP port on which the server will receive connection requests with a
call to bind(). Calling listen() puts the server into listening mode which then blocks on the
accept() waiting for incoming connections.

The client connects to the server by calling socket() then connect() with a socket address that
includes the IP address and TCP port specifying used for the bind() call on the server. On the
server the accept() function returns with a connection socket descriptor when the clients
connection request is received.
After connecting the server blocks on a call to read() waiting for a client request. The client
calls write() to send a request then blocks on a call to read() waiting for the servers response.
When the server is done processing the request, it sends back a response to the client. The
exchange of requests and responses repeats until the client is done, at which time it closes the
connection. The server detects this event when read() returns 0. The server responds by closing
its end of the connection then returning to get another connection.
In most servers connections are accepted in one thread and a new thread or process is created to
handle each connection. To keep things simple the example here describes an iterative server
where each request is handled one at a time.

Network Programming Patterns


The key to designing an object-oriented network programming API is to recognize that TCP/IP
network programs involve three basic pattens of usage or behaviors: actively connecting to
servers, passively accepting connections from clients and transferring data between network
peers in other words clients and servers. Each behavior suggests a distinct abstraction that can
be implemented in a separate class.

Encapsulates the socket mechanisms to actively connect to a server.


This is a factory class which produces TCPStream objects when client applications
establish connections with servers.
TCPAcceptor - Encapsulates the socket mechanisms to passively accept connections
from a client. This is also a factory class which produces TCPStream objects when server
applications establish connections with clients
TCPConnector -

Provides network I/O mechanisms and returns IP address and TCP port of
peer applications.
TCPStream -

For the code examples in this blog, each of these classes has an include file (.h) and source file
(.cpp) of the same name. For example, tcpconnector.h and tcpconnector.cpp for
the TCPConnector class.

TCPStream Class
Interface
The TCPStream class provides methods to send and receive data over a TCP/IP connection. It
contains a connected socket descriptor and information about the peer either client or server
in the form of the IP address and TCP port. TCPStream includes simple get methods that return

address and port, but not the socket descriptor which is kept private. One of the advantages of
programming with objects is the ability to logically group data members and methods to avoid
exposing data, in this case the socket descriptor, to the calling program that it does not need to
see. Each connection is completely encapsulated in each TCPStream object.
TCPStream objects
are
the TCPStream constructors

created by TCPConnector and TCPAcceptor objects only, so


must be declared private to prevent them from being called directly
by
any
other
objects.
The TCPStream class
grants
friend
privileges
to
the TCPConnector and TCPAcceptor classes so they can access the TCPStream constructors to
supply connected socket descriptors.
1
2
3 #include <sys/types.h>
#include <sys/socket.h>
4 #include <unistd.h>
5 #include <string>
6
7 using namespace std
8
9 class TCPStream
10{
int
m_sd;
11
string m_peerIP;
12
int
m_peerPort;
13
14 public:
15
friend class TCPAcceptor;
friend class TCPConnector;
16
17
~TCPStream();
18
19
ssize_t send(char* buffer, size_t len);
20
ssize_t receive(char* buffer, size_t len);
21
22
string getPeerIP();
23
int getPeerPort();
24
25 private:
TCPStream(int sd, struct sockaddr_in* address);
26
TCPStream();
27
TCPStream(const TCPStream& stream);
28};
29
30

Constructor

The constructor stores the connected socket descriptor then converts the socket information
structure fields to a peer IP address string and peer TCP port. These parameters can be inspected
with calls to TCPStream::getPeerIP() and TCPStream::getPeerPort().
1
<arpa/inet.h>
2 #include
#include "tcpstream.h"
3
4 TCPStream::TCPStream(int sd, struct sockaddr_in* address) : m_sd(sd) {
5
char ip[50];
inet_ntop(PF_INET, (struct in_addr*)&(address->sin_addr.s_addr),
6
ip, sizeof(ip)-1);
7
m_peerIP = ip;
8
m_peerPort = ntohs(address->sin_port);
9 }
10

Destructor
The destructor simply closes the connection.
1TCPStream::~TCPStream()
2{
close(m_sd);
3
}
4

Network I/O Methods


and TCPStream::receive() simply wrap calls to read() and write(),
returning the number of bytes sent and bytes received, respectively. No additional buffering or
other capabilities are added.
TCPStream::send()

Get Peer Information


and TCPStream::getPeerPort() return the IP address and TCP port
information of the peer to which the network application, client or server, are connected. You can
get the same information from the sockets getpeername() function but it far easier to just
capture that information when the connections are established. Clients know in advance to where
they are connecting and the clients socket address is returned the accept() function when the
server accepts a client connection see the TCPAcceptor::accept() method definition. In both
cases the socket address information is passed to the TCPStream object when it is constructed.
TCPStream::getPeerIP()

TCPConnector Class
Interface

provides the connect() method to actively establish a connection with a server.


It accepts the server port and a string containing the server host name or IP address. If successful,
a pointer to a TCPStream object is returned to the caller.
TCPConnector

1
<netinet/in.h>
2 #include
#include "tcpstream.h"
3
4 class TCPConnector
5 {
public:
6
TCPStream* connect(int port, const char* server);
7
8
private:
9
int resolveHost(const char* host, struct in_addr* addr);
10};
11

Constructor/Destructor
The TCPConnector class does not use any member variables so the default constructor and
destructor generated by the C++ compiler are fine. No others are defined.

Connect to Server
[Lines 6-12] TCPConnector::connect() call takes a server host name or IP address string and
the server listening port as arguments. The server struct sockaddr_in sin_family is set to
PF_INET and the sin_port is set to the TCP port on which the server is listening for connections.
1 #include <string.h>
2 #include <netdb.h>
#include <arpa/inet.h>
3 #include "tcpconnector.h"
4
5 TCPStream* TCPConnector::connect(const char* server, int port)
6 {
struct sockaddr_in address;
7
8
memset (&address, 0, sizeof(address));
9
address.sin_family = AF_INET;
10
address.sin_port = htons(port);
11
if (resolveHostName(server, &(address.sin_addr)) != 0) {
inet_pton(PF_INET, server, &(address.sin_addr));
12
}
13
int sd = socket(AF_INET, SOCK_STREAM, 0);
14
if (::connect(sd, (struct sockaddr*)&address, sizeof(address)) != 0) {
15
return NULL;
}
16
return new TCPStream(sd, &address);
17
}
18
19

20
21
[Lines 13-15] TCPConnector::resolveHost() to convert the DNS host name string to an IP
address. If this call fails the assumption is made the server string contains an IP address and it is
converted to an IP address in network byte order.
[Lines 16] The first argument to socket() selects the protocol family and the second specifies
the nature of the network communication. Together PF_INET and SOCK_STREAM mandate the
TCP/IP protocol.
[Lines 17-20] We call ::connect() passing it the socket descriptor, pointer to the server struct
sockaddr_in structure, cast to a struct sockaddr pointer, and the length of the server address
structure. The ::connect() call is prefeced with the :: qualifier so the compiler does not confuse
this function with TCPConnector::connect(). If ::connect() succeeds a TCPStream object
is created with the connected socket descriptor and the server socket address information and a
pointer to the TCPStream object is returned to the caller.

Resolve Host Name


[Lines 5-10] TCPConnector::resolveHostName() converts a DNS host name to an IP address
in network byte order by calling getaddrinfo(). This function was chosen over
gethostbyname() since it is thread safe whereas gethostbyname() is not. If the host name is
not a valid DNS name, i.e. it is an IP address string or something else, -1 is returned, otherwise 0
is returned.
1
2 int TCPConnector::resolveHostName(const char* hostname, struct in_addr* addr)
3 {
struct addrinfo *res;
4
5
int result = getaddrinfo (hostname, NULL, NULL, &res);
6
if (result == 0) {
memcpy(addr, &((struct sockaddr_in *) res->ai_addr)->sin_addr,
7
sizeof(struct in_addr));
8
freeaddrinfo(res);
9
}
10
return result;
11}
12

TCPAcceptor Class
Interface

includes member variables for the listening socket descriptor, the socket address
information IP address and TCP port and a flag that indicates whether or not the
TCPAcceptor has started listening for connections.
TCPAcceptor

Two public methods are supported. One to start the listening and the other to accept connections.
1
2 #include <string>
3 #include <netinet/in.h>
4 #include "tcpstream.h"
5
using namespace std;
6
7 class TCPAcceptor
8 {
9
int m_lsd;
string m_address;
10
int m_port;
11
12
bool m_listening;
13
14 public:
TCPAcceptor(int port, const char* address="");
15
~TCPAcceptor();
16
17
int
start();
18
TCPStream* accept();
19
20 private:
TCPAcceptor() {}
21
};
22
23

Constructor
The constructor sets the member variables to as shown here. Setting m_lsd indicates that the
lisetning socket has not been created.
1#include <stdio.h>
2#include <string.h>
3#include <arpa/inet.h>
4#include "tcpacceptor.h"
5
6TCPAcceptor::TCPAcceptor(int port, const char* address)
: m_lsd(0), m_port(port), m_address(address), m_listening(false) {}
7

Destructor
If the listening socket has been created then it is closed in the destructor.

1TCPAcceptor::~TCPAcceptor()
2{
if (m_lsd > 0) {
3
close(m_lsd);
4
}
5
}
6

Start Listening for Connections


[Line 3-5] Creating a listening socket involves the most socket calls of any operation. Before
going through the series of calls, TCPAcceptor::start() checks to see if a listening socket
already exists. If so, the method just returns 0.
1 int TCPAcceptor::start()
2 {
if (m_listening == true) {
3
return 0;
4
}
5
m_lsd = socket(PF_INET, SOCK_STREAM, 0);
6
7
struct sockaddr_in address;
8
memset(&address, 0, sizeof(address));
9
address.sin_family = PF_INET;
10
address.sin_port = htons(m_port);
11
if (m_address.size() > 0) {
inet_pton(PF_INET, m_address.c_str(), &(address.sin_addr));
12
}
13
else {
14
address.sin_addr.s_addr = INADDR_ANY;
15
}
16
int optval = 1;
17
setsockopt(m_lsd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof optval);
18
19
int result = bind(m_lsd, (struct sockaddr*)&address, sizeof(address));
20
if (result != 0) {
21
perror("bind() failed");
22
return result;
}
23
result = listen(m_lsd, 5);
24
if (result != 0) {
25
perror("listen() failed");
26
return result;
27
}
m_listening = true;
28
return result;
29
}
30
31
32
33

34
35
[Line 7] First we create a listening socket descriptor for TCP/IP. The socket() call for servers is
the same as it is for clients.
[Line 9-12] Next we initialize a socket address structure setting the protocol family PF_INET
and the listening TCP port.
[Line 13-18] If the server listening IP address has m_address has been set, inet_ntop() is
called to convert it to a numerical IP address in network byte order. If inet_ntop() fails then the
socket listening address is set to any IP address meaning the server will listening for connections
on all the network interfaces.
[Line 20-21] Normally when you stop a server listening on a given IP address and port, it takes a
few seconds before you can starting listening on the same IP address and port when you restart
your server. To disable this condition and make it possible to immediately resue a listening port,
we set the SO_REUSEADDR socket option for the listening socket before calling bind().
[Line 23-27] Bind the listening socket address to the socket descriptor. If bind() fails display
and error message then return value returned by bind().
[Line 28-34] Turn on server listening with the listen() function. The second argument of this
function sets the number of connection requests TCP will queue. This may not be supported for
your particular operating system. If listen() fails, display an error message. Otherwise, set the
m_listening flag to true and return the listen() call return value

Accept Connections from Clients


[Lines 3-10] TCPAcceptor::accept() returns NULL if the socket is not in a listening state.
Otherwise a sockaddr_in structure is set to NULL and a pointer to it, cast as a sockaddr
structure, is passed to ::accept(). The ::accept() call is qualified by the :: operator so the
compiler does not confuse this function with the TCPAcceptor::accept(). The ::accept()
blocks until a connections is received.
1 TCPStream* TCPAcceptor::accept()
2 {
if (m_listening == false) {
3
return NULL;
4
}
5
struct sockaddr_in address;
6
socklen_t len = sizeof(address);
7
memset(&address, 0, sizeof(address));
8
int sd = ::accept(m_lsd, (struct sockaddr*)&address, &len);
9
if (sd < 0) {
10
perror("accept() failed");
return NULL;
11

12
13
14
15}
16

}
return new TCPStream(sd, &address);

[Lines 11-15] When a connection with a client is established, the socket address structure is
populated with the clients socket information and ::accept() returns 0. Then a TCPStream

Test Applications
Echo Server
First lets build a server with the TCPAcceptor class. To keep things simple well just make an
iterative server that handles one connection at a time. The server will be defined in the file
server.cpp.
1 #include <stdio.h>
2 #include <stdlib.h>
#include "tcpacceptor.h"
3
4 int main(int argc, char** argv)
5 {
if (argc < 2 || argc > 4) {
6
printf("usage: server <port> [<ip>]\n");
7
exit(1);
8
}
9
10
TCPStream* stream = NULL;
11
TCPAcceptor* acceptor = NULL;
if (argc == 3) {
12
acceptor = new TCPAcceptor(atoi(argv[1]), argv[2]);
13
}
14
else {
15
acceptor = new TCPAcceptor(atoi(argv[1]));
}
16
if (acceptor->start() == 0) {
17
while (1) {
18
stream = acceptor->accept();
19
if (stream != NULL) {
20
size_t len;
char line[256];
21
while ((len = stream->receive(line, sizeof(line))) > 0) {
22
line[len] = NULL;
23
printf("received - %s\n", line);
24
stream->send(line, len);
25
}
delete stream;
26
}
27
}
28
}
29
perror("Could not start the server");

30
31
32
33
34}
35
36
37

exit(-1);

[Lines 5-10] The server accepts the listening TCP port and optionally the listening IP Address on
the command line. If the number of arguments is not correct an error message is displayed
informing the user how to correctly invoke the application.
[Lines 12-20] The TCPAcceptor object is created with the command line arguments. Minimally
the IP address must be specified. Then the server starts listening for connections.
[Lines 21-32] If the call to TCPAcceptor::start() is successful, the server continually and
indefinitely accepts connections from clients and processes each connection one at a time.
Processing consists of getting a string of bytes from the client, displaying the string and returning
it to the client. The string of bytes is NULL terminated at the index in the receive buffer equal to
the value returned by the receive operation. This is repeated until the client closes the connection
indicated by a return value of 0 from TCPStream::receive(). Deleting the stream object closes
the connection on the server side.

Echo Client
The client application takes the server TCP port and IP address on the command line. For each
connection a string is displayed and sent to the server, the echoed string is received back and
displayed, then the connection is closed. The client will be defined in the file client.cpp.
1 #include <stdio.h>
2 #include <stdlib.h>
#include <string>
3 #include "tcpconnector.h"
4
5 using namespace std;
6
7 int main(int argc, char** argv)
8 {
if (argc != 3) {
9
printf("usage: %s <port> <ip>\n", argv[0]);
10
exit(1);
11
}
12
int len;
13
string message;
14
char line[256];
15
TCPConnector* connector = new TCPConnector();
16
TCPStream* stream = connector->connect(argv[2], atoi(argv[1]));

17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38}
39
40
41

if (stream) {
message = "Is there life on Mars?";
stream->send(message.c_str(), message.size());
printf("sent - %s\n", message.c_str());
len = stream->receive(line, sizeof(line));
line[len] = NULL;
printf("received - %s\n", line);
delete stream;
}
stream = connector->connect(argv[2], atoi(argv[1]));
if (stream) {
message = "Why is there air?";
stream->send(message.c_str(), message.size());
printf("sent - %s\n", message.c_str());
len = stream->receive(line, sizeof(line));
line[len] = NULL;
printf("received - %s\n", line);
delete stream;
}
exit(0);

Build and Run


You
can
get
the
source
code
for
the
project
from
Github
https://github.com/vichargrave/tcpsockets.git. Create the test apps by running make. You can
build the client and server separately by running:

make -f Makefile.client
make -f Makefile.server

First run the server on port 9999 and localhost in a terminal window:
$ server 9999 localhost

In another terminal window run the client and you should get the following output:
$ client 9999 localhost
sent - Is there life on Mars?
received - Is there life on Mars?
sent - Why is there air?
received - Why is there air?

The server output should look like this:


received - Is there life on Mars?
received - Why is there air?

You might also like