This tutorial is about a single-threaded server, communicating over several TCP connections to clients. It also shows how to implement to use multiple server ports besides the client connections.
All implementations in this tutorial are in plain C.
Please note: this is not a beginners tutorial. Knowledge of the programming languange and a development environment are assumed. The tutorials were tested on Linux and Cygwin.
This tutorial does not indent do deliver a complete networking library.
This chapter shows the used data structures used by both server and client.
This chapter does not show how to use the data structures or functions. It is just an overview.
Excerpt from conn.h
:
typedef struct server_t {
int sock;
struct sockaddr_in addr;
} server_t;
typedef struct conn_t {
int sock;
struct sockaddr_in addr; /* address of the server (remote) host */
struct sockaddr_in local; /* address on the local host */
} conn_t;
The server structure contains a socket (the server socket) and its address (the local address on which the server is listening).
The structure conn_t
is used for both, server and client. It contains the socket to communicate, the address of the server and the local address.
Clears and initializes one server structure.
Clears all server structures.
Closes a server socket. No new connections are possible on the specified one.
Closes all server sockets. No new connections are possible on any of the specified server sockets.
Opens a server socket on the specified address and port.
Clears and initializes the connection.
Closes the connection.
Opens a connection. This is used only on the client side.
Accepts a connection. This is used only on the server side.
Clears all connections.
Closes all connections.
Adds a connection to the list.
Closes a connection and removes it from the list.
Waits on a connections until it is ready to read data. This function also returns in case of an execption and if the connection is closed on the the other side.
Writes data to the connection.
Reads from the connection.
This is a basic example and shows an echo server. The server itself is capable of handling multiple concurrent connections. However in this example no connection is open for a long time.
First, let’s check the main loop of the server.
We’ll need some variables.
Then some initialization.
The following part opens the server socket. The socket is opened on the address 0.0.0.0
with the specified port (or use default 9900
). After this, the server port is ready to accept connections.
port = (argc < 2) ? 9900 : atoi(argv[1]);
if (server_open(&srv, INADDR_ANY, port)) {
printf("error: cannot open server.\n");
return -1;
}
printf("server ready.\n");
Now to the main loop.
First we need to prepare all file descriptors. All the file descriptors of the server and client connections are put into the structure fds
and the highest file descriptor is put into fd_max
. This is all the information which the system call select needs to wait on all file descriptors (all connections) at the same time.
In this example, the server waits for any of the connections without timeout and without doing anything else.
If the system call select fails, the server will close all connections and terminate.
The return value of select, in this case held in variable rc
, tells how many connections got ready.
prepare_fds(&fds, con, MAX_CONN, &srv, 1, &fd_max);
rc = select(fd_max, &fds, NULL, NULL, NULL);
if (rc < 0) {
perror("select");
printf("errno=%d\n", errno);
server_close(&srv);
conn_close_all(con, MAX_CONN);
return -1;
}
Since any of the connection could have caused select to wake up, we’ll have to check which connection(s) are ready.
First we check the server. This example assumes, that every time a client connects to the server port, it wants to establish a new client connection.
/* check server */
if ((srv.sock >= 0) && FD_ISSET(srv.sock, &fds)) {
if (new_connection(&srv) < 0) {
printf("error: could not open/add new connection.\n");
}
--rc;
}
After checking the server socket, we’ll check all client connections until all ready connections are checked (see variable rc
). If a client connection got ready, it will be handled.
Please note: it is not possible for two client connections to access the content of the server at the same time. All connections are handled one after another. No synchronization is needed here.
/* check all connections */
for (i = 0; rc && (i < MAX_CONN); ++i) {
conn_t * c = con+i;
if ((c->sock >= 0) && FD_ISSET(c->sock, &fds)) {
handle_connection(c);
--rc;
}
}
}
Well, that’s about it. A very simple server (single threaded) handling multiple client connections.
Let’s have a look at the rest of the code.
At one point we need some variables to store all information about the server connection as well as all client connections. For now it is sufficient to keep all client connections in one big array. To differentiate between established and closed connections, we just evaluate the socket.
enum { MAX_CONN = 64 };
static server_t srv;
static conn_t con[MAX_CONN];
static int n_cnt = 0;
static fd_set fds;
There is also a need to close a connection and delete it from the array. Pretty straight forward.
static void close_connection(conn_t * c)
{
if (conn_close_del(con, MAX_CONN, c) < 0) {
printf("cannot remove connection from list\n");
} else {
--n_cnt;
}
}
The function to accept a new client connection. The call to conn_accept
will not block because all the waiting was done calling select (see line 143).
There is a guard implemented (line 71) that ensures a maximum concurrent client connections. Every additional connection is closed right away. If the connection is OK, it will be added to the array (line 72).
static int new_connection(server_t * s)
{
assert(s);
conn_t c;
if (conn_accept(s, &c) < 0) return -1;
if (n_cnt < MAX_CONN) {
if (conn_add(con, MAX_CONN, &c) < 0) {
conn_close(&c);
printf("not able to handle new connection\n");
return -1;
} else {
++n_cnt;
}
} else {
/* this is necessary to let the other know
the connection is closed. */
conn_close(&c);
printf("cannot accept more connections\n");
return -1;
}
return 0;
}
The connection handling is easy. Receive data (at most 128 bytes) and send them back on the same connection. A simple echo.
If there is any exception, just close the client connection.
static void handle_connection(conn_t * c)
{
int rc;
char buf[128];
assert(c);
assert(c->sock >= 0);
memset(buf, 0, sizeof(buf));
rc = conn_read(c, buf, sizeof(buf));
if (rc == 0) goto close_conn;
if (rc < 0) {
if (errno == ECONNABORTED) goto close_conn;
perror("read");
goto close_conn;
}
rc = conn_write(c, buf, sizeof(buf));
if (rc != sizeof(buf)) {
printf("error while writing to connection.\n");
goto close_conn;
}
return;
close_conn:
close_connection(c);
return;
}
The cleanup handler just closes all client connections as well as the server socket.
Last but not least, the preparation of the file descriptor table. On lines 32..38 all server socket are added to the table and on lines 39..45 all client connections are added.
Please note: although we used just one server socket, this function is able to process multiple server sockets.
The variable fd_max
will be already in a form to be used directly with select (see manpage of select).
static void prepare_fds(fd_set * fds,
conn_t * arr_conn, const int n_conn,
server_t * arr_srv, const int n_srv,
int * fd_max)
{
int i;
int m = -1;
assert(fds);
assert(arr_conn);
assert(n_conn >= 0);
assert(arr_srv);
assert(n_srv >= 0);
assert(fd_max);
FD_ZERO(fds);
for (i = 0; i < n_srv; ++i) { /* add all servers */
server_t * s = arr_srv+i;
if (s->sock >= 0) {
if (m `< s->`sock) m = s->sock;
FD_SET(s->sock, fds);
}
}
for (i = 0; i < n_conn; ++i) { /* add all connections */
conn_t * c = arr_conn+i;
if (c->sock >= 0) {
if (m `< c->`sock) m = c->sock;
FD_SET(c->sock, fds);
}
}
*fd_max = m + 1;
}
Some variables needed.
The cleanup handler.
The entire client fits into one main:
For this tutorial, the host address is hardcoded to localhost
, and we’ll need some variables:
long host = (127 << 24) + (0 << 16) + (0 << 8) + (1 << 0);
struct timeval t;
int rc;
char sbuf[128];
char rbuf[128];
short port;
atexit(cleanup);
port = (argc < 2) ? 9900 : atoi(argv[1]);
if (conn_open(&con, host, port) < 0) {
exit(-1);
}
memset(sbuf, 0, sizeof(sbuf));
strcpy(sbuf, "Hello World!");
rc = conn_write(&con, sbuf, sizeof(sbuf));
if (rc < 0) {
printf("error while writing to connection.\n");
exit(-1);
}
memset(rbuf, 0, sizeof(rbuf));
rc = conn_read(&con, rbuf, sizeof(rbuf));
if (rc != sizeof(rbuf)) {
printf("error while reading from connection.\n");
exit(-1);
}
if (memcmp(sbuf, rbuf, sizeof(sbuf)) != 0) {
printf("error: send and receive buffer not the same!\n");
exit(-1);
}
printf("send: [%s]\n", sbuf);
printf("recv: [%s]\n", rbuf);
conn_close(&con);
return 0;
}
Compile server and client:
$ gcc -c conn.c
$ gcc -c srv-1.c
$ gcc -c client-1.c
$ gcc -o srv-1 srv-1.o conn.o
$ gcc -o client-1 client-1.o conn.o
Run the server in one shell, the client (or clients) in another shell.
Server:
Client:
This example shows a very simple chat program using the single threaded server.
The server is all the same as it was in the first example, except the function handle_connection
:
static void handle_connection(conn_t * c)
{
int len;
int rc;
char buf[128];
int i;
assert(c);
assert(c->sock >= 0);
memset(buf, 0, sizeof(buf));
len = conn_read(c, buf, sizeof(buf));
if (len == 0) goto close_conn;
if (len < 0) {
if (errno == ECONNABORTED) goto close_conn;
perror("read");
goto close_conn;
}
/* send received message to all other clients */
for (i = 0; i < MAX_CONN; ++i) {
conn_t * co = con+i;
if (co->sock == -1) continue;
if (co->sock == c->sock) continue;
rc = conn_write(co, buf, len);
if (rc < 0) {
printf("error while writing to connection.\n");
perror("write");
close_connection(co);
}
}
return;
close_conn:
close_connection(c);
return;
}
The real difference are the lines 109..119. The received data buf
will be sent to all other connection (not the sending connection).
If there is any exception on the sending connection it will be closed.
Again, the host address is hard coded and we’ll need some variables.
int main(int argc, char ** argv)
{
long host = (127 << 24) + (0 << 16) + (0 << 8) + (1 << 0);
struct timeval t;
int rc;
char buf[128];
fd_set fds;
short port;
atexit(cleanup);
port = (argc < 2) ? 9900 : atoi(argv[1]);
if (conn_open(&con, host, port) & 0) {
exit(-1);
}
This client is implemented as one big loop. It reads from stdin
and sends the data to the server. All incoming data from the server is printed on stdout
.
We prepare the file descriptor table to be used with select
. In this case we put 0 (stdin
) and the connection socket into the table and wait for action.
FD_ZERO(&fds);
FD_SET(0, &fds);
FD_SET(con.sock, &fds);
rc = select(con.sock+1, &fds, NULL, NULL, NULL);
if (rc < 0) {
printf("error while waiting for input.\n");
perror("select");
break;
}
In the first case we’ll check the stdin
for data to send. If this device is ready, we read the data and send it to the server, as simple as that. If an error occurs, we give up.
if (FD_ISSET(0, &fds)) { /* input from stdin */
memset(buf, 0, sizeof(buf));
rc = read(0, buf, sizeof(buf));
if (rc < 0) {
printf("error while reading from stdin.\n");
perror("read");
continue;
}
rc = conn_write(&con, buf, rc);
if (rc < 0) {
printf("error while writing to connection.\n");
perror("write");
break;
}
}
Secondly we check for the connection. Data is read and put to stdout
.
if (FD_ISSET(con.sock, &fds)) { /* input from connection */
memset(buf, 0, sizeof(buf));
rc = conn_read(&con, buf, sizeof(buf));
if (rc < 0) {
printf("error while reading from connection.\n");
perror("read");
break;
}
printf("recv: [%s]\n", buf);
}
}
If an error occurred, the connection is closed and the client terminates.
Compile server and client:
$ gcc -c conn.c
$ gcc -c srv-2.c
$ gcc -c client-2.c
$ gcc -o srv-2 srv-2.o conn.o
$ gcc -o client-2 client-2.o conn.o
Run the server in one shell, the client (or clients) in another shell.
Server:
Client 1:
Client 2:
As you can see, the newline is transmitted as well.
This example shows the single threaded server maintaining a game state plus a very basic graphical client to visualize this state.
The protocol to distribute the current game state is far from optimum, but it works for demonstration purposes.
To compile and run this example you will need OpenGL.
This chapter shows the basic data structures used for the game state. It also shows the basic function to maintain the game state and move players.
Excerpt from game.h
.
The field size .
The maximum number of players.
The data type to hold the player ID, starting with 1.
The playing field.
The type describing a cooridinate on the playing field.
The data structure which will be sent from clients to the server indicating a change of game state. The connection from client to the server implies the player ID.
Datatype to hold the entire game state.
Initializes the field structure.
Initializes the specified coordinate.
Initializes the game state.
Returns the player ID from the specified game state and the specified (x,y) coordinates.
Returns the player ID from the specified game state and the specified coordinates.
Sets the player ID at the specified (x,y) coordinates within the game state.
Sets the player ID at the specified coordinates within the game state.
Moves the player from the first to the second coordinate.
Removes the player from the game state.
The function prepare_fds keeps the same.
The maximum connections is redefined to the maximum players allowed on the playfield.
The game state instance:
The following function is to send the entire game state to the specified connection (client). As mentioned above, this is far from optimal but works for this tutorial.
It just sends the entire state, using the well known function, introduced in previous tutorials.
static int send_state(conn_t * c)
{
assert(c);
if (conn_write(c, &state, sizeof(state)) != sizeof(state))
return -1;
return 0;
}
This function sends the game state to all clients, except the specified one. This is used to update the game state after a client reported a change.
static void send_state_to_all_except(conn_t * c)
{
int i;
for (i = 0; i < MAX_CONN; ++i) {
conn_t * co = con+i;
if (co->sock == -1) continue;
if (c && co->sock == c->sock) continue;
if (send_state(co)) {
printf("error while writing to connection.\n");
perror("write");
close_connection(co);
}
}
}
The following function is used while establishing a connection to a client. It sends the player info (ID and position) as well as the game state.
This function is straight forward and does not need further explanation.
static int send_game_info(conn_t * c)
{
player_t player;
coord_t pos;
int i;
assert(c);
/* determine player ID */
player = -1;
for (i = 0; i < MAX_CONN; ++i) {
if (con[i].sock == c->sock) {
player = i;
break;
}
}
if (player < 0) return -1;
/* send player ID to client */
++player;
if (conn_write(c, &player, sizeof(player)) != sizeof(player))
return -1;
/* determine random coordinates */
coord_init(&pos);
for (;;) {
pos.x = rand() % FIELD_N;
pos.y = rand() % FIELD_N;
if (state_get_coord(&state, &pos) == 0) {
state_set_coord(&state, &pos, player);
break;
}
}
/* send coordingates to client */
if (conn_write(c, &pos, sizeof(pos)) != sizeof(pos)) goto error;
/* send game state to client */
if (send_state(c)) goto error;
return 0;
error:
state_set_coord(&state, &pos, 0);
return -1;
}
The main difference (marked bold) to previous version of this function is to keep track of the game state after a connection has been established successfully. The connection is considered complete after it is established and the client has received all the player information and game state it needs.
static int new_connection(server_t * s)
{
assert(s);
conn_t c;
if (conn_accept(s, &c) < 0) return -1;
if (n_cnt < MAX_CONN) {
if (conn_add(con, MAX_CONN, &c) < 0) {
conn_close(&c);
printf("not able to handle new connection\n");
return -1;
} else {
++n_cnt;
if (send_game_info(&c)) {
--n_cnt;
conn_close(&c);
return -1;
} else {
send_state_to_all_except(&c);
}
}
} else {
/* this is necessary to let the other know
the connection is closed. */
conn_close(&c);
printf("cannot accept more connections\n");
return -1;
}
return 0;
}
The function handles the client connection. Its either a moving player or a closing connection. Both ways, the game state is changed. While the moving player just changes the position of the player, a closing connection removes the player from the field. For both cases all other clients have to be notified (updating the game state).
static void handle_connection(conn_t * c)
{
int rc;
int i;
move_t move;
player_t player;
assert(c);
assert(c->sock >= 0);
memset(&move, 0, sizeof(move));
rc = conn_read(c, &move, sizeof(move));
if (rc == 0) goto close_conn;
if (rc < 0) {
if (errno == ECONNABORTED) goto close_conn;
perror("read");
goto close_conn;
}
/* perform move */
player = state_get_coord(&state, &move.from);
if (player) {
state_set_coord(&state, &move.to, player);
state_set_coord(&state, &move.from, 0);
send_state_to_all_except(c);
}
return;
close_conn:
close_connection(c);
player = -1;
for (i = 0; i < MAX_CONN; ++i) {
if (con[i].sock == c->sock) {
player = i;
break;
}
}
if (player >= 0) state_remove_player(&state, player+1);
send_state_to_all_except(NULL);
return;
}
The rest of the code is essentially the same as in the previous examples and does not need further explanation.
The client utilizes OpenGL to display (in a very simple way) the current game state.
All the variables needed within the client. The connection, the game state, player information and data structures to poll the connection.
static conn_t con;
static state_t state;
static coord_t pos;
static player_t player;
static struct pollfd fds[1];
static int timeout = 20;
This method moves the player in the local game state and sends the state change to the server, which will distribute it to the other clients.
static void move(const coord_t * from, const coord_t * to)
{
move_t move;
assert(to);
assert(con.sock >= 0);
move.from = *from;
move.to = *to;
state_move_player(&state, from, to, player);
if (conn_write(&con, &move, sizeof(move)) != sizeof(move)) {
printf("error: cannot send move.\n");
perror("write");
exit(-1);
}
}
This is the function that draws the entire scene. It draws the grid as well as the players. They are displayed by colored rectangles. Each players has a different color (red, blue, green, yellow).
static void display(void)
{
int x, y;
glClear(GL_COLOR_BUFFER_BIT);
glLoadIdentity();
/* draw grid */
glColor4f(1.0, 1.0, 1.0, 1.0);
glBegin(GL_LINES);
for (y = 0; y <= FIELD_N; ++y) {
glVertex2i(0, y * 20);
glVertex2i(20 * FIELD_N, y * 20);
}
for (x = 0; x <= FIELD_N; ++x) {
glVertex2i(x * 20, 0);
glVertex2i(x * 20, 20 * FIELD_N);
}
glEnd();
/* draw players */
for (y = 0; y < FIELD_N; ++y) {
for (x = 0; x < FIELD_N; ++x) {
player_t player = state_get(&state, x, y);
if (player == 0) continue;
switch (player) {
case 1: glColor4f(1.0, 0.0, 0.0, 1.0); break;
case 2: glColor4f(0.0, 1.0, 0.0, 1.0); break;
case 3: glColor4f(0.0, 0.0, 1.0, 1.0); break;
case 4: glColor4f(1.0, 1.0, 0.0, 1.0); break;
}
glBegin(GL_QUADS);
glVertex2i((x+0) * 20 + 1, (y+0) * 20 + 0);
glVertex2i((x+0) * 20 + 1, (y+1) * 20 - 1);
glVertex2i((x+1) * 20 - 0, (y+1) * 20 - 1);
glVertex2i((x+1) * 20 - 0, (y+0) * 20 + 0);
glEnd();
}
}
glFlush();
glutSwapBuffers();
}
This function gets called whenever the window is resized. The scene is displayed as 2D grid.
static void reshape(int w, int h)
{
glViewport(0, 0, w, h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrtho(0, w, h, 0, 0, 1);
glMatrixMode(GL_MODELVIEW);
}
As long as the client is idle, this function is called. It polls the connection to the server and receives a new game state if one is available. It also triggers a redraw of the scene.
For more information about the system call poll, see its manpage.
static void idle(void)
{
/* poll connection */
if (poll(fds, 1, timeout)) {
if (fds[0].revents & POLLIN) {
if (conn_read(&con, &state, sizeof(state)) != sizeof(state)) {
perror("read");
exit(-1);
}
}
}
glutPostRedisplay();
}
Whenever the keyboard is pressed, this function is executed. It changes the game state according to the key presses W
, A
, S
and D
. Those keys move the player around the game field. The local game state is updated and the server is notified about the change, which will notify the other players.
static void keyboard(unsigned char key, int x, int y)
{
coord_t old_pos = pos;
coord_t new_pos = pos;
int mov = 0;
switch (key) {
case 27: exit(0);
case 'w':
if (pos.y > 0) { --new_pos.y; mov = 1; }
break;
case 's':
if (pos.y < FIELD_N-1) { ++new_pos.y; mov = 1; }
break;
case 'a':
if (pos.x > 0) { --new_pos.x; mov = 1; }
break;
case 'd':
if (pos.x < FIELD_N-1) { ++new_pos.x; mov = 1; }
break;
}
if (mov) {
if (state_get_coord(&state, &new_pos) == 0) {
pos = new_pos;
move(&old_pos, &new_pos);
glutPostRedisplay();
}
}
}
The lines 160..179 establish a connection to the server and receive the player information and game state.
The rest of the code is initialization, mostly OpenGL stuff.
int main(int argc, char ** argv)
{
atexit(cleanup);
state_init(&state);
/* establish connection to server */
if (conn_open(&con, 0x7f000001, 9900) < 0) {
printf("error: cannot connect to server.\n");
perror("open");
exit(-1);
}
if (conn_read(&con, &player, sizeof(player)) != sizeof(player)) {
printf("error: cannot receive player number.\n");
perror("read");
exit(-1);
}
if (conn_read(&con, &pos, sizeof(pos)) != sizeof(pos)) {
printf("error: cannot receive position.\n");
perror("read");
exit(-1);
}
if (conn_read(&con, &state, sizeof(state)) != sizeof(state)) {
printf("error: cannot receive game state.\n");
perror("read");
exit(-1);
}
/* information about client */
printf("player: %d\n", player);
printf("pos : { %d , %d }\n", pos.x, pos.y);
/* prepare structure for poll */
fds[0].fd = con.sock;
fds[0].events = POLLIN;
/* init GL */
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_ALPHA);
glutInitWindowPosition(0, 0);
glutInitWindowSize(320, 320);
glutCreateWindow("Multiplayer");
glutDisplayFunc(display);
glutReshapeFunc(reshape);
glutKeyboardFunc(keyboard);
glutIdleFunc(idle);
glClearColor(0.0, 0.0, 0.0, 0.0);
glFrontFace(GL_CCW);
glEnable(GL_NORMALIZE);
glPolygonMode(GL_FRONT, GL_FILL);
glPolygonMode(GL_BACK, GL_LINE);
glutMainLoop();
return 0;
}
On Linux link the client as shown:
On Linux link the client like this:
Run the server in one shell, three clients in other shells.
Server:
One of the clients:
All files listed for download are free to modify, distribute or to use them in any kind you like.
Use them on your own risk.