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; -}