mario::konrad
programming / C++ / sailing / nerd stuff
Single Threaded Server in C
© 2008 / Mario Konrad
Profiling eines C Programms mit gprof

Introduction

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.

Basic Data Structures and Functions

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.

Data Structures

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.

Server Functions

void server_clear(server_t *)

Clears and initializes one server structure.

void server_clear_all(server_t *, const int)

Clears all server structures.

void server_close(server_t *)

Closes a server socket. No new connections are possible on the specified one.

void server_close_all(server_t *, const int)

Closes all server sockets. No new connections are possible on any of the specified server sockets.

int server_open(server_t *, long, short)

Opens a server socket on the specified address and port.

Connection Functions

void conn_clear(conn_t *)

Clears and initializes the connection.

void conn_close(conn_t *)

Closes the connection.

int conn_open(conn_t *, long, short)

Opens a connection. This is used only on the client side.

int conn_accept(server_t *, conn_t *)

Accepts a connection. This is used only on the server side.

void conn_clear_all(conn_t *, const int)

Clears all connections.

void conn_close_all(conn_t *, const int)

Closes all connections.

int conn_add(conn_t *, const int, conn_t *)

Adds a connection to the list.

int conn_close_del(conn_t *, const int, conn_t *)

Closes a connection and removes it from the list.

int conn_wait_to_read(conn_t *, struct timeval *)

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.

int conn_write(conn_t *, const void *, int)

Writes data to the connection.

int conn_read(conn_t *, void *, int)

Reads from the connection.

Basic Example: Echo

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.

Server

First, let's check the main loop of the server.

We'll need some variables.

123
124
125
126
127
128
int main(int argc, char ** argv)
{
    int fd_max = -1;
    int rc;
    int i;
    short port;

Then some initialization.

130
131
132
    atexit(cleanup);
    server_clear(&srv);
    conn_clear_all(con, MAX_CONN);

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.

134
135
136
137
138
139
    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.

141
    for (;;) {

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.

142
143
144
145
146
147
148
149
150
        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.

151
152
153
154
155
156
157
        /* 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.

158
159
160
161
162
163
164
165
166
        /* 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.

49
50
51
52
53
54
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.

56
57
58
59
60
61
62
63
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).

65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
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.

89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
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.

117
118
119
120
121
static void cleanup(void)
{
    conn_close_all(con, MAX_CONN);
    server_close(&srv);
}

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).

16
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
42
43
44
45
46
47
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;
}

Client

Some variables needed.

15
static conn_t con;

The cleanup handler.

17
18
19
20
static void cleanup(void)
{
    conn_close(&con);
}

The entire client fits into one main:

22
23
int main(int argc, char ** argv)
{

For this tutorial, the host address is hardcoded to localhost, and we'll need some variables:

123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
    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 and Run

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:

$ ./srv-1
server ready.

Client:

$ ./client-1
send: [Hello World!]
recv: [Hello World!]

Second Example: Chat

This example shows a very simple chat program using the single threaded server.

Server

The server is all the same as it was in the first example, except the function handle_connection:

89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
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.

Client

Again, the host address is hard coded and we'll need some variables.

22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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.

38
    for (;;) {

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.

39
40
41
42
43
44
45
46
47
    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.

48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
    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.

63
64
65
66
67
68
69
70
71
72
73
        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.

74
75
76
    conn_close(&con);
    return 0;
}

Compile and Run

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:

$ ./srv-2
server ready.

Client 1:

$ ./client-2
hello world
recv: [foobar
]

Client 2:

$ ./client-2
recv: [hello world
]
foobar

As you can see, the newline is transmitted as well.

Multiplayer Game

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.

Common

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.

Data Structures

Excerpt from game.h.

The field size NxNN x N.

The maximum number of players.

enum { FIELD_N = 16 };
enum { MAX_PLAYERS = 4 };

The data type to hold the player ID, starting with 1.

typedef int32_t player_t;

The playing field.

typedef struct field_t {
    player_t data[FIELD_N][FIELD_N];
} field_t;

The type describing a cooridinate on the playing field.

typedef struct coord_t {
    int32_t x;
    int32_t y;
} coord_t;

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.

typedef struct move_t {
    coord_t from;
    coord_t to;
} move_t;

Datatype to hold the entire game state.

typedef struct state_t {
    field_t field;
} state_t;

Game Functions

void field_init(field_t *)

Initializes the field structure.

void coord_init(coord_t *)

Initializes the specified coordinate.

void state_init(state_t *)

Initializes the game state.

player_t state_get(state_t *, int32_t, int32_t)

Returns the player ID from the specified game state and the specified (x,y) coordinates.

player_t state_get_coord(state_t *, const coord_t *)

Returns the player ID from the specified game state and the specified coordinates.

void state_set(state_t *, int32_t, int32_t, player_t)

Sets the player ID at the specified (x,y) coordinates within the game state.

void state_set_coord(state_t *, const coord_t *, player_t)

Sets the player ID at the specified coordinates within the game state.

int state_move_player(state_t *, const coord_t *, const coord_t *, player_t)

Moves the player from the first to the second coordinate.

    int state_remove_player(state_t *, player_t)

Removes the player from the game state.

Server

The function prepare_fds keeps the same.

The maximum connections is redefined to the maximum players allowed on the playfield.

50
enum { MAX_CONN = MAX_PLAYERS };

The game state instance:

57
static state_t state;

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.

68
69
70
71
72
73
74
75
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.

77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
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.

93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
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.

138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
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).

169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
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.

Client

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.

26
27
28
29
30
31
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.

33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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).

52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
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.

96
97
98
99
100
101
102
103
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.

105
106
107
108
109
110
111
112
113
114
115
116
117
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.

119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
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.

154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
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;
}

Compile and Run

$ gcc -c conn.c
$ gcc -c game.c
$ gcc -c srv-3.c
$ gcc -c client-3.c
$ gcc -o srv-3 srv-3.o conn.o

On Linux link the client as shown:

$ gcc -o client-3 client-3.o conn.o game.o -lGL -lGLU -lglut

On Linux link the client like this:

$ gcc -o client-3 client-3.o conn.o game.o -lglut32 -lglu32 -lopengl32

Run the server in one shell, three clients in other shells.

Server:

$ ./srv-3
server ready.

One of the clients:

Download

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.