commit 04c671220ea6dc0878e8e5848985d114ef056b06 Author: seajee Date: Fri Apr 25 01:52:14 2025 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bd16395 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +hello_server +hello_client +chat_server +chat_client +udp_server +udp_client diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bfa2314 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 seajee + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1b675a7 --- /dev/null +++ b/Makefile @@ -0,0 +1,31 @@ +CC=gcc +CFLAGS=-Wall -Wextra -ggdb -I. +LDFLAGS=-lpthread + +all: hello_server hello_client chat_server chat_client udp_server udp_client + +chat_server: examples/chat_server.c + $(CC) $(CFLAGS) -o chat_server examples/chat_server.c $(LDFLAGS) + +chat_client: examples/chat_client.c + $(CC) $(CFLAGS) -o chat_client examples/chat_client.c $(LDFLAGS) + +hello_server: examples/hello_server.c + $(CC) $(CFLAGS) -o hello_server examples/hello_server.c $(LDFLAGS) + +hello_client: examples/hello_client.c + $(CC) $(CFLAGS) -o hello_client examples/hello_client.c $(LDFLAGS) + +udp_server: examples/udp_server.c + $(CC) $(CFLAGS) -o udp_server examples/udp_server.c $(LDFLAGS) + +udp_client: examples/udp_client.c + $(CC) $(CFLAGS) -o udp_client examples/udp_client.c $(LDFLAGS) + +clean: + rm -rf chat_server + rm -rf chat_client + rm -rf hello_server + rm -rf hello_client + rm -rf udp_server + rm -rf udp_client diff --git a/README.md b/README.md new file mode 100644 index 0000000..d25e751 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# sock.h + +A simple C socket library wrapper. Supporting TCP and UDP via IPv4 and IPv6. + +## Quick start + +There are many examples in the repository that show how this library +simplifies the usage of sockets in C. To build them you can use the Makefile +provided. + +```console +make +``` diff --git a/examples/chat_client.c b/examples/chat_client.c new file mode 100644 index 0000000..5b91582 --- /dev/null +++ b/examples/chat_client.c @@ -0,0 +1,74 @@ +#include +#include +#include + +#define SOCK_IMPLEMENTATION +#include "sock.h" + +#define DEFAULT_ADDRESS "127.0.0.1" +#define PORT 6969 +#define BUFFER_CAPACITY 4096 + +bool disconnected = false; + +void *listen_thread(void *user_data) +{ + Sock *server = (Sock*) user_data; + + char buffer[BUFFER_CAPACITY]; + ssize_t received = 0; + + while ((received = sock_recv(server, buffer, sizeof(buffer))) > 0) { + printf("%.*s\n", (int)received, buffer); + } + + disconnected = true; + return NULL; +} + +int main(int argc, char **argv) +{ + const char *address = DEFAULT_ADDRESS; + if (argc > 1) { + address = argv[1]; + } + + Sock *client = sock_create(SOCK_IPV4, SOCK_TCP); + if (client == NULL) { + fprintf(stderr, "ERROR: Could not create socket\n"); + return EXIT_FAILURE; + } + + if (!sock_connect(client, sock_addr(address, PORT))) { + fprintf(stderr, "ERROR: Could not connect to server\n"); + sock_close(client); + return EXIT_FAILURE; + } + + pthread_t ltid; + if (pthread_create(<id, NULL, &listen_thread, client) != 0) { + fprintf(stderr, "ERROR: Could not create listening thread\n"); + sock_close(client); + return EXIT_FAILURE; + } + pthread_detach(ltid); + + printf("INFO: Connected to the server\n"); + printf("INFO: Type `disconnect` to disconnect from the server\n"); + + char buffer[BUFFER_CAPACITY]; + while (!disconnected) { + memset(buffer, 0, sizeof(buffer)); + fgets(buffer, sizeof(buffer), stdin); + if (strcmp(buffer, "disconnect\n") == 0) { + break; + } + size_t len = strlen(buffer)-1; + sock_send(client, buffer, len); + } + + sock_close(client); + printf("INFO: Closed socket\n"); + + return EXIT_SUCCESS; +} diff --git a/examples/chat_server.c b/examples/chat_server.c new file mode 100644 index 0000000..28bfedf --- /dev/null +++ b/examples/chat_server.c @@ -0,0 +1,179 @@ +#include +#include +#include +#include + +#define SOCK_IMPLEMENTATION +#include "sock.h" + +#define PORT 6969 +#define POOL_CAPACITY 16 +#define BUFFER_CAPACITY 4096 +#define USERNAME_CAPACITY 16 + +Sock *socket_pool[POOL_CAPACITY]; +pthread_mutex_t pool_lock; + +bool add_client(Sock *sock) +{ + bool added = false; + pthread_mutex_lock(&pool_lock); + for (size_t i = 0; i < POOL_CAPACITY; ++i) { + if (socket_pool[i] == NULL) { + socket_pool[i] = sock; + added = true; + break; + } + } + pthread_mutex_unlock(&pool_lock); + + return added; +} + +bool remove_client(Sock *sock) { + bool removed = false; + pthread_mutex_lock(&pool_lock); + for (size_t i = 0; i < POOL_CAPACITY; ++i) { + if (socket_pool[i] == sock) { + socket_pool[i] = NULL; + removed = true; + break; + } + } + pthread_mutex_unlock(&pool_lock); + + return removed; +} + +void broadcast(const Sock *from, const char *msg, size_t msg_len) +{ + pthread_mutex_lock(&pool_lock); + for (size_t i = 0; i < POOL_CAPACITY; ++i) { + Sock *r = socket_pool[i]; + if (r != NULL && r != from) { + sock_send(r, msg, msg_len); + } + } + pthread_mutex_unlock(&pool_lock); +} + +void *handle_client(void *user_data) +{ + Sock *client = (Sock*) user_data; + ssize_t received = 0; + + char buffer[BUFFER_CAPACITY]; + memset(buffer, 0, sizeof(buffer)); + + char username[USERNAME_CAPACITY]; + size_t username_length = 0; + memset(username, 0, sizeof(username)); + + const char *username_prompt = "username:"; + size_t prompt_length = strlen(username_prompt); + memcpy(buffer, "username: ", prompt_length); + sock_send(client, buffer, prompt_length); + received = sock_recv(client, username, sizeof(username)); + if (received < 0) { + goto disconnect; + } + username_length = received; + + printf("INFO: Client login with username `%.*s`\n", (int)username_length, + username); + received = snprintf(buffer, sizeof(buffer), "[Server] `%.*s` joined the chat", + (int)username_length, username); + broadcast(client, buffer, received); + + memcpy(buffer, username, username_length); + buffer[username_length] = ':'; + buffer[username_length + 1] = ' '; + size_t prefix_len = username_length + 2; + + size_t read_len = sizeof(buffer) - prefix_len; + char *read_buf = buffer + prefix_len; + + while ((received = sock_recv(client, read_buf, read_len)) > 0) { + printf("INFO: %.*s: %.*s\n", (int)username_length, username, + (int)received, read_buf); + broadcast(client, buffer, received + prefix_len); + } + +disconnect: + sock_close(client); + remove_client(client); + + printf("INFO: Client `%.*s` disconnected\n", (int)username_length, + username); + received = snprintf(buffer, sizeof(buffer), "[Server] `%.*s` left the chat", + (int)username_length, username); + broadcast(client, buffer, received); + + return NULL; +} + +int main(void) +{ + Sock *server = sock_create(SOCK_IPV4, SOCK_TCP); + if (server == NULL) { + fprintf(stderr, "ERROR: Could not create socket\n"); + return EXIT_FAILURE; + } + + printf("INFO: Created socket\n"); + + if (!sock_bind(server, sock_addr("0.0.0.0", PORT))) { + fprintf(stderr, "ERROR: Could not bind socket\n"); + sock_close(server); + return EXIT_FAILURE; + } + + printf("INFO: Bind socket\n"); + + if (!sock_listen(server, 10)) { + fprintf(stderr, "ERROR: Could not listen on socket\n"); + sock_close(server); + return EXIT_FAILURE; + } + + printf("INFO: Listen socket\n"); + + while (true) { + Sock *client = sock_accept(server); + if (client == NULL) { + fprintf(stderr, "ERROR: Could not accept client\n"); + sock_close(client); + continue; + } + + if (!add_client(client)) { + fprintf(stderr, "ERROR: TCP_Socket pool buffer is full (%d)\n", + POOL_CAPACITY); + sock_close(client); + continue; + } + + // printf("INFO: Pool:\n"); + // pthread_mutex_lock(&pool_lock); + // for (size_t i = 0; i < POOL_CAPACITY; ++i) { + // printf(" %ld: %p\n", i, socket_pool[i]); + // } + // pthread_mutex_unlock(&pool_lock); + // printf("INFO: End pool:\n"); + + pthread_t thread; + if (pthread_create(&thread, NULL, &handle_client, client) != 0) { + fprintf(stderr, "ERROR: Could not create thread\n"); + sock_close(client); + continue; + } + pthread_detach(thread); + + printf("INFO: New client connected\n"); + } + + sock_close(server); + printf("INFO: Closed socket\n"); + + return EXIT_SUCCESS; +} diff --git a/examples/hello_client.c b/examples/hello_client.c new file mode 100644 index 0000000..f217860 --- /dev/null +++ b/examples/hello_client.c @@ -0,0 +1,36 @@ +#include +#include + +#define SOCK_IMPLEMENTATION +#include "sock.h" + +int main(void) +{ + char *err = "None"; + + Sock *sock = sock_create(SOCK_IPV4, SOCK_TCP); + if (sock == NULL) { err = "create"; goto defer; } + printf("create\n"); + + SockAddr addr = sock_addr("127.0.0.1", 6969); + + if (!sock_connect(sock, addr)) { err = "connect"; goto defer; } + printf("connect\n"); + + const char *msg = "Hello from client!"; + char buf[128]; + memset(buf, 0, sizeof(buf)); + + sock_send(sock, msg, strlen(msg)); + printf("send\n"); + sock_recv(sock, buf, sizeof(buf)); + printf("recv\n"); + printf("%.*s\n", (int)sizeof(buf), buf); + +defer: + sock_close(sock); + printf("close\n"); + fprintf(stderr, "ERROR: %s: %s\n", err, strerror(errno)); + + return 0; +} diff --git a/examples/hello_server.c b/examples/hello_server.c new file mode 100644 index 0000000..161079a --- /dev/null +++ b/examples/hello_server.c @@ -0,0 +1,43 @@ +#include +#include + +#define SOCK_IMPLEMENTATION +#include "sock.h" + +int main(void) { + char *err = "None"; + + Sock *sock = sock_create(SOCK_IPV4, SOCK_TCP); + if (sock == NULL) { err = "create"; goto defer; } + printf("create\n"); + + SockAddr addr = sock_addr("0.0.0.0", 6969); + if (!sock_bind(sock, addr)) { err = "bind"; goto defer; } + printf("bind\n"); + if (!sock_listen(sock, 16)) { err = "listen"; goto defer; } + printf("listen\n"); + + Sock *client = sock_accept(sock); + if (client == NULL) { err = "accept"; goto defer; } + printf("accept\n"); + + const char *msg = "Hello from server!"; + char buf[128]; + memset(buf, 0, sizeof(buf)); + + sock_send(client, msg, strlen(msg)); + printf("send\n"); + sock_recv(client, buf, sizeof(buf)); + printf("recv\n"); + printf("%.*s\n", (int)sizeof(buf), buf); + + sock_close(client); + printf("close\n"); + +defer: + sock_close(sock); + printf("close\n"); + fprintf(stderr, "ERROR: %s: %s\n", err, strerror(errno)); + + return 0; +} diff --git a/examples/udp_client.c b/examples/udp_client.c new file mode 100644 index 0000000..752df37 --- /dev/null +++ b/examples/udp_client.c @@ -0,0 +1,27 @@ +#include + +#define SOCK_IMPLEMENTATION +#include "sock.h" + +int main(void) +{ + Sock *sock = sock_create(SOCK_IPV4, SOCK_UDP); + if (sock == NULL) { + perror("sock_create"); + return 1; + } + + const char *msg = "Hello from client!"; + sock_sendto(sock, msg, strlen(msg), sock_addr("127.0.0.1", 6969)); + + SockAddr server_addr; + char buf[128]; + memset(buf, 0, sizeof(buf)); + + sock_recvfrom(sock, buf, sizeof(buf), &server_addr); + + printf("Received \"%.*s\" from %s:%d\n", (int)sizeof(buf), buf, + server_addr.str, server_addr.port); + + return 0; +} diff --git a/examples/udp_server.c b/examples/udp_server.c new file mode 100644 index 0000000..31fbdf3 --- /dev/null +++ b/examples/udp_server.c @@ -0,0 +1,35 @@ +#include + +#define SOCK_IMPLEMENTATION +#include "sock.h" + +int main(void) +{ + Sock *server = sock_create(SOCK_IPV4, SOCK_UDP); + if (server == NULL) { + perror("sock_create"); + return 1; + } + + if (!sock_bind(server, sock_addr("0.0.0.0", 6969))) { + perror("sock_bind"); + sock_close(server); + return 1; + } + + SockAddr client_addr; + char buf[128]; + memset(buf, 0, sizeof(buf)); + + sock_recvfrom(server, buf, sizeof(buf), &client_addr); + + printf("Received \"%.*s\" from %s:%d\n", (int)sizeof(buf), buf, + client_addr.str, client_addr.port); + + const char *msg = "Hello from server!"; + sock_sendto(server, msg, strlen(msg), client_addr); + + sock_close(server); + + return 0; +} diff --git a/sock.h b/sock.h new file mode 100644 index 0000000..5bc8138 --- /dev/null +++ b/sock.h @@ -0,0 +1,225 @@ +#ifndef SOCK_H_ +#define SOCK_H_ + +#include +#include +#include +#include +#include +#include +#include +#include + +#define SOCK_ADDR_STR_CAPACITY 64 + +typedef enum { + SOCK_ADDR_INVALID, + SOCK_IPV4, + SOCK_IPV6 +} SockAddrType; + +typedef struct { + SockAddrType type; + int port; + struct sockaddr sockaddr; + socklen_t len; + char str[SOCK_ADDR_STR_CAPACITY]; +} SockAddr; + +typedef enum { + SOCK_TCP, + SOCK_UDP +} SockType; + +typedef struct { + SockType type; + SockAddr addr; + int fd; +} Sock; + +SockAddr sock_addr(const char *addr, int port); + +Sock *sock_create(SockAddrType domain, SockType type); +bool sock_bind(Sock *sock, SockAddr addr); +bool sock_listen(Sock *sock, int backlog); +Sock *sock_accept(Sock *sock); +bool sock_connect(Sock *sock, SockAddr addr); +ssize_t sock_send(Sock *sock, const void *buf, size_t size); +ssize_t sock_recv(Sock *sock, void *buf, size_t size); +ssize_t sock_sendto(Sock *sock, const void *buf, size_t size, SockAddr addr); +ssize_t sock_recvfrom(Sock *sock, void *buf, size_t size, SockAddr *addr); +void sock_close(Sock *sock); + +#endif // SOCK_H_ + +#ifdef SOCK_IMPLEMENTATION + +SockAddr sock_addr(const char *addr, int port) +{ + SockAddr sa = {0}; + + if (inet_pton(AF_INET, addr, &sa.sockaddr) == 1) { + sa.type = SOCK_IPV4; + sa.len = sizeof(struct sockaddr_in); + struct sockaddr_in *in = (struct sockaddr_in*)&sa.sockaddr; + in->sin_family = AF_INET; + in->sin_port = htons(port); + } else if (inet_pton(AF_INET6, addr, &sa.sockaddr) == 1) { + sa.type = SOCK_IPV6; + sa.len = sizeof(struct sockaddr_in6); + struct sockaddr_in6 *in6 = (struct sockaddr_in6*)&sa.sockaddr; + in6->sin6_family = AF_INET6; + in6->sin6_port = htons(port); + } else { + sa.type = SOCK_ADDR_INVALID; + return sa; + } + + sa.port = port; + strncpy(sa.str, addr, sizeof(sa.str)); + + return sa; +} + +Sock *sock_create(SockAddrType domain, SockType type) +{ + Sock *sock = malloc(sizeof(*sock)); + if (sock == NULL) { + return NULL; + } + + memset(sock, 0, sizeof(*sock)); + + sock->type = type; + + int sock_domain = 0; + int sock_type = 0; + + switch (domain) { + case SOCK_IPV4: sock_domain = AF_INET; break; + case SOCK_IPV6: sock_domain = AF_INET6; break; + default: { + free(sock); + return NULL; + } break; + } + + switch (type) { + case SOCK_TCP: sock_type = SOCK_STREAM; break; + case SOCK_UDP: sock_type = SOCK_DGRAM; break; + default: { + free(sock); + return NULL; + } break; + } + + sock->fd = socket(sock_domain, sock_type, 0); + if (sock->fd < 0) { + free(sock); + return NULL; + } + + return sock; +} + +bool sock_bind(Sock *sock, SockAddr addr) +{ + if (bind(sock->fd, &addr.sockaddr, addr.len) < 0) { + return false; + } + + sock->addr = addr; + + return true; +} + +bool sock_listen(Sock *sock, int backlog) +{ + if (listen(sock->fd, backlog) < 0) { + return false; + } + + return true; +} + +Sock *sock_accept(Sock *sock) +{ + int fd = accept(sock->fd, &sock->addr.sockaddr, &sock->addr.len); + if (fd < 0) { + return NULL; + } + + Sock *res = malloc(sizeof(*res)); + if (res == NULL) { + return NULL; + } + + memset(res, 0, sizeof(*res)); + + res->type = sock->type; + res->fd = fd; + + return res; +} + +bool sock_connect(Sock *sock, SockAddr addr) +{ + if (connect(sock->fd, &addr.sockaddr, addr.len) < 0) { + return false; + } + + return true; +} + +ssize_t sock_send(Sock *sock, const void *buf, size_t size) +{ + return send(sock->fd, buf, size, 0); +} + +ssize_t sock_recv(Sock *sock, void *buf, size_t size) +{ + return recv(sock->fd, buf, size, 0); +} + +ssize_t sock_sendto(Sock *sock, const void *buf, size_t size, SockAddr addr) +{ + return sendto(sock->fd, buf, size, 0, &addr.sockaddr, addr.len); +} + +ssize_t sock_recvfrom(Sock *sock, void *buf, size_t size, SockAddr *addr) +{ + ssize_t res = recvfrom(sock->fd, buf, size, 0, &addr->sockaddr, + &addr->len); + + switch (addr->sockaddr.sa_family) { + case AF_INET: { + struct sockaddr_in *sa = (struct sockaddr_in*)&addr->sockaddr; + addr->type = SOCK_IPV4; + addr->port = ntohs(sa->sin_port); + addr->len = sizeof(*sa); + inet_ntop(AF_INET, &sa->sin_addr, addr->str, sizeof(addr->str)); + } break; + + case AF_INET6: { + struct sockaddr_in6 *sa = (struct sockaddr_in6*)&addr->sockaddr; + addr->type = SOCK_IPV6; + addr->port = ntohs(sa->sin6_port); + addr->len = sizeof(*sa); + inet_ntop(AF_INET6, &sa->sin6_addr, addr->str, sizeof(addr->str)); + } break; + + default: { + assert(false && "Unsupported address family"); + } break; + } + + return res; +} + +void sock_close(Sock *sock) +{ + close(sock->fd); + free(sock); +} + +#endif // SOCK_IMPLEMENTATION