├── .gitignore ├── Makefile ├── README.md ├── yc_poll.c ├── yc_select.c ├── yc_kqueue.c ├── LICENSE.txt ├── yc_epoll.c └── yc_uring.c /.gitignore: -------------------------------------------------------------------------------- 1 | yc_select 2 | yc_poll 3 | yc_epoll 4 | yc_uring 5 | yc_kqueue 6 | .DS_Store 7 | *.dSYM 8 | .clang-format -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CFLAGS := -Wall -ggdb 2 | 3 | PROGRAMS_SIMPLE := yc_select yc_poll 4 | PROGRAMS_URING := 5 | 6 | UNAME_S := $(shell uname -s) 7 | ifeq ($(UNAME_S),Linux) 8 | PROGRAMS_SIMPLE += yc_epoll 9 | PROGRAMS_URING += yc_uring 10 | endif 11 | ifeq ($(UNAME_S),FreeBSD) 12 | PROGRAMS_SIMPLE += yc_kqueue 13 | endif 14 | 15 | all: $(PROGRAMS_SIMPLE) $(PROGRAMS_URING) 16 | 17 | $(PROGRAMS_SIMPLE): %: %.c 18 | $(CC) $(CFLAGS) -o $@ $< 19 | 20 | $(PROGRAMS_URING): %: %.c 21 | $(CC) $(CFLAGS) -o $@ $< -luring 22 | 23 | clean: 24 | rm -f $(PROGRAMS_SIMPLE) $(PROGRAMS_URING) 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # yoctochat 2 | 3 | ## The tiniest chat servers on earth! 4 | 5 | Here will be a collection of the simplest possible TCP chat servers, to demonstrate how to write multiuser, multiplexing server software using various techniques. 6 | 7 | ### The "spec" 8 | 9 | A `yoctochat` server will: 10 | 11 | * take a single commandline argument, the port to listen on 12 | * open a listening port 13 | * handle multiple connections and disconnections on that port 14 | * receive text on a connection, and forward it on to all other connections 15 | * produce simple output about what its doing 16 | * demonstrate a single IO multiplexing technique as simply as possible 17 | * be well commented! 18 | 19 | ### Why? 20 | 21 | 20+ years ago, during my University days, I started writing little chat servers like this to teach myself C, UNIX, systems programming, internet programming, and so on. I went on to write bigger and better ones. It has been useful knowledge! 22 | 23 | Lately, I found myself wanting to experiment with [io_uring](https://unixism.net/loti/), and I realised I'd forgotten how the "classic" `select()` loop is constructed. So I started there, and here we are! 24 | -------------------------------------------------------------------------------- /yc_poll.c: -------------------------------------------------------------------------------- 1 | /* yc_poll - a yoctochat server using a classic poll() IO loop */ 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | /* number of pollfds in our array. because of the wey we're implementing this, 15 | * that's roughly the maximum number of connections we can handle. */ 16 | #define NUM_POLLFDS (128) 17 | 18 | int main(int argc, char **argv) { 19 | if (argc < 2) { 20 | printf("usage: %s \n", argv[0]); 21 | exit(1); 22 | } 23 | 24 | int port = atoi(argv[1]); 25 | if (port <= 0) { 26 | printf("'%s' not a valid port number\n", argv[1]); 27 | exit(1); 28 | } 29 | 30 | /* create the server socket */ 31 | int server_fd = socket(AF_INET, SOCK_STREAM, 0); 32 | if (server_fd < 0) { 33 | perror("socket"); 34 | exit(1); 35 | } 36 | 37 | /* arrange for the listening address to be reusable. This makes TCP 38 | * marginally "less safe" (for a whole bunch of obscure reasons) but allows 39 | * us to kill and restart the program with ease */ 40 | int onoff = 1; 41 | if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &onoff, sizeof(onoff)) < 0) { 42 | perror("setsockopt"); 43 | exit(1); 44 | } 45 | 46 | /* set up the address structure for binding, which is *: */ 47 | struct sockaddr_in sin = { 48 | .sin_family = AF_INET, 49 | .sin_port = htons(port), 50 | .sin_addr = { 51 | .s_addr = htonl(INADDR_ANY) 52 | } 53 | }; 54 | 55 | /* bind the server socket to the wanted address */ 56 | if (bind(server_fd, (struct sockaddr *) &sin, sizeof(sin)) < 0) { 57 | perror("bind"); 58 | exit(1); 59 | } 60 | 61 | /* and open it for connections! */ 62 | if (listen(server_fd, 10) < 0) { 63 | perror("listen"); 64 | exit(1); 65 | } 66 | 67 | printf("listening on port %d\n", port); 68 | 69 | /* create an array of pollfd structs. each one carries a file descriptor, a 70 | * set of wanted events, and after the poll() call, a set of events that 71 | * occurred. for our own convenience we keep a static list and use the file 72 | * descriptor as the index. */ 73 | struct pollfd pollfds[NUM_POLLFDS]; 74 | for (int fd = 0; fd < NUM_POLLFDS; fd++) { 75 | /* setting -1 fd disables */ 76 | pollfds[fd].fd = -1; 77 | } 78 | 79 | /* add the server socket, and request read/input events */ 80 | pollfds[server_fd].fd = server_fd; 81 | pollfds[server_fd].events = POLLIN; 82 | 83 | /* wait forever for something to happen */ 84 | while (poll(pollfds, NUM_POLLFDS, -1) >= 0) { 85 | 86 | /* if the server socket has activity, someone connected */ 87 | if (pollfds[server_fd].revents & POLLIN) { 88 | /* create storage for their address */ 89 | struct sockaddr_in sin; 90 | socklen_t sinlen = sizeof(sin); 91 | 92 | /* let them in! */ 93 | int new_fd = accept(server_fd, (struct sockaddr *) &sin, &sinlen); 94 | if (new_fd < 0) { 95 | perror("accept"); 96 | } 97 | else { 98 | /* hello */ 99 | printf("[%d] connect from %s:%d\n", new_fd, inet_ntoa(sin.sin_addr), ntohs(sin.sin_port)); 100 | 101 | /* make them non-blocking. this is necessary, because a disconnect will 102 | * cause a descriptor to become readable, but reading will block 103 | * forever (because they're disconnected. non-blocking will cause 104 | * read() to return 0 on a disconnected descriptor, so we can take the 105 | * right action */ 106 | int onoff = 1; 107 | if (ioctl(new_fd, FIONBIO, &onoff) < 0) { 108 | printf("fcntl(%d): %s\n", new_fd, strerror(errno)); 109 | close(new_fd); 110 | continue; 111 | } 112 | 113 | /* enable the pollfd for this fd, and request read events */ 114 | pollfds[new_fd].fd = new_fd; 115 | pollfds[new_fd].events = POLLIN; 116 | } 117 | } 118 | 119 | for (int fd = 0; fd < NUM_POLLFDS; fd++) { 120 | if (!(pollfds[fd].revents & POLLIN) || fd == server_fd) 121 | continue; 122 | 123 | printf("[%d] activity\n", fd); 124 | 125 | /* create a buffer to read into */ 126 | char buf[1024]; 127 | int nread = read(fd, buf, sizeof(buf)); 128 | 129 | /* see how much we read */ 130 | if (nread < 0) { 131 | /* less then zero is some error. disconnect them */ 132 | fprintf(stderr, "read(%d): %s\n", fd, strerror(errno)); 133 | close(fd); 134 | pollfds[fd].fd = -1; 135 | } 136 | 137 | else if (nread > 0) { 138 | /* we got some stuff from them! */ 139 | printf("[%d] read: %.*s\n", fd, nread, buf); 140 | 141 | /* loop over all our connections, and send stuff onto them! */ 142 | for (int dest_fd = 0; dest_fd < NUM_POLLFDS; dest_fd++) { 143 | 144 | /* take active connections, but not ourselves */ 145 | if (pollfds[dest_fd].fd >= 0 && pollfds[dest_fd].fd != fd && pollfds[dest_fd].fd != server_fd) { 146 | /* write to them */ 147 | if (write(dest_fd, buf, nread) < 0) { 148 | /* disconnect if it fails; they might have legitimately gone away without telling us */ 149 | fprintf(stderr, "write(%d): %s\n", dest_fd, strerror(errno)); 150 | close(dest_fd); 151 | pollfds[dest_fd].fd = -1; 152 | } 153 | } 154 | } 155 | } 156 | 157 | /* zero byes read */ 158 | else { 159 | /* so they gracefully disconnected and we should forget them */ 160 | printf("[%d] closed\n", fd); 161 | close(fd); 162 | pollfds[fd].fd = -1; 163 | } 164 | } 165 | } 166 | 167 | /* poll failed. in a real server you might actually need to handle 168 | * non-error cases like EINTR, but it complicates this example so we won't 169 | * bother */ 170 | perror("poll"); 171 | exit(1); 172 | } 173 | -------------------------------------------------------------------------------- /yc_select.c: -------------------------------------------------------------------------------- 1 | /* yc_select - a yoctochat server using a classic select() IO loop */ 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | int main(int argc, char **argv) { 15 | if (argc < 2) { 16 | printf("usage: %s \n", argv[0]); 17 | exit(1); 18 | } 19 | 20 | int port = atoi(argv[1]); 21 | if (port <= 0) { 22 | printf("'%s' not a valid port number\n", argv[1]); 23 | exit(1); 24 | } 25 | 26 | /* create the server socket */ 27 | int server_fd = socket(AF_INET, SOCK_STREAM, 0); 28 | if (server_fd < 0) { 29 | perror("socket"); 30 | exit(1); 31 | } 32 | 33 | /* arrange for the listening address to be reusable. This makes TCP 34 | * marginally "less safe" (for a whole bunch of obscure reasons) but allows 35 | * us to kill and restart the program with ease */ 36 | int onoff = 1; 37 | if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &onoff, sizeof(onoff)) < 0) { 38 | perror("setsockopt"); 39 | exit(1); 40 | } 41 | 42 | /* set up the address structure for binding, which is *: */ 43 | struct sockaddr_in sin = { 44 | .sin_family = AF_INET, 45 | .sin_port = htons(port), 46 | .sin_addr = { 47 | .s_addr = htonl(INADDR_ANY) 48 | } 49 | }; 50 | 51 | /* bind the server socket to the wanted address */ 52 | if (bind(server_fd, (struct sockaddr *) &sin, sizeof(sin)) < 0) { 53 | perror("bind"); 54 | exit(1); 55 | } 56 | 57 | /* and open it for connections! */ 58 | if (listen(server_fd, 10) < 0) { 59 | perror("listen"); 60 | exit(1); 61 | } 62 | 63 | printf("listening on port %d\n", port); 64 | 65 | /* create storage for our active connections. in a real server, this would be 66 | * some mapping from file descriptor -> connection object. here the only 67 | * thing we're interested is if the descriptor is connected at all, so a bool 68 | * (int) is enough: if conns[fd] is true, then fd is connected right now */ 69 | int conns[FD_SETSIZE]; 70 | memset(&conns, 0, sizeof(conns)); 71 | 72 | /* create the fd_set we will use to register interest in read events */ 73 | fd_set rfds; 74 | FD_ZERO(&rfds); 75 | 76 | /* add the server socket; when it becomes "readable", someone connected! */ 77 | FD_SET(server_fd, &rfds); 78 | 79 | /* we need to tell select() what the upper descriptor in the set is, so it 80 | * knows when to stop scanning. honestly these days we could just use 81 | * FD_SETSIZE because its laughably small (1024), but this is history */ 82 | int max_fd = server_fd+1; 83 | 84 | /* the main IO loop! call select, ask it to check the descriptors we're 85 | * interested in. any descriptors in the set that aren't have no new activity 86 | * will be cleared; any remaining set have activity on them */ 87 | while (select(max_fd, &rfds, NULL, NULL, NULL) >= 0) { 88 | 89 | /* if the server socket has activity, someone connected */ 90 | if (FD_ISSET(server_fd, &rfds)) { 91 | /* create storage for their address */ 92 | struct sockaddr_in sin; 93 | socklen_t sinlen = sizeof(sin); 94 | 95 | /* let them in! */ 96 | int new_fd = accept(server_fd, (struct sockaddr *) &sin, &sinlen); 97 | if (new_fd < 0) { 98 | perror("accept"); 99 | } 100 | else { 101 | /* hello */ 102 | printf("[%d] connect from %s:%d\n", new_fd, inet_ntoa(sin.sin_addr), ntohs(sin.sin_port)); 103 | 104 | /* make them non-blocking. this is necessary, because a disconnect will 105 | * cause a descriptor to become readable, but reading will block 106 | * forever (because they're disconnected. non-blocking will cause 107 | * read() to return 0 on a disconnected descriptor, so we can take the 108 | * right action */ 109 | int onoff = 1; 110 | if (ioctl(new_fd, FIONBIO, &onoff) < 0) { 111 | printf("fcntl(%d): %s\n", new_fd, strerror(errno)); 112 | close(new_fd); 113 | continue; 114 | } 115 | 116 | /* remember our new connection. in a real server, you'd create a 117 | * connection or user object of some sort, maybe send them a greeting, 118 | * begin authentication, etc */ 119 | conns[new_fd] = 1; 120 | } 121 | } 122 | 123 | /* loop over all our connections, seeing if anything happend */ 124 | for (int fd = 0; fd < FD_SETSIZE; fd++) { 125 | /* skip if no connection */ 126 | if (!conns[fd]) 127 | continue; 128 | 129 | /* is their activity on their fd? */ 130 | if (FD_ISSET(fd, &rfds)) { 131 | /* yes! */ 132 | printf("[%d] activity\n", fd); 133 | 134 | /* create a buffer to read into */ 135 | char buf[1024]; 136 | int nread = read(fd, buf, sizeof(buf)); 137 | 138 | /* see how much we read */ 139 | if (nread < 0) { 140 | /* less then zero is some error. disconnect them */ 141 | fprintf(stderr, "read(%d): %s\n", fd, strerror(errno)); 142 | close(fd); 143 | conns[fd] = 0; 144 | } 145 | 146 | else if (nread > 0) { 147 | /* we got some stuff from them! */ 148 | printf("[%d] read: %.*s\n", fd, nread, buf); 149 | 150 | /* loop over all our connections, and send stuff onto them! */ 151 | for (int dest_fd = 0; dest_fd < FD_SETSIZE; dest_fd++) { 152 | 153 | /* take active connections, but not ourselves */ 154 | if (conns[dest_fd] && dest_fd != fd) { 155 | 156 | /* write to them */ 157 | if (write(dest_fd, buf, nread) < 0) { 158 | /* disconnect if it fails; they might have legitimately gone away without telling us */ 159 | fprintf(stderr, "write(%d): %s\n", dest_fd, strerror(errno)); 160 | close(dest_fd); 161 | conns[dest_fd] = 0; 162 | } 163 | } 164 | } 165 | } 166 | 167 | /* zero byes read */ 168 | else { 169 | /* so they gracefully disconnected and we should forget them */ 170 | printf("[%d] closed\n", fd); 171 | close(fd); 172 | conns[fd] = 0; 173 | } 174 | } 175 | } 176 | 177 | /* we've processed all activity, so now we need to set up the descriptor 178 | * set again (remember, select() removes descriptors that had no activity) */ 179 | FD_ZERO(&rfds); 180 | 181 | /* add the server */ 182 | FD_SET(server_fd, &rfds); 183 | max_fd = server_fd+1; 184 | 185 | /* and all the active connections */ 186 | for (int fd = 0; fd < FD_SETSIZE; fd++) { 187 | if(conns[fd]) { 188 | FD_SET(fd, &rfds); 189 | max_fd = fd+1; 190 | } 191 | } 192 | } 193 | 194 | /* select failed. in a real server you might actually need to handle 195 | * non-error cases like EINTR, but it complicates this example so we won't 196 | * bother */ 197 | perror("select"); 198 | exit(1); 199 | } 200 | -------------------------------------------------------------------------------- /yc_kqueue.c: -------------------------------------------------------------------------------- 1 | /* yc_kqueue - a yoctochat server using a BSD kqueue IO loop */ 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | /* max number of connections. in a real program you probably wouldn't do this, 15 | * and instead use a more dynamic structure for tracking connections */ 16 | #define NUM_CONNS (128) 17 | int main(int argc, char **argv) { 18 | if (argc < 2) { 19 | printf("usage: %s \n", argv[0]); 20 | exit(1); 21 | } 22 | 23 | int port = atoi(argv[1]); 24 | if (port <= 0) { 25 | printf("'%s' not a valid port number\n", argv[1]); 26 | exit(1); 27 | } 28 | 29 | /* create the server socket */ 30 | int server_fd = socket(AF_INET, SOCK_STREAM, 0); 31 | if (server_fd < 0) { 32 | perror("socket"); 33 | exit(1); 34 | } 35 | 36 | /* arrange for the listening address to be reusable. This makes TCP 37 | * marginally "less safe" (for a whole bunch of obscure reasons) but allows 38 | * us to kill and restart the program with ease */ 39 | int onoff = 1; 40 | if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &onoff, sizeof(onoff)) < 0) { 41 | perror("setsockopt"); 42 | exit(1); 43 | } 44 | 45 | /* set up the address structure for binding, which is *: */ 46 | struct sockaddr_in sin = { 47 | .sin_family = AF_INET, 48 | .sin_port = htons(port), 49 | .sin_addr = { 50 | .s_addr = htonl(INADDR_ANY)}}; 51 | 52 | /* bind the server socket to the wanted address */ 53 | if (bind(server_fd, (struct sockaddr *) &sin, sizeof(sin)) < 0) { 54 | perror("bind"); 55 | exit(1); 56 | } 57 | 58 | /* and open it for connections! */ 59 | if (listen(server_fd, 10) < 0) { 60 | perror("listen"); 61 | exit(1); 62 | } 63 | 64 | printf("listening on port %d\n", port); 65 | 66 | int conns[NUM_CONNS]; 67 | memset(&conns, 0, sizeof(conns)); 68 | // Prepare the kqueue. 69 | int kq = kqueue(); 70 | 71 | 72 | int new_events; 73 | 74 | struct kevent change_event[4], event[4]; 75 | 76 | // Create event 'filter', these are the events we want to monitor. 77 | // Here we want to monitor: socket_listen_fd, for the events: EVFILT_READ 78 | // (when there is data to be read on the socket), and perform the following 79 | // actions on this kevent: EV_ADD and EV_ENABLE (add the event to the kqueue 80 | // and enable it). 81 | EV_SET(change_event, server_fd, EVFILT_READ, EV_ADD | EV_ENABLE, 0, 0, 0); 82 | 83 | // Register kevent with the kqueue. 84 | if (kevent(kq, change_event, 1, NULL, 0, NULL) == -1) { 85 | perror("kevent"); 86 | exit(1); 87 | } 88 | 89 | for (;;) { 90 | // Check for new events, but do not register new events with 91 | // the kqueue. Hence the 2nd and 3rd arguments are NULL, 0. 92 | // Only handle 1 new event per iteration in the loop; 5th 93 | // argument is 1. 94 | new_events = kevent(kq, NULL, 0, event, 1, NULL); 95 | if (new_events == -1) { 96 | perror("kevent"); 97 | exit(1); 98 | } 99 | 100 | for (int i = 0; i < new_events; i++) { 101 | int event_fd = event[i].ident; 102 | 103 | // When the client disconnects an EOF is sent. By closing the file 104 | // descriptor the event is automatically removed from the kqueue. 105 | if (event[i].flags & EV_EOF) { 106 | printf("[%d] closed\n", event_fd); 107 | close(event_fd); 108 | conns[event_fd] = 0; 109 | } 110 | // If the new event's file descriptor is the same as the listening 111 | // socket's file descriptor, we are sure that a new client wants 112 | // to connect to our socket. 113 | else if (event_fd == server_fd) { 114 | 115 | socklen_t sinlen = sizeof(sin); 116 | // Incoming socket connection on the listening socket. 117 | // Create a new socket for the actual connection to client. 118 | int new_fd = accept(event_fd, (struct sockaddr *) &sin, &sinlen); 119 | if (new_fd < 0) { 120 | perror("accept"); 121 | } else { 122 | printf("[%d] connect from %s:%d\n", new_fd, inet_ntoa(sin.sin_addr), ntohs(sin.sin_port)); 123 | // Put this new socket connection also as a 'filter' event 124 | // to watch in kqueue, so we can now watch for events on this 125 | // new socket. 126 | EV_SET(change_event, new_fd, EVFILT_READ, EV_ADD, 0, 0, NULL); 127 | if (kevent(kq, change_event, 1, NULL, 0, NULL) < 0) { 128 | perror("kevent error"); 129 | } 130 | /* remember our new connection. in a real server, you'd create a 131 | * connection or user object of some sort, maybe send them a greeting, 132 | * begin authentication, etc */ 133 | conns[new_fd] = 1; 134 | } 135 | } 136 | 137 | else if (event[i].filter & EVFILT_READ) { 138 | printf("[%d] activity\n", event_fd); 139 | // Read bytes from socket 140 | char buf[1024]; 141 | size_t nread = recv(event_fd, buf, sizeof(buf), 0); 142 | printf("read %zu bytes\n", nread); 143 | /* see how much we read */ 144 | if (nread < 0) { 145 | /* less then zero is some error. disconnect them */ 146 | fprintf(stderr, "read(%d): %s\n", event_fd, strerror(errno)); 147 | close(event_fd); 148 | conns[event_fd] = 0; 149 | } else if (nread > 0) { 150 | /* we got some stuff from them! */ 151 | printf("[%d] read: %.*s\n", event_fd, (int) nread, buf); 152 | 153 | /* loop over all our connections, and send stuff onto them! */ 154 | for (int dest_fd = 0; dest_fd < NUM_CONNS; dest_fd++) { 155 | 156 | /* take active connections, but not ourselves */ 157 | if (conns[dest_fd] && dest_fd != event_fd) { 158 | 159 | /* write to them */ 160 | if (write(dest_fd, buf, nread) < 0) { 161 | /* disconnect if it fails; they might have legitimately gone away without telling us */ 162 | fprintf(stderr, "write(%d): %s\n", dest_fd, strerror(errno)); 163 | close(dest_fd); 164 | conns[dest_fd] = 0; 165 | } 166 | } 167 | } 168 | } 169 | } 170 | } 171 | } 172 | 173 | /* kqueue failed. in a real server you might actually need to handle 174 | * non-error cases like EINTR, but it complicates this example so we won't 175 | * bother */ 176 | perror("kevent"); 177 | exit(1); 178 | } 179 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /yc_epoll.c: -------------------------------------------------------------------------------- 1 | /* yc_epoll - a yoctochat server using a Linux epoll IO loop */ 2 | 3 | /* NOTE: epoll is simple on the surface, but kinda weird once you get into the 4 | * high-performance situations where you might actually want to use it. 5 | * Recommended reading: 6 | * https://copyconstruct.medium.com/the-method-to-epolls-madness-d9d2d6378642 7 | * https://idea.popcount.org/2017-02-20-epoll-is-fundamentally-broken-12/ 8 | * https://idea.popcount.org/2017-03-20-epoll-is-fundamentally-broken-22/ 9 | */ 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | /* max number of connections. in a real program you probably wouldn't do this, 22 | * and instead use a more dynamic structure for tracking connections */ 23 | #define NUM_CONNS (128) 24 | 25 | /* max events per call to epoll_wait(). more of them just means fewer calls to 26 | * epoll_wait() in a busy server, but too many would be a waste of memory. our 27 | * server is tiny so there's no point having many. */ 28 | #define NUM_EVENTS (16) 29 | 30 | int main(int argc, char **argv) { 31 | if (argc < 2) { 32 | printf("usage: %s \n", argv[0]); 33 | exit(1); 34 | } 35 | 36 | int port = atoi(argv[1]); 37 | if (port <= 0) { 38 | printf("'%s' not a valid port number\n", argv[1]); 39 | exit(1); 40 | } 41 | 42 | /* create the server socket */ 43 | int server_fd = socket(AF_INET, SOCK_STREAM, 0); 44 | if (server_fd < 0) { 45 | perror("socket"); 46 | exit(1); 47 | } 48 | 49 | /* arrange for the listening address to be reusable. This makes TCP 50 | * marginally "less safe" (for a whole bunch of obscure reasons) but allows 51 | * us to kill and restart the program with ease */ 52 | int onoff = 1; 53 | if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &onoff, sizeof(onoff)) < 0) { 54 | perror("setsockopt"); 55 | exit(1); 56 | } 57 | 58 | /* set up the address structure for binding, which is *: */ 59 | struct sockaddr_in sin = { 60 | .sin_family = AF_INET, 61 | .sin_port = htons(port), 62 | .sin_addr = { 63 | .s_addr = htonl(INADDR_ANY) 64 | } 65 | }; 66 | 67 | /* bind the server socket to the wanted address */ 68 | if (bind(server_fd, (struct sockaddr *) &sin, sizeof(sin)) < 0) { 69 | perror("bind"); 70 | exit(1); 71 | } 72 | 73 | /* and open it for connections! */ 74 | if (listen(server_fd, 10) < 0) { 75 | perror("listen"); 76 | exit(1); 77 | } 78 | 79 | printf("listening on port %d\n", port); 80 | 81 | /* create the epoll context */ 82 | int epoll = epoll_create1(0); 83 | if (epoll < 0) { 84 | perror("epoll_create1"); 85 | exit(1); 86 | } 87 | 88 | /* create storage for our active connections. in a real server, this would be 89 | * some mapping from file descriptor -> connection object. here the only 90 | * thing we're interested is if the descriptor is connected at all, so a bool 91 | * (int) is enough: if conns[fd] is true, then fd is connected right now */ 92 | int conns[NUM_CONNS]; 93 | memset(&conns, 0, sizeof(conns)); 94 | 95 | /* make room for incoming events */ 96 | struct epoll_event events[NUM_EVENTS]; 97 | 98 | /* add the server socket; when it becomes "readable", someone connected! note 99 | * that we use the first element in our events list to set this up just 100 | * because its convenient; the "event" passed to epoll_ctl() is entirely 101 | * unrelated to the events returned by epoll_wait() */ 102 | events[0].events = EPOLLIN; 103 | events[0].data.fd = server_fd; 104 | if (epoll_ctl(epoll, EPOLL_CTL_ADD, server_fd, &events[0])) { 105 | perror("epoll_ctl"); 106 | exit(1); 107 | } 108 | 109 | /* main loop. ask epoll_wait() to tell us if anything interesting happened, or block */ 110 | int nevents; 111 | while ((nevents = epoll_wait(epoll, events, NUM_EVENTS, -1)) >= 0) { 112 | /* in theory, nothing could have happened. that should be impossible the 113 | * way we've set this up but its not an error so might as well quietly 114 | * handle it */ 115 | if (!nevents) 116 | continue; 117 | 118 | for (int n = 0; n < nevents; n++) { 119 | int fd = events[n].data.fd; 120 | 121 | if (fd == server_fd) { 122 | /* create storage for their address */ 123 | struct sockaddr_in sin; 124 | socklen_t sinlen = sizeof(sin); 125 | 126 | /* let them in! */ 127 | int new_fd = accept(server_fd, (struct sockaddr *) &sin, &sinlen); 128 | if (new_fd < 0) { 129 | perror("accept"); 130 | } 131 | else { 132 | /* hello */ 133 | printf("[%d] connect from %s:%d\n", new_fd, inet_ntoa(sin.sin_addr), ntohs(sin.sin_port)); 134 | 135 | /* make them non-blocking. this is necessary, because a disconnect will 136 | * cause a descriptor to become readable, but reading will block 137 | * forever (because they're disconnected. non-blocking will cause 138 | * read() to return 0 on a disconnected descriptor, so we can take the 139 | * right action */ 140 | int onoff = 1; 141 | if (ioctl(new_fd, FIONBIO, &onoff) < 0) { 142 | printf("fcntl(%d): %s\n", new_fd, strerror(errno)); 143 | close(new_fd); 144 | continue; 145 | } 146 | 147 | /* register the connection with epoll so we can be told when 148 | * something interesting happens to it. again, its safe to reuse the 149 | * first element of the events list; even if we're currently 150 | * processing the first event, we're already done with it */ 151 | events[0].events = EPOLLIN; 152 | events[0].data.fd = new_fd; 153 | if (epoll_ctl(epoll, EPOLL_CTL_ADD, new_fd, &events[0]) < 0) { 154 | printf("epoll_ctl(%d): %s\n", new_fd, strerror(errno)); 155 | close(new_fd); 156 | continue; 157 | } 158 | 159 | /* remember our new connection. in a real server, you'd create a 160 | * connection or user object of some sort, maybe send them a greeting, 161 | * begin authentication, etc */ 162 | conns[new_fd] = 1; 163 | } 164 | } 165 | 166 | else { 167 | /* yes! */ 168 | printf("[%d] activity\n", fd); 169 | 170 | /* create a buffer to read into */ 171 | char buf[1024]; 172 | int nread = read(fd, buf, sizeof(buf)); 173 | 174 | /* see how much we read */ 175 | if (nread < 0) { 176 | /* less then zero is some error. disconnect them */ 177 | fprintf(stderr, "read(%d): %s\n", fd, strerror(errno)); 178 | epoll_ctl(epoll, EPOLL_CTL_DEL, fd, NULL); 179 | close(fd); 180 | conns[fd] = 0; 181 | } 182 | 183 | else if (nread > 0) { 184 | /* we got some stuff from them! */ 185 | printf("[%d] read: %.*s\n", fd, nread, buf); 186 | 187 | /* loop over all our connections, and send stuff onto them! */ 188 | for (int dest_fd = 0; dest_fd < NUM_CONNS; dest_fd++) { 189 | 190 | /* take active connections, but not ourselves */ 191 | if (conns[dest_fd] && dest_fd != fd) { 192 | 193 | /* write to them */ 194 | if (write(dest_fd, buf, nread) < 0) { 195 | /* disconnect if it fails; they might have legitimately gone away without telling us */ 196 | fprintf(stderr, "write(%d): %s\n", dest_fd, strerror(errno)); 197 | epoll_ctl(epoll, EPOLL_CTL_DEL, dest_fd, NULL); 198 | close(dest_fd); 199 | conns[dest_fd] = 0; 200 | } 201 | } 202 | } 203 | } 204 | 205 | /* zero byes read */ 206 | else { 207 | /* so they gracefully disconnected and we should forget them */ 208 | printf("[%d] closed\n", fd); 209 | /* must deregister before close, for obscure reasons around epoll's 210 | * implementation (see notes above) */ 211 | epoll_ctl(epoll, EPOLL_CTL_DEL, fd, NULL); 212 | close(fd); 213 | conns[fd] = 0; 214 | } 215 | } 216 | } 217 | } 218 | 219 | /* epoll_wait failed. in a real server you might actually need to handle 220 | * non-error cases like EINTR, but it complicates this example so we won't 221 | * bother */ 222 | perror("epoll_wait"); 223 | exit(1); 224 | } 225 | -------------------------------------------------------------------------------- /yc_uring.c: -------------------------------------------------------------------------------- 1 | /* yc_uring - a yoctochat server using Linux io_uring */ 2 | 3 | /* The most important thing to understand about io_uring is its actually a 4 | * facility for asynchronous system calls; that is, it decouples the call from 5 | * the return. A bit like a future or a promise, if you squint. 6 | * 7 | * This is different from earlier event readiness facilities like select(), 8 | * poll() and epoll. Those tell you that something has happened on a 9 | * descriptor, and then you can make a blocking, synchronous call. 10 | * 11 | * io_uring, on the other hand, gives you set of alternatives for many 12 | * IO-related syscalls. You give them the same kind of args, and submit them to 13 | * the kernel via the submission ring. When and if they finish, the results are 14 | * left on the completion ring. If they don't finish (eg accept() but there's 15 | * no new connection, readv() but there's nothing waiting to be read) they just 16 | * sit quietly in the kernel and that's it. 17 | * 18 | * So the whole model is around requests and results, not descriptors and 19 | * readiness. Its interesting! 20 | * 21 | * It does make the code more complicated though, because you need to track 22 | * state so you know what you were doing as you process each completed 23 | * response. 24 | * 25 | * I've spelled a lot the request creation and submission out in full here to 26 | * avoid the need for jumping around the code while you're trying to follow it! 27 | * 28 | * Recommended reading: 29 | * https://unixism.net/loti/ 30 | */ 31 | 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | #include 40 | #include 41 | 42 | /* max number of connections. in a real program you probably wouldn't do this, 43 | * and instead use a more dynamic structure for tracking connections */ 44 | #define NUM_CONNS (128) 45 | 46 | /* max number of requests in flight. there will be an accept request, one read 47 | * request per active conn, and potentionally a write per active conn too. so 48 | * twice NUM_CONNS should be enough. */ 49 | #define QUEUE_DEPTH (256) 50 | 51 | 52 | /* our request objects. we need to make space for request and result data to be 53 | * stored, as well as things we need to match responses to requests */ 54 | 55 | /* the kinds of requests will we issue. we store this so we will recognise the 56 | * responses as they come in */ 57 | typedef enum { 58 | YCR_KIND_ACCEPT, 59 | YCR_KIND_READ, 60 | YCR_KIND_WRITE, 61 | YCR_KIND_CLOSE, 62 | } ycr_kind_t; 63 | 64 | /* minimal request; just the kind and the file descriptor it relates to. used 65 | * as a header for more complex requests */ 66 | typedef struct { 67 | ycr_kind_t ycr_event; 68 | int ycr_fd; 69 | } yc_request_t; 70 | 71 | /* read/write request. only readv/writev equivalents are available, so we put 72 | * an iovec in here, and enough buffer space to handle whatever we might read 73 | * or write. you definitely wouldn't do it this way in a real server */ 74 | typedef struct { 75 | yc_request_t ycr_req; 76 | struct iovec ycr_iovec; 77 | char ycr_iobuf[1024]; 78 | } yc_io_request_t; 79 | 80 | /* an accept request. carries space for the client IP and port */ 81 | typedef struct { 82 | yc_request_t ycr_req; 83 | struct sockaddr_in ycr_addr; 84 | socklen_t ycr_addrlen; 85 | } yc_accept_request_t; 86 | 87 | 88 | /* allocate a minimal request */ 89 | static yc_request_t *yc_req_new(ycr_kind_t event, int fd) { 90 | yc_request_t *req = malloc(sizeof(yc_request_t)); 91 | req->ycr_event = event; 92 | req->ycr_fd = fd; 93 | return req; 94 | } 95 | 96 | /* allocate a read/write request for the given fd */ 97 | static yc_io_request_t *yc_io_req_new(ycr_kind_t event, int fd) { 98 | yc_io_request_t *req = malloc(sizeof(yc_io_request_t)); 99 | req->ycr_req.ycr_event = event; 100 | req->ycr_req.ycr_fd = fd; 101 | req->ycr_iovec.iov_base = req->ycr_iobuf; 102 | req->ycr_iovec.iov_len = sizeof(req->ycr_iobuf); 103 | return req; 104 | } 105 | 106 | /* allocate an accept request for the given server fd */ 107 | static yc_accept_request_t *yc_accept_req_new(int fd) { 108 | yc_accept_request_t *req = malloc(sizeof(yc_accept_request_t)); 109 | req->ycr_req.ycr_event = YCR_KIND_ACCEPT; 110 | req->ycr_req.ycr_fd = fd; 111 | req->ycr_addrlen = sizeof(req->ycr_addr); 112 | return req; 113 | } 114 | 115 | /* free a request; here just for symmetry */ 116 | static void yc_req_free(yc_request_t *req) { 117 | free(req); 118 | } 119 | 120 | 121 | int main(int argc, char **argv) { 122 | if (argc < 2) { 123 | printf("usage: %s \n", argv[0]); 124 | exit(1); 125 | } 126 | 127 | int port = atoi(argv[1]); 128 | if (port <= 0) { 129 | printf("'%s' not a valid port number\n", argv[1]); 130 | exit(1); 131 | } 132 | 133 | /* create the server socket */ 134 | int server_fd = socket(AF_INET, SOCK_STREAM, 0); 135 | if (server_fd < 0) { 136 | perror("socket"); 137 | exit(1); 138 | } 139 | 140 | /* arrange for the listening address to be reusable. This makes TCP 141 | * marginally "less safe" (for a whole bunch of obscure reasons) but allows 142 | * us to kill and restart the program with ease */ 143 | int onoff = 1; 144 | if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &onoff, sizeof(onoff)) < 0) { 145 | perror("setsockopt"); 146 | exit(1); 147 | } 148 | 149 | /* set up the address structure for binding, which is *: */ 150 | struct sockaddr_in sin = { 151 | .sin_family = AF_INET, 152 | .sin_port = htons(port), 153 | .sin_addr = { 154 | .s_addr = htonl(INADDR_ANY) 155 | } 156 | }; 157 | socklen_t sin_len = sizeof(sin); 158 | 159 | /* bind the server socket to the wanted address */ 160 | if (bind(server_fd, (struct sockaddr *) &sin, sin_len) < 0) { 161 | perror("bind"); 162 | exit(1); 163 | } 164 | 165 | /* and open it for connections! */ 166 | if (listen(server_fd, 10) < 0) { 167 | perror("listen"); 168 | exit(1); 169 | } 170 | 171 | printf("listening on port %d\n", port); 172 | 173 | struct io_uring ring; 174 | if (io_uring_queue_init(QUEUE_DEPTH, &ring, 0) < 0) { 175 | perror("io_uring_queue_init"); 176 | exit(1); 177 | } 178 | 179 | /* create storage for our active connections. in a real server, this would be 180 | * some mapping from file descriptor -> connection object. here the only 181 | * thing we're interested is if the descriptor is connected at all, so a bool 182 | * (int) is enough: if conns[fd] is true, then fd is connected right now */ 183 | int conns[NUM_CONNS]; 184 | memset(&conns, 0, sizeof(conns)); 185 | 186 | /* start with async form of accept(). just like the traditional version, it 187 | * will "block" until there's something to read, but that all happens inside 188 | * the kernel so we don't have to worry about it. 189 | * 190 | * we acquire a free submission queue entry (SQE) from the kernel, set it up 191 | * for the an async accept(), include our own request state so we can 192 | * understand the completion queue entry (CQE) that comes back, and submit it 193 | * for processing */ 194 | struct io_uring_sqe *sqe = io_uring_get_sqe(&ring); 195 | yc_accept_request_t *req = yc_accept_req_new(server_fd); 196 | io_uring_prep_accept(sqe, server_fd, (struct sockaddr *) &req->ycr_addr, &req->ycr_addrlen, 0); 197 | io_uring_sqe_set_data(sqe, req); 198 | io_uring_submit(&ring); 199 | 200 | /* main loop. we just wait until a CQE is available, then process it */ 201 | struct io_uring_cqe *cqe; 202 | while (io_uring_wait_cqe(&ring, &cqe) >= 0) { 203 | 204 | /* get our own request back. for the moment, just the header */ 205 | yc_request_t *req = (yc_request_t *) cqe->user_data; 206 | int fd = req->ycr_fd; 207 | 208 | /* the return value of the underlying syscall. typically a negative value 209 | * will be the negated errno value for the call, so we can still do error 210 | * handling */ 211 | int res = cqe->res; 212 | 213 | /* do the right thing depending on what kind of request just completed */ 214 | switch (req->ycr_event) { 215 | 216 | /* someone connected! */ 217 | case YCR_KIND_ACCEPT: { 218 | /* get a handle on the more specialised request */ 219 | yc_accept_request_t *areq = (yc_accept_request_t *) req; 220 | 221 | /* maybe it failed? */ 222 | if (res < 0) { 223 | /* note negation of return value in place of errno */ 224 | fprintf(stderr, "accept: %s\n", strerror(-res)); 225 | } 226 | 227 | else { 228 | /* hello! client address is in the req object, because that's what we 229 | * pointed the request to in io_uring_prep_accept */ 230 | printf("[%d] connect from %s:%d\n", res, inet_ntoa(areq->ycr_addr.sin_addr), ntohs(areq->ycr_addr.sin_port)); 231 | 232 | /* remember our new connection. in a real server, you'd create a 233 | * connection or user object of some sort, maybe send them a 234 | * greeting, begin authentication, etc */ 235 | conns[res] = 1; 236 | 237 | /* set up an async read for the new connection */ 238 | sqe = io_uring_get_sqe(&ring); 239 | yc_io_request_t *rreq = yc_io_req_new(YCR_KIND_READ, res); 240 | io_uring_prep_readv(sqe, res, &rreq->ycr_iovec, 1, 0); 241 | io_uring_sqe_set_data(sqe, rreq); 242 | io_uring_submit(&ring); 243 | } 244 | 245 | /* make a new async accept, since the previous one was consumed. note 246 | * that we're reusing the request object, but its not special - freeing 247 | * it and making a new one would also be just fine */ 248 | sqe = io_uring_get_sqe(&ring); 249 | io_uring_prep_accept(sqe, fd, (struct sockaddr *) &areq->ycr_addr, &areq->ycr_addrlen, 0); 250 | io_uring_sqe_set_data(sqe, areq); 251 | io_uring_submit(&ring); 252 | 253 | break; 254 | } 255 | 256 | /* someone sent something */ 257 | case YCR_KIND_READ: { 258 | /* get a handle on the more specialised request */ 259 | yc_io_request_t *rreq = (yc_io_request_t *) req; 260 | 261 | /* some error, disconnect them */ 262 | if (res < 0) { 263 | fprintf(stderr, "readv(%d): %s\n", fd, strerror(-res)); 264 | 265 | /* free the read request, since we're not going to be reissuing it */ 266 | yc_req_free(req); 267 | 268 | /* make a async close request. we use a minimal request object 269 | * because close has no interesting args or return; we just need a 270 | * marker so we can recognise the response for what it is */ 271 | yc_request_t *clreq = yc_req_new(YCR_KIND_CLOSE, fd); 272 | sqe = io_uring_get_sqe(&ring); 273 | io_uring_prep_close(sqe, fd); 274 | io_uring_sqe_set_data(sqe, clreq); 275 | io_uring_submit(&ring); 276 | 277 | /* mark them "disconnected", so we don't try to send to them while the close request is pending */ 278 | conns[fd] = 0; 279 | } 280 | 281 | /* zero read, they gracefully closed the connection */ 282 | else if (res == 0) { 283 | printf("[%d] closed\n", fd); 284 | 285 | /* see error block above, this is the same behaviour */ 286 | 287 | yc_req_free(req); 288 | 289 | yc_request_t *clreq = yc_req_new(YCR_KIND_CLOSE, fd); 290 | sqe = io_uring_get_sqe(&ring); 291 | io_uring_prep_close(sqe, fd); 292 | io_uring_sqe_set_data(sqe, clreq); 293 | io_uring_submit(&ring); 294 | 295 | conns[fd] = 0; 296 | } 297 | 298 | else { 299 | /* they sent some data, which is now in the request iobuf (via the 300 | * iovec we sent in) */ 301 | printf("[%d] read: %.*s\n", fd, res, rreq->ycr_iobuf); 302 | 303 | /* loop over all our connections, and send stuff onto them! */ 304 | for (int dest_fd = 0; dest_fd < NUM_CONNS; dest_fd++) { 305 | 306 | /* take active connections, but not ourselves */ 307 | if (conns[dest_fd] && dest_fd != fd) { 308 | 309 | /* async write request. create new write requests, one for each 310 | * connection. we copy the data into it but if we were being much 311 | * cleverer about memory management we could just point the the 312 | * buffers in the read request, resulting a zero-copy forwarder! */ 313 | yc_io_request_t *wreq = yc_io_req_new(YCR_KIND_WRITE, dest_fd); 314 | memcpy(wreq->ycr_iobuf, rreq->ycr_iobuf, res); 315 | wreq->ycr_iovec.iov_len = res; 316 | 317 | sqe = io_uring_get_sqe(&ring); 318 | io_uring_prep_writev(sqe, dest_fd, &wreq->ycr_iovec, 1, 0); 319 | io_uring_sqe_set_data(sqe, wreq); 320 | io_uring_submit(&ring); 321 | } 322 | } 323 | 324 | /* make a new async read, since the previous one was consumed. note 325 | * that we're reusing the request object, but its not special - freeing 326 | * it and making a new one would also be just fine */ 327 | sqe = io_uring_get_sqe(&ring); 328 | io_uring_prep_readv(sqe, fd, &rreq->ycr_iovec, 1, 0); 329 | io_uring_sqe_set_data(sqe, rreq); 330 | io_uring_submit(&ring); 331 | } 332 | 333 | break; 334 | } 335 | 336 | /* they finished receiving what we sent */ 337 | case YCR_KIND_WRITE: { 338 | 339 | /* failed write, so disconnect them */ 340 | if (res < 0) { 341 | fprintf(stderr, "writev(%d): %s\n", fd, strerror(-res)); 342 | yc_req_free(req); 343 | 344 | /* see read error handling */ 345 | 346 | yc_request_t *clreq = yc_req_new(YCR_KIND_CLOSE, fd); 347 | sqe = io_uring_get_sqe(&ring); 348 | io_uring_prep_close(sqe, fd); 349 | io_uring_sqe_set_data(sqe, clreq); 350 | io_uring_submit(&ring); 351 | 352 | conns[fd] = 0; 353 | } 354 | 355 | else { 356 | /* written successfully, so just free the read req */ 357 | yc_req_free(req); 358 | } 359 | 360 | break; 361 | } 362 | 363 | /* async close completed */ 364 | case YCR_KIND_CLOSE: { 365 | /* just free the request, we've already cleaned up and there's nothing 366 | * useful we could do if the close failed anyway */ 367 | yc_req_free(req); 368 | break; 369 | } 370 | } 371 | 372 | /* mark the CQE "seen", returning it to the ring for reuse */ 373 | io_uring_cqe_seen(&ring, cqe); 374 | } 375 | 376 | /* io_uring_wait_cqe failed. in a real server you might actually need to 377 | * handle non-error cases like EINTR, but it complicates this example so we 378 | * won't bother */ 379 | perror("io_uring_wait_cqe"); 380 | exit(1); 381 | } 382 | --------------------------------------------------------------------------------