Lay down some server-client foundations

This commit is contained in:
Přemysl Eric Janouch 2014-10-26 18:57:28 +01:00
parent 3ea5918fee
commit 29bec0c0e0
2 changed files with 372 additions and 8 deletions

View File

@ -39,6 +39,22 @@
#define BITMAP_BLOCK_SIZE 50 ///< Step for extending bitmap size
typedef enum network_mode network_mode_t;
enum network_mode
{
NETWORK_MODE_STANDALONE, ///< No networking taking place
NETWORK_MODE_SERVER, ///< We're the server
NETWORK_MODE_CLIENT ///< We're a client
};
typedef struct client client_t;
struct client
{
LIST_HEADER (client_t)
uv_tcp_t handle; ///< TCP connection handle
};
typedef struct app_context app_context_t;
struct app_context
{
@ -49,6 +65,16 @@ struct app_context
uv_timer_t tty_timer; ///< TTY timeout timer
uv_signal_t winch_watcher; ///< SIGWINCH watcher
network_mode_t mode; ///< Networking mode
char read_buf[8192]; ///< Global read buffer for libuv
// Client:
uv_tcp_t server_fd; ///< Connection to the server
// Server:
uv_tcp_t listen_fd; ///< Listening FD
client_t *clients; ///< Client connections
chtype palette[2 * 9]; ///< Attribute palette
uint8_t *bitmap; ///< Canvas data for drawing
@ -78,11 +104,23 @@ app_init (app_context_t *self)
memset (self, 0, sizeof *self);
}
static void
remove_client (app_context_t *app, client_t *client)
{
// TODO: cancel any write requests?
// XXX: should we unref it?
uv_close ((uv_handle_t *) &client->handle, NULL);
LIST_UNLINK (app->clients, client);
free (client);
}
static void
app_free (app_context_t *self)
{
if (self->tk)
termo_destroy (self->tk);
while (self->clients)
remove_client (self, self->clients);
free (self->bitmap);
}
@ -465,7 +503,7 @@ export_irc (app_context_t *app)
fclose (fp);
}
// -----------------------------------------------------------------------------
// --- Event handlers ----------------------------------------------------------
static bool
on_key (app_context_t *app, termo_key_t *key)
@ -602,14 +640,158 @@ on_tty_readable (uv_poll_t *handle, int status, int events)
}
static void
parse_program_arguments (app_context_t *app, int argc, char **argv)
app_uv_allocator (uv_handle_t *handle, size_t suggested_size, uv_buf_t *buf)
{
(void) app;
// Let's just use a single "global" buffer
(void) suggested_size;
app_context_t *app = handle->loop->data;
buf->base = app->read_buf;
buf->len = sizeof app->read_buf;
}
static void
on_server_data (uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf)
{
(void) buf;
app_context_t *app = stream->loop->data;
if (nread == UV_EOF || nread < 0)
{
// TODO: cancel any write requests?
// XXX: should we unref it?
uv_close ((uv_handle_t *) &app->server_fd, NULL);
display ("Disconnected!");
beep (); // Beep beep! Made a boo-boo.
// Let the user save the picture at least.
// Also prevents us from trying to use the dead server handle.
app->mode = NETWORK_MODE_STANDALONE;
return;
}
// TODO: process the data
}
static void
on_client_data (uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf)
{
(void) buf;
app_context_t *app = stream->loop->data;
client_t *client = stream->data;
if (nread == UV_EOF || nread < 0)
{
remove_client (app, client);
return;
}
// TODO: process the data
}
static void
on_new_client (uv_stream_t *server, int status)
{
app_context_t *app = server->loop->data;
if (status)
return;
int err;
client_t *client = xcalloc (1, sizeof *client);
if ((err = uv_tcp_init (server->loop, &client->handle)))
goto free_client;
if ((err = uv_accept (server, (uv_stream_t *) &client->handle))
|| (err = uv_read_start ((uv_stream_t *) &client->handle,
app_uv_allocator, on_client_data)))
// XXX: do we need to un-accept?
goto free_handle;
client->handle.data = client;
LIST_PREPEND (app->clients, client);
return;
free_handle:
uv_close ((uv_handle_t *) &client->handle, NULL);
// XXX: should we unref it?
free_client:
free (client);
}
// --- Program startup ---------------------------------------------------------
typedef struct app_options app_options_t;
struct app_options
{
struct addrinfo *client_address; ///< Address to connect to
struct addrinfo *server_address; ///< Address to listen at
};
static void
app_options_init (app_options_t *self)
{
memset (self, 0, sizeof *self);
}
static void
app_options_free (app_options_t *self)
{
if (self->client_address) freeaddrinfo (self->client_address);
if (self->server_address) freeaddrinfo (self->server_address);
}
static struct addrinfo *
parse_address (const char *address, int flags)
{
char address_copy[strlen (address) + 1];
strcpy (address_copy, address);
char *colon = strrchr (address_copy, ':');
if (!colon)
{
fprintf (stderr, "error: no port number specified in `%s'\n", address);
return false;
}
char *host = address_copy, *service = colon + 1;
if (host == colon)
host = NULL;
else if (host < colon && *host == '[' && colon[-1] == ']')
{
// Remove IPv6 RFC 2732-style [] brackets from the host, if present.
// This also makes it possible to take the usage string literally. :))
host++;
colon[-1] = '\0';
}
else
*colon = '\0';
struct addrinfo *result, hints =
{
.ai_socktype = SOCK_STREAM,
.ai_protocol = IPPROTO_TCP,
.ai_flags = flags,
};
int err = getaddrinfo (host, service, &hints, &result);
if (err)
{
fprintf (stderr, "error: cannot resolve `%s', port `%s': %s\n",
host, service, gai_strerror (err));
return false;
}
return result;
}
static void
parse_program_arguments (app_options_t *options, int argc, char **argv)
{
static const struct opt opts[] =
{
{ 'h', "help", NULL, 0, "display this help and exit" },
{ 'V', "version", NULL, 0, "output version information and exit" },
{ 's', "server", "[ADDRESS]:PORT", 0, "start a server" },
{ 'c', "client", "[ADDRESS]:PORT", 0, "connect to a server" },
{ 0, NULL, NULL, 0, NULL }
};
@ -627,12 +809,39 @@ parse_program_arguments (app_context_t *app, int argc, char **argv)
case 'V':
printf (PROJECT_NAME " " PROJECT_VERSION "\n");
exit (EXIT_SUCCESS);
case 's':
if (options->server_address)
{
fprintf (stderr, "%s: %s\n",
"error", "cannot specify multiple listening addresses");
exit (EXIT_FAILURE);
}
if (!(options->server_address = parse_address (optarg, AI_PASSIVE)))
exit (EXIT_FAILURE);
break;
case 'c':
if (options->client_address)
{
fprintf (stderr, "%s: %s\n",
"error", "cannot specify multiple addresses to connect to");
exit (EXIT_FAILURE);
}
if (!(options->client_address = parse_address (optarg, 0)))
exit (EXIT_FAILURE);
break;
default:
fprintf (stderr, "%s: %s", "error", "wrong options");
fprintf (stderr, "%s: %s\n", "error", "wrong options");
opt_handler_usage (&oh, stderr);
exit (EXIT_FAILURE);
}
if (options->client_address && options->server_address)
{
fprintf (stderr, "%s: %s\n",
"error", "cannot be both a server and a client");
exit (EXIT_FAILURE);
}
argc -= optind;
argv += optind;
@ -645,6 +854,77 @@ parse_program_arguments (app_context_t *app, int argc, char **argv)
opt_handler_free (&oh);
}
static void
initialize_client (app_context_t *app, struct addrinfo *address)
{
app->mode = NETWORK_MODE_CLIENT;
int sock_fd, err;
for (; address; address = address->ai_next)
{
sock_fd = socket (address->ai_family,
address->ai_socktype, address->ai_protocol);
if (sock_fd == -1)
continue;
char host_buf[NI_MAXHOST], serv_buf[NI_MAXSERV];
err = getnameinfo (address->ai_addr, address->ai_addrlen,
host_buf, sizeof host_buf, serv_buf, sizeof serv_buf,
NI_NUMERICHOST | NI_NUMERICSERV);
if (err)
{
fprintf (stderr, "%s: %s: %s\n",
"error", "getnameinfo", gai_strerror (err));
fprintf (stderr, "connecting...\n");
}
else
{
char *x = format_host_port_pair (host_buf, serv_buf);
fprintf (stderr, "connecting to %s...\n", x);
free (x);
}
if (!connect (sock_fd, address->ai_addr, address->ai_addrlen))
break;
xclose (sock_fd);
}
if (!address)
{
fprintf (stderr, "%s: %s\n", "error", "connection failed");
exit (EXIT_FAILURE);
}
set_blocking (sock_fd, false);
if ((err = uv_tcp_init (uv_default_loop (), &app->server_fd))
|| (err = uv_tcp_open (&app->server_fd, sock_fd))
|| (err = uv_tcp_keepalive (&app->server_fd, true, 30))
|| (err = uv_read_start ((uv_stream_t *) &app->server_fd,
app_uv_allocator, on_server_data)))
{
fprintf (stderr, "%s: %s: %s\n",
"error", "initialization failed", uv_strerror (err));
exit (EXIT_FAILURE);
}
}
static void
initialize_server (app_context_t *app, struct addrinfo *address)
{
app->mode = NETWORK_MODE_SERVER;
int err;
if ((err = uv_tcp_init (uv_default_loop (), &app->listen_fd))
|| (err = uv_tcp_bind (&app->listen_fd, address->ai_addr, 0))
|| (err = uv_listen ((uv_stream_t *) &app->listen_fd, 10, on_new_client)))
{
fprintf (stderr, "%s: %s: %s\n",
"error", "initialization failed", uv_strerror (err));
exit (EXIT_FAILURE);
}
}
int
main (int argc, char *argv[])
{
@ -653,12 +933,25 @@ main (int argc, char *argv[])
app_context_t app;
app_init (&app);
parse_program_arguments (&app, argc, argv);
app_options_t options;
app_options_init (&options);
parse_program_arguments (&options, argc, argv);
if (options.client_address)
initialize_client (&app, options.client_address);
else if (options.server_address)
initialize_server (&app, options.server_address);
else
app.mode = NETWORK_MODE_STANDALONE;
app_options_free (&options);
termo_t *tk = termo_new (STDIN_FILENO, NULL, 0);
if (!tk)
{
fprintf (stderr, "Cannot allocate termo instance\n");
fprintf (stderr, "%s: %s\n",
"error", "cannot allocate termo instance\n");
exit (EXIT_FAILURE);
}
@ -669,7 +962,7 @@ main (int argc, char *argv[])
// Set up curses for our drawing needs
if (!initscr () || nonl () == ERR || curs_set (0) == ERR)
{
fprintf (stderr, "Cannot initialize curses\n");
fprintf (stderr, "%s: %s\n", "error", "cannot initialize curses");
exit (EXIT_FAILURE);
}
@ -694,8 +987,8 @@ main (int argc, char *argv[])
uv_run (loop, UV_RUN_DEFAULT);
endwin ();
uv_loop_close (loop);
app_free (&app);
uv_loop_close (loop);
return 0;
}

71
utils.c
View File

@ -34,6 +34,9 @@
#define N_ELEMENTS(a) (sizeof (a) / sizeof ((a)[0]))
#define BLOCK_START do {
#define BLOCK_END } while (0)
// --- Safe memory management --------------------------------------------------
// When a memory allocation fails and we need the memory, we're usually pretty
@ -76,6 +79,31 @@ xrealloc (void *o, size_t n)
return p;
}
// --- Double-linked list helpers ----------------------------------------------
#define LIST_HEADER(type) \
type *next; \
type *prev;
#define LIST_PREPEND(head, link) \
BLOCK_START \
(link)->prev = NULL; \
(link)->next = (head); \
if ((link)->next) \
(link)->next->prev = (link); \
(head) = (link); \
BLOCK_END
#define LIST_UNLINK(head, link) \
BLOCK_START \
if ((link)->prev) \
(link)->prev->next = (link)->next; \
else \
(head) = (link)->next; \
if ((link)->next) \
(link)->next->prev = (link)->prev; \
BLOCK_END
// --- Dynamically allocated strings -------------------------------------------
// Basically a string builder to abstract away manual memory management.
@ -190,6 +218,49 @@ str_append_printf (struct str *self, const char *fmt, ...)
// --- Utilities ---------------------------------------------------------------
static bool
set_blocking (int fd, bool blocking)
{
int flags = fcntl (fd, F_GETFL);
bool prev = !(flags & O_NONBLOCK);
if (blocking)
flags &= ~O_NONBLOCK;
else
flags |= O_NONBLOCK;
return prev;
}
static void
xclose (int fd)
{
while (close (fd) == -1)
if (errno != EINTR)
break;
}
static char *xstrdup_printf (const char *, ...) ATTRIBUTE_PRINTF (1, 2);
static char *
xstrdup_printf (const char *format, ...)
{
va_list ap;
struct str tmp;
str_init (&tmp);
va_start (ap, format);
str_append_vprintf (&tmp, format, ap);
va_end (ap);
return str_steal (&tmp);
}
static char *
format_host_port_pair (const char *host, const char *port)
{
// IPv6 addresses mess with the "colon notation"; let's go with RFC 2732
if (strchr (host, ':'))
return xstrdup_printf ("[%s]:%s", host, port);
return xstrdup_printf ("%s:%s", host, port);
}
static bool
xstrtoul (unsigned long *out, const char *s, int base)
{