From 33f7d4bcc47a4125e9cc73916db9bf1d6fcae9b3 Mon Sep 17 00:00:00 2001 From: jazz Date: Mon, 23 Mar 2026 17:01:32 -0500 Subject: [PATCH] initial commit --- .clang-format | 20 +++++++ Makefile | 18 ++++++ README.md | 28 +++++++++- STYLE.md | 117 ++++++++++++++++++++++++++++++++++++++ client.cc | 103 ++++++++++++++++++++++++++++++++++ client.h | 8 +++ ct.1 | 22 ++++++++ disp.cc | 55 ++++++++++++++++++ disp.h | 11 ++++ log.cc | 56 +++++++++++++++++++ log.h | 9 +++ main.cc | 151 ++++++++++++++++++++++++++++++++++++++++++++++++++ public.h | 30 ++++++++++ server.cc | 131 +++++++++++++++++++++++++++++++++++++++++++ server.h | 9 +++ test.c | 8 --- 16 files changed, 767 insertions(+), 9 deletions(-) create mode 100644 .clang-format create mode 100644 Makefile create mode 100644 STYLE.md create mode 100644 client.cc create mode 100644 client.h create mode 100644 ct.1 create mode 100644 disp.cc create mode 100644 disp.h create mode 100644 log.cc create mode 100644 log.h create mode 100644 main.cc create mode 100644 public.h create mode 100644 server.cc create mode 100644 server.h delete mode 100644 test.c diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..e2079be --- /dev/null +++ b/.clang-format @@ -0,0 +1,20 @@ +{ + "BasedOnStyle": "LLVM", + "UseTab": "Always", + "IndentWidth": 8, + "TabWidth": 8, + "ColumnLimit": 100, + "LineEnding": "LF", + "RemoveBracesLLVM": true, + "AlwaysBreakAfterReturnType": "AllDefinitions", + "BreakBeforeBraces": "Custom", + "BraceWrapping": { + "AfterFunction": true, + "AfterClass": false, + "AfterControlStatement": false, + "AfterNamespace": false, + "AfterStruct": false, + "BeforeElse": false, + "BeforeCatch": false + } +} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..672e38e --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +# Compiler and flags +TARGET = ct +CXX = g++ +CXXFLAGS = -pthread -Wall -Wextra -O2 -std=c++17 -lncurses -g + +SRC = $(wildcard *.cc) +OBJ = $(SRC:.cc=.o) + +all: $(TARGET) + +clean: + rm -f $(TARGET) *.o *.txt + +$(TARGET): $(OBJ) + $(CXX) $(OBJ) $(CXXFLAGS) -o $(TARGET) + +%.o: %.cc + $(CXX) -c $< $(CXXFLAGS) -o $@ diff --git a/README.md b/README.md index 9a5984d..835758e 100644 --- a/README.md +++ b/README.md @@ -1 +1,27 @@ -# Placeholder +# Threaded Network Chat + +## TODO + +- Finalize coding style, + - **see [STYLE.md](STYLE.md)** +- Get everyone's editors set up for clangd/clang-format and git +- Make sure everyone's compiler toolchains work +- Assign responsibilities to each team member + +## Build + +``` +make +``` + +## Usage + +``` +./ct # run as server +``` + +``` +./ct client # run as client +``` + +Chat logs are stored in `./server.txt` and `./client.txt`. diff --git a/STYLE.md b/STYLE.md new file mode 100644 index 0000000..7747386 --- /dev/null +++ b/STYLE.md @@ -0,0 +1,117 @@ +# Project Coding Style + +These are guidelines for the coding style for this project. Most of it should be handled +automatically by `clang-format` if you set it up correctly. + +**Break any rules that completely destroy your productivity.** + +**Please install the [C/C++ Extension Pack](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools-extension-pack) and [clang-format](https://marketplace.visualstudio.com/items?itemName=xaver.clang-format) for Visual Studio Code.** + +## TL;DR + +Call files `.cc` instead of `.c` or `.cpp` and use `.h` headers for everything. + +If you follow the setup instructions you should be fine. Otherwise: + +Use [BSD Kernel Normal Form (KNF)](https://en.wikipedia.org/wiki/Indentation_style#BSD_KNF) for function bodies +and [Kernighan & Ritchie (K&R)](https://en.wikipedia.org/wiki/Indentation_style#K&R) style (with a few modifications stated in **General Guidelines**) everywhere else. + +## Existing Literature + +I recommend skimming over these for inspiration. + +- [Names](https://research.swtch.com/names) - Russ Cox +- [Notes on Programming in C](http://doc.cat-v.org/bell_labs/pikestyle) - Rob Pike + +## General Guidelines + +- Use 8-width tabs, not spaces, for indentation. +- If you copy code from online, copy it by hand. +- Use [K&R style](https://en.wikipedia.org/wiki/Indentation_style#K&R) for indentation and brace placement where possible. + - When writing function bodies: + - return type goes on its own line, + - function name() is on a line below that, and + - open bracket `{` after that. + - no brackets on `for`, `if`, `while`, etc. statements that only contain one line +- Use `/* comments */` for permanent comments, `// comments` for temporary ones e.g. `TODO`'s or notices +- **Try to make every line 100 characters wide or less.** +- Start source files as `.cc` straight away, even if they're pure **C**. + +### C++-specific guidelines + +- file extension: `.cc`, `.h` +- C++ strings are fine +- Vectors are fine +- Use `printf` and co., not `std::cout`, unless somehow absolutely necessary +- `fstream`s for input/output/log files are fine +- Use C's `struct`s instead of objects unless you really really need an object. +- Use C-style type casting where possible + +TL;DR pretend you're using C with basic modern conveniences. **When in doubt, ask.** + +### Naming and Abbreviations + +The length of a function name, variable name, et cetera should be directly proportional to its importance and lifetime. + +#### Don't + +- abbreviate global variables **EVER**, +- mention the data type in a variable name, +- use a full word where an abbreviation will do + - (especially not in a variable that will die 5 lines later), +- use single-letter abbreviations for anything that lasts more than 15 lines, or +- abbreviate a word when an apt abbreviation does not exist. + +#### Do + +- abbreviate extremely short-lived variables to one letter, + - e.g. `for (int index = 0; index < 10; i++)` -> `for (int i = 0; i < 10; i++)` +- break any rule if following it ruins the readability + +## Source File layout + +```C++ +/* C++ includes */ +#include + +/* C (.h) includes */ +#include + +/* local header includes */ +#include "unicorns.h" + +/* "using" directives */ +using namespace std; + +/* global constants */ +const int life = 42; + +/* global variables */ +int degrees = 90; + +/* function prototypes */ +int div_numb(int n, int d); + +/* main function body */ +int +main(int argc, char *argv[]) +{ + int numerator = 32; + int denominator = 16; + int result; + + puts("What is 32 divided by 16?"); + + result = div_numb(32, 16); + + return 0; +} + +/* other function bodies */ +int +div_numb(int n, int d) +{ + return n / d; +} +``` + diff --git a/client.cc b/client.cc new file mode 100644 index 0000000..bbb21d1 --- /dev/null +++ b/client.cc @@ -0,0 +1,103 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "disp.h" +#include "log.h" +#include "public.h" + +struct waitClientArgs { + sockaddr_in newSockAddr; + socklen_t newSockAddrSize; + int auxInt; // used in client mode because I didn't want to make another + // struct to keep track of +}; + +void +closeClient() +{ + long e = end1.tv_sec - start1.tv_sec; + + gettimeofday(&end1, NULL); + close(clientSocketDescriptor); + + puts("********Session********"); + printf("Bytes written: %i\n", bytesWritten); + printf("Bytes read: %i\n", bytesRead); + printf("Elapsed time: %ld secs\n", e); + puts("Connection closed."); +} + +void * +pollForSever(void *args) +{ + waitClientArgs *aaa = static_cast(args); + int socketDescriptor = (int)aaa->auxInt; + char msg[1024]; + while (1) { + server_message_loop: + memset(&msg, 0, sizeof(msg)); // clear the buffer + bytesRead += recv(socketDescriptor, (char *)&msg, sizeof(msg), 0); + + // not needed for proofs of concept testing + /*if(!strcmp(msg, "exit")) + { + writeToFile(logFileName, msg); + displayFile(logFileName, linePos, LOG_LENGTH); + break; + }*/ + // cout << "Server: " << msg << endl; + // printf("Server: %s\n"); + + if (msg[0] == '\0') + goto server_message_loop; + + writeToFile(logFileName, msg); + + if (linesInFile(logFileName) > LOG_LENGTH) + linePos++; + + displayFile(logFileName, linePos, LOG_LENGTH); + } + + return nullptr; +} + +void +setupClient() +{ + // we need 2 things: ip address and port number, in that order + // if(argc != 3) + //{ + // cerr << "Usage: ip_address port" << endl; exit(0); + // } //grab the IP address and port number + // create a message buffer + char msg[1024]; + // setup a socket and connection tools + struct hostent *host = gethostbyname(IP_ADDRESS.c_str()); + sockaddr_in sendSockAddr; + bzero((char *)&sendSockAddr, sizeof(sendSockAddr)); + sendSockAddr.sin_family = AF_INET; + sendSockAddr.sin_addr.s_addr = inet_addr(inet_ntoa(*(struct in_addr *)*host->h_addr_list)); + sendSockAddr.sin_port = htons(PORT_NUM); + clientSocketDescriptor = socket(AF_INET, SOCK_STREAM, 0); + // try to connect... + int status = + connect(clientSocketDescriptor, (sockaddr *)&sendSockAddr, sizeof(sendSockAddr)); + + if (status < 0) + writeToFile(logFileName, "Error connecting to socket!"); + + writeToFile(logFileName, "Connected to the server!"); + int bytesRead, bytesWritten = 0; + struct timeval start1, end1; + gettimeofday(&start1, NULL); + auto *aaa = new waitClientArgs{}; // it looks stupid but it works + aaa->auxInt = clientSocketDescriptor; + int rc = pthread_create(&client_wait_thread, nullptr, pollForSever, aaa); + pthread_detach(client_wait_thread); +} diff --git a/client.h b/client.h new file mode 100644 index 0000000..edbb52b --- /dev/null +++ b/client.h @@ -0,0 +1,8 @@ +#ifndef CLIENT_H +#define CLIENT_H + +void closeClient(); +void *pollForServer(void *args); +void setupClient(); + +#endif diff --git a/ct.1 b/ct.1 new file mode 100644 index 0000000..30f216e --- /dev/null +++ b/ct.1 @@ -0,0 +1,22 @@ +.TH "Threaded Network Chat" 1 + +.SH NAME +\fBct\fR - Threaded Network Chat + +.SH SYNOPSIS +.SY + ./ct # start as server + + ./ct # start as client + + /quit # while program running +.YS + +.SH DESCRIPTION + +.LP +This program is meant to run as two instances. One for a "host" or "server" user +to initiate a session and the other for a "client" user to join the host. + +.LP +Messages are sent as buffered plain text with minimal processing, if any. diff --git a/disp.cc b/disp.cc new file mode 100644 index 0000000..88c3e04 --- /dev/null +++ b/disp.cc @@ -0,0 +1,55 @@ +#include + +#include + +#include "disp.h" +#include "public.h" + +// clears ncurses rows in a specific region only +void +clearRows(int startingRow, int endingRow) +{ + // preserve the cursor location + int yBefore, xBefore; + getyx(stdscr, yBefore, xBefore); + + for (int i = startingRow; i < endingRow; i++) { + move(i, 0); + clrtoeol(); // clear to end of line + } + + // restore original cursor location + move(yBefore, xBefore); +} + +// display a file using ncurses +int +displayFile(string path, int startLineNum = 0, int numLines = 10) +{ + ifstream file(path); + if (!file) { + return 1; + } else { + clearRows(0, numLines + 1); // clear the chat area + int lineNum = 0; + string line; + + // print each line directly to the screen + int num = 0; + while (getline(file, line) && + num <= numLines + startLineNum + 1) // while there is file content and the + // line number isn't too high + { + if (num >= startLineNum) { + move(lineNum, 0); + printw("%s", line.c_str()); + lineNum++; // increment the row number after + // printing each line + } + num++; + } + move(DEFAULT_CUR_Y, DEFAULT_CUR_X); + refresh(); + return 0; + } +} diff --git a/disp.h b/disp.h new file mode 100644 index 0000000..58e6ecb --- /dev/null +++ b/disp.h @@ -0,0 +1,11 @@ +#ifndef DISP_H +#define DISP_H + +#include + +using namespace std; + +void clearRows(int startingRow, int endingRow); +int displayFile(string path, int startLineNum, int numLines); + +#endif diff --git a/log.cc b/log.cc new file mode 100644 index 0000000..5e02198 --- /dev/null +++ b/log.cc @@ -0,0 +1,56 @@ +#include + +#include +#include + +#include "log.h" +#include "public.h" + +// gets the number of lines in a file. returns -1 if there was an error. +int +linesInFile(string path) +{ + ifstream file(path); + if (!file) { + return 1; + } else { + int lineNum = 0; + string line; + + int num = 0; + while (getline(file, line)) + num++; + return num; + } +} + +// appends a line of text to the end of a given file. returns 0 if the file +// existed, 1 if it didn't work +int +writeToFile(string path, string line, bool incLineNum) +{ + ofstream file; + struct stat s; + + /* + * It APPEARS this fixes our problem where the program has to be run twice to work correctly. + */ + if (stat(path.c_str(), &s) != 0) { + file.open(path, ios_base::out); + file << "SYSTEM: No log file found. Created a new one." << endl; + file.close(); + linePos += 5; /* This seems to be what actually fixes everything. Not sure. */ + } + + file.open(path, ios_base::app); + + if (file.is_open()) { + file << line << endl; + file.close(); + //linePos++ // We might also want this here at some point? + return 0; + } else { + // do something if it didn't work + return 1; // i guess that's good enough for now + } +} diff --git a/log.h b/log.h new file mode 100644 index 0000000..d6c6a1a --- /dev/null +++ b/log.h @@ -0,0 +1,9 @@ +#ifndef LOG_H +#define LOG_H + +#include "public.h" + +int linesInFile(string path); +int writeToFile(string path, string line, bool incLineNum); + +#endif diff --git a/main.cc b/main.cc new file mode 100644 index 0000000..31b1991 --- /dev/null +++ b/main.cc @@ -0,0 +1,151 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include //try to make the program work without this without doing anything awful but do it later + +#include "client.h" +#include "disp.h" +#include "log.h" +#include "public.h" +#include "server.h" + +using namespace std; + +const int PORT_NUM = 8888; +string IP_ADDRESS = "127.0.0.1"; +const int DEFAULT_CUR_X = 2; // the x position of the preferred default cursor + // position for message entry +const int DEFAULT_CUR_Y = 12; // the x position of the preferred default cursor + // position for message entry + +chatmode_t mode = NO_MODE; // what mode is this program in. 0 = nothing. 1 = server. 2 = client + +int serverSocketDescriptor; // a global variable for storing the server socket + // descriptor. +int clientSocketDescriptor; + +// This has not been used: +//vector clientSocketDescriptors; // a global vector for storing client + // socket descriptors + +// keep track of the session time using global variables +struct timeval start1, end1; + +// also keep track of the amount of data sent as well +int bytesRead, bytesWritten = 0; + +const int LOG_LENGTH = 10; // number of text log file lines to display +int linePos = 0; // counter for what current position to use in the file + +string logFileName; // the name of the log file + +pthread_t client_wait_thread; + +struct waitClientArgs { + sockaddr_in newSockAddr; + socklen_t newSockAddrSize; + int auxInt; // used in client mode because I didn't want to make another + // struct to keep track of +}; + +int +main(int argc, char *argv[]) +{ + if (argc > 1 && (!strncmp(argv[1], "client", 6))) { + logFileName = "client.txt"; + mode = CLIENT_MODE; + } else { + logFileName = "server.txt"; + mode = SERVER_MODE; + } + + // putting this lower down to circumvent the terminal brick when the + // socket was already in use results in all network functonality no + // longer working. + initscr(); // creates stdscr. important step + use_default_colors(); // this apparently tells it to use default + // terminal colors whenever there is no color + // attribute applied + + // printw("Starting server..."); + + // you have to do this to make the scrolling still work correctly if + // there was already content in the log file + linePos = linesInFile(logFileName) - LOG_LENGTH + 1; + + char *userInput = new char[1024]; + bool exit = false; + + switch (mode) { + case CLIENT_MODE: + writeToFile(logFileName, "CLIENT MODE"); + setupClient(); + linePos++; + break; + case SERVER_MODE: + writeToFile(logFileName, "SERVER MODE"); + setupServer(PORT_NUM); + linePos++; + break; + default: + goto leave; + break; + } + + while (!exit) { + displayFile(logFileName, linePos, LOG_LENGTH); + + // scroll along the screen if and when required so that it stays + // in sync + if (linesInFile(logFileName) > LOG_LENGTH) + linePos++; + + /* clear message box / reset cursor */ + move(12, 0); + printw(">\t\t\t"); + move(12, 2); + + getstr(userInput); + writeToFile(logFileName, userInput); + + if (!strncmp(userInput, "/quit", 5)) + exit = true; + + if (mode == 1) + send(clientSocketDescriptor, (char *)userInput, strlen(userInput), 0); + else + send(clientSocketDescriptor, (char *)userInput, strlen(userInput), 0); + } + +leave: + endwin(); + // closeServer(); + + switch (mode) { + case SERVER_MODE: + closeServer(); + break; + case CLIENT_MODE: + closeServer(); + break; + default: + puts("Warn: program appears to have successfully finished without ever setting " + "mode."); + return 1; + break; + } + + return 0; +} diff --git a/public.h b/public.h new file mode 100644 index 0000000..5e8b510 --- /dev/null +++ b/public.h @@ -0,0 +1,30 @@ +#ifndef PUBLIC_H +#define PUBLIC_H + +#include + +using namespace std; + +typedef enum { NO_MODE = 0, SERVER_MODE = 1, CLIENT_MODE = 2 } chatmode_t; + +extern chatmode_t mode; +extern const int PORT_NUM; +extern string IP_ADDRESS; +extern const int DEFAULT_CUR_X; +extern const int DEFAULT_CUR_Y; +extern int serverSocketDescriptor; +extern int clientSocketDescriptor; +extern struct timeval start1, end1; +extern int bytesRead, bytesWritten; +extern const int LOG_LENGTH; +extern int linePos; +extern string logFileName; +extern pthread_t client_wait_thread; +struct waitClientArgs; + +int writeToFile(string path, string line, bool incLineNum = true); +void *waitForClient(void *argss); +void *pollForClient(); +void *pollForSever(void *args); + +#endif diff --git a/server.cc b/server.cc new file mode 100644 index 0000000..6970b14 --- /dev/null +++ b/server.cc @@ -0,0 +1,131 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "disp.h" +#include "log.h" +#include "public.h" +#include "server.h" + +struct waitClientArgs { + sockaddr_in newSockAddr; + socklen_t newSockAddrSize; + int auxInt; // used in client mode because I didn't want to make another + // struct to keep track of +}; + +void * +waitForClient(void *argss) +{ + waitClientArgs *args = static_cast(argss); + clientSocketDescriptor = + accept(serverSocketDescriptor, (sockaddr *)&args->newSockAddr, &args->newSockAddrSize); + if (clientSocketDescriptor >= 0) { + writeToFile(logFileName, "client connected"); + if (linesInFile(logFileName) > LOG_LENGTH) + linePos++; + displayFile(logFileName, linePos, LOG_LENGTH); + } + delete args; // delete to avoid memory leaks + + return pollForClient(); +} + +void * +pollForClient() +{ + char msg[1024]; + while (1) { + client_message_loop: + // receive a message from the client (listen) + memset(&msg, 0, sizeof(msg)); // clear the buffer + bytesRead += recv(clientSocketDescriptor, (char *)&msg, sizeof(msg), 0); + + if (msg[0] == '\0') + goto client_message_loop; + + writeToFile(logFileName, msg); + + if (linesInFile(logFileName) > LOG_LENGTH) + linePos++; + + displayFile(logFileName, linePos, LOG_LENGTH); + } + + return nullptr; +} + +// set up a suitable server for this +int +setupServer(int port) +{ + // setup a socket and connection tools + sockaddr_in servAddr; + bzero((char *)&servAddr, sizeof(servAddr)); + servAddr.sin_family = AF_INET; + servAddr.sin_addr.s_addr = htonl(INADDR_ANY); + servAddr.sin_port = htons(port); + + // open stream oriented socket with internet address + // also keep track of the socket descriptor + serverSocketDescriptor = socket(AF_INET, SOCK_STREAM, 0); + if (serverSocketDescriptor < 0) { + // keeps from bricking the terminal if this happens + endwin(); + + fprintf(stderr, "Error establishing the server socket!\n"); + exit(0); + } + // bind the socket to its local address + int bindStatus = + bind(serverSocketDescriptor, (struct sockaddr *)&servAddr, sizeof(servAddr)); + if (bindStatus < 0) { + // keeps from bricking the terminal if this happens + endwin(); + + fprintf(stderr, "Error binding socket to local address!\n"); + exit(0); + } + writeToFile(logFileName, "Waiting for a client to connect..."); + // listen for up to 5 requests at a time + listen(serverSocketDescriptor, 5); + // receive a request from client using accept + // we need a new address to connect with the client + sockaddr_in newSockAddr; + socklen_t newSockAddrSize = sizeof(newSockAddr); + // accept, create a new socket descriptor to + // handle the new connection with client + auto *aaa = new waitClientArgs{}; // it looks stupid but it works + aaa->newSockAddr = newSockAddr; + aaa->newSockAddrSize = newSockAddrSize; + int rc = pthread_create(&client_wait_thread, nullptr, waitForClient, aaa); + pthread_detach(client_wait_thread); + writeToFile(logFileName, "Server started successfully"); + gettimeofday(&start1, NULL); + + return 0; +} + +void +closeServer() +{ + long e = end1.tv_sec - start1.tv_sec; /* the linter freaks out if you + don't save it as a variable */ + + // we need to close the socket descriptors after we're all done + gettimeofday(&end1, NULL); + close(clientSocketDescriptor); + close(serverSocketDescriptor); + + /* Don't just die silently */ + puts("********Session********"); + printf("Bytes written: %i\n", bytesWritten); + printf("Bytes read: %i\n", bytesRead); + printf("Elapsed time: %ld secs\n", e); + puts("Connection closed."); +} diff --git a/server.h b/server.h new file mode 100644 index 0000000..7cb058a --- /dev/null +++ b/server.h @@ -0,0 +1,9 @@ +#ifndef SERVER_H +#define SERVER_H + +void *waitForClient(void *argss); +void *pollForClient(); +int setupServer(int port); +void closeServer(); + +#endif diff --git a/test.c b/test.c deleted file mode 100644 index 67159ba..0000000 --- a/test.c +++ /dev/null @@ -1,8 +0,0 @@ -#include - -int -main(void) -{ - puts("Hello, world!"); - return 0; -}