一直想整理一下关于网络编程模型方面的文章,下面就是本博文了。
socket是每个看这篇博文的人都很熟悉的,一般在使用socket选取一些IO策略的时候,可以考虑select /poll/epoll之类的,本博文不对这个IO策略如何选择进行讨论,只是用宏选择如何对策略进行配置。在下现以epoll作为使用的IO策略[ 参照”epoll的使用”]
一,文件结构
该模型包含几个文件,说明和功能定义如下:
mio.c 和 mio.h :这两个文件负责对mio_st结构的定义,并声明创建新的mio_st结构的方法。
mio_impl.h :模型的主要实现文件 ,触发多个操作都是从该文件触发的。
mio_select.h和mio_select.c :用于实现SELECT IO策略文件,包含一些和SELECT策略相关的宏。
mio_poll.h和mio_poll.c:同上。用于实现POLL IO策略的文件 。
mio_epoll.h mio_epoll.c:同上。用于实现EPOLL IO策略的文件 。
二,抽象IO管理结构
mio_st 结构用于对IO的管理,包括建立监听和连接,发送或者读取数据以及其它的一些数据回调。
typedef struct mio_st
{
void (*mio_free)(struct mio_st **m);
struct mio_fd_st *(*mio_listen)(struct mio_st **m, int port, char *sourceip,
mio_handler_t app, void *arg);
struct mio_fd_st *(*mio_connect)(struct mio_st **m, int port, char *hostip,
mio_handler_t app, void *arg);
struct mio_fd_st *(*mio_register)(struct mio_st **m, int fd,
mio_handler_t app, void *arg);
void (*mio_app)(struct mio_st **m, struct mio_fd_st *fd,
mio_handler_t app, void *arg);
void (*mio_close)(struct mio_st **m, struct mio_fd_st *fd);
void (*mio_write)(struct mio_st **m, struct mio_fd_st *fd);
void (*mio_read)(struct mio_st **m, struct mio_fd_st *fd);
void (*mio_run)(struct mio_st **m, int timeout);
} **mio_t;
上面结构中的mio_fd_st用于封装连接文件的描述符,如下所示:
typedef struct mio_fd_st
{
int fd;
} *mio_fd_t;
另外封装主要的行为动作和处理回调函数指针,如下:
typedef enum { action_ACCEPT, action_READ, action_WRITE, action_CLOSE } mio_action_t;
typedef int (*mio_handler_t) (struct mio_st **m, mio_action_t a, struct mio_fd_st *fd, void* data, void *arg);
上面表示接受客户端连接,读取数据,写数据,关闭连接等操作的枚举类型,另一个是定义用于处理这些枚举类型所代表的动作发生时的处理函数。
三,初始化
初始化的时候,可以根据配置(宏信息)选择用不同的IO策略进行初始化模型。初始化函数如下:
mio_t mio_new(int maxfd)
{
mio_t m = NULL;
#ifdef MIO_EPOLL
m = mio_epoll_new(maxfd);
if (m != NULL) return m;
#endif
#ifdef MIO_WSASYNC
m = mio_wsasync_new(maxfd);
if (m != NULL) return m;
#endif
#ifdef MIO_SELECT
m = mio_select_new(maxfd);
if (m != NULL) return m;
#endif
#ifdef MIO_POLL
m = mio_poll_new(maxfd);
if (m != NULL) return m;
#endif
return m;
}
以mio_epoll_new为例,会调用如下方法:
static mio_t _mio_new(int maxfd)
{
static struct mio_st mio_impl = {
_mio_free,
_mio_listen, _mio_connect, _mio_setup_fd,
_mio_app,
_mio_close,
_mio_write, _mio_read,
_mio_run
};
mio_t m;
/* init winsock if we are in Windows */
#ifdef _WIN32
WSADATA wsaData;
if (WSAStartup(MAKEWORD( 1, 1 ), &wsaData))
return NULL;
#endif
/* allocate and zero out main memory */
if((m = malloc(sizeof(struct mio_priv_st))) == NULL) return NULL;
/* set up our internal vars */
*m = &mio_impl;
MIO(m)->maxfd = maxfd;
MIO_INIT_VARS(m);
return m;
}
初始化一个mio_st结构,并且定义了一些回调函数,这些函数全部
mio_impl.h文件中实现,并且根据所选定的IO策略,调用不同的宏去实现。MIO宏定义如下:
typedef struct mio_priv_st
{
struct mio_st *mio;
int maxfd;
MIO_VARS
} *mio_priv_t;
#define MIO(m) ((mio_priv_t) m) // 就是一个强制类型转换,两个结构,第一个元素都指向同一个副本。
#define FD(m,f) ((mio_priv_fd_t) f)
#define ACT(m,f,a,d) (*(FD(m,f)->app))(m,a,&FD(m,f)->mio_fd,d,FD(m,f)->arg)
MIO_VARS 会根据不同的IO策略有不同的展开,它是IO策略相关的。这个在决定用哪个IO策略的时候就决定了,对我们来说,选取的epoll,那么 MIO_VARS如下定义:
#define MIO_VARS \
int defer_free; \
int epoll_fd; \
struct epoll_event res_event[32];
#define MIO_INIT_VARS(m) \
do { \
MIO(m)->defer_free = 0; \
if ((MIO(m)->epoll_fd = epoll_create(maxfd)) < 0) \
{ \
mio_debug(ZONE,"unable to initialize epoll mio"); \
free(m); \
return NULL; \
} \
} while(0)
上面两个宏都是在处理epoll的文件中定义的,如果是选用其它的(比如SELECT),会有不同的宏定义和初始化。
这就完成了初始化。
四,连接处理
在mio_imph.h文件中实现,如下:
static mio_fd_t _mio_connect(mio_t m, int port, char *hostip, mio_handler_t app, void *arg)
{
int fd, flag, flags;
mio_fd_t mio_fd;
struct sockaddr_storage sa;
memset(&sa, 0, sizeof(sa));
if(m == NULL || port <= 0 || hostip == NULL) return NULL;
mio_debug(ZONE, "mio connecting to %s, port=%d",hostip,port);
/* convert the hostip */
if(j_inet_pton(hostip, &sa)<=0) {
MIO_SETERROR(EFAULT);
return NULL;
}
if(!sa.ss_family) sa.ss_family = AF_INET;
/* attempt to create a socket */
if((fd = socket(sa.ss_family,SOCK_STREAM,0)) < 0) return NULL;
/* set the socket to non-blocking before connecting */
#if defined(HAVE_FCNTL)
flags = fcntl(fd, F_GETFL);
flags |= O_NONBLOCK;
fcntl(fd, F_SETFL, flags);
#elif defined(HAVE_IOCTL)
flags = 1;
ioctl(fd, FIONBIO, &flags);
#endif
/* set up address info */
j_inet_setport(&sa, port);
/* try to connect */
flag = connect(fd,(struct sockaddr*)&sa,j_inet_addrlen(&sa));
mio_debug(ZONE, "connect returned %d and %s", flag, MIO_STRERROR(MIO_ERROR));
/* already connected? great! */
if(flag == 0)
{
mio_fd = _mio_setup_fd(m,fd,app,arg);
if(mio_fd != NULL) return mio_fd;
}
/* gotta wait till later */
#ifdef _WIN32
if(flag == -1 && WSAGetLastError() == WSAEWOULDBLOCK)
#else
if(flag == -1 && errno == EINPROGRESS)
#endif
{
mio_fd = _mio_setup_fd(m,fd,app,arg);
if(mio_fd != NULL)
{
mio_debug(ZONE, "connect processing non-blocking mode");
FD(m,mio_fd)->type = type_CONNECT;
MIO_SET_WRITE(m,FD(m,mio_fd));
return mio_fd;
}
}
/* bummer dude */
close(fd);
return NULL;
}
在调用 connect连接成功之后,得到的是一个连接描述符,需要将这个SOCKET描述符和我们的mio_st模型连接起来,怎么连接呢?_mio_setup_fd方法完成这一步:
static mio_fd_t _mio_setup_fd(mio_t m, int fd, mio_handler_t app, void *arg)
{
int flags;
mio_fd_t mio_fd;
mio_fd = MIO_ALLOC_FD(m, fd);
/* ok to process this one, welcome to the family */
FD(m,mio_fd)->type = type_NORMAL;
FD(m,mio_fd)->app = app;
FD(m,mio_fd)->arg = arg;
/* set the socket to non-blocking */
#if defined(HAVE_FCNTL)
flags = fcntl(fd, F_GETFL);
flags |= O_NONBLOCK;
fcntl(fd, F_SETFL, flags);
#elif defined(HAVE_IOCTL)
flags = 1;
ioctl(fd, FIONBIO, &flags);
#endif
return mio_fd;
}
这究竟做了哪些事情呢?MIO_ALLOC_FD是IO策略相关的,定义如下:
static mio_fd_t _mio_alloc_fd(mio_t m, int fd) \
{ \
struct epoll_event event; \
mio_priv_fd_t priv_fd = malloc(sizeof (struct mio_priv_fd_st)); \
memset(priv_fd, 0, sizeof (struct mio_priv_fd_st)); \
\
priv_fd->mio_fd.fd = fd; \
priv_fd->events = 0; \
\
event.events = priv_fd->events; \
event.data.u64 = 0; \
event.data.ptr = priv_fd; \
epoll_ctl(MIO(m)->epoll_fd, EPOLL_CTL_ADD, fd, &event); \
\
return (mio_fd_t)priv_fd; \
}
将fd和mio_st关联起来,通过mio_priv_fd_t结构,这个结构的定义如下:
typedef enum {
type_CLOSED = 0x00,
type_NORMAL = 0x01,
type_LISTEN = 0x02,
type_CONNECT = 0x10,
type_CONNECT_READ = 0x11,
type_CONNECT_WRITE = 0x12
} mio_type_t;
typedef struct mio_priv_fd_st
{
struct mio_fd_st mio_fd;
mio_type_t type;
mio_handler_t app;
void *arg;
MIO_FD_VARS
} *mio_priv_fd_t;
这里的MIO_FD_VARS宏是IO策略相关的,当用EPOLL的时候,其定义如下:
#define MIO_FD_VARS \
uint32_t events;
回顾一下connect的过程:
- 根据IP和端口调用系统调用connect,返回连接文件描述符fd。
- 调用_mio_alloc_fd函数将fd和mio_st实例关联起来,通过mio_priv_fd_t结构关联起来,并且将该fd添加到epoll实例中。从这以后,如果这个描述符(fd)有读写或者错误时,epoll会通知给上层应用。
这是一个很主要的,就是IO复用机制。下面分析一下这个,这个是通过在模块上层应用,通过指定mio_st结构和一个超时时间。
static void _mio_run(mio_t m, int timeout)
{
int retval;
int iter;
mio_debug(ZONE, "mio running for %d", timeout);
/* wait for a socket event */
retval = MIO_CHECK(m, timeout); // 这个宏是IO策略相关的,在IO策略是epoll时,这个宏最后展开是:epoll_wait(MIO(m)->epoll_fd,MIO(m)->res_event, 32, t*1000);看到这个,大家就明白了是怎么回事了。
/* nothing to do */
if(retval == 0) return;
/* an error */
if(retval < 0)
{
return;
}
/* loop through the sockets, check for stuff to do */
MIO_ITERATE_RESULTS(m, retval, iter)
{
mio_fd_t fd = MIO_ITERATOR_FD(m,iter);
if (fd == NULL) continue;
/* skip already dead slots */
if(FD(m,fd)->type == type_CLOSED) continue;
/* new conns on a listen socket */
if(FD(m,fd)->type == type_LISTEN && MIO_CAN_READ(m,iter))
{
_mio_accept(m, fd);
continue;
}
/* check for connecting sockets */
if(FD(m,fd)->type & type_CONNECT &&
(MIO_CAN_READ(m,iter) || MIO_CAN_WRITE(m,iter)))
{
_mio__connect(m, fd);
continue;
}
/* read from ready sockets */
if(FD(m,fd)->type == type_NORMAL && MIO_CAN_READ(m,iter))
{
/* if they don't want to read any more right now */
if(ACT(m, fd, action_READ, NULL) == 0)
MIO_UNSET_READ(m, FD(m,fd));
}
/* write to ready sockets */
if(FD(m,fd)->type == type_NORMAL && MIO_CAN_WRITE(m,iter))
{
/* don't wait for writeability if nothing to write anymore */
if(ACT(m, fd, action_WRITE, NULL) == 0)
MIO_UNSET_WRITE(m, FD(m,fd));
}
/* deferred closing fd
* one of previous actions might change the state of fd */
if(FD(m,fd)->type == type_CLOSED)
{
MIO_FREE_FD(m, fd);
}
}
}
FD宏用于将fd,转换成mio_priv_fd_t 结构,几个主要是的宏都是IO策略相关的,定义如下:
#define MIO_ITERATE_RESULTS(m, retval, iter) \
for(MIO(m)->defer_free = 1, iter = 0; (iter < retval) || ((MIO(m)->defer_free = 0)); iter++)
#define MIO_ITERATOR_FD(m, iter) \
(MIO(m)->res_event[iter].data.ptr)
#define MIO_CAN_READ(m,iter) \
(MIO(m)->res_event[iter].events & (EPOLLIN|EPOLLERR|EPOLLHUP))
#define MIO_CAN_WRITE(m,iter) \
(MIO(m)->res_event[iter].events & EPOLLOUT)
#define MIO_CAN_FREE(m) (!MIO(m)->defer_free)
在IO复用机制了解之后,其它就容易明白了。比如在监听之后,如果文件描述符有数据可以读,则说明有外部连接请求接入,这时调用_mio_accept
static void _mio_accept(mio_t m, mio_fd_t fd)
{
struct sockaddr_storage serv_addr;
socklen_t addrlen = (socklen_t) sizeof(serv_addr);
int newfd;
mio_fd_t mio_fd;
char ip[INET6_ADDRSTRLEN];
/* pull a socket off the accept queue and check */
newfd = accept(fd->fd, (struct sockaddr*)&serv_addr, &addrlen);
if(newfd <= 0) return;
if(addrlen <= 0) {
close(newfd);
return;
}
j_inet_ntop(&serv_addr, ip, sizeof(ip));
/* set up the entry for this new socket */
mio_fd = _mio_setup_fd(m, newfd, FD(m,fd)->app, FD(m,fd)->arg);
/* tell the app about the new socket, if they reject it clean up */
if (ACT(m, mio_fd, action_ACCEPT, ip))
{
mio_debug(ZONE, "accept was rejected for %s:%d", ip, newfd);
MIO_REMOVE_FD(m, FD(m,mio_fd));
/* close the socket, and reset all memory */
close(newfd);
MIO_FREE_FD(m, mio_fd);
}
return;
}
这个也是比较容易理解的,在accept之后,调用_mio_setup_fd,这个函数也是将accept返回的SOCKET文件描述符和mio_st结构关联起来,其实就是和epoll结构关联起来,将新连接加入到epoll的监听队列。
static mio_fd_t _mio_setup_fd(mio_t m, int fd, mio_handler_t app, void *arg)
{
int flags;
mio_fd_t mio_fd;
mio_fd = MIO_ALLOC_FD(m, fd);
/* ok to process this one, welcome to the family */
FD(m,mio_fd)->type = type_NORMAL;
FD(m,mio_fd)->app = app;
FD(m,mio_fd)->arg = arg;
/* set the socket to non-blocking */
#if defined(HAVE_FCNTL)
flags = fcntl(fd, F_GETFL);
flags |= O_NONBLOCK;
fcntl(fd, F_SETFL, flags);
#elif defined(HAVE_IOCTL)
flags = 1;
ioctl(fd, FIONBIO, &flags);
#endif
return mio_fd;
}
在有数据读写之后,都是通过ACT(m, fd, action_WRITE, NULL)宏调用用户的自定义数据处理回调。