文章目录
- 前言
- 一、结构体及其功能
- 二、函数
- MG_LOG
- mg_http_listen
- mg_mgr_poll
- question
- 参考链接
前言
mongoose是一款基于C/C++的网络库,可以实现TCP, UDP, HTTP, WebSocket, MQTT通讯。mongoose是的嵌入式网络程序更快、健壮,易于实现。
mongoose只有mongoose.c和mongoose.h两个文件,其它的例子基本是基于这两个文件加上对应的xxx.c文件。mongoose.ws/documentation/介绍了mongoose.h文件中的API。
它通过单向链表维护所有连接的client对象,其数据结构不是线程安全的。在httserver中,该程序是单线成的通过epoll模式处理连接,因此比较适合嵌入式等硬件资源有限的条件,在不修改源码的情况下并发性能受限。
一、结构体及其功能
struct mg_mgr
struct mg_mgr为时间管理结构体,保存一系列的链接
conns指向单向链表的头部,每有新的链接都LIST_ADD_HEAD
struct mg_mgr {
struct mg_connection *conns; // 每个连接都是一个struct mg_connection,指向连接组成的单向链表的头部
struct mg_dns dns4; // DNS for IPv4
struct mg_dns dns6; // DNS for IPv6
int dnstimeout; // DNS resolve timeout in milliseconds
bool use_dns6; // Use DNS6 server by default, see #1532
unsigned long nextid; // Next connection ID,该数字逐渐增大,不会减小 ?
unsigned long timerid; // Next timer ID
void *userdata; // Arbitrary user data pointer
void *tls_ctx; // TLS context shared by all TLS sessions
uint16_t mqtt_id; // MQTT IDs for pub/sub
void *active_dns_requests; // DNS requests in progress
struct mg_timer *timers; // Active timers
int epoll_fd; // Used when MG_EPOLL_ENABLE=1
void *priv; // Used by the MIP stack
size_t extraconnsize; // Used by the MIP stack
MG_SOCKET_TYPE pipe; // Socketpair end for mg_wakeup()
#if MG_ENABLE_FREERTOS_TCP
SocketSet_t ss; // NOTE(lsm): referenced from socket struct
#endif
};
struct mg_connection
每次调用accept函数获得一个有效的fd时都新建一个该对象并将其加入单向链表中,保存了连接的client的相关信息
struct mg_connection {
struct mg_connection *next; // 指向下一个client
struct mg_mgr *mgr; // Our container
struct mg_addr loc; // host地址信息
struct mg_addr rem; // client地址信息
void *fd; // Connected socket, or LWIP data
unsigned long id; // Auto-incrementing unique connection ID,给client的唯一id,但不一定连续不知道有什么含义
struct mg_iobuf recv; // Incoming data
struct mg_iobuf send; // Outgoing data
struct mg_iobuf prof; // Profile data enabled by MG_ENABLE_PROFILE
struct mg_iobuf rtls; // TLS only. Incoming encrypted data
mg_event_handler_t fn; // User-specified event handler function,在main中,cb
void *fn_data; // User-specified function parameter
mg_event_handler_t pfn; // Protocol-specific handler function,处理协议的函数,如http_cb
void *pfn_data; // Protocol-specific function parameter
char data[MG_DATA_SIZE]; // Arbitrary connection data
void *tls; // TLS specific data
// 位域,下面这么多总共占4个字节
unsigned is_listening : 1; // Listening connection, 是否为监听fd
unsigned is_client : 1; // Outbound (client) connection mongoose作为client程序时会用到
unsigned is_accepted : 1; // Accepted (server) connection 接受来自client的链接时设为1(在accept_conn处)
// 在http server中并 监听描述符以及client描述符都是非阻塞的
unsigned is_resolving : 1; // Non-blocking DNS resolution is in progress
unsigned is_arplooking : 1; // Non-blocking ARP resolution is in progress
unsigned is_connecting : 1; // Non-blocking connect is in progress
unsigned is_tls : 1; // TLS-enabled connection
unsigned is_tls_hs : 1; // TLS handshake is in progress
unsigned is_udp : 1; // UDP connection
unsigned is_websocket : 1; // WebSocket connection
unsigned is_mqtt5 : 1; // For MQTT connection, v5 indicator
unsigned is_hexdumping : 1; // Hexdump in/out traffic
unsigned is_draining : 1; // Send remaining data, then close and free
unsigned is_closing : 1; // Close and free the connection immediately
unsigned is_full : 1; // Stop reads, until cleared
unsigned is_resp : 1; // Response is still being generated,生在生成c->send
unsigned is_readable : 1; // Connection is ready to read
unsigned is_writable : 1; // Connection is ready to write
};
struct mg_http_message
存储解析后的http信息
struct mg_str {
const char *ptr; // Pointer to string data
size_t len; // String len
};
struct mg_http_header {
struct mg_str name; // Header name
struct mg_str value; // Header value
};
struct mg_http_message {
struct mg_str method, uri, query, proto; // Request/response line
struct mg_http_header headers[MG_MAX_HTTP_HEADERS]; // Headers MG_MAX_HTTP_HEADERS=30
struct mg_str body; // Body
struct mg_str head; // Request + headers
struct mg_str message; // Request + headers + body
};
二、函数
MG_LOG
默认log级别为MG_LL_INFO=2
#define MG_ERROR(args) MG_LOG(MG_LL_ERROR, args)
#define MG_INFO(args) MG_LOG(MG_LL_INFO, args)
#define MG_DEBUG(args) MG_LOG(MG_LL_DEBUG, args)
#define MG_VERBOSE(args) MG_LOG(MG_LL_VERBOSE, args)
#define MG_LOG(level, args) \
do { \
if ((level) <= mg_log_level) { \
mg_log_prefix((level), __FILE__, __LINE__, __func__); \
mg_log args; \
} \
} while (0)
// log的前缀
void mg_log_prefix(int level, const char *file, int line, const char *fname) {
const char *p = strrchr(file, '/');
char buf[41];
size_t n;
if (p == NULL) p = strrchr(file, '\\');
n = mg_snprintf(buf, sizeof(buf), "%-6llx %d %s:%d:%s", mg_millis(), level,
p == NULL ? file : p + 1, line, fname);
if (n > sizeof(buf) - 2) n = sizeof(buf) - 2;
while (n < sizeof(buf)) buf[n++] = ' ';
logs(buf, n - 1);
}
// 打印内容
void mg_log(const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
mg_vxprintf(s_log_func, s_log_func_param, fmt, &ap);
va_end(ap);
logs("\r\n", 2);
}
mg_http_listen
struct mg_connection *mg_http_listen(struct mg_mgr *mgr, const char *url,
mg_event_handler_t fn, void *fn_data)
该函数精简后类似于:
if ( (fd = socket(af, type, proto)) == -1 ) {
MG_ERROR(("socket: %d", MG_SOCK_ERR(-1)));
} else if ((rc = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (char *) &on, sizeof(on))) != 0) {
MG_ERROR(("setsockopt(SO_REUSEADDR): %d", MG_SOCK_ERR(rc)));
} else if ((rc = bind(fd, &usa.sa, slen)) != 0) {
MG_ERROR(("bind: %d", MG_SOCK_ERR(rc)));
} else if ( (rc = listen(fd, 128)) != 0 ) {
MG_ERROR(("listen: %d", MG_SOCK_ERR(rc)));
} else {
// 这里考虑到了是否有ipv6所以掉了个函数处理
setlocaddr(fd, &c->loc); // 将host地址写入监听描述符对应的struct mg_connection中
mg_set_non_blocking_mode(fd); // 设置描述符的O_NONBLOCK以及FD_CLOEXEC
c->fd = S2PTR(fd);
MG_EPOLL_ADD(c); // 加入epoll
success = true;
}
设置监听描述符对应的结构体并将其加入mgr的conns链表
MG_EPOLL_X宏
水平触发模式
#define MG_EPOLL_ADD(c) \
do { \
struct epoll_event ev = {EPOLLIN | EPOLLERR | EPOLLHUP, {c}}; \
epoll_ctl(c->mgr->epoll_fd, EPOLL_CTL_ADD, (int) (size_t) c->fd, &ev); \
} while (0)
#define MG_EPOLL_MOD(c, wr) \
do { \
struct epoll_event ev = {EPOLLIN | EPOLLERR | EPOLLHUP, {c}}; \
if (wr) ev.events |= EPOLLOUT; \
epoll_ctl(c->mgr->epoll_fd, EPOLL_CTL_MOD, (int) (size_t) c->fd, &ev); \
} while (0)
mg_mgr_poll
man函数最终会进入while (s_signo == 0) mg_mgr_poll(&mgr, 1000); 死循环中
在1000并发量时测试点:
- max有多大
- n = epoll_wait有多大
- 链表的大小是多少,其中有效的有多少个,无效的有多少个
- 最大文件描述符的值是多少
void mg_mgr_poll(struct mg_mgr *mgr, int ms) {
struct mg_connection *c, *tmp;
// 获取epoll通知(epoll_wait),设置对应mg_connection的标志位
mg_iotest(mgr, ms);
// 遍历单向链表
for (c = mgr->conns; c != NULL; c = tmp) {
tmp = c->next;
if (c->is_resolving || c->is_closing) {
// Do nothing
} else if (c->is_listening && c->is_udp == 0) {
if (c->is_readable) accept_conn(mgr, c);
} else if (c->is_connecting) { // http server中这里几乎一直处于0
if (c->is_readable || c->is_writable) connect_conn(c);
} else {
if (c->is_readable) read_conn(c);
if (c->is_writable) write_conn(c);
}
if (c->is_draining && c->send.len == 0) c->is_closing = 1;
if (c->is_closing) close_conn(c);
}
}
static void mg_iotest(struct mg_mgr *mgr, int ms) {
size_t max = 1;
for (struct mg_connection *c = mgr->conns; c != NULL; c = c->next) {
c->is_readable = c->is_writable = 0;
if (c->rtls.len > 0) ms = 1, c->is_readable = 1;
if (can_write(c)) MG_EPOLL_MOD(c, 1); // 只要c->send.len > 0就触发EPOLLOUT
if (c->is_closing) ms = 1;
max++;
}
struct epoll_event *evs = (struct epoll_event *) alloca(max * sizeof(evs[0]));
int n = epoll_wait(mgr->epoll_fd, evs, (int) max, ms);
for (int i = 0; i < n; i++) {
struct mg_connection *c = (struct mg_connection *) evs[i].data.ptr;
if (evs[i].events & EPOLLERR) {
mg_error(c, "socket error"); // 当c->send.len的大小和http报文的Content-Length大小不对应时会导致EPOLLERR,原因未知
} else if (c->is_readable == 0) {
bool rd = evs[i].events & (EPOLLIN | EPOLLHUP);
bool wr = evs[i].events & EPOLLOUT;
c->is_readable = can_read(c) && rd ? 1U : 0;
c->is_writable = can_write(c) && wr ? 1U : 0;
if (c->rtls.len > 0) c->is_readable = 1;
}
}
}
在一个mg_mgr_poll循环中受限通过mg_iotest将对应事件的标志位进行设置is_readable,is_writable,再通过循环即标志位处理对应事件
accept_conn函数通过accept获取client fd然后对其结构体和fd进行设置
static void accept_conn(struct mg_mgr *mgr, struct mg_connection *lsn) {
struct mg_connection *c = NULL;
union usa usa; socklen_t sa_len = sizeof(usa);
int fd = accept(FD(lsn), &usa->sa, &sa_len);
if (fd < 0) {
MG_ERROR(("%lu accept failed, errno %d", lsn->id, MG_SOCK_ERR(-1)));
} else if ((c = mg_alloc_conn(mgr)) == NULL) {
MG_ERROR(("%lu OOM", lsn->id));
close(fd);
} else {
tomgaddr(&usa, &c->rem, sa_len != sizeof(usa.sin)); // 将remote地址写入c->rem
LIST_ADD_HEAD(struct mg_connection, &mgr->conns, c);
c->fd = S2PTR(fd);
MG_EPOLL_ADD(c);
mg_set_non_blocking_mode(FD(c)); // 设置描述符的O_NONBLOCK以及FD_CLOEXEC
setsockopts(c); // setsockopt
c->is_accepted = 1;
c->is_hexdumping = lsn->is_hexdumping;
c->loc = lsn->loc;
c->pfn = lsn->pfn;
c->pfn_data = lsn->pfn_data;
c->fn = lsn->fn;
c->fn_data = lsn->fn_data;
MG_DEBUG(("%lu %ld accepted %M -> %M", c->id, c->fd, mg_print_ip_port,
&c->rem, mg_print_ip_port, &c->loc));
// http server 下无事发生
mg_call(c, MG_EV_OPEN, NULL);
mg_call(c, MG_EV_ACCEPT, NULL);
}
}
read_conn函数非阻塞读到c->recv.buf中,然后交由iolog处理。iolog会判断n的返回值。if EINPROGRESS || EWOULDBLOCK则什么也不做;elif<=0则设is_closing = 1,随后会TODO:;elif n>0 则调用mg_call(c, MG_EV_READ, &n),它通过函数指针调用http_cb函数解析http协议。
static void http_cb(struct mg_connection *c, int ev=待处理事件(该函数只处理MG_EV_READ,MG_EV_CLOSE), void *ev_data=未使用)函数会解析c->recv.buf中的http协议(解析字符),之后f (c->is_accepted) c->is_resp = 1;调用mg_call(c, MG_EV_HTTP_MSG, &hm);将http解析结果交由cb函数进行处理该函数会写c->send.buf, return; 然后清除c->recv,http_cb return。
iolog(c, buf, n, true);返回后read_conn结束。
n = recv(FD(c), (char *) buf, len, MSG_NONBLOCKING);
static void read_conn(struct mg_connection *c) {
if (ioalloc(c, &c->recv)) {
char *buf = (char *) &c->recv.buf[c->recv.len];
size_t len = c->recv.size - c->recv.len;
long n = -1;
n = recv(FD(c), (char *) buf, len, MSG_NONBLOCKING); // 文件描述符是非阻塞的,非阻塞接收
// n 经过处理后可为 -1 -2 -3
iolog(c, buf, n, true);
}
}
write_conn函数通过send函数将c->send.buf发送,然后调用iolog
对n处理。然后清理c->send,然后if (c->send.len == 0)MG_EPOLL_MOD(c, 0);再掉mg_call(c, MG_EV_WRITE, &n);iolog return后write_conn也结束
static void write_conn(struct mg_connection *c) {
long n = send(FD(c), c->send.buf, c->send.len, MSG_NONBLOCKING);// 文件描述符是非阻塞的,非阻塞接收
// n 经过处理后可为 -1 -2 -3
iolog(c, buf, n, false);
}
question
- mg_http_reply函数,当继续调用mg_printf(c, fmt, …)而不修改Content-Length时 浏览器不能接收全部的数据?
- is_closing何时被设置为1的?如果client发一次,server发一次不会触发is_closing=1,莫非是等待conn_fd断开的时候触发EPOLLHUP然后间接关闭?若是如此如何触发的问题1?
参考链接
https://mongoose.ws/documentation/
https://github.com/cesanta/mongoose