Contents
22. Client-server
|
How to program a text adventure in C
by Ruud Helderman
<r.helderman@hccnet.nl>
Licensed under
MIT License
22. Client-server
It would be perfectly retro
to have a single person type in every command for every player.
What we made in the previous chapter, might already be enough to run your own
play-by-mail game.
But in the 21st century,
‘multi-player’ has become synonymous with
online gaming.
So let’s see if we can make that happen!
To make our game multi-user, we will turn it into a
TCP
server.
Each user will connect to the server with a standard
Telnet
client.
Sounds old-school? It is.
But it’s probably closest to how the early MUDs were made.
And it’s fun to write your own server almost from scratch.
So basically,
our program lilcave should be running continuously on a computer.
The computer should always be online; it should never be turned off.
In the early days of the internet,
that typically meant the program would be running on a university
mini
or
mainframe,
under the supervision of the super user
(or more likely, covertly scheduled by a smart student).
With today’s hardware, any
PC
will do fine running such a game.
But since it is an online game, there are a few things to keep in mind.
- Assuming your machine has a
firewall,
you will need to add a rule allowing inbound traffic to your program,
protocol TCP, port 18811
(or whatever number is being specified in server.c; see below).
With the built-in firewall of
MS Windows,
that’s easy enough: the firewall will automatically
propose the necessary adjustment as soon as you first launch your program.
- Assuming your home network is separated from the internet by a
router,
you will need to configure
port forwarding
on the router.
- You may want to keep the game running day and night.
That warrants an energy-efficient machine, for example a
Raspberry Pi.
- Rather than running your own
home server,
you might consider having your game
hosted
by a third party at a monthly fee.
A
text-based
game typically has small demands, so you could probably settle
for a minimal amount of memory, disk space and bandwidth.
But do make sure the package comes with a C compiler,
as you will need to build your game from source on the hosted server.
Implementing a TCP server in C is pretty straightforward.
It involves writing quite a bit of
boilerplate code
around
Berkeley sockets,
the de facto standard for a
client-server
application.
Fortunately, code samples can be found anywhere on the internet; even in
the Wikipedia article about select.
server.h |
- extern void server(void (*action)(char *, int));
|
server.c |
- #include <stdbool.h>
- #include <stdio.h>
- #include <unistd.h>
- #include <netinet/in.h>
- #include "break.h"
- #include "object.h"
- #include "print.h"
- #include "outbuf.h"
- #include "telnet.h"
- #include "client.h"
- #include "tcp.h"
- #define PORT 18811
- void server(void (*action)(char *, int))
- {
- CLIENT *client;
- int i;
- struct sockaddr_in address;
- int listener = tcpListen(&address, PORT);
- if (listener == -1) return;
- for (clientInit(), breakInit(); breakTest(); )
- {
- fd_set fds;
- int fd = listener;
- FD_ZERO(&fds);
- FD_SET(listener, &fds);
- for (i = 0; (client = clientGet(i)) != NULL; i++)
- {
- if (client->fd != -1) FD_SET(client->fd, &fds);
- if (client->fd > fd) fd = client->fd;
- }
- if (tcpSelect(fd + 1, &fds) == -1) break;
- if (FD_ISSET(listener, &fds))
- {
- fd = tcpAccept(&address, listener);
- if (fd == -1) break;
- printConsole("Socket %d connected.\n", fd);
- outbufClear();
- telnetConfigure();
- outbufFormat("Welcome to Little Cave Adventure.\n");
- client = clientGetFree();
- if (client != NULL)
- {
- client->fd = fd;
- client->obj = nobody;
- telnetInit(&client->inbuf);
- telnetAppendPrompt(&client->inbuf);
- outbufFlush(fd);
- }
- else
- {
- outbufFormat("All sockets occupied, please try again later.\n");
- outbufFlush(fd);
- tcpDisconnect(fd);
- }
- }
- for (i = 0; breakTest() && (client = clientGet(i)) != NULL; i++)
- {
- if (FD_ISSET(client->fd, &fds))
- {
- static char buffer[1024];
- int len = read(client->fd, buffer, sizeof buffer);
- if (len > 0)
- {
- player = client->obj;
- printSetCurrent(client->fd);
- telnetParse(&client->inbuf, client->fd, action, buffer, len);
- if (player != client->obj)
- {
- printConsole("Socket %d is %s.\n", fd, player->description);
- client->obj = player;
- }
- }
- else if (len == 0)
- {
- tcpDisconnect(client->fd);
- client->fd = -1;
- }
- }
- }
- }
- printConsole("Interrupted by signal %d.\n", breakSignalNumber());
- for (i = 0; (client = clientGet(i)) != NULL; i++)
- {
- tcpDisconnect(client->fd);
- }
- tcpClose(listener, PORT);
- }
|
Explanation:
- Line 13:
I just randomly picked a port number
that was not already in use by any popular online game.
Users need this number, as well as your server’s
(external) IP address, to connect to your game.
- Line 15:
function server has a function pointer action as its parameter.
Through this parameter,
we will inject the game engine we have created in the previous chapters.
- Line 19-21:
we start by setting up a listening
socket.
- Line 23-84:
this is the main loop of the game server.
We could stay here forever,
but sooner or later we will be forced to leave by a signal, or by an error.
- Line 25-33:
we build up a collection of clients that are currently connected.
- Line 34:
here, the process waits until some activity is detected from any of the clients.
If an error occurs, we will leave the loop. This will end the game.
- Line 36:
activity on the listening socket means a new client is connecting to the server.
- Line 38-58:
the new client is welcomed.
- Line 60:
looping through all the clients that are currently connected.
- Line 62:
any activity from this client?
- Line 65:
calling
read
to receive data from the client’s
socket.
Here we pull in everything typed in by a user.
- Line 68:
in the previous chapter,
we changed player from a fixed object into a variable.
Here, we use this to switch to the correct
player character.
- Line 70:
this is the heart of the server.
It processes the client’s input (buffer)
by letting our game engine (parameter action) execute each command,
sending responses back to client sockets.
- Line 71-75:
when a client first connects, they will be nobody (see line 48).
Once the game engine has established who you are,
it will be persisted here.
- Line 77-81:
in this context, no data means the client is disconnecting,
i.e. the user has closed the Telnet client.
- Line 86-90:
when leaving the main loop, close all remaining sockets.
For readability, I implemented a number of functions in a separate module.
tcp.h |
- extern int tcpListen(struct sockaddr_in *addr, uint16_t port);
- extern void tcpClose(int fd, uint16_t port);
- extern int tcpSelect(int nfds, fd_set *readfds);
- extern int tcpAccept(struct sockaddr_in *addr, int listener);
- extern void tcpDisconnect(int fd);
- extern void tcpSend(int fd, const char *data, int len);
|
tcp.c |
- #include <stdbool.h>
- #include <stdio.h>
- #include <unistd.h>
- #include <errno.h>
- #include <netinet/in.h>
- #include "object.h"
- #include "print.h"
- static int assert(const char *name, int retval)
- {
- if (retval == -1) perror(name);
- return retval;
- }
- static struct sockaddr *setPort(struct sockaddr_in *addr, uint16_t port)
- {
- addr->sin_family = AF_INET;
- addr->sin_addr.s_addr = INADDR_ANY;
- addr->sin_port = htons(port);
- return (struct sockaddr *)addr;
- }
- int tcpListen(struct sockaddr_in *addr, uint16_t port)
- {
- int fd = assert("socket", socket(AF_INET, SOCK_STREAM, 0));
- if (fd != -1)
- {
- int opt = 1;
- if (-1 != assert("setsockopt", setsockopt(fd, SOL_SOCKET, SO_REUSEADDR,
- (char *)&opt, sizeof opt)) &&
- -1 != assert("bind", bind(fd, setPort(addr, port), sizeof *addr)) &&
- -1 != assert("listen", listen(fd, 3)))
- {
- printConsole("Listening to port %u.\n", (unsigned int)port);
- }
- else
- {
- close(fd);
- fd = -1;
- }
- }
- return fd;
- }
- void tcpClose(int fd, uint16_t port)
- {
- close(fd);
- printConsole("No longer listening to port %u.\n", (unsigned int)port);
- }
- int tcpSelect(int nfds, fd_set *readfds)
- {
- return assert("select", select(nfds, readfds, NULL, NULL, NULL));
- }
- int tcpAccept(struct sockaddr_in *addr, int listener)
- {
- socklen_t len = sizeof *addr;
- return assert("accept", accept(listener, (struct sockaddr *)addr, &len));
- }
- void tcpDisconnect(int fd)
- {
- if (fd != -1)
- {
- close(fd);
- printConsole("Socket %d disconnected.\n", fd);
- }
- }
- void tcpSend(int fd, const char *data, int len)
- {
- int written;
- while (len > 0 && ((written = write(fd, data, len)) >= 0 || errno == EINTR))
- {
- if (written > 0) data += written, len -= written;
- }
- }
|
Explanation:
- Line 9-13:
a little helper function to print an error message to
stderr
in case a
system call
fails.
- Line 25:
calling system call socket.
- Line 31:
calling system call bind.
- Line 32:
calling system call listen.
- Line 53:
calling system call
select.
- Line 59:
calling system call accept.
- Lines 42, 53, 59:
tcpListen, tcpSelect and tcpAccept
follow the convention of returning -1 in case of an error.
- Line 71-78:
function tcpSend will be used later in this chapter
to send output to client sockets.
The following module provides access to an array of
structs
holding information about the clients that have connected to the game.
For each client, there is a file descriptor (the
socket),
an object (the
player character)
and an input buffer (see telnet.h below).
client.h |
- typedef struct
- {
- int fd;
- OBJECT *obj;
- INBUF inbuf;
- }
- CLIENT;
- extern void clientInit(void);
- extern CLIENT *clientGet(int i);
- extern CLIENT *clientGetFree(void);
|
client.c |
- #include <stdbool.h>
- #include <stdio.h>
- #include "object.h"
- #include "telnet.h"
- #include "client.h"
- #define MAX_CLIENTS 30
- static CLIENT clients[MAX_CLIENTS];
- void clientInit(void)
- {
- int i;
- for (i = 0; i < MAX_CLIENTS; i++) clients[i].fd = -1;
- }
- CLIENT *clientGet(int i)
- {
- return i < MAX_CLIENTS ? clients + i : NULL;
- }
- CLIENT *clientGetFree(void)
- {
- CLIENT *client;
- int i;
- for (i = 0; (client = clientGet(i)) != NULL && client->fd != -1; i++);
- return client;
- }
|
Explanation:
- Line 11-15:
initially, all ‘slots’ in the array will be empty.
- Line 17-20:
function clientGet is used to find existing clients.
- Line 22-28:
function clientGetFree is used to find a free slot.
If all slots are taken, then it will return NULL.
The following module handles the peculiarities of the
Telnet
protocol.
It also takes care of buffering client input
(i.e. everything typed in by the users),
and it prevents server output
(which could be pushed at any time by activity from other users)
getting mixed up with
echo
on the same text line in the client’s Telnet window.
telnet.h |
- typedef struct
- {
- bool iac;
- int negotiate;
- unsigned index;
- char data[100];
- }
- INBUF;
- extern void telnetInit(INBUF *inbuf);
- extern void telnetConfigure(void);
- extern void telnetInsertSpaces(INBUF *inbuf);
- extern void telnetDeleteSpaces(INBUF *inbuf);
- extern void telnetAppendPrompt(INBUF *inbuf);
- extern void telnetDeletePrompt(INBUF *inbuf);
- extern void telnetParse(INBUF *inbuf, int fd, void (*action)(char *, int),
- const char *data, int length);
|
telnet.c |
- #include <stdbool.h>
- #include <stdio.h>
- #include "outbuf.h"
- #include "telnet.h"
- static const char prompt[] = "--> ";
- void telnetInit(INBUF *inbuf)
- {
- inbuf->iac = false;
- inbuf->negotiate = 0;
- inbuf->index = 0;
- }
- void telnetConfigure(void)
- {
- static const char config[] = {
- '\xFF', '\xFD', 34, '\xFF', '\xFA', 34, 1, 0, '\xFF', '\xF0',
- '\xFF', '\xFB', 1
- };
- outbufBytes(config, sizeof config);
- }
- void telnetInsertSpaces(INBUF *inbuf)
- {
- outbufInsertString(0, "\r\r");
- outbufInsertSpaces(1, inbuf->data, inbuf->index);
- outbufInsertSpaces(1, prompt, sizeof prompt - 1);
- }
- void telnetDeleteSpaces(INBUF *inbuf)
- {
- outbufMove(inbuf->index + sizeof prompt + 1, 0);
- }
- void telnetAppendPrompt(INBUF *inbuf)
- {
- outbufBytes(prompt, sizeof prompt - 1);
- outbufBytes(inbuf->data, inbuf->index);
- }
- void telnetDeletePrompt(INBUF *inbuf)
- {
- outbufRewind(inbuf->index + sizeof prompt - 1);
- }
- void telnetParse(INBUF *inbuf, int fd, void (*action)(char *, int),
- const char *data, int length)
- {
- int i;
- outbufClear();
- for (i = 0; i < length; i++)
- {
- int c = data[i];
- if (c == '\xFF' || inbuf->iac)
- {
- if (c == '\xF0' || inbuf->negotiate != '\xFA') inbuf->negotiate = c;
- inbuf->iac = !inbuf->iac;
- }
- else if (inbuf->negotiate >= '\xFA' && inbuf->negotiate <= '\xFE')
- {
- if (inbuf->negotiate != '\xFA') inbuf->negotiate = 0;
- }
- else if (c == '\r')
- {
- outbufFormat("\n");
- outbufFlush(fd);
- inbuf->data[inbuf->index] = '\0';
- (*action)(inbuf->data, sizeof inbuf->data);
- inbuf->index = 0;
- outbufClear();
- outbufBytes(prompt, sizeof prompt - 1);
- }
- else if (c == '\b' || c == '\x7F')
- {
- if (inbuf->index > 0)
- {
- outbufByte('\b');
- outbufAsSpace(inbuf->data[--inbuf->index]);
- outbufByte('\b');
- }
- }
- else if (c >= ' ' && c < '\x7F')
- {
- if (inbuf->index < sizeof inbuf->data - 1)
- {
- outbufByte(c);
- inbuf->data[inbuf->index++] = c;
- }
- }
- }
- outbufFlush(fd);
- }
|
Explanation:
- Line 15-22:
Telnet commands that will be sent from server to client
as soon as a connection has been made.
- Line 18:
DO LINEMODE.
Server is telling client that linemode must not be used.
That way, the Telnet client knows it must send every character to the server
the moment the character is typed in,
rather than save up everything until the user presses
enter.
- Line 19:
WILL ECHO.
Server is telling client that server will
echo
every character sent by client (i.e. typed in by user).
That way, the Telnet client knows it must not do a local echo.
- Line 24-29:
function telnetInsertSpaces outputs a sequence of
whitespace
characters to erase the current line of text from the Telnet window.
The first character is always a
carriage return,
causing the subsequent spaces to overwrite the current prompt
as well as any characters typed in by the user.
The final character being output is also a carriage return,
so that any subsequent text output by the server will be displayed
on the same line of the Telnet window, starting from the left border.
This is used by print.c (see below)
to re-arrange lines of text on the Telnet window.
Note: I could output Telnet’s ‘Erase Line’ command
instead, but I am not sure this is implemented by every Telnet client.
- Line 31-34:
function telnetDeleteSpaces reverts the effect of
telnetInsertSpaces.
- Line 36-40:
function telnetAppendPrompt
outputs not only the prompt,
but also any characters typed in by the user following the prompt.
This is useful as both may have been wiped from the Telnet window
by function telnetInsertSpaces.
- Line 42-45:
function telnetDeletePrompt reverts the effect of
telnetAppendPrompt.
- Line 47-93:
function telnetParse
processes the user input read from the socket.
- Line 55-63:
Telnet commands in user input are ignored,
but as these commands span multiple characters,
we need some logic to skip them.
The INBUF structure has two fields to keep track of this:
iac is set after reading 0xFF
to indicate the next character must be ‘interpreted as command’,
and negotiate is used to detect and skip subnegotiations.
- Line 64-73:
when the user presses enter, we let the game engine (parameter action)
execute a single command (the client’s input buffer)
and send responses back to the relevant client sockets.
After that, the input buffer is cleared and a new prompt is sent to the client.
- Line 74-82:
pressing
backspace
or
delete
will cause the last character to be removed from the input buffer
and erased from the screen (by overwriting it with a
whitespace
character).
Note: I could output Telnet’s ‘Erase Character’ command
instead, but I am not sure this is implemented by every Telnet client.
- Line 83-90:
printable ASCII characters are stored in the client’s input buffer.
Characters will be discarded if necessary to prevent buffer overflow.
Non-ASCII characters (other than the Telnet commands) are discarded as well;
I have not implemented
Unicode
support yet.
Output produced by the game, should be written to sockets rather than
standard output.
This is less straightforward than it seems.
When two or more players are in the same room,
then the actions of one player should be reported to all.
So output may need to go to more than one client.
print.h |
- extern void printSetCurrent(int fd);
- extern void printConsole(const char *format, ...);
- extern void printPrivate(const char *format, ...);
- extern void printSee(const char *format, ...);
- extern void printAny(OBJECT *obj1, OBJECT *obj2, const char *sense,
- const char *format, ...);
|
print.c |
- #include <stdarg.h>
- #include <stdbool.h>
- #include <stdio.h>
- #include <unistd.h>
- #include "object.h"
- #include "outbuf.h"
- #include "telnet.h"
- #include "client.h"
- #define VA(print) va_list ap; va_start(ap, format); print; va_end(ap)
- static int currentSocket = STDOUT_FILENO;
- static bool filter(OBJECT *obj, OBJECT *obj1, OBJECT *obj2)
- {
- return obj2 == NULL ? obj == obj1 : obj != obj1 && obj != obj2;
- }
- static void printDemux(OBJECT *obj1, OBJECT *obj2)
- {
- if (currentSocket == STDOUT_FILENO || player == nobody)
- {
- if (filter(player, obj1, obj2)) outbufFlush(currentSocket);
- }
- else
- {
- CLIENT *client;
- int i;
- for (i = 0; (client = clientGet(i)) != NULL; i++)
- {
- if (client->fd != -1 && client->obj->location == player->location &&
- filter(client->obj, obj1, obj2))
- {
- if (client->fd == currentSocket)
- {
- outbufFlush(client->fd);
- }
- else
- {
- telnetInsertSpaces(&client->inbuf);
- telnetAppendPrompt(&client->inbuf);
- outbufFlush(client->fd);
- telnetDeletePrompt(&client->inbuf);
- telnetDeleteSpaces(&client->inbuf);
- }
- }
- }
- }
- }
- static void printObserve(OBJECT *obj1, OBJECT *obj2, const char *sense,
- const char *format, va_list ap)
- {
- outbufClear();
- outbufFormatVar(format, ap);
- if (obj1 != obj2)
- {
- printDemux(obj1, NULL);
- }
- if (sense != NULL)
- {
- outbufInsertString(3, obj1->description);
- outbufInsertString(3, sense);
- printDemux(obj2, obj1);
- }
- }
- static void printFd(int fd, const char *format, va_list ap)
- {
- outbufClear();
- outbufFormatVar(format, ap);
- outbufFlush(fd);
- }
- void printSetCurrent(int fd)
- {
- currentSocket = fd;
- }
- void printConsole(const char *format, ...)
- {
- VA(printFd(STDOUT_FILENO, format, ap));
- }
- void printPrivate(const char *format, ...)
- {
- VA(printFd(currentSocket, format, ap));
- }
- void printSee(const char *format, ...)
- {
- VA(printObserve(player, NULL, " see ", format, ap));
- }
- void printAny(OBJECT *obj1, OBJECT *obj2, const char *sense,
- const char *format, ...)
- {
- VA(printObserve(obj1, obj2, sense, format, ap));
- }
|
Explanation:
- Line 10:
a little helper
macro
for injecting
boilerplate code
in
variadic functions.
- Line 12:
the game starts in single-user mode, with all output going to
stdout.
Once our TCP server is running, this variable will be continuously overwritten
with the file descriptor of the socket owned by the client being serviced.
Our code is single-threaded, so we can get away with using a static variable.
- Line 19-49:
function printDemux acts like a
demultiplexer.
Output that has been produced by the game
and buffered by outbuf.c (see below),
is being sent to one or more clients
by repeatedly calling outbufFlush, each time with a different socket.
- Line 40-44:
functions from telnet.c are used
to keep input and output apart on the same Telnet window.
This is not necessary for the client who initiated the output;
that user has just finished entering a command,
and a new input prompt has not been displayed yet,
so it is safe to directly send the output (line 36).
- Line 62-63:
this changes simple sentences from second person to third person perspective.
For example, “You go north”
will be changed to “You see Jack go north.”
The former sentence will be sent to the player who entered the command
(“go north”),
the latter to other players present in the same location.
The game engine is never sending output
directly to any sockets or other file handles.
Instead, everything is sent to an output buffer.
This makes it possible to manipulate the text
before sending it off to the various sockets,
as seen in telnet.c and print.c (above).
outbuf.h |
- extern void outbufClear(void);
- extern void outbufRewind(int len);
- extern void outbufByte(char c);
- extern void outbufBytes(const char *data, int length);
- extern void outbufAsSpace(char c);
- extern void outbufFormatVar(const char *format, va_list ap);
- extern void outbufFormat(const char *format, ...);
- extern bool outbufStartsWith(const char *prefix, int len);
- extern bool outbufMove(int from, int to);
- extern void outbufInsertString(int pos, const char *string);
- extern void outbufInsertSpaces(int pos, const char *data, int len);
- extern void outbufFlush(int fd);
|
outbuf.c |
- #include <ctype.h>
- #include <stdarg.h>
- #include <stdbool.h>
- #include <stdio.h>
- #include <string.h>
- #include <netinet/in.h>
- #include "tcp.h"
- #define MAX_LEN 4095
- static int outbufLen;
- static char outbufData[MAX_LEN + 1];
- static int asSpace(int c)
- {
- return isspace(c) ? c : ' ';
- }
- void outbufClear(void)
- {
- outbufLen = 0;
- }
- void outbufRewind(int len)
- {
- outbufLen -= len;
- }
- void outbufByte(char c)
- {
- if (outbufLen < MAX_LEN) outbufData[outbufLen++] = c;
- }
- void outbufBytes(const char *data, int length)
- {
- for (; length > 0; length--) outbufByte(*data++);
- }
- void outbufAsSpace(char c)
- {
- outbufByte(asSpace(c));
- }
- void outbufFormatVar(const char *format, va_list ap)
- {
- char *ptr = outbufData + outbufLen;
- outbufLen += vsnprintf(ptr, sizeof outbufData - outbufLen, format, ap);
- if (outbufLen > MAX_LEN) outbufLen = MAX_LEN;
- while (outbufLen < MAX_LEN && (ptr = strchr(ptr, '\n')) != NULL)
- {
- memmove(ptr + 1, ptr, outbufData + ++outbufLen - ptr);
- *ptr = '\r';
- ptr += 2;
- }
- }
- void outbufFormat(const char *format, ...)
- {
- va_list ap;
- va_start(ap, format);
- outbufFormatVar(format, ap);
- va_end(ap);
- }
- bool outbufStartsWith(const char *prefix, int len)
- {
- return outbufLen >= len && strncmp(outbufData, prefix, len) == 0;
- }
- bool outbufMove(int from, int to)
- {
- outbufLen += to - from;
- if (outbufLen > MAX_LEN) outbufLen = MAX_LEN;
- if (to < outbufLen)
- {
- memmove(outbufData + to, outbufData + from, outbufLen - to);
- }
- return to <= outbufLen;
- }
- void outbufInsertString(int pos, const char *string)
- {
- int len = strlen(string);
- if (outbufMove(pos, pos + len)) strncpy(outbufData + pos, string, len);
- }
- void outbufInsertSpaces(int pos, const char *data, int len)
- {
- if (outbufMove(pos, pos + len))
- {
- int i;
- for (i = 0; i < len; i++) outbufData[pos + i] = asSpace(data[i]);
- }
- }
- void outbufFlush(int fd)
- {
- tcpSend(fd, outbufData, outbufLen);
- }
|
Explanation:
- Line 14-17:
function asSpace translates a character to a
whitespace
character that can be used to overwrite (erase) the character.
Most ASCII characters can be overwritten with
space,
but a
tab
should be overwritten with another tab, to match its size on screen.
- Line 98:
this is where the contents of the output buffer
are actually sent off to a socket or to
stdout.
The following module allows for
a graceful exit from the server’s main loop
in case the process is forced to stop by an outside signal.
break.h |
- extern void breakInit(void);
- extern bool breakTest(void);
- extern int breakSignalNumber(void);
|
break.c |
- #include <stdbool.h>
- #include <string.h>
- #include <signal.h>
- static volatile sig_atomic_t done = 0;
- static void handler(int signum)
- {
- done = signum;
- }
- void breakInit(void)
- {
- struct sigaction action;
- memset(&action, 0, sizeof(action));
- action.sa_handler = handler;
- sigaction(SIGINT, &action, NULL);
- sigaction(SIGTERM, &action, NULL);
- }
- bool breakTest(void)
- {
- return done == 0;
- }
- int breakSignalNumber(void)
- {
- return done;
- }
|
Explanation:
- Line 7-10:
function handler is automatically called when the program receives
one of the signals specified in function breakInit.
- Line 17-18:
SIGINT and
SIGTERM
are caught to allow the server to stop by the normal flow of the program.
- Line 21-24:
the program must regularly call function breakTest.
As long as the function returns true, it is safe to continue.
As soon as the function returns false,
the program should finish its current activity and exit as soon as possible.
So we now have a new multi-user game loop (the TCP server),
but that does not mean we will discard the original single-user game loop.
We still need this loop to resume a saved game, as discussed in chapter 16.
After the log file of an earlier game session has been processed,
it is even possible to enter additional commands manually,
but in most cases, you will want to exit this loop right away
by entering ‘quit’ or pressing
EOF.
The program will then continue to start up its TCP server.
main.c |
- #include <stdbool.h>
- #include <stdio.h>
- #include <string.h>
- #include "object.h"
- #include "print.h"
- #include "expand.h"
- #include "parsexec.h"
- #include "turn.h"
- #include "server.h"
- static char input[100] = "look around";
- static bool getFromFP(FILE *fp)
- {
- bool ok = fgets(input, sizeof input, fp) != NULL;
- if (ok) input[strcspn(input, "\n")] = '\0';
- return ok;
- }
- static bool getInput(const char *filename)
- {
- static FILE *fp = NULL;
- bool ok;
- if (fp == NULL)
- {
- if (filename != NULL) fp = fopen(filename, "rt");
- if (fp == NULL) fp = stdin;
- }
- else if (fp == stdin && filename != NULL)
- {
- FILE *out = fopen(filename, "at");
- if (out != NULL)
- {
- fprintf(out, "%s\n", input);
- fclose(out);
- }
- }
- printConsole("\n--> ");
- ok = getFromFP(fp);
- if (fp != stdin)
- {
- if (ok)
- {
- printConsole("%s\n", input);
- }
- else
- {
- fclose(fp);
- ok = getFromFP(fp = stdin);
- }
- }
- return ok;
- }
- static bool processInput(char *ptr, int size)
- {
- return turn(parseAndExecute(expand(ptr, size)));
- }
- static void processInputAndLog(char *ptr, int size)
- {
- static FILE *fp = NULL;
- if (size > 0)
- {
- if (fp != NULL && player != nobody)
- {
- static OBJECT *lastPlayer = NULL;
- if (player != lastPlayer)
- {
- fprintf(fp, "play %s\n", (lastPlayer = player)->description);
- }
- fprintf(fp, "%s\n", ptr);
- fflush(fp);
- }
- processInput(ptr, size);
- }
- else
- {
- if (fp != NULL) fclose(fp);
- fp = ptr == NULL ? NULL : fopen(ptr, "at");
- }
- }
- int main(int argc, char *argv[])
- {
- (void)argc;
- printConsole("Welcome to Little Cave Adventure.\n");
- printConsole("You are in single-user mode; enter 'quit' for multi-user.\n");
- player = nobody;
- while (processInput(input, sizeof input) && getInput(argv[1]));
- printConsole("\nGoing into multi-user mode; press ^C to stop.\n");
- processInputAndLog(argv[1], 0);
- server(processInputAndLog);
- processInputAndLog(NULL, 0);
- printConsole("\nBye!\n");
- return 0;
- }
|
Explanation:
- Line 60-82:
function processInputAndLog
is a wrapper around processInput
that logs every command of every player while in multi-user mode.
In single-user mode,
it is more convenient to do the logging in function getInput,
as we need to make the switch there
between reading from and writing to the log (line 48-49).
- Line 90:
the initial single-user game loop.
- Line 93:
this is the multi-user game loop.
It will continue running until the program receives a signal
from the administrator or from the
OS
(see break.c above).
By today’s standards,
our program can hardly be called a mature game server.
It has several flaws:
- No
authentication.
This could be fixed by demanding a password to be included
in command play.
- Vulnerable to
eavesdropping:
everything is transmitted unencrypted, including passwords!
This could be fixed with
tcpcrypt
or with a secure tunnel (through
SSH or
VPN).
- Vulnerable to
man-in-the-middle attacks.
Again, a user’s password could be intercepted.
To mitigate that, you’d need a
digital certificate.
- Does not
scale
well. First of all,
select
suffers from the
C10k problem
and should be replaced by
poll,
epoll or
kqueue.
Secondly, it is not straightforward to scale out to multiple
nodes.
A solution might be to partition the game into multiple ‘worlds.’
It would make more sense to turn the whole game into a web application.
For that, it’s probably best to port the program to
a different programming language, for example JavaScript.
We will give that a shot in chapter 25.
Next chapter: 23. Database