Echo Client and Server
In this section we will put everything we've learned about TCP/IP together, and implement a simple networking application - the echo server and client. The echo server/client is a set of (at least) two applications. The echo server listens for incoming TCP connections, and once a connection is established, will return any message sent do it by the client right back to the very same client - slightly transformed. For this example, the client will send text to the server, and the server will send back the same text, capitalized.
Here's the sequence of events:
- Echo server starts, and begins listening for incoming connections
- A client connects to the server
- A client sends text via the TCP socket (the text will be entered by the user)
- The server will transform the text into all capital letters and send it back to the client
- The client will receive the capitalized text and print it to the screen.
If the client sends the word "quit", then the server will respond with "Good bye" and terminate the connection. After terminating the connection, it will continue to listen for more connections from additional clients.
Implementation - C++ Echo Server
Most of the code in this book is JavaScript. It's important to understand that the web, networking, and TCP / IP are all language agnostic however. Applications can communication with TCP/IP no matter what programming language they are written in, and there is no reason to ever believe the server and client will be written in the same programming language.
To reinforce this, we'll present the server and client in C++ first. The C++ code presented here might seem really foreign to you - don't worry about it! It's specific to the POSIX environment (actually, MacOS). Don't worry about understanding the code in detail - instead, closely look at the steps involved. We will then substituted the C++ client with a JavaScript implementation, and show how it can still talk to the C++ echo server. Finally, we'll replace the C++ server with a JavaScript server.
// Headers for MacOS
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netdb.h>
// Standard C++ headers
#include <iostream>
#include <string>
#include <thread>
const u_short LISTENING_PORT = 8080;
// Capitalizes the input recieved from client
// and returns the response to be sent back.
std::string make_echo_response(std::string input)
{
std::string response(input);
for (int i = 0; i < response.length(); i++)
{
response[i] = toupper(response[i]);
}
return response;
}
// The client connection is handled in a new thread.
// This is necessary in order to allow the server to
// continue to accept connections from other clients.
// While not necessary, this is almost always what servers
// do - they should normally be able to handle multiple
// simulusatneous connections.
void do_echo(int client_socket)
{
std::cout << "A new client has connected." << std::endl;
while (true)
{
char buffer[1024];
std::string input;
int bytes_read = read(client_socket, buffer, 1024);
if (bytes_read <= 0)
{
std::cout << "Client has disconnected." << std::endl;
break;
}
input = std::string(buffer, bytes_read);
std::cout << "Received: " << input << std::endl;
std::string response = make_echo_response(input);
std::cout << "Sending: " << response << std::endl;
// Send the message back to the client
write(client_socket, response.c_str(), response.length());
if (response == "QUIT")
{
std::cout << "QUIT command received. Closing connection." << std::endl;
break;
}
}
// Close the client socket
close(client_socket);
}
int main()
{
// Create the listening socket
// This call creates a "file descriptor" for the socket we will listen
// on for incoming connections.
int listening_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// Next we initialize a data structure that will be used to attach
// the listening socket to the correct port number, along with some
// other standard attributes.
struct sockaddr_in ss;
memset((char *)&ss, 0, sizeof(struct sockaddr_in));
ss.sin_family = AF_INET;
ss.sin_addr.s_addr = inet_addr("127.0.0.1"); // Just accept local connections
// Otherwise we need to deal with
// firewall/security issues -
// not needed for our little example!
ss.sin_port = htons(LISTENING_PORT); // port number
// Now we bind the listening socket to the port number
// Should check that bind returns 0, anything else indicates an
// error (perhaps an inability to bind to the port number, etc.)
bind(listening_socket, (struct sockaddr *)&ss, sizeof(struct sockaddr_in));
// Now we tell the socket to listen for incoming connections.
// The 100 is limiting the number of pending incoming connections
// to 100. This is a common number, but could be different.
// Should check that listen returns 0, anything else indicates an
// error (perhaps the socket is not in the correct state, etc.)
listen(listening_socket, 100);
// At this point, the server is listening, a client can connect to it.
// We will loop forever, accepting new connections as they come.
std::cout << "Listening for incoming connections on port "
<< LISTENING_PORT << std::endl;
while (true)
{
// Accept a new connection
struct sockaddr_in client;
socklen_t len = sizeof(struct sockaddr_in);
// The accept call will block until a client connects. When a client connects,
// the new socket connected to the client will be returned. This is a different
// socket than the listening socket - which remains in the listening state.
int client_socket = accept(listening_socket, (struct sockaddr *)&client, &len);
// Now we have a new socket connected to the client. We can handle this
// connection in a new thread, so that the server can continue to accept
// connections from other clients.
std::thread echo_thread(do_echo, client_socket);
echo_thread.detach();
}
}
Implementation - C++ Echo Client
// Headers for MacOS
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netdb.h>
// Standard C++ headers
#include <iostream>
#include <string>
using namespace std;
// Notice that this lines up with the listening
// port for the server.
const u_short SERVER_PORT = 8080;
int main()
{
// Create the socket that will connect to the server.
// sock is a "file descriptor".
int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// Next we initialize a data structure that will be used
// to connect to the server - it contains information about
// which IP address and port number to connect to.
struct sockaddr_in ss;
memset((char *)&ss, 0, sizeof(ss));
ss.sin_family = AF_INET;
// This is the IP address of the server. For this simple example,
// the server is running on the same machine as the client, so "localhost"
// can be used. If the server was elsewhere, we can use the same code, but
// with the name of the machine (or IP address) replacing "localhost".
struct hostent *sp; // struct to hold server's IP address
sp = gethostbyname("localhost");
memcpy(&ss.sin_addr, sp->h_addr, sp->h_length);
// This is the port number of the server. This must match the port number
// the server is listening on.
ss.sin_port = htons(SERVER_PORT);
// Now we connect to the server. This call will return when the connection
// is established, or if it fails for some reason.
int result = connect(sock, (struct sockaddr *)&ss, sizeof(ss));
if (result != 0)
{
std::cerr << "Error connecting to server " << strerror(errno) << endl;
return result;
}
while (true)
{
// We are connected (or write will fail below)
int n;
char buffer[1024];
string echo_input;
string echo_response;
// Read a message from the user
cout << "Enter a message: ";
getline(cin, echo_input);
// Send the message to the server, should always check
// that n == echo_input.length() to ensure the entire message
// was written...
cout << "Sending: " << echo_input << endl;
n = write(sock, echo_input.c_str(), echo_input.length());
// Read the message from the server. Should check if n < 0,
// in case the read fails.
n = read(sock, buffer, 1024);
echo_response = string(buffer, n);
cout << "Received: " << echo_response << endl;
if (echo_response == "QUIT")
{
break;
}
}
// Close the socket
close(sock);
}
Implementation - JavaScript Echo Client
We can implement a compatible client in any language, there is no need for client and server to be written in the same language! If you aren't familiar with JavaScript, or callback functions, then the following code may seem a bit mysterious to you. Rather than focusing on those mechanics, try to focus on what's happening with sockets - you should notice the similarities between the C++ example and this. The main difference is that callback take the place of synchronous loops, and the Node.js interface for sockets is quite a bit simpler than the C++ version.
The easiest way of thinking about the difference between the C++ and JavaScript versions is that JavaScript is event driven. In the C++ version, everything is sequential - we make function calls like getline
, connect
, write
and read
. Everything executes in order, and we use loops to do things over and over again.
In the JavaScript version, we identify events - when the socket gets connected, when the user types something in, when a response is received from the server. We write functions (usually anonymous) that contain code that executes whenever these events occur. Notice in the code below there are no loops - we simply specify, send the entered text whenever the user types something and print the response and prompt for more input whenever the server response is received. Those callbacks happen many times - and the sequence is kicked off by connecting to the server.
We will talk a lot about callback in JavaScript in later chapters - don't get too bogged down on this now!
// The net package comes with the Node.js JavaScript environment,
// it exposes the same type of functionality as the API calls used
// in C++ and C implementations - just wrapped in a more convenient
// JavaScript interface.
const net = require('net');
// This is also part of Node.js, it provides a simple way to read
// from the terminal, like the C++ iostream library.
const readline = require('readline');
// Notice that this lines up with the listening
// port for the server.
const SERVER_PORT = 8080;
// This just sets up node to read some lines from the terminal/console
const terminal = readline.createInterface({
input: process.stdin,
output: process.stdout
});
// This is a callback function. Whenever a user types anything on stdin,
// and hits return, this anonymous function gets called with the text
// that was entered. The text is sent to the socket.
// We'll cover callbacks later in depth - but for now, just know
// this is a function that gets called when a user types something. It's
// not getting called "now", or just once - it gets called whenever a line
// of text is entered.
terminal.on('line', function (text) {
console.log("Sending: " + text);
client.write(text);
});
// Now we create a client socket, which will connect to the server.
const client = new net.Socket();
client.connect(SERVER_PORT, "localhost", function () {
// Much like terminal.on('line', ...), this is a callback function,
// the function gets called when the client successfully connects to
// the server. This takes some time, the TCP handshake has to happen.
// So the "connect" function starts the process, and when the connection
// process is done, this function gets called.
// We just prompt the user to type something in and when they do, the
// terminal.on('line', ...) function above will get called.
console.log("Enter a message: ");
});
// And another callback - this time for when data is recieved on the socket.
// This is the server's response to the message we sent.
// We quit if it's time to, otherwise we prompt the user again.
client.on('data', function (data) {
console.log('Server Response: ' + data);
if (data == "QUIT") {
// This closes the socket
client.destroy();
// This shuts down our access to the terminal.
terminal.close();
// And now we can just exit the program.
process.exit(0);
} else {
console.log("Enter a message: ");
}
});
Implementation - JavaScript Echo Server
We can write a server in JavaScript too, and the C++ and JavaScript clients can connect to it - even at the same time. In this example, Node.js's net
library along with it's asynchronous callback design really shines. We don't need to deal directly with threads, while still retaining the ability to serve many clients simultaneously.
// The net package comes with the Node.js JavaScript environment,
// it exposes the same type of functionality as the API calls used
// in C++ and C implementations - just wrapped in a more convenient
// JavaScript interface.
const net = require('net');
const LISTENING_PORT = 8080;
// The concept of "server" is so universal, that much of the functionality
// is built right into the Node.js "createServer" function. This function call
// creates a socket - we are just providing a function that will be called
// (a callback) when a new client connects to the server.
const server = net.createServer(function (socket) {
// A new socket is created for each client that connects,
// and many clients can connect - this function will be called
// with a different "client" socket for any client that connects.
console.log("A new client has connected.");
// Now we just add a callback to implemenent the echo protocol for
// the connected client - by looking at what the client sends is.
socket.on('data', function (data) {
const input = data.toString('utf8');
console.log("Received: ", input);
response = input.toUpperCase();
console.log("Sending: " + response);
socket.write(response);
if (response == "QUIT") {
console.log("QUIT command received. Closing connection.");
socket.destroy();
}
// otherwise just let the socket be, more data should come our way...
});
socket.on('close', function () {
console.log("Client has disconnected.");
});
});
// The last little bit is to tell the server to start listening - on port 8080
// Now any client can connect.
console.log("Listening for incoming connections on port ", LISTENING_PORT);
server.listen(LISTENING_PORT);
It's actually a pretty amazing little program - in just a few lines of code we have implemented the same TCP echo server as we did using over 100 in C++!. It's the same functionality though, and completely interoperable!
Echo is just a protocol
We've discussed the Internet Protocol as a Layer 3 network layer protocol. It's a standard way of addressing machines, and passing data through a network. We've discussed TCP as a Layer 4 transport layer protocol. TCP defines ports to facilitate independent streams of data mapped to applications, along with reliability mechanisms. In both cases, protocol is being used to mean "a set of rules". IP is the rules of addressing and moving data, TCP is the rules of making reliable data streams.
Echo is a protocol too, but it's a higher level protocol. It defines what is being communicated (text gets sent, capitalized text gets returned) - not how. It also defines how the communication is terminated (the client sends the word "quit"). Echo has aspects of OSI model's Layers 5-7, but it's probably easier to think of it as an application layer protocol.
Notice, any application that speaks the "echo protocol" can play the echo game! Go ahead and check out all of the examples in the /echo
directory of the code section - included are implementations in Python and Java to go along with JavaScript and C++. They all play together. Taking a look at examples in languages you already know might help you understand the mechanics of sockets a bit better!
The Protocol of the Web
The protocol of the web defines what web clients and web servers communicate. Normally, TCP / IP is used at the network and transport layer - but as we've seen, that doesn't describe what is sent - just how. In order for all web clients and servers to be able to play happily together, we need an application layer protocol. This protocol is the subject of the next chapter - the HyperText Transfer Protocol - HTTP.
Just like for the echo server and client, HTTP isn't about a specific programming language. Any program, regardless of the language it is written in, can speak HTTP. Most web browsers (clients) are written in C, C++ (and some partially in Rust). Web servers are written in all sorts of languages - from C, Java, Ruby, and of course Node.js / JavaScript!