From 49948b4cc0f73d02a8932c525690a35e8efb6ac5 Mon Sep 17 00:00:00 2001 From: Lizzy Fleckenstein Date: Wed, 19 Jun 2024 18:07:47 +0200 Subject: add client Signed-off-by: Lizzy Fleckenstein --- include/net.h | 51 ++++++++++++ include/peer.h | 41 ---------- include/ticker.h | 23 ++++++ meson.build | 19 ++++- src/client.c | 125 ++++++++++++++++++++++++++++++ src/net.c | 231 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/peer.c | 164 --------------------------------------- src/server.c | 75 ++++-------------- src/ticker.c | 43 +++++++++++ 9 files changed, 501 insertions(+), 271 deletions(-) create mode 100644 include/net.h delete mode 100644 include/peer.h create mode 100644 include/ticker.h create mode 100644 src/client.c create mode 100644 src/net.c delete mode 100644 src/peer.c create mode 100644 src/ticker.c diff --git a/include/net.h b/include/net.h new file mode 100644 index 0000000..fcb12a4 --- /dev/null +++ b/include/net.h @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: 2024 Lizzy Fleckenstein +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +#ifndef NET_H +#define NET_H + +#include +#include +#include +#include +#include + +#include "str.h" + +#define SET_NONBLOCK(X) fcntl((X), F_SETFL, fcntl((X), F_GETFL, 0) | O_NONBLOCK) + +#define PEER_INBUFFER_SIZE 0x100000 // 1MB +#define PEER_OUTBUFFER_SIZE 0x200000 // 2MB + +typedef uint32_t pkt_header; + +typedef struct { + int socket; + bool disco; + struct { + bool header; + size_t len; + size_t promised; + uint8_t *buffer; + } in; + // TODO: ring buffer + struct { + size_t avail; + size_t cursor; + uint8_t *buffer; + } out; +} peer; + +void invalid_pkt(str from, str pkt); + +int socket_create(const char *host, const char *port, bool server); +int socket_accept(int accept_fd); + +void peer_init(peer *p, int socket); +void peer_free(peer *p); +struct pollfd peer_prepare(peer *p); +str peer_recv(peer *p, struct pollfd pfd); +bool peer_send(peer *p, void *data, size_t len); + +#endif diff --git a/include/peer.h b/include/peer.h deleted file mode 100644 index f6baa4b..0000000 --- a/include/peer.h +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Lizzy Fleckenstein -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -#ifndef PEER_H -#define PEER_H - -#include -#include -#include -#include - -#define PEER_INBUFFER_SIZE 0x100000 // 1MB -#define PEER_OUTBUFFER_SIZE 0x200000 // 2MB - -typedef uint32_t pkt_header; - -typedef struct { - int socket; - bool disco; - struct { - bool header; - size_t len; - size_t promised; - uint8_t *buffer; - } in; - // TODO: ring buffer - struct { - size_t avail; - size_t cursor; - uint8_t *buffer; - } out; -} peer; - -void peer_init(peer *p, int socket); -void peer_free(peer *p); -struct pollfd peer_prepare(peer *p); -bool peer_ready(peer *p, struct pollfd revents); -bool peer_send(peer *p, void *data, size_t len); - -#endif diff --git a/include/ticker.h b/include/ticker.h new file mode 100644 index 0000000..f6387c4 --- /dev/null +++ b/include/ticker.h @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2024 Lizzy Fleckenstein +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +#ifndef TICKER_H +#define TICKER_H + +#include +#include +#include + +#define NANOS 1000000000 + +typedef struct { + struct timespec timestamp; + uint64_t freq_nanos; +} ticker; + +void ticker_init(ticker *t, uint64_t f); +bool ticker_tick(ticker *t, uint64_t *dtime); +int ticker_timeout(ticker *t); + +#endif diff --git a/meson.build b/meson.build index e1b8dce..81dc491 100644 --- a/meson.build +++ b/meson.build @@ -11,16 +11,19 @@ deps = [ src = [ 'src/str.c', - 'src/peer.c', + 'src/net.c', 'src/ser.c', + 'src/ticker.c', ] -sillyserver = executable('sillyserver', +include = 'include/' + +executable('sillyserver', sources: [ src, 'src/server.c', ], - include_directories: 'include/', + include_directories: include, dependencies: [ deps, dependency('libpng'), @@ -28,4 +31,12 @@ sillyserver = executable('sillyserver', install: true, ) -run_target('run', command: sillyserver) +executable('sillyclient', + sources: [ + src, + 'src/client.c', + ], + include_directories: include, + dependencies: deps, + install: true, +) diff --git a/src/client.c b/src/client.c new file mode 100644 index 0000000..0dabbbf --- /dev/null +++ b/src/client.c @@ -0,0 +1,125 @@ +#include +#include +#include +#include +#include +#include +#include +#include "str.h" +#include "ser.h" +#include "net.h" +#include "ticker.h" + +typedef struct { + peer conn; + struct termios oldtio; + str name; + str pass; +} client; + +void gfx_alt_buffer(bool enable) +{ + if (enable) + printf("\e[?1049h\e[?25l"); + else + printf("\e[?1049l\e[?25h"); +} + +void gfx_render(client *c, uint64_t dtime) +{ + // TODO: render game +} + +void client_exit(client *c, int ret) +{ + free(c->name.data); + free(c->pass.data); + + peer_free(&c->conn); + tcsetattr(STDIN_FILENO, TCSANOW, &c->oldtio); + gfx_alt_buffer(false); + + exit(ret); +} + +void handle_input(client *c, char ch) +{ + ch = tolower(ch); + switch (ch) { + case 'q': client_exit(c, EXIT_SUCCESS); break; + default: break; + } +} + +bool handle_pkt(client *c, str pkt) +{ + // TODO: handle network traffic + return true; +} + +int main() +{ + client c = {0}; + + c.name = str_clone(S("kitten")); + c.pass = str_clone(S("")); + + c.conn.socket = -1; + + tcgetattr(STDIN_FILENO, &c.oldtio); // TODO: handle error + + struct termios newtio = c.oldtio; + newtio.c_lflag &= ~(ICANON | ECHO); + tcsetattr(STDIN_FILENO, TCSANOW, &newtio); // TODO: handle error + + SET_NONBLOCK(STDIN_FILENO); // TODO: handle error + + int socket = socket_create("127.0.0.1", "4560", false); + if (socket < 0) + client_exit(&c, EXIT_FAILURE); + peer_init(&c.conn, socket); + + gfx_alt_buffer(true); + + ticker t; + ticker_init(&t, NANOS/60); + + for (;;) { + struct pollfd fds[2]; + + fds[0].fd = STDIN_FILENO; + fds[0].events = POLLIN; + + fds[1] = peer_prepare(&c.conn); + + if (poll(fds, 2, ticker_timeout(&t)) < 0) { + switch (errno) { + case EINTR: continue; + default: perror("poll"); continue; + } + } + + if (fds[0].revents) { + // this is cursed + // maybe dont do this idk + char ch; + errno = 0; + while (read(STDIN_FILENO, &ch, 1) == 1) + handle_input(&c, ch); + switch (errno) { + case 0: case EWOULDBLOCK: case EINTR: break; + default: perror("read"); break; + } + } + + str pkt = peer_recv(&c.conn, fds[1]); + if (c.conn.disco) + client_exit(&c, EXIT_SUCCESS); + if (pkt.len > 0 && !handle_pkt(&c, pkt)) + invalid_pkt(S("server"), pkt); + + uint64_t dtime; + if (ticker_tick(&t, &dtime)) + gfx_render(&c, dtime); + } +} diff --git a/src/net.c b/src/net.c new file mode 100644 index 0000000..18970c1 --- /dev/null +++ b/src/net.c @@ -0,0 +1,231 @@ +// SPDX-FileCopyrightText: 2024 Lizzy Fleckenstein +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "net.h" + +void invalid_pkt(str from, str pkt) +{ + // TODO: maybe inform peer about failure? not sure + // FIXME: hexdumping a gazillon bytes to stderr might be an issue + fprintf(stderr, "invalid pkt from %*s: ", PSTR(from)); + for (size_t i = 0; i < pkt.len; i++) + fprintf(stderr, "%02x%c", (uint8_t) pkt.data[i], i+1 == pkt.len ? '\n' : ' '); +} + +int socket_create(const char *host, const char *port, bool server) +{ + struct addrinfo *info = NULL, hints = {0}; + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + + int err; + if ((err = getaddrinfo(host, port, &hints, &info))) { + fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(err)); + return -1; + } + +#define TRY(op, val) if ((val) < 0) { perror(op); close(fd); freeaddrinfo(info); return -1; } + + int fd = -1; + TRY("socket", fd = socket(info->ai_family, info->ai_socktype, info->ai_protocol)) + + int flag = 1; + TRY("setsockopt", setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void *) &flag, sizeof flag)) + + if (server) { + TRY("bind", bind(fd, info->ai_addr, info->ai_addrlen)) + TRY("listen", listen(fd, 3)) + } else { + TRY("connect", connect(fd, info->ai_addr, info->ai_addrlen)) + TRY("fcntl", SET_NONBLOCK(fd)) + } + +#undef TRY + + freeaddrinfo(info); + return fd; +} + +int socket_accept(int accept_fd) +{ + int socket = accept(accept_fd, NULL, NULL); // TODO: save address + if (socket < 0) { + perror("accept"); + return -1; + } + + if (SET_NONBLOCK(socket) < 0) { + perror("fcntl"); + close(socket); + return -1; + } + + return socket; +} + +void peer_init(peer *p, int socket) +{ + p->socket = socket; + p->disco = false; + + p->in.header = true; + p->in.len = 0; + p->in.promised = 0; + p->in.buffer = malloc(PEER_INBUFFER_SIZE); + + p->out.cursor = 0; + p->out.avail = 0; + p->out.buffer = malloc(PEER_OUTBUFFER_SIZE); +} + +void peer_free(peer *p) +{ + close(p->socket); + free(p->in.buffer); + free(p->out.buffer); +} + +// in + +static void next_in(peer *p, bool header, size_t len) +{ + p->in.header = header; + p->in.len = 0; + p->in.buffer = malloc(p->in.promised = len); +} + +static bool peer_in_ready(peer *p) +{ + errno = 0; + ssize_t got = read(p->socket, p->in.buffer + p->in.len, p->in.promised - p->in.len); + if (got <= 0) { + switch (errno) { + case 0: case ECONNRESET: + p->disco = true; + return true; + case EINTR: + return peer_in_ready(p); // retry + default: + perror("read"); + return false; + } + } + + p->in.len += got; + if (p->in.len == p->in.promised && p->in.len != 0) { + if (!p->in.header) + return true; + + size_t len = *(pkt_header *) p->in.buffer; + if (len > PEER_INBUFFER_SIZE || len == 0) + // TODO: figure out what to do if packet too large (disconnect?) + next_in(p, true, sizeof(pkt_header)); + else + next_in(p, false, len); + } + + return false; +} + +// out + +static void send_raw(peer *p, void *data, size_t len) +{ + memcpy(p->out.buffer + p->out.cursor + p->out.avail, data, len); + p->out.avail += len; +} + +static bool out_space(peer *p, size_t len) +{ + if (len + p->out.avail > PEER_OUTBUFFER_SIZE) + return false; + + if (p->out.cursor + p->out.avail + len > PEER_OUTBUFFER_SIZE) { + memmove(p->out.buffer, p->out.buffer + p->out.cursor, p->out.avail); + p->out.cursor = 0; + } + + return true; +} + +bool peer_send(peer *p, void *data, size_t len) +{ + if (len > PEER_INBUFFER_SIZE) + return false; + + pkt_header hdr = (pkt_header) len; + + if (!out_space(p, sizeof hdr + len)) + return false; + + send_raw(p, &hdr, sizeof hdr); + send_raw(p, data, len); + + return true; +} + +static bool peer_out_ready(peer *p) +{ + ssize_t written = write(p->socket, p->out.buffer + p->out.cursor, p->out.avail); + if (written < 0) { + switch (errno) { + case ECONNRESET: + p->disco = true; + return true; + case EINTR: + return peer_out_ready(p); + default: + perror("write"); + return false; + } + } + + p->out.avail -= written; + if (p->out.avail == 0) + p->out.cursor = 0; + + return false; +} + +// poll + +struct pollfd peer_prepare(peer *p) +{ + struct pollfd pfd = {0}; + + pfd.fd = p->socket; + if (p->in.len == p->in.promised) + next_in(p, true, sizeof(pkt_header)); + + pfd.events = POLLIN | (p->out.avail ? POLLOUT : 0); + + return pfd; +} + +str peer_recv(peer *p, struct pollfd pfd) +{ + if (pfd.revents & (POLLHUP | POLLERR)) + p->disco = true; + + if (!p->disco && pfd.revents & POLLOUT) + peer_out_ready(p); + + if (!p->disco && pfd.revents & POLLIN) + if (peer_in_ready(p)) + return (str) { p->in.len, (char *) p->in.buffer }; + + return NILS; +} diff --git a/src/peer.c b/src/peer.c deleted file mode 100644 index f984aec..0000000 --- a/src/peer.c +++ /dev/null @@ -1,164 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Lizzy Fleckenstein -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -#include -#include -#include -#include -#include -#include -#include "peer.h" - -void peer_init(peer *p, int socket) -{ - p->socket = socket; - p->disco = false; - - p->in.header = true; - p->in.len = 0; - p->in.promised = 0; - p->in.buffer = malloc(PEER_INBUFFER_SIZE); - - p->out.cursor = 0; - p->out.avail = 0; - p->out.buffer = malloc(PEER_OUTBUFFER_SIZE); -} - -void peer_free(peer *p) -{ - close(p->socket); - free(p->in.buffer); - free(p->out.buffer); -} - -// in - -static void next_in(peer *p, bool header, size_t len) -{ - p->in.header = header; - p->in.len = 0; - p->in.buffer = malloc(p->in.promised = len); -} - -static bool peer_in_ready(peer *p) -{ - ssize_t got = read(p->socket, p->in.buffer + p->in.len, p->in.promised - p->in.len); - if (got < 0) { - switch (errno) { - case ECONNRESET: - p->disco = true; - return true; - case EINTR: - return peer_in_ready(p); // retry - default: - perror("read"); - return false; - } - } - - p->in.len += got; - if (p->in.len == p->in.promised && p->in.len != 0) { - if (!p->in.header) - return true; - - size_t len = *(pkt_header *) p->in.buffer; - if (len > PEER_INBUFFER_SIZE) - // TODO: figure out what to do if packet too large (disconnect?) - next_in(p, true, sizeof(pkt_header)); - else - next_in(p, false, len); - } - - return false; -} - -// out - -static void send_raw(peer *p, void *data, size_t len) -{ - memcpy(p->out.buffer + p->out.cursor + p->out.avail, data, len); - p->out.avail += len; -} - -static bool out_space(peer *p, size_t len) -{ - if (len + p->out.avail > PEER_OUTBUFFER_SIZE) - return false; - - if (p->out.cursor + p->out.avail + len > PEER_OUTBUFFER_SIZE) { - memmove(p->out.buffer, p->out.buffer + p->out.cursor, p->out.avail); - p->out.cursor = 0; - } - - return true; -} - -bool peer_send(peer *p, void *data, size_t len) -{ - if (len > PEER_INBUFFER_SIZE) - return false; - - pkt_header hdr = (pkt_header) len; - - if (!out_space(p, sizeof hdr + len)) - return false; - - send_raw(p, &hdr, sizeof hdr); - send_raw(p, data, len); - - return true; -} - -static bool peer_out_ready(peer *p) -{ - ssize_t written = write(p->socket, p->out.buffer + p->out.cursor, p->out.avail); - if (written < 0) { - switch (errno) { - case ECONNRESET: - p->disco = true; - return true; - case EINTR: - return peer_out_ready(p); - default: - perror("write"); - return false; - } - } - - p->out.avail -= written; - if (p->out.avail == 0) - p->out.cursor = 0; - - return false; -} - -// poll - -struct pollfd peer_prepare(peer *p) -{ - struct pollfd pfd = {0}; - - pfd.fd = p->socket; - if (p->in.len == p->in.promised) - next_in(p, true, sizeof(pkt_header)); - - pfd.events = POLLIN | (p->out.avail ? POLLOUT : 0); - - return pfd; -} - -bool peer_ready(peer *p, struct pollfd pfd) -{ - bool x = false; - - if (pfd.revents & (POLLHUP | POLLERR)) - p->disco = true; - - if (pfd.revents & POLLIN) - x = x || peer_in_ready(p); - if (pfd.revents & POLLOUT) - x = x || peer_out_ready(p); - - return x; -} diff --git a/src/server.c b/src/server.c index fa48b25..621acae 100644 --- a/src/server.c +++ b/src/server.c @@ -11,18 +11,12 @@ #include #include #include -#include #include #include -#include #include -#include -#include -#include -#include #include "array.h" #include "content.h" -#include "peer.h" +#include "net.h" #include "str.h" #include "ser.h" @@ -56,6 +50,7 @@ typedef struct { } player; typedef struct { + int accept_fd; str motd; str passphrase; array(map) maps; @@ -179,36 +174,6 @@ bool map_load(map *m) #undef TRY } -int net_listen(const char *host, const char *port) -{ - struct addrinfo *info = NULL, hints = {0}; - hints.ai_family = AF_UNSPEC; - hints.ai_socktype = SOCK_STREAM; - - int err; - if ((err = getaddrinfo(host, port, &hints, &info))) { - fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(err)); - return -1; - } - -#define TRY(op, val) if ((val) < 0) { perror(op); freeaddrinfo(info); return -1; } - - int fd; - TRY("socket", fd = socket(info->ai_family, info->ai_socktype, info->ai_protocol)) - - int flag = 1; - TRY("setsockopt", setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void *) &flag, sizeof flag)) - - TRY("bind", bind(fd, info->ai_addr, info->ai_addrlen)) - TRY("listen", listen(fd, 3)) - -#undef TRY - - freeaddrinfo(info); - return fd; - -} - void ser_color(strbuf *w, color c) { ser_u8(w, c.r); @@ -336,7 +301,7 @@ bool handle_hi(str pkt, player *p, game *g) return true; } -bool process_pkt(str pkt, player *p, game *g) +bool handle_pkt(str pkt, player *p, game *g) { pkt_type type; if (!deser_pkt_type(&pkt, &type)) @@ -355,6 +320,7 @@ int main() { game g = {0}; + g.accept_fd = -1; g.entity_id = 1; g.motd = str_clone(S("Welcome to test server")); g.passphrase = str_clone(S("")); @@ -376,8 +342,7 @@ int main() if (!map_load(&g.maps.data[0])) game_exit(&g, EXIT_FAILURE); - int accept_fd = net_listen("0.0.0.0", "4560"); - if (accept_fd < 0) + if ((g.accept_fd = socket_create("0.0.0.0", "4560", true)) < 0) game_exit(&g, EXIT_FAILURE); for (;;) { @@ -394,7 +359,7 @@ int main() fds[i++] = peer_prepare(&p->conn); } - fds[g.players.len].fd = accept_fd; + fds[g.players.len].fd = g.accept_fd; fds[g.players.len].events = POLLIN; if (poll(fds, g.players.len + 1, -1) < 0) { @@ -406,32 +371,18 @@ int main() for (size_t i = 0; i < g.players.len; i++) { player *p = &g.players.data[i]; - if (!peer_ready(&p->conn, fds[i])) - continue; - if (p->conn.disco) + + str pkt = peer_recv(&p->conn, fds[i]); + if (p->conn.disco || pkt.len == 0) continue; - str pkt = { p->conn.in.len, (char *) p->conn.in.buffer }; - if (!process_pkt(pkt, p, &g)) { - // TODO: maybe inform client about failure? not sure - // FIXME: hexdumping a gazillon bytes to stderr might be an issue - fprintf(stderr, "invalid pkt from %*s: ", PSTR(p->name)); - for (size_t i = 0; i < pkt.len; i++) - fprintf(stderr, "%02x%c", (uint8_t) pkt.data[i], i+1 == pkt.len ? '\n' : ' '); - } + if (!handle_pkt(pkt, p, &g)) + invalid_pkt(p->name, pkt); } if (fds[g.players.len].revents) { - int socket = accept(accept_fd, NULL, NULL); // TODO: save ip - if (socket < 0) { - perror("accept"); + int socket = socket_accept(g.accept_fd); + if (socket < 0) continue; - } - - if (fcntl(socket, F_SETFL, fcntl(socket, F_GETFL, 0) | O_NONBLOCK) < 0) { - close(socket); - perror("fcntl"); - continue; - } printf("new player\n"); player *p = ARR_APPEND(g.players); diff --git a/src/ticker.c b/src/ticker.c new file mode 100644 index 0000000..d3c25b5 --- /dev/null +++ b/src/ticker.c @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2024 Lizzy Fleckenstein +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +#include "ticker.h" + +static struct timespec timestamp_now() +{ + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); // TODO: handle error + return ts; +} + +static uint64_t timestamp_diff(struct timespec a, struct timespec b) +{ + return NANOS * ((uint64_t) a.tv_sec - b.tv_sec) + ((int64_t) a.tv_nsec - b.tv_nsec); +} + +void ticker_init(ticker *t, uint64_t f) +{ + t->freq_nanos = f; + t->timestamp = timestamp_now(); +} + +bool ticker_tick(ticker *t, uint64_t *dtime) +{ + struct timespec ts = timestamp_now(); + *dtime = timestamp_diff(ts, t->timestamp); + if (*dtime < t->freq_nanos) + return false; + t->timestamp = ts; + return true; +} + +// millis +int ticker_timeout(ticker *t) +{ + uint64_t nanos = timestamp_diff(timestamp_now(), t->timestamp); + if (t->freq_nanos > nanos) + return (t->freq_nanos - nanos) / 1000000; // nanos to millis + else + return 0; +} -- cgit v1.2.3