Last active
November 14, 2024 13:48
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| //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 <nilputs@gmail.com> | |
| // 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