server03 started
This commit is contained in:
@@ -25,4 +25,6 @@ target_link_libraries(server01 PRIVATE wsock32 ws2_32 json-c::json-c)
|
||||
|
||||
|
||||
add_executable(server02 server02.c server02.h ${COMMON_SOURCES})
|
||||
target_link_libraries(server02 wsock32 ws2_32)
|
||||
target_link_libraries(server02 wsock32 ws2_32)
|
||||
add_executable(server03 server03.c server03.h ${COMMON_SOURCES})
|
||||
target_link_libraries(server03 wsock32 ws2_32)
|
||||
175
server03.c
Normal file
175
server03.c
Normal file
@@ -0,0 +1,175 @@
|
||||
//
|
||||
// Created by Ajurna on 29/07/2025.
|
||||
//
|
||||
|
||||
#include "server03.h"
|
||||
|
||||
#include <pthread.h>
|
||||
|
||||
#include "data.h"
|
||||
#include <stdio.h>
|
||||
#include <ctype.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
pthread_mutex_t mutex;
|
||||
pthread_cond_t cond;
|
||||
#define WELCOME "Welcome to budgetchat! What shall I call you?\n"
|
||||
|
||||
|
||||
|
||||
int main() {
|
||||
SOCKET server = get_listen_socket();
|
||||
SOCKADDR_IN clientAddr;
|
||||
SOCKET client;
|
||||
connections_t *connections = connections_create(64);
|
||||
int clientAddrSize = sizeof(clientAddr);
|
||||
int connection_number = 1;
|
||||
printf("Listening for incoming connections...\n");
|
||||
while((client = accept(server, (SOCKADDR *)&clientAddr, &clientAddrSize)) != INVALID_SOCKET)
|
||||
{
|
||||
handle_args_t *args = malloc(sizeof(handle_args_t));
|
||||
args->client = client;
|
||||
args->connection = connection_number++;
|
||||
args->connections = *connections;
|
||||
pthread_t thread;
|
||||
pthread_create(&thread, nullptr, handle_connection, args);
|
||||
}
|
||||
connections_destroy(connections);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void *handle_connection(void *args) {
|
||||
handle_args_t *handleArgs = args;
|
||||
char buffer[1024] = {0};
|
||||
int bytesReceived;
|
||||
char *message[1024];
|
||||
char_array_t *data = char_array_create(1024);
|
||||
send(handleArgs->client, WELCOME, sizeof(WELCOME), 0);
|
||||
bytesReceived = recv(handleArgs->client, buffer, sizeof(buffer), 0);
|
||||
char_array_append(data, buffer, bytesReceived);
|
||||
char *username = char_array_get_until_char(data, '\n');
|
||||
if (username == NULL) {
|
||||
printf("{%d} Failed to parse username\n", handleArgs->connection);
|
||||
connections_remove(&handleArgs->connections, handleArgs->client);
|
||||
free(handleArgs);
|
||||
exit(3);
|
||||
}
|
||||
username = trim(username);
|
||||
if (!username_is_valid(username)) {
|
||||
printf("{%d} Invalid username\n", handleArgs->connection);
|
||||
connections_remove(&handleArgs->connections, handleArgs->client);
|
||||
free(handleArgs);
|
||||
exit(3);
|
||||
}
|
||||
pthread_mutex_lock(&mutex);
|
||||
strcat(message, "* The room contains: ");
|
||||
for (int i = 0; i < handleArgs->connections.size; i++) {
|
||||
strcat(message, handleArgs->connections.clients[i].username);
|
||||
strcat(message, " ");
|
||||
}
|
||||
message[strlen(message)-1] = '\n';
|
||||
send(handleArgs->client, message, strlen(message), 0);
|
||||
pthread_mutex_unlock(&mutex);
|
||||
|
||||
connections_append(&handleArgs->connections, handleArgs->client, username);
|
||||
sprintf(message, "* %s has entered the room\n", username);
|
||||
broadcast(&handleArgs->connections, &handleArgs->client, message);
|
||||
|
||||
printf("{%d} Username: %s\n", handleArgs->connection, username);
|
||||
|
||||
while ((bytesReceived = recv(handleArgs->client, buffer, sizeof(buffer), 0)) > 0) {
|
||||
printf("{%d} Client sent: |%d| \n", handleArgs->connection, bytesReceived);
|
||||
char_array_append(data, buffer, bytesReceived);
|
||||
char *request;
|
||||
while ((request = char_array_get_until_char(data, '\n')) != NULL) {
|
||||
sprintf(message, "[%s] %s\n", username, request);
|
||||
}
|
||||
memset(buffer, 0, sizeof(buffer));
|
||||
}
|
||||
|
||||
free(args);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
connections_t *connections_create(const int size) {
|
||||
connections_t *connections = malloc(sizeof(connections_t));
|
||||
connections->size = size;
|
||||
connections->clients = calloc(size, sizeof(client_connection_t));
|
||||
return connections;
|
||||
}
|
||||
void connections_destroy(connections_t *connections) {
|
||||
free(connections->clients);
|
||||
free(connections);
|
||||
}
|
||||
void connections_append(connections_t *connections, SOCKET client, char *username) {
|
||||
if (connections == NULL) {
|
||||
exit(1);
|
||||
}
|
||||
pthread_mutex_lock(&mutex);
|
||||
size_t new_size = connections->size + 1;
|
||||
if (new_size > connections->size) {
|
||||
connections->size = connections->size+64;
|
||||
client_connection_t *new_array = realloc(connections->clients, connections->size * sizeof(client_connection_t));
|
||||
connections->clients = new_array;
|
||||
if (connections->clients == NULL) {
|
||||
printf("Failed to allocate memory for array\n");
|
||||
}
|
||||
}
|
||||
connections->clients[connections->size].client = client;
|
||||
connections->clients[connections->size].username = username;
|
||||
connections->size = new_size;
|
||||
printf("{%llu} Connection added\n", connections->size);
|
||||
pthread_mutex_unlock(&mutex);
|
||||
|
||||
|
||||
};
|
||||
void connections_remove(connections_t *connections, SOCKET client) {
|
||||
if (connections == NULL) {
|
||||
exit(1);
|
||||
}
|
||||
pthread_mutex_lock(&mutex);
|
||||
|
||||
for (int i = 0; i < connections->size; i++) {
|
||||
if (connections->clients[i].client == client) {
|
||||
closesocket(connections->clients[i].client);
|
||||
free(connections->clients[i].username);
|
||||
for (int j = i; j < connections->size; j++) {
|
||||
connections->clients[j] = connections->clients[j+1];
|
||||
}
|
||||
connections->size--;
|
||||
printf("{%llu} Connection removed\n", connections->size);
|
||||
pthread_mutex_unlock(&mutex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void broadcast(const connections_t *connections, const SOCKET *source, const char *message) {
|
||||
pthread_mutex_lock(&mutex);
|
||||
for (size_t i = 0; i < connections->size; i++) {
|
||||
if (connections->clients[i].client != *source) {
|
||||
send(connections->clients[i].client, message, strlen(message), 0);
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&mutex);
|
||||
}
|
||||
char *trim(char *str) {
|
||||
char *end;
|
||||
while (isspace(*str)) str++;
|
||||
if (*str == 0) return str;
|
||||
end = str + strlen(str) - 1;
|
||||
while (end > str && isspace(*end)) end--;
|
||||
end[1] = '\0';
|
||||
return str;
|
||||
}
|
||||
bool username_is_valid(const char *username) {
|
||||
if (username == NULL) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < strlen(username); i++) {
|
||||
if (!isalnum(username[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
42
server03.h
Normal file
42
server03.h
Normal file
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// Created by Ajurna on 29/07/2025.
|
||||
//
|
||||
|
||||
#ifndef SERVER03_H
|
||||
#define SERVER03_H
|
||||
|
||||
|
||||
#endif //SERVER03_H
|
||||
#pragma once
|
||||
#include <winsock2.h>
|
||||
|
||||
typedef struct ClientConnection {
|
||||
SOCKET client;
|
||||
char *username;
|
||||
} client_connection_t;
|
||||
|
||||
typedef struct Connections {
|
||||
size_t size;
|
||||
size_t len;
|
||||
client_connection_t *clients;
|
||||
} connections_t;
|
||||
|
||||
typedef struct handleArgs {
|
||||
int connection;
|
||||
SOCKET client;
|
||||
connections_t connections;
|
||||
} handle_args_t;
|
||||
|
||||
|
||||
|
||||
void *handle_connection(void *args);
|
||||
|
||||
connections_t *connections_create(int size);
|
||||
void connections_destroy(connections_t *connections);
|
||||
void connections_append(connections_t *connections, SOCKET client, char *username);
|
||||
void connections_remove(connections_t *connections, SOCKET client);
|
||||
|
||||
char *trim(char *str);
|
||||
bool username_is_valid(const char *username);
|
||||
|
||||
void broadcast(const connections_t *connections, const SOCKET *source, const char *message);
|
||||
135
server03_chat_server_tests.py
Normal file
135
server03_chat_server_tests.py
Normal file
@@ -0,0 +1,135 @@
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
import unittest
|
||||
|
||||
class TestBudgetChat(unittest.TestCase):
|
||||
SERVER_HOST = 'localhost'
|
||||
SERVER_PORT = 40000
|
||||
|
||||
def create_client(self):
|
||||
"""Helper function to create a new client socket"""
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.connect((self.SERVER_HOST, self.SERVER_PORT))
|
||||
return sock
|
||||
|
||||
def receive_message(self, sock, timeout=2):
|
||||
"""Helper function to receive a message with timeout"""
|
||||
sock.settimeout(timeout)
|
||||
try:
|
||||
return sock.recv(1024).decode('ascii').strip()
|
||||
except socket.timeout:
|
||||
return None
|
||||
|
||||
def test_initial_connection(self):
|
||||
"""Test the initial connection and welcome message"""
|
||||
client = self.create_client()
|
||||
welcome_msg = self.receive_message(client)
|
||||
self.assertIsNotNone(welcome_msg)
|
||||
self.assertTrue("Welcome" in welcome_msg)
|
||||
client.close()
|
||||
|
||||
def test_invalid_username(self):
|
||||
"""Test that invalid usernames are rejected"""
|
||||
invalid_names = ["", "user@name", "user name", "!invalid"]
|
||||
|
||||
for name in invalid_names:
|
||||
with self.subTest(name=name):
|
||||
client = self.create_client()
|
||||
_ = self.receive_message(client) # Welcome message
|
||||
client.send(f"{name}\n".encode('ascii'))
|
||||
# Server should disconnect us
|
||||
response = self.receive_message(client)
|
||||
with self.assertRaises((ConnectionResetError, ConnectionAbortedError, socket.error)):
|
||||
client.send("test\n".encode('ascii'))
|
||||
client.close()
|
||||
|
||||
def test_valid_username(self):
|
||||
"""Test that valid usernames are accepted"""
|
||||
client = self.create_client()
|
||||
_ = self.receive_message(client) # Welcome message
|
||||
client.send("validuser123\n".encode('ascii'))
|
||||
room_info = self.receive_message(client)
|
||||
self.assertIsNotNone(room_info)
|
||||
self.assertTrue(room_info.startswith("*"))
|
||||
client.close()
|
||||
|
||||
def test_chat_message_broadcast(self):
|
||||
"""Test that chat messages are broadcast to other users"""
|
||||
# First client
|
||||
client1 = self.create_client()
|
||||
_ = self.receive_message(client1) # Welcome message
|
||||
client1.send("user1\n".encode('ascii'))
|
||||
_ = self.receive_message(client1) # Room info
|
||||
|
||||
# Second client
|
||||
client2 = self.create_client()
|
||||
_ = self.receive_message(client2) # Welcome message
|
||||
client2.send("user2\n".encode('ascii'))
|
||||
_ = self.receive_message(client2) # Room info
|
||||
|
||||
# User1 should see User2's join message
|
||||
join_msg = self.receive_message(client1)
|
||||
self.assertTrue("user2" in join_msg)
|
||||
|
||||
# Send message from user1
|
||||
test_message = "Hello, everyone!"
|
||||
client1.send(f"{test_message}\n".encode('ascii'))
|
||||
|
||||
# User2 should receive the message
|
||||
received = self.receive_message(client2)
|
||||
self.assertEqual(f"[user1] {test_message}", received)
|
||||
|
||||
client1.close()
|
||||
client2.close()
|
||||
|
||||
def test_multiple_clients(self):
|
||||
"""Test that the server can handle multiple clients"""
|
||||
clients = []
|
||||
usernames = [f"user{i}" for i in range(10)]
|
||||
|
||||
# Connect 10 clients
|
||||
for username in usernames:
|
||||
client = self.create_client()
|
||||
_ = self.receive_message(client) # Welcome message
|
||||
client.send(f"{username}\n".encode('ascii'))
|
||||
_ = self.receive_message(client) # Room info
|
||||
clients.append(client)
|
||||
time.sleep(0.1) # Small delay to prevent race conditions
|
||||
|
||||
# Send a message from the last client
|
||||
test_message = "Hello from last user!"
|
||||
clients[-1].send(f"{test_message}\n".encode('ascii'))
|
||||
|
||||
# Check that all other clients received the message
|
||||
expected = f"[{usernames[-1]}] {test_message}"
|
||||
for i in range(len(clients)-1):
|
||||
received = self.receive_message(clients[i])
|
||||
self.assertEqual(expected, received)
|
||||
|
||||
# Cleanup
|
||||
for client in clients:
|
||||
client.close()
|
||||
|
||||
def test_user_departure(self):
|
||||
"""Test that user departure is announced"""
|
||||
client1 = self.create_client()
|
||||
_ = self.receive_message(client1)
|
||||
client1.send("user1\n".encode('ascii'))
|
||||
_ = self.receive_message(client1)
|
||||
|
||||
client2 = self.create_client()
|
||||
_ = self.receive_message(client2)
|
||||
client2.send("user2\n".encode('ascii'))
|
||||
_ = self.receive_message(client2)
|
||||
|
||||
# Close client2 and check if client1 receives departure message
|
||||
client2.close()
|
||||
departure_msg = self.receive_message(client1)
|
||||
self.assertTrue("user2" in departure_msg)
|
||||
self.assertTrue(departure_msg.startswith("*"))
|
||||
|
||||
client1.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user