[ccan] Networking: Asynchronous Server+Client socket code

Rusty Russell rusty at rustcorp.com.au
Mon Nov 5 15:06:46 EST 2012


Ahmed Samy <f.fallen45 at gmail.com> writes:

> Hi,
>
> This is a lightweight networking code that I'd like to add in The ccan tree.
> Reading the code should be straight forward, it uses epoll etc, and has
> support for callbacks, it can be used a server or a client with 3 simple
> calls:
>
>     struct socket_t *sock = socket_create();
>     if (!socket_listen(sock, NULL, 1337))
>         fatal("failed to listen on port 1337.");
>     /*
>      * callbacks should be setup before polling on the socket
>      */
>     socket_poll(sock);
>
>
> For client side, replace socket_listen with socket_connect.
> The code can be viewed at: at:
> https://github.com/otfallen/csnippets/blob/master/src/socket.c (to view the
> header change '.c' to '.h')
>
> Please leave any kind of feedback.
>
> Thanks
>       asamy

Hi Asamy,

        Sorry I didn't get to this earlier...  Here's some detailed
feedback, I hope it helps.  You've chosen a difficult first module :)

> #define STREAM_SERVER 0x1
> 
> #define MAX_EVENTS 1024

It's unclear how these are used...

> #define sock_cast(self) (struct socket_t *)(self)

If you didn't use void *self, you'd be able to avoid this, and get
the compiler to typecheck.

> struct socket_t {

_t here is weird: it's usually for typedefs, and I believe also
reserved.  Just 'struct socket' perhaps?

>     int fd;
>     char ip[16];
>     time_t idle;
>     int type;

Perhaps an enum for type?

>     struct {
>         void (*on_read) (void *self, const char *read, int len);
>         void (*on_accept) (void *self, void *new_socket);
>         void (*on_write) (void *self, const char *written, int len);
>         void (*on_disconnect) (void *self);
>         void (*on_connect) (void *self);
>     };

Anonymous structs are not that portable, but that's OK.  I assume these
are the user-settable callbacks...

>     struct {
>         int (*write) (void *self, const char *data, int len);
>         int (*close) (void *self);
>         int (*shutdown) (void *self);
>     };

These are always set to the same thing, right?  Why are they pointers?

> 
>     struct list_node node;
>     struct list_head children;
> };

Ah, I see you create a tree off your listening socket.  Perhaps you want
to differentiate the two types more clearly, by having a struct
listening_socket and a struct connected_socket, both starting with a
common struct socket?

> extern struct socket_t *__attribute__((warn_unused_result))
> socket_create(void);

I dislike warn_unused_result; it's a mistake anyone will actually make.

> extern void socket_close(struct socket_t *socket);

This might be better called socket_free().

> extern bool socket_connect(struct socket_t *socket, const char *addr,
>         int32_t port);

You might want to used ccan/net here (you may need to adapt it).

> extern bool socket_listen(struct socket_t *socket, const char *address,
>         int32_t port);

Why not combine these functions with socket_create(), and have them make
and (start to?) connect the socket?

> extern void socket_poll(struct socket_t *socket);

This only works because you internally keep track of the children.  That
may be a bit limiting in future (you might want to listen on two sockets
at once).  But sufficient for simple programs.

> #include "socket.h"
> #include "platform.h"

I prefer to test for individual features, but we could fix that up later.

> #ifdef _WIN32
> #include <winsock2.h>
> #include <ws2tcpip.h>    /* socklen_t */
> #else

It would be nice for configurator to turn this into HAVE_WINDOWS_API,
which is the style used by most of config.h.

> #include <sys/socket.h>
> #include <arpa/inet.h>
> #include <unistd.h>
> #include <netdb.h>
> #include <errno.h>
> #endif
> #include <fcntl.h>
> #include <stdarg.h>
> #include <sys/time.h>
> #include <time.h>
> #include <stdlib.h>
> #include <stdio.h>
> #include <string.h>
> 
> extern void __socket_set_init(const struct socket_t *socket) __attribute__((weak));

You don't want weak here; you're prefer it not to link if there's no
implementation, rather than crash.

> extern void __socket_set_deinit(void) __attribute__((weak));
> extern void __socket_set_add(const struct socket_t *socket) __attribute__((weak));
> extern void __socket_set_del(const struct socket_t *socket) __attribute__((weak));
> extern int  __socket_set_poll(const struct socket_t *socket) __attribute__((weak));
> extern int  __socket_set_get_active_fd(int i) __attribute__((weak));
> 
> #ifdef _WIN32
> static bool is_initialized = false;
> #endif

> static int __write(void *self, const char *data, int len)
> {
>     int32_t ret;
>     struct socket_t *socket = sock_cast(self);
>     if (!socket)
>         return -1;

Seems to me that you should just crash if they hand you NULL.

>     socket->write = __write;
>     socket->close = __close;
>     socket->idle  = time(NULL);

So, this is really "last_active" not idle?

> bool socket_connect(struct socket_t *socket, const char *addr, int32_t port)
> {
>     time_t start;
>     struct hostent *hp;
>     bool connected = false;
>     struct sockaddr_in srv;
> 
>     if (socket->fd < -1)
>         return false;
>     if (!(hp = gethostbyname(addr)))
>         return false;
> #ifdef WIN32
>     memcpy((char*)&srv.sin_addr, (char*)hp->h_addr, hp->h_length);
> #else
>     bcopy((char*)hp->h_addr, (char*)&srv.sin_addr, hp->h_length);
> #endif
> 
>     srv.sin_family = AF_INET;
>     srv.sin_port = htons(port);

IPv6 will hate you.  Recommend ccan/net/.

>     start = time(NULL);
>     while (!connected && time(NULL) - start < 10) {

10 seconds?  You're nonblocking, so why spin at all?  You'll only be
spinning around for 10 seconds if you're failing to connect the whole
time.

> bool socket_listen(struct socket_t *socket, const char *address, int32_t port)
> {
>     struct sockaddr_in srv;
>     int reuse_addr = 1;
>     if (!socket)
>         return false;
>     if (socket->fd < 0)
>         return false;
> 
>     srv.sin_family = AF_INET;
>     srv.sin_addr.s_addr = !address ? INADDR_ANY : inet_addr(address);
>     srv.sin_port = htons(port);
> 
>     setsockopt(socket->fd, SOL_SOCKET, SO_REUSEADDR, &reuse_addr, sizeof(reuse_addr));
>     if (bind(socket->fd, (struct sockaddr *)&srv, sizeof(srv)) == -1)
>         return false;
>     if (listen(socket->fd, MAX_EVENTS) == -1)
>         return false; 
>     socket->type = STREAM_SERVER;
>     return true;
> }

We should probably add a net_server_lookup() for this case.

> void socket_poll(struct socket_t *socket)
> {
>     int n_fds, i;
>     __socket_set_init(socket);
>     __socket_set_add(socket);
>     for (;;) {
>         n_fds = __socket_set_poll(socket);
>         for (i = 0; i < n_fds; i++) {
>             struct socket_t *sock = NULL;
>             int active_fd = __socket_set_get_active_fd(i);
>             if (active_fd < 0)
>                 continue;

OK, so this idea is that this is the main loop of your program, as it
never returns?

It doesn't handle failure (eg. failure to create epoll fd) gracefully.

>             if (socket->fd == active_fd) {
>                 if (socket->type == STREAM_SERVER) {
>                     struct socket_t *new_socket;
>                     struct sockaddr_in their_addr;
>                     socklen_t len = sizeof(their_addr);
>                     int their_fd;
> 
>                     their_fd = accept(socket->fd, (struct sockaddr *)&their_addr, &len);
>                     if (their_fd == -1) {
>                         if (ERRNO != E_AGAIN && ERRNO != E_BLOCK)
>                             perror("accept()");
>                         break;
>                     }
> 
>                     new_socket = malloc(sizeof(*new_socket));
>                     if (!new_socket)
>                         break;

Leaks their_fd; if you really want to handle allocation failures, your
life will be more difficult.

>                     setup_socket(new_socket);
>                     setup_async(new_socket);
> 
>                     new_socket->fd = their_fd;
>                     strncpy(new_socket->ip, inet_ntoa(their_addr.sin_addr), 16);
> 
>                     if (socket->on_accept) {
>                         socket->on_accept(socket, new_socket);
>                         if (new_socket->on_connect)
>                             new_socket->on_connect(new_socket);
>                     }
>                     list_add(&socket->children, &new_socket->node);
>                     __socket_set_add(new_socket);
>                     continue;
>                 }
>                 sock=socket;

Why do you set sock here?

>             if (socket->type == STREAM_SERVER && !list_empty(&socket->children)) {
>                 list_for_each(&socket->children, sock, node) {
>                     if (sock->fd == active_fd)
>                         break;
>                 }
>             }

Why the if()?  Surely list_for_each() will do nothing of there are no
children.

>             bool done = false;
>             for (;;) {
>                 ssize_t count;
>                 char buffer[4096];
> 
>                 count = read(active_fd, buffer, sizeof buffer);
>                 if (count == -1) {
>                     if (ERRNO != E_AGAIN && errno != E_BLOCK)
>                         done = true;
>                     break;
>                 } else if (count == 0) { /* EOF */
>                     done = true;
>                     break;
>                 }
> 
>                 if (sock && sock->on_read)
>                     sock->on_read(sock, buffer, count);
>             }
>             if (done && sock) {
>                 list_del(&sock->node);
>                 sock->close(sock);
>             }
>         }
>     }
> 
>     __socket_set_deinit();
>     socket->close(socket);

AFAICT these lines can never be executed.

> }
> 
> void socket_close(struct socket_t *socket)
> {
>     if (!socket)
>        return;
> 
>     socket->close(socket);
>     free(socket);
> }

You should probably use this internally yourself, rather than calling
socket->close() & free manually.

Hope that helps,
Rusty.
PS.  And of course, documentation and tests would be nice :)


More information about the ccan mailing list