Skip to content

Instantly share code, notes, and snippets.

@nilput
Last active November 14, 2024 13:48
Show Gist options
  • Select an option

  • Save nilput/29669262905dbb4b6ac373aff7430a62 to your computer and use it in GitHub Desktop.

Select an option

Save nilput/29669262905dbb4b6ac373aff7430a62 to your computer and use it in GitHub Desktop.
C socket server/client example, handles multiple clients on a single thread, single process.
//C tcp server/client example
// some of this is based on https://gist.github.com/oleksiiBobko/43d33b3c25c03bcc9b2b
// which has a couple of problems in the time of writing this.
//
// this handles multiple clients without blocking, without threads, without multiprocessing, using poll()
//
// Author: github.com/nilput <[email protected]>
// license: BSD-3
#include <stdio.h>
#include <stdarg.h>
#include <ctype.h>
#include <assert.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <poll.h>
#include <unistd.h>
#include <errno.h>
#include <time.h>
#include <fcntl.h>
void die(const char *msg, ...) {
va_list ap;
va_start(ap, msg);
vfprintf(stderr, msg, ap);
va_end(ap);
fprintf(stderr, "\n");
exit(1);
}
static void ewarn(const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
fprintf(stderr, "\n");
}
//returns 0 on success
int socket_set_async(int fd, char is_async) {
//source: https://stackoverflow.com/a/1549344
if (fd < 0)
return 1;
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1)
return 1;
flags = is_async ? (flags | O_NONBLOCK) : (flags & ~O_NONBLOCK);
if (fcntl(fd, F_SETFL, flags) != 0)
return 1;
return 0;
}
#define MAX_CLIENTS 50
#define STR_BUFFSZ 256
struct client_info {
int sock;
struct sockaddr_in client_addr;
char is_greeted;
int fgets_state; //used by async_fd_gets()
char recv_buff[STR_BUFFSZ];
};
struct client_info clients[MAX_CLIENTS]; //used for our response logic
struct pollfd fds[MAX_CLIENTS]; //used by poll()
int nclients = 0; //number of active clients
enum {
CAN_READ_FLAGS = POLLRDNORM,
CAN_WRITE_FLAGS = POLLWRNORM,
};
void add_client(int sock, struct sockaddr_in client_addr) {
if (nclients >= MAX_CLIENTS)
die("too many client. server died");
clients[nclients].sock = sock;
clients[nclients].client_addr = client_addr;
clients[nclients].is_greeted = 0;
fds[nclients].fd = sock;
fds[nclients].events = CAN_READ_FLAGS | CAN_WRITE_FLAGS;
fds[nclients].revents = 0;
nclients++;
}
int find_client_index(int sock) {
for (int i=0; i<nclients; i++) {
if (clients[i].sock == sock)
return i;
}
return -1;
}
void del_client(int sock) {
int idx = find_client_index(sock);
if (idx != -1) {
//swapback
if (idx != nclients - 1) {
clients[idx] = clients[nclients-1];
fds[idx] = fds[nclients-1];
}
nclients--;
}
}
static char *curtime(void) {
//source: https://stackoverflow.com/a/5142028
time_t rawtime;
struct tm * timeinfo;
time(&rawtime);
timeinfo = localtime(&rawtime);
static char buff[128];
if (snprintf(buff, 128, "Server's local time and date: %s", asctime(timeinfo)) >= 128) {
buff[0] = 0;
}
return buff;
}
//reads a line from fd, asynchronously
//state must be initialized to zero
//return values:
// return value > 0 : length of the line read
// return value == 0 : error, or EOF
// return value == -1: try again
int async_fd_gets(int fd, char *buff, size_t buffsz, int *state) {
if (buffsz <= 1) {
buff[0] = 0;
return 0; //not continuable
}
int len = 0;
if (*state > 0) { //^[garbage] \n\0 [new stuff..]
int begin_idx = strlen(buff) + 1;
len = *state;
memmove(buff, buff + begin_idx, len);
*state = 0;
}
else if (*state < 0) { //^[new stuff ..]
len = - (*state);
}
int rem = buffsz - len - 1;
socket_set_async(fd, 1);
errno = 0;
ssize_t cur = read(fd, buff+len, rem);
char is_continuable = 1;
if (cur > 0) {
len += cur;
}
else if (cur == 0 && rem > 0) {
is_continuable = 0;
}
else if (cur == -1 && errno != EAGAIN) {
is_continuable = 0;
}
while (len > 0 && buff[0] == '\0') {
len--;
memmove(buff, buff + 1, len);
}
for (int i=0; i<len; i++) {
if (buff[i] == '\n') {
*state = len - (i + 1);
memmove(buff + i + 2, buff + i + 1, *state); //make space for '\0'
buff[i+1] = '\0';
return i;
}
else if (buff[i] == '\0') {
assert(i > 0);
*state = len - (i + 1);
return i;
}
}
if ((size_t)len == buffsz - 1 || !is_continuable) {
buff[len] = '\0';
return len;
}
*state = -len;
return -1; //next invoc can try again
}
//strip a string, " foo bar " -> "foo bar"
void sstrip(char *str) {
int left = 0;
while (*str && isspace(*str))
left++;
char *last = str + strlen(str) - 1;
while (last >= str && isspace(*last))
*last = '\0';
int sz = strlen(str) + 1;
memmove(str, str + left, sz - left);
}
void greet_client(int client_index) {
struct client_info *inf = clients + client_index;
const char *gr =
"Hello!, welcome\n"
" trying entering commands:\n"
" time: print current server time\n"
" echo: repeat what you say\n";
write(inf->sock, gr, strlen(gr) + 1);
}
//a return value of 0 means client is closed
int respond_to_client(int client_index) {
assert(client_index >= 0 && client_index < nclients);
struct client_info *inf = clients + client_index;
char buff[STR_BUFFSZ * 2];
int rv;
while ((rv = async_fd_gets(inf->sock, buff, sizeof buff, &inf->fgets_state)) > 0) {
sstrip(buff);
socket_set_async(inf->sock, 0); //make this write synchornous
if (strcmp(buff, "time") == 0) {
char *tm = curtime();
write(inf->sock, tm, strlen(tm) + 1);
}
else if (strncmp(buff, "echo", 4) == 0) {
char *rest = buff + 4;
write(inf->sock, rest, strlen(rest) + 1);
}
else {
const char *wat = "wat";
write(inf->sock, wat, strlen(wat) + 1);
}
}
return rv;
}
void handle_clients(void) {
int nevents = poll(fds, nclients, 200);
if (nevents < 0) {
perror("poll failed:");
return;
}
for (int i=0; i<nclients && nevents; i++) {
struct client_info *inf = clients + i;
struct pollfd *pfd = fds + i;
char do_delete_client = 0;
if (pfd->revents != 0) {
if (pfd->revents & POLLERR) {
//An error has occurred on the device or stream.
ewarn("An error has occurred on the device or stream. socket: %d", inf->sock);
do_delete_client = 1;
}
if (pfd->revents & POLLHUP) {
/* A device has been disconnected, or a pipe or FIFO has been closed by the last */
/* process that had it open for writing.*/
ewarn("A device has been disconnected, socket: %d", inf->sock);
do_delete_client = 1;
}
if (pfd->revents & POLLNVAL) {
//The specified fd value is invalid
ewarn("The specified fd value is invalid, socket: %d", inf->sock);
do_delete_client = 1;
}
if (pfd->revents & CAN_WRITE_FLAGS && !inf->is_greeted) {
greet_client(i);
inf->is_greeted = 1;
}
if (pfd->revents & CAN_READ_FLAGS) {
if (respond_to_client(i) == 0)
do_delete_client = 1;
}
}
if (do_delete_client) {
ewarn("closed client socket %d\n", inf->sock);
close(inf->sock);
del_client(inf->sock);
nevents--;
i--;
}
}
}
void serve(int port) {
int server_sock;
struct sockaddr_in server;
server_sock = socket(AF_INET , SOCK_STREAM , 0);
if (server_sock == -1) {
die("Could not create socket");
}
puts("Socket created");
socket_set_async(server_sock, 1);
//Prepare the sockaddr_in structure
server.sin_family = AF_INET;
server.sin_addr.s_addr = INADDR_ANY;
server.sin_port = htons(port);
if (bind(server_sock,(struct sockaddr *)&server , sizeof(server)) < 0) {
perror("bind");
die("");
}
puts("bind succeded.");
//Listen
listen(server_sock, 3);
puts("Waiting for incoming connections...");
int client_sock;
struct sockaddr_in client;
int stdin_fgets_state = 0;
char cmd_buff[STR_BUFFSZ];
printf( "You can enter commands for controlling the server:\n"
" info number of active clients\n"
" broadcast [msg] send message to all clients\n"
" exit close the server\n");
while (1) {
while (1) {
errno = 0;
socklen_t accept_size = sizeof(struct sockaddr_in);
client_sock = accept(server_sock, (struct sockaddr *)&client, &accept_size);
if (client_sock >= 0) {
puts("Connection accepted");
add_client(client_sock, client);
puts("Handler assigned");
}
else {
if (errno != EAGAIN) {
perror("accept: failed to accept connection.");
}
goto do_handle_clients;
}
}
do_handle_clients:
handle_clients();
usleep(50 * 1000);
//handle server commands (recieved from stdin)
if (async_fd_gets(0, cmd_buff, STR_BUFFSZ - 1, &stdin_fgets_state) > 0) {
sstrip(cmd_buff);
if (strncmp(cmd_buff, "broadcast", 9) == 0) {
char *msg = cmd_buff + 9;
int len = strlen(msg) + 1;
for (int i=0; i<nclients; i++) {
write(clients[i].sock, msg, len);
}
}
else if (strncmp(cmd_buff, "info", 4) == 0) {
printf("number of active clients: %d\n", nclients);
}
else if (strncmp(cmd_buff, "exit", 4) == 0) {
goto end;
}
}
}
end:
close(server_sock);
for (int i=0; i<nclients; i++) {
close(clients[i].sock);
}
nclients = 0;
return;
}
void client_connect(char *address, int port) {
int server_sock;
struct sockaddr_in serv_addr;
if((server_sock = socket(AF_INET, SOCK_STREAM, 0)) < 0)
die("Failed creating socket\n");
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(address);
serv_addr.sin_port = htons(port);
if (connect(server_sock, (struct sockaddr *) &serv_addr, sizeof (serv_addr)) < 0) {
die("Failed to connect to server\n");
}
printf("[client] Connected successfully\n");
char sbuff[STR_BUFFSZ];
int stdin_fgets_state = 0;
char rbuff[STR_BUFFSZ];
int server_fgets_state = 0;
int rv;
while (1) {
if ((rv = async_fd_gets(0, sbuff, STR_BUFFSZ, &stdin_fgets_state)) > 0) {
send(server_sock, sbuff, strlen(sbuff) + 1, 0);
printf("[client] sent \"%s\"\n", sbuff);
}
else if (rv == 0) {
printf("exiting because stdin is closed\n");
break;
}
if ((rv = async_fd_gets(server_sock, rbuff, STR_BUFFSZ, &server_fgets_state)) > 0) {
printf("[server] %s\n", rbuff);
}
else if (rv == 0) {
printf("exiting because socket is closed\n");
break;
}
usleep(50 * 1000);
}
close(server_sock);
}
void print_help(char *argv0) {
ewarn("usage: %s [options]\n"
" options:\n"
" -c client\n"
" -i address (defaults to 127.0.0.1)\n"
" -s server\n"
" -p port\n", argv0);
}
int main(int argc , char *argv[]) {
char as_client = 0;
char as_server = 0;
char help = 0;
char *address = NULL;
char *port_str = NULL;
for (int i=1; i<argc; i++) {
if (strncmp(argv[i], "-c", 2) == 0)
as_client = 1;
if (strncmp(argv[i], "-s", 2) == 0)
as_server = 1;
if (strncmp(argv[i], "-h", 2) == 0)
help = 1;
if (strncmp(argv[i], "-p", 2) == 0) {
if (strlen(argv[i]) > 2)
port_str = argv[i] + 2;
else {
if (i == argc - 1) {
print_help(argv[0]);
die("expected port argument");
}
port_str = argv[i + 1];
i++;
}
}
if (strncmp(argv[i], "-i", 2) == 0) {
if (strlen(argv[i]) > 2)
address = argv[i] + 2;
else {
if (i == argc - 1) {
print_help(argv[0]);
die("expected address argument");
}
address = argv[i + 1];
i++;
}
}
}
int port = 0;
if (port_str)
port = atoi(port_str);
if (!port)
port = 8888;
if (help || (!as_server && !as_client)) {
print_help(argv[0]);
return 1;
}
if (as_server) {
serve(port);
}
else if (as_client) {
if (!address)
address = "127.0.0.1";
client_connect(address, port);
}
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment