From b9aeccef208d6d5b7d40b71886981723f1e14b95 Mon Sep 17 00:00:00 2001 From: Raymaekers Luca Date: Thu, 31 Oct 2024 00:32:07 +0100 Subject: Added ID system with 1 and 2-way communication Each client now has an ID that is permanently stored to ID_FILE location. To implement this each client now uses two connections to the server, one for bidirectional communication and one for unidirectional communication. This makes it easier to not receive unexpected message. Also each client and server now has a Client struct that represents a client and a clientsArena associated with it. Minor changes: - Added logging to LOGFILE, that can be turned with LOGGING macro. - Added more error types - Added error handling on server - Added error messages - Added convenience functions - Added disconnectAndNotify() function for convenience - Use recvTextMessageResult as multiple-value-return-type instead of ** - Separated protocol stuff into protocol.h - Added Result types when wanting to return multiple values - Do not allocate arena's with malloc - Added recvAnyMessageType for receiving messages that do not need to be stored - Add UNIFD and BIFD file descriptors for separating requests chatty.c: - Convert ID to string in screen_home() - Removed the fds global variable - Pass fds to threadReconnect - Implement faster sleep with nanosleep(2) - Close file descriptors when failed so we do not have too many file descriptors open server.c: - Send presence messages on disconnect & connect - renamed i to conn --- protocol.h | 360 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 360 insertions(+) create mode 100644 protocol.h (limited to 'protocol.h') diff --git a/protocol.h b/protocol.h new file mode 100644 index 0000000..84d636f --- /dev/null +++ b/protocol.h @@ -0,0 +1,360 @@ +#ifndef PROTOCOL_H +#define PROTOCOL_H + +#include "chatty.h" + +/// Protocol +// - every message has format Header + Message +// TODO: security +// +/// ID +// - So clients can be identified uniquely. +// - 8 bytes +// - number that increments for each new client +// +/// Strings +// - strings are sent with their null terminator +// +/// Authentication +// This is what happens when the first time a client connects. +// Scenario 1. We alreayd have an ID +// 1. client-> Send own ID +// 2. server-> knows ID? +// y. server-> Success +// n. 1. server-> Error 'notfound' +// 2. client-> exit +// Scenario 2. We do not have an ID +// 1. client-> Introduces +// 2. server-> Sends & Saves ID +// 3. client-> Saves ID +// +/// Naming convention +// Messages end with the Message suffix (eg. TextMessag, HistoryMessage) +// +// A function that is coupled to a type works like +// eg. (printTextMessage, formatTimestamp) + +#define PROTOCOL_VERSION 0 +// Size of author string including null terminator +#define AUTHOR_LEN 13 +// Size of formatted timestamp string including null terminator +#define TIMESTAMP_LEN 9 +#define TIMESTAMP_FORMAT "%H:%M:%S" + +typedef u64 ID; + +// - 2 bytes for version +// - 1 byte for message type +// - 16 bytes for checksum +typedef struct { + u16 version; + u8 type; +} HeaderMessage; + +typedef enum { + HEADER_TYPE_TEXT = 0, + HEADER_TYPE_HISTORY, + HEADER_TYPE_PRESENCE, + HEADER_TYPE_ID, + HEADER_TYPE_INTRODUCTION, + HEADER_TYPE_ERROR +} HeaderType; +// shorthand for creating a header with a value from the enum +#define HEADER_INIT(t) {.version = PROTOCOL_VERSION, .type = t} +// from Tsoding video on minicel (https://youtu.be/HCAgvKQDJng?t=4546) +// sv(https://github.com/tsoding/sv) +#define HEADER_FMT "header: v%d %s(%d)" +#define HEADER_ARG(header) header.version, headerTypeString(header.type), header.type + +// For sending texts to other clients +// - 13 bytes for the author +// - 8 bytes for the timestamp +// - 8 bytes for id +// - 2 bytes for the text length +// - x*4 bytes for the text +typedef struct { + ID id; + u64 timestamp; // timestamp of when the message was sent + u16 len; + wchar_t* text; // placeholder for indexing + // wchar_t* is used, because this renders the text in the debugger +} TextMessage; +// Size of TextMessage without text pointer +#define TEXTMESSAGE_SIZE (sizeof(TextMessage) - sizeof(u32*)) + +// Requesting messages sent after a timestamp. +// - 8 bytes for the timestamp +typedef struct { + u64 timestamp; +} HistoryMessage; + +// Introduce the client to the server by sending the client's information. +// See "First connection". +// - 13 bytes for author +typedef struct { + u8 author[AUTHOR_LEN]; +} IntroductionMessage; +#define INTRODUCTION_FMT "introduction: %s" +#define INTRODUCTION_ARG(message) message.author + +// Request IntroductionMessage for client with that id. +// See "First connection" if this message is used when the client connects for the first time. +// be used to retrieve information about a client with an unknown ID. +// - 8 bytes for id +typedef struct { + ID id; +} IDMessage; + +// Notifying the sender's state, such as "connected", "disconnected", "AFK", ... +// - 8 bytes for id +// - 1 byte for type +typedef struct { + ID id; + u8 type; +} PresenceMessage; +typedef enum { + PRESENCE_TYPE_CONNECTED = 0, + PRESENCE_TYPE_DISCONNECTED, + PRESENCE_TYPE_AFK +} PresenceType; + +// Send an error message +// - 1 byte for type +typedef struct { + u8 type; +} ErrorMessage; +typedef enum { + ERROR_TYPE_BADMESSAGE = 0, + ERROR_TYPE_NOTFOUND, + ERROR_TYPE_SUCCESS, + ERROR_TYPE_ALREADYCONNECTED, + ERROR_TYPE_TOOMANYCONNECTIONS +} ErrorType; +#define ERROR_INIT(t) {.type = t} + +typedef struct { + s32 nrecv; + TextMessage* message; +} recvTextMessageResult; + +// Returns string for type byte in HeaderMessage +u8* +headerTypeString(HeaderType type) +{ + switch (type) { + case HEADER_TYPE_TEXT: return (u8*)"TextMessage"; + case HEADER_TYPE_HISTORY: return (u8*)"HistoryMessage"; + case HEADER_TYPE_PRESENCE: return (u8*)"PresenceMessage"; + case HEADER_TYPE_ID: return (u8*)"IDMessage"; + case HEADER_TYPE_INTRODUCTION: return (u8*)"IntroductionMessage"; + case HEADER_TYPE_ERROR: return (u8*)"ErrorMessage"; + default: return (u8*)"Unknown"; + } +} + +u8* +presenceTypeString(PresenceType type) +{ + switch (type) { + case PRESENCE_TYPE_CONNECTED: return (u8*)"connected"; + case PRESENCE_TYPE_DISCONNECTED: return (u8*)"disconnected"; + case PRESENCE_TYPE_AFK: return (u8*)"afk"; + default: return (u8*)"Unknown"; + } +} + +u8* +errorTypeString(ErrorType type) +{ + switch (type) { + case ERROR_TYPE_BADMESSAGE: return (u8*)"bad message"; + case ERROR_TYPE_NOTFOUND: return (u8*)"not found"; + case ERROR_TYPE_SUCCESS: return (u8*)"success"; + case ERROR_TYPE_ALREADYCONNECTED: return (u8*)"already connected"; + case ERROR_TYPE_TOOMANYCONNECTIONS: return (u8*)"too many connections"; + default: return (u8*)"Unknown"; + } +} + +// Formats time t into tmsp string +void +formatTimestamp(u8 timestamp_str[TIMESTAMP_LEN], u64 timestamp) +{ + struct tm* ltime; + ltime = localtime((time_t*)×tamp); + strftime((char*)timestamp_str, TIMESTAMP_LEN, TIMESTAMP_FORMAT, ltime); +} + +// Receive a message from fd and store it in the msgsArena, +// Returns pointer to the allocated memory +TextMessage* +recvTextMessage(Arena* msgsArena, u32 fd) +{ + TextMessage* message = ArenaPush(msgsArena, TEXTMESSAGE_SIZE); + + // Receive everything but the text so we can know the text's size and act accordingly + s32 nrecv = recv(fd, message, TEXTMESSAGE_SIZE, 0); + assert(nrecv != -1); + assert(nrecv == TEXTMESSAGE_SIZE); + + // Allocate memory for text and receive in that memory + u32 text_size = message->len * sizeof(*message->text); + ArenaPush(msgsArena, text_size); + + nrecv = recv(fd, (u8*)&message->text, text_size, 0); + assert(nrecv != -1); + assert(nrecv == message->len * sizeof(*message->text)); + + return message; +} + +typedef struct { + HeaderMessage* header; + void* message; +} Message; + +u32 +getMessageSize(HeaderType type) +{ + u32 size = 0; + switch (type) { + case HEADER_TYPE_ERROR: size = sizeof(ErrorMessage); break; + case HEADER_TYPE_HISTORY: size = sizeof(HistoryMessage); break; + case HEADER_TYPE_ID: size = sizeof(IDMessage); break; + case HEADER_TYPE_INTRODUCTION: size = sizeof(IntroductionMessage); break; + case HEADER_TYPE_PRESENCE: size = sizeof(PresenceMessage); break; + default: assert(0); + } + return size; +} + +s32 +recvAnyMessageType(s32 fd, HeaderMessage* header, void *anyMessage, HeaderType type) +{ + s32 nrecv = recv(fd, header, sizeof(*header), 0); + if (nrecv == -1 || nrecv == 0) + return nrecv; + assert(nrecv == sizeof(*header)); + + s32 size = 0; + switch (type) { + case HEADER_TYPE_ERROR: + case HEADER_TYPE_HISTORY: + case HEADER_TYPE_ID: + case HEADER_TYPE_INTRODUCTION: + case HEADER_TYPE_PRESENCE: + size = getMessageSize(header->type); + break; + case HEADER_TYPE_TEXT: { + TextMessage* message = anyMessage; + size = TEXTMESSAGE_SIZE + message->len * sizeof(*message->text); + } break; + default: assert(0); break; + } + assert(header->type == type); + + nrecv = recv(fd, anyMessage, size, 0); + assert(nrecv != -1); + assert(nrecv == size); + + return size; +} + +// Get any message into arena +Message +recvAnyMessage(Arena* arena, s32 fd) +{ + HeaderMessage* header = ArenaPush(arena, sizeof(*header)); + s32 nrecv = recv(fd, header, sizeof(*header), 0); + assert(nrecv != -1); + assert(nrecv == sizeof(*header)); + + s32 size = 0; + switch (header->type) { + case HEADER_TYPE_ERROR: + case HEADER_TYPE_HISTORY: + case HEADER_TYPE_ID: + case HEADER_TYPE_INTRODUCTION: + case HEADER_TYPE_PRESENCE: + size = getMessageSize(header->type); + break; + case HEADER_TYPE_TEXT: { + Message result; + result.header = header; + result.message = recvTextMessage(arena, fd); + return result; + } break; + default: assert(0); break; + } + + void* message = ArenaPush(arena, size); + nrecv = recv(fd, message, size, 0); + assert(nrecv != -1); + assert(nrecv == size); + + Message result; + result.header = header; + result.message = message; + + return result; +} + +Message +waitForMessageType(Arena* arena, Arena* queueArena, u32 fd, HeaderType type) +{ + Message message; + while (1) { + message = recvAnyMessage(arena, fd); + if (message.header->type == type) + break; + ArenaPush(queueArena, getMessageSize(message.header->type)); + } + return message; +} + +// Generic sending function for sending any type of message to fd +// Returns number of bytes sent in message or -1 if there was an error. +s32 +sendAnyMessage(u32 fd, HeaderMessage* header, void* anyMessage) +{ + s32 nsend_total; + s32 nsend = send(fd, header, sizeof(*header), 0); + if (nsend == -1) return nsend; + assert(nsend == sizeof(*header)); + nsend_total = nsend; + + s32 size = 0; + switch (header->type) { + case HEADER_TYPE_ERROR: + case HEADER_TYPE_HISTORY: + case HEADER_TYPE_ID: + case HEADER_TYPE_INTRODUCTION: + case HEADER_TYPE_PRESENCE: + size = getMessageSize(header->type); + break; + case HEADER_TYPE_TEXT: { + nsend = send(fd, anyMessage, TEXTMESSAGE_SIZE, 0); + assert(nsend != -1); + assert(nsend == TEXTMESSAGE_SIZE); + nsend_total += nsend; + // set size to remaning text size that should be sent + TextMessage* message = (TextMessage*)anyMessage; + size = message->len * sizeof(*message->text); + nsend = 0; + + anyMessage = &message->text; + } break; + default: + fprintf(stdout, "sendAnyMessage(%d)|Cannot send %s\n", fd, headerTypeString(header->type)); + return 0; + } + + nsend = send(fd, anyMessage, size, 0); + if (nsend == -1) return nsend; + assert(nsend == size); + nsend_total += nsend; + + return nsend_total; +} + +#endif -- cgit v1.2.3