dyad源码分析

背景介绍

dyad是一个异步网络库,目标是可移植、轻量级、易于使用。地址位于https://github.com/rxi/dyad。

echoserver如何写

我们来看看一个简单的echo Server来看看其API的设计

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "dyad.h"
/* An echo server: Echos any data received by a client back to the client */
static void onData(dyad_Event *e) {
  dyad_write(e->stream, e->data, e->size);
}
static void onAccept(dyad_Event *e) { 
    puts("Accept a connection ");
    dyad_addListener(e->remote, DYAD_EVENT_DATA, onData, NULL);

}
static void onError(dyad_Event *e) {
  printf("server error: %s\n", e->msg);
}

static void onListen(dyad_Event* e){ 
    fprintf(stdout, "listen success %d", e->type); 
}
int main(void) {
  dyad_Stream *s;
  dyad_init();
  s = dyad_newStream();
  dyad_addListener(s, DYAD_EVENT_LISTEN, onListen, NULL);
  /* 添加错误的回调 */
  dyad_addListener(s, DYAD_EVENT_ERROR,  onError,  NULL);
   /* 添加连接成功后的回调 */
  dyad_addListener(s, DYAD_EVENT_ACCEPT, onAccept, NULL);
  dyad_listen(s, 8000);
  fprintf(stdout, "stream num %d", dyad_getStreamCount());
  while (dyad_getStreamCount() > 0) {
    dyad_update();
  }
  return 0;
}

我们从这个例子中,看到了几个简单的API:

  • dyad_init();
  • dyad_newStream();
  • dyad_addListener
  • dyad_update

dyad_init分析

这个函数非常简单,就是要改变SIGPIPE的默认行为(终止进程),忽略它。

void dyad_init(void) {
#ifdef _WIN32
  WSADATA dat;
  int err = WSAStartup(MAKEWORD(2, 2), &dat);
  if (err != 0) {
    panic("WSAStartup failed (%d)", err);
  }
#else
  /* Stops the SIGPIPE signal being raised when writing to a closed socket */
  signal(SIGPIPE, SIG_IGN);
#endif
}

SIGPIPE信号的产生

当服务器close一个连接时,若client端接着发数据。根据TCP协议的规定,会收到一个RST响应,client再往这个服务器发送数据时,系统会发出一个SIGPIPE信号给进程,告诉进程这个连接已经断开了,不要再写了。

又或者当一个进程向某个已经收到RST的socket执行写操作是,内核向该进程发送一个SIGPIPE信号。该信号的缺省学位是终止进程,因此进程必须捕获它以免不情愿的被终止。

dyad_Stream

dyad_stream 用来表示一个套接字链接,最重要的几个字段分别是listener,linebuffer,和writeBuffer,分别对应为回调函数,读缓冲区,写缓冲区。我们应该注意的是所有的tcp连接都是通过list来连在一起。

struct dyad_Stream {
  int state, flags;
  dyad_Socket sockfd;
  char *address;
  int port;
  int bytesSent, bytesReceived;
  double lastActivity, timeout;
   /* 该连接的回调 */
  Vec(Listener) listeners;
  Vec(char) lineBuffer;
  Vec(char) writeBuffer;
   /* 指向下一个连接 */
  dyad_Stream *next;
};

Vec宏定义

dyad用宏来模拟了C++模板的功能,我们来看看,这里的Vec主要的作用就是模拟C++中的vector也就是动态数组。

#define Vec(T)\
  struct { T *data; int length, capacity; }

#define vec_unpack(v)\
  (char**)&(v)->data, &(v)->length, &(v)->capacity, sizeof(*(v)->data)

#define vec_init(v)\
  memset((v), 0, sizeof(*(v)))

#define vec_deinit(v)\
  dyad_free((v)->data)

#define vec_clear(v)\
  ((v)->length = 0)

#define vec_push(v, val)\
  ( vec_expand(vec_unpack(v)),\
    (v)->data[(v)->length++] = (val) )

#define vec_splice(v, start, count)\
  ( vec_splice(vec_unpack(v), start, count),\
    (v)->length -= (count) )

dyad_newStream函数

我们再来看看dyad_newStream函数,这个函数很简单,就是要分批额一个dyad_Stream内存,并初始化

dyad_Stream *dyad_newStream(void) {
  dyad_Stream *stream = dyad_realloc(NULL, sizeof(*stream));
  memset(stream, 0, sizeof(*stream));
  stream->state = DYAD_STATE_CLOSED;
  // 
  stream->sockfd = INVALID_SOCKET;
  // 上次活动的时间为当前时间
  stream->lastActivity = dyad_getTime();
  /* Add to list and increment count */
  stream->next = dyad_streams;
  dyad_streams = stream;
  // 流数目增加
  dyad_streamCount++;
  return stream;
}

注意到dyad_streams这个变量,其是一个全局的变量,表示当前和服务器请求的TCP连接队列的头部,当我们新分配一个tcp连接的资源的时候,头部发生变化,指向了当前新分配的连接资源(Stream)。

static dyad_Stream *dyad_streams;
static int dyad_streamCount;

dyad_addListener函数

这个函数就比较简单了,就是给当前的套接字,添加相应事件的回调函数,在上面的例子中,我们看到了添加了listen,accept和error三个回调函数。我们来看看事件类型的定义:


enum {
DYAD_EVENT_NULL,
DYAD_EVENT_DESTROY,
DYAD_EVENT_ACCEPT,
DYAD_EVENT_LISTEN,
DYAD_EVENT_CONNECT,
DYAD_EVENT_CLOSE,
DYAD_EVENT_READY,
DYAD_EVENT_DATA,
DYAD_EVENT_LINE,
DYAD_EVENT_ERROR,
DYAD_EVENT_TIMEOUT,
DYAD_EVENT_TICK
};

再看看添加相应的回调函数

typedef void (*dyad_Callback)(dyad_Event*);

/* 每个事件回调的定义 */
typedef struct {
   // 事件的类型
  int event;
  // 事件的回调
  dyad_Callback callback;
  // 参数类型
  void *udata;
} Listener;



void dyad_addListener(
  dyad_Stream *stream, int event, dyad_Callback callback, void *udata
) {
  Listener listener;
  listener.event = event;
  listener.callback = callback;
  listener.udata = udata;
  vec_push(&stream->listeners, listener);
}

dyad_listen

这个函数看命字就知道干什么,就是创建监听套接字,注意到这个套接字是non-block的,注意到。过程就是典型的sock-->bind->listen的过程。

int dyad_listenEx(
  dyad_Stream *stream, const char *host, int port, int backlog
) {
  struct addrinfo hints, *ai = NULL;
  int err, optval;
  char buf[64];
  dyad_Event e;

  /* Get addrinfo */
  memset(&hints, 0, sizeof(hints));
  hints.ai_family = AF_UNSPEC;
  hints.ai_socktype = SOCK_STREAM;
  hints.ai_flags = AI_PASSIVE;
  sprintf(buf, "%d", port);

  // 获取ip地址
  err = getaddrinfo(host,  buf,  &hints, &ai);
  if (err) {
    stream_error(stream, "could not get addrinfo", errno);
    goto fail;
  }
  /* Init socket */
 // 为连接创建套接字描述符
  err = stream_initSocket(stream, ai->ai_family, ai->ai_socktype,
                          ai->ai_protocol);
  if (err) goto fail;

  /* Set SO_REUSEADDR so that the socket can be immediately bound without
   * having to wait for any closed socket on the same port to timeout */
  optval = 1;
 // 能够使用端口地址,
  setsockopt(stream->sockfd, SOL_SOCKET, SO_REUSEADDR,
             &optval, sizeof(optval));
  /* Bind and listen */
  err = bind(stream->sockfd, ai->ai_addr, ai->ai_addrlen);
  if (err) {
    stream_error(stream, "could not bind socket", errno);
    goto fail;
  }
  err = listen(stream->sockfd, backlog);
  if (err) {
    stream_error(stream, "socket failed on listen", errno);
    goto fail;
  }

  stream->state = DYAD_STATE_LISTENING;
  stream->port = port;
  // 第二次绑定地址信息
  stream_initAddress(stream);
  /* Emit listening event */
  e = createEvent(DYAD_EVENT_LISTEN);
  e.msg = "socket is listening";
  // 调用回调
  stream_emitEvent(stream, &e);
  freeaddrinfo(ai);
  return 0;
  fail:
  if (ai) freeaddrinfo(ai);
  return -1;
}
// 监听套接字
int dyad_listen(dyad_Stream *stream, int port) {
  return dyad_listenEx(stream, NULL, port, 511);
}

getaddrinfo系统调用

我们在上面看到了getaddrinfo,这个系统调用的man page 如下:

int getaddrinfo(const char *node, const char *service,
                       const struct addrinfo *hints,
                       struct addrinfo **res);

Given node and service, which identify an Internet host and a service, getaddrinfo() returns one or more addrinfo structures, each of which contains an Internet address that can be specified in a call to bind(2) or connect(2). The getaddrinfo() function combines the functionality provided by the gethostbyname(3) and getservbyname(3) functions into a single interface, but unlike the latter functions, getaddrinfo() is reentrant and allows programs to eliminate IPv4-versus-IPv6 dependencies.

实际上,上面的形参node表示的是主机名称,service表示的是端口号。这个函数目的是获取其internet address,为后面的bind系统调用所使用。注意到在经历socket-->bind--->listen之后,套接字被设置成non-blocking,然后是“发射了”一个事件,这就是listen事件。我们来看看这个发射事件的函数。

static void stream_emitEvent(dyad_Stream *stream, dyad_Event *e) {
  int i;
  e->stream = stream;
  // 遍历整个该socket的回调
  for (i = 0; i < stream->listeners.length; i++) {
    Listener *listener = &stream->listeners.data[i];
    // 如果回调的事件类型为e
    if (listener->event == e->type) {
      e->udata = listener->udata;
      // 调用回调函数
      listener->callback(e);
    }
    /* Check to see if this listener was removed: If it was we decrement `i`
     * since the next listener will now be in this ones place */
    if (listener != &stream->listeners.data[i]) {
      i--;
    }
  }
}

从上面的代码中可以看到,就是遍历该连接所有的回调函数,如果该连接的事件类型满足则应该调用。

dyad_update函数

dyad_update这个函数是非常重要的,这就是这个简单的异步库的核心,相当于Reactor中的Event Loop,所以现在来好好好分析这个函数。

void dyad_update(void) {
  dyad_Stream *stream;
  struct timeval tv;
  destroyClosedStreams();
  updateTickTimer();
  // 处理超时回调
  updateStreamTimeouts();
  /* Create fd sets for select() */
  select_zero(&dyad_selectSet);

  stream = dyad_streams;
  while (stream) {
    switch (stream->state) {
      case DYAD_STATE_CONNECTED:
        select_add(&dyad_selectSet, SELECT_READ, stream->sockfd);
        if (!(stream->flags & DYAD_FLAG_READY) ||
            stream->writeBuffer.length != 0
        ) {
          select_add(&dyad_selectSet, SELECT_WRITE, stream->sockfd);
        }
        break;
      case DYAD_STATE_CLOSING:
        select_add(&dyad_selectSet, SELECT_WRITE, stream->sockfd);
        break;
      case DYAD_STATE_CONNECTING:
        select_add(&dyad_selectSet, SELECT_WRITE, stream->sockfd);
        select_add(&dyad_selectSet, SELECT_EXCEPT, stream->sockfd);
        break;
      case DYAD_STATE_LISTENING:
        select_add(&dyad_selectSet, SELECT_READ, stream->sockfd);
        break;
    }
    stream = stream->next;
  }

  /* Init timeout value and do select */
  #ifdef _MSC_VER
    #pragma warning(push)
    /* Disable double to long implicit conversion warning,
     * because the type of timeval's fields don't agree across platforms */
    #pragma warning(disable: 4244)
  #endif
  tv.tv_sec = dyad_updateTimeout;
  tv.tv_usec = (dyad_updateTimeout - tv.tv_sec) * 1e6;
  #ifdef _MSC_VER
    #pragma warning(pop)
  #endif

  select(dyad_selectSet.maxfd + 1,
         dyad_selectSet.fds[SELECT_READ],
         dyad_selectSet.fds[SELECT_WRITE],
         dyad_selectSet.fds[SELECT_EXCEPT],
         &tv);

  /* Handle streams */
  stream = dyad_streams;
  while (stream) {
    switch (stream->state) {

      case DYAD_STATE_CONNECTED:
        if (select_has(&dyad_selectSet, SELECT_READ, stream->sockfd)) {
          stream_handleReceivedData(stream);
          if (stream->state == DYAD_STATE_CLOSED) {
            break;
          }
        }
        /* Fall through */

      case DYAD_STATE_CLOSING:
        if (select_has(&dyad_selectSet, SELECT_WRITE, stream->sockfd)) {
          stream_flushWriteBuffer(stream);
        }
        break;

      case DYAD_STATE_CONNECTING:
        if (select_has(&dyad_selectSet, SELECT_WRITE, stream->sockfd)) {
          /* Check socket for error */
          int optval = 0;
          socklen_t optlen = sizeof(optval);
          dyad_Event e;
          getsockopt(stream->sockfd, SOL_SOCKET, SO_ERROR, &optval, &optlen);
          if (optval != 0) goto connectFailed;
          /* Handle succeselful connection */
          stream->state = DYAD_STATE_CONNECTED;
          stream->lastActivity = dyad_getTime();
          stream_initAddress(stream);
          /* Emit connect event */
          e = createEvent(DYAD_EVENT_CONNECT);
          e.msg = "connected to server";
          stream_emitEvent(stream, &e);
        } else if (
          select_has(&dyad_selectSet, SELECT_EXCEPT, stream->sockfd)
        ) {
          /* Handle failed connection */
connectFailed:
          stream_error(stream, "could not connect to server", 0);
        }
        break;

      case DYAD_STATE_LISTENING:
        if (select_has(&dyad_selectSet, SELECT_READ, stream->sockfd)) {
          stream_acceptPendingConnections(stream);
        }
        break;
    }

    /* If data was just now written to the stream we should immediately try to
     * send it */
    if (
      stream->flags & DYAD_FLAG_WRITTEN &&
      stream->state != DYAD_STATE_CLOSED
    ) {
      stream_flushWriteBuffer(stream);
    }

    stream = stream->next;
  }
}

我们来看一下,这个事件循环的思路,首先destroyClosedStreams遍历整个stream,将死连接释放掉,当然会调用起注册的回调函数,如果有的话。然后更新循环中的一个滴答数。接着初始化Set中文件描述集合为全部为0,要注意使用select函数时,每次都要重新设置关注的文件描述符事件。

我们分几种情况,来跟踪以下源码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "dyad.h"
/* An echo server: Echos any data received by a client back to the client */
static void onData(dyad_Event *e) {
  dyad_write(e->stream, e->data, e->size);
}
static void onAccept(dyad_Event *e) { 
    puts("Accept a connection ");
    dyad_addListener(e->remote, DYAD_EVENT_DATA, onData, NULL);
    dyad_writef(e->remote, "echo server\r\n");
}
static void onError(dyad_Event *e) {
  printf("server error: %s\n", e->msg);
}

static void onListen(dyad_Event* e){ 
    fprintf(stdout, "listen success %d", e->type); 
}
int main(void) {
  dyad_Stream *s;
  dyad_init();
  s = dyad_newStream();
  dyad_addListener(s, DYAD_EVENT_LISTEN, onListen, NULL);
  /* 添加错误的回调 */
  dyad_addListener(s, DYAD_EVENT_ERROR,  onError,  NULL);
   /* 添加连接成功后的回调 */
  dyad_addListener(s, DYAD_EVENT_ACCEPT, onAccept, NULL);
  dyad_listen(s, 8000);
  fprintf(stdout, "stream num %d", dyad_getStreamCount());
  while (dyad_getStreamCount() > 0) {
    dyad_update();
  }
  return 0;
}

(1) 没有任何客户端尝试进行连接
我们在24行, s = dyad_newStream();,这行打下断点,发现

注意到这里的创建的listen fd的值为3,因为0, 1, 2都被标准I/O占用,这也说明每个进程能打开的文件描述符都是独立的,不会因为别的进程占用了3,你的进程就所能够打开的文件描述符必须从4开始。这里我们应该注意state为4, 对应的状态为DYAD_EVENT_LISTEN,在dyad_listen那句打上断点,我们看看s的状态:

这个时候,address和port都已经初始化好了。断点现在设置到dyad_getStreamCount调用,我们发现这里返回的只有1,只有一个listen socket 值为3,s的next指向NULL,现在进入到dyad_update(),因为s的状态是DYAD_EVENT_LISTEN同样对应的DYAD_STATE_LISTENING的值,此时将调用:

select_add(&dyad_selectSet, SELECT_READ, stream->sockfd);
break;

意思很明显,将3的读事件加入到关注列表中。因为此时整个服务器后台处理的连接只有一个,s的next为NULL,所以马上跳出while循环。接着就是设置超时的时间为1s:

tv.tv_sec = dyad_updateTimeout;
tv.tv_usec = (dyad_updateTimeout - tv.tv_sec) * 1e6;

然后调用select进行多路I/O复用,由前面可知,select最多1秒后,返回。

select(dyad_selectSet.maxfd + 1,
         dyad_selectSet.fds[SELECT_READ],
         dyad_selectSet.fds[SELECT_WRITE],
         dyad_selectSet.fds[SELECT_EXCEPT],
         &tv);

从select返回后,然后就对整个stream链表进行遍历,因为只有一个listen fd 所以遍历一次,就结束了,遍历的时候,会根据当前fd的状态来进行不同的操作,因为listen fd 此时的状态为DYAD_STATE_LISTENING,那么就会进入:


if (select_has(&dyad_selectSet, SELECT_READ, stream->sockfd)) {
    stream_acceptPendingConnections(stream);
}

显然判断,是否是当前的套接字3,是否有读事件发生,如果有读事件,那么if里面为true。在这里因为没有客户端端接入,所以并没有进入到这if里面。然后就是判断是否该套接字是可写的,如果是的,那么就应该将数据发送出去。

 if (
      stream->flags & DYAD_FLAG_WRITTEN &&
      stream->state != DYAD_STATE_CLOSED
    ) {
      stream_flushWriteBuffer(stream);
    }

没有任何客户端连接就这样结束了。
(2)有客户端接入
首先,还是stream的listen sock fd为3,状态还是DYAD_STATE_LISTENING,所以,首先添加的select set集合中,将其读事件注册,然后调用select等待超时,或者是listen fd读事件发生,因为这里有客户端接入,那么就应该返回listen fd 可读。然后进入到下面的代码:

 case DYAD_STATE_LISTENING:
        if (select_has(&dyad_selectSet, SELECT_READ, stream->sockfd)) {
          stream_acceptPendingConnections(stream);
        }
        break;

因为这里的stream sockfd 可读,那么将会调用stream_acceptPendingConnections(stream)。下面是这个函数的关键字代码:

// 创建accept 套接字
dyad_Socket sockfd = accept(stream->sockfd, NULL, NULL);
// Create client stream 
remote = dyad_newStream();
remote->state = DYAD_STATE_CONNECTED;
/* Set stream's socket */
stream_setSocket(remote, sockfd);
 e = createEvent(DYAD_EVENT_ACCEPT);
 e.msg = "accepted connection";
 e.remote = remote;
 stream_emitEvent(stream, &e);

这个里面最重要的是创建remote Stream,创建后其会加入到全局的stream链表,设置remote的 sockfd为accept套接字。然后调用accept事件的回调。此时,sockef为4,注意:我们的echoSever中有onAccept回调函数,这个函数将创建remote stream读事件回调。

static void onAccept(dyad_Event *e) { 
    puts("Accept a connection ");
    // 添加对remote stream中套接字的回调
    dyad_addListener(e->remote, DYAD_EVENT_DATA, onData, NULL);
}

stream_acceptPendingConnections调用完成后,这时也设置当前socket为4的回调也就是onData,同样也是调用select函数,那么这个时候,同样遍历整个stream 流,包含socket值为3和4,根据stream的状态来选择相应的状态,现在4套接字排在了3,而且,4的状态为DYAD_STATE_CONNECTED,所以进入到:

case DYAD_STATE_CONNECTED:
        if (select_has(&dyad_selectSet, SELECT_READ, stream->sockfd)) {
          stream_handleReceivedData(stream);
          if (stream->state == DYAD_STATE_CLOSED) {
            break;
          }
        }

首先判断当前套接字4是否可读,如果可读,那么进入到stream_handleReceivedData(stream),这里我们假设有数据读入,那么select_has将会返回true,我们看看stream_handleReceivedData读入:

  char data[8192];
  int size = recv(stream->sockfd, data, sizeof(data) - 1, 0);
    if (size <= 0) {
      // 这样size为0、可能
      if (size == 0 || errno != EWOULDBLOCK) {
        /* Handle disconnect */
        dyad_close(stream);
        return;
      } else {
        /* No more data */
        return;
      }
    }
    data[size] = 0;

关键recv是用来读数据,应该注意的是最多读取8192-1个数据,这次获取数据后,将会产生事件。产生事件后,调用回调函数。

data[size] = 0;
/* Update status */
stream->bytesReceived += size;
stream->lastActivity = dyad_getTime();
/* Emit data event */
e = createEvent(DYAD_EVENT_DATA);
e.msg = "received data";
e.data = data;
e.size = size;
stream_emitEvent(stream, &e);

destroyClosedStreams()

在dyad_update中,首先调用的是destroyClosedStreams这个函数的作用是,在遍历整个系统接收到的请求队列中,释放掉已经关闭的请求连接。应该注意到的是,所有的socket连接是拉了一个链表串起来的,所以,就是遍历整个链表,将处于close状态的连接删掉

static void destroyClosedStreams(void) {
  dyad_Stream *stream = dyad_streams;
  while (stream) {
    if (stream->state == DYAD_STATE_CLOSED) {
      dyad_Stream *next = stream->next;
      stream_destroy(stream);
      stream = next;
    } else {
      stream = stream->next;
    }
  }
}

注意到这里的删掉,除了是释放掉相应的dyad_Stream的内存,还有就是要对回调函数进行调用,这个过程在stream_destroy中实现。

stream_destroy

static void stream_destroy(dyad_Stream *stream) {
  dyad_Event e;
  dyad_Stream **next;
  /* Close socket */
  if (stream->sockfd != INVALID_SOCKET) {
    close(stream->sockfd);
  }
  /* Emit destroy event */
  e = createEvent(DYAD_EVENT_DESTROY);
  e.msg = "the stream has been destroyed";
  stream_emitEvent(stream, &e);
  /* Remove from list and decrement count */
  next = &dyad_streams;
  while (*next != stream) {
    next = &(*next)->next;
  }
  *next = stream->next;
  dyad_streamCount--;
  /* Destroy and free */
  vec_deinit(&stream->listeners);
  vec_deinit(&stream->lineBuffer);
  vec_deinit(&stream->writeBuffer);
  dyad_free(stream->address);
  dyad_free(stream);
}

在stream_destroy中,我们可以看到,首先要关闭的是套接字描述符,然后是调用相应的回调函数,最后从连接链表中删除连接,最后是释放相应的内存。

updateTickTimer();

这个函数主要的作用就是更新上次遍历整个循环的时间,然后对整个已经建立连接进行遍历,如果你关注了从上次loop时间到现在loop时间的每一秒事件(),那么就会调用该事件的回调函数。

static void updateTickTimer(void) {
  /* Update tick timer */
  if (dyad_lastTick == 0) {
    dyad_lastTick = dyad_getTime();
  }
  // 如果上次遍历的时间小于当前时间
  // 那么关注了DYAD_EVENT_TICK事件
  // 对其中的每一秒都调用相应的回调函数
  while (dyad_lastTick < dyad_getTime()) {
    /* Emit event on all streams */
    dyad_Stream *stream;
    dyad_Event e = createEvent(DYAD_EVENT_TICK);
    e.msg = "a tick has occured";
    stream = dyad_streams;
    while (stream) {
        // 调用回调
      stream_emitEvent(stream, &e);
      stream = stream->next;
    }
    dyad_lastTick += dyad_tickInterval;
  }
}

select_zero

这个函数就是要初始化select 使用的fd_set为0。

static void select_zero(SelectSet *s) {
  int i;
  if (s->capacity == 0) return;
  s->maxfd = 0;
  for (i = 0; i < SELECT_MAX; i++) {
#if _WIN32
    s->fds[i]->fd_count = 0;
#else
    memset(s->fds[i], 0, s->capacity * sizeof(fd_set));
#endif
  }
}

dyad_getStreamCount

这个函数是返回当前有多少个套接字流要处理。

posted @ 2017-08-13 13:20  BruceChen7  阅读(1104)  评论(0编辑  收藏  举报