【Mongoose笔记】TCP 客户端与服务器
简介
Mongoose 笔记系列用于记录学习 Mongoose 的一些内容。
Mongoose 是一个 C/C++ 的网络库。它为 TCP、UDP、HTTP、WebSocket、MQTT 实现了事件驱动的、非阻塞的 API。
项目地址:
https://github.com/cesanta/mongoose
学习
下面通过学习 Mongoose 项目代码中的 tcp 示例程序 ,来学习如何使用 Mongoose 实现简单的 TCP 通讯。使用树莓派平台进行开发验证。
tcp 的示例程序内同时包含了 TCP 客户端与服务器的实现,同时创建一个客户端和一个服务器,客户端连接到服务器,发送一些文本信息到服务器,然后服务器将信息原原本本地回复回来,客户端断开连接。
代码如下:
// Copyright (c) 2022 Cesanta Software Limited
// All rights reserved#include "mongoose.h"static const char *s_lsn = "tcp://localhost:8765"; // Listening address
static const char *s_conn = "tcp://localhost:8765"; // Connect to address// client resources
static struct c_res_s {int i;struct mg_connection *c;
} c_res;// CLIENT event handler
static void cfn(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {int *i = &((struct c_res_s *) fn_data)->i;if (ev == MG_EV_OPEN) {MG_INFO(("CLIENT has been initialized"));} else if (ev == MG_EV_CONNECT) {MG_INFO(("CLIENT connected"));
#if (MG_ENABLE_MBEDTLS == 1) || (MG_ENABLE_OPENSSL == 1)struct mg_tls_opts opts = {.ca = "ss_ca.pem"};mg_tls_init(c, &opts);
#endif*i = 1; // do something} else if (ev == MG_EV_READ) {struct mg_iobuf *r = &c->recv;MG_INFO(("CLIENT got data: %.*s", r->len, r->buf));} else if (ev == MG_EV_CLOSE) {MG_INFO(("CLIENT disconnected"));// signal we are done((struct c_res_s *) fn_data)->c = NULL;} else if (ev == MG_EV_ERROR) {MG_INFO(("CLIENT error: %s", (char *) ev_data));} else if (ev == MG_EV_POLL && *i != 0) {switch ((*i)++) {case 50: // 50 x 100ms = 5smg_send(c, "Hi, there", 9);MG_INFO(("CLIENT sent data"));break;case 100: // another 5s// send any possible outstanding data and close the connectionc->is_draining = 1;break;}}
}// SERVER event handler
static void sfn(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {if (ev == MG_EV_OPEN && c->is_listening == 1) {MG_INFO(("SERVER is listening"));} else if (ev == MG_EV_ACCEPT) {MG_INFO(("SERVER accepted a connection"));
#if (MG_ENABLE_MBEDTLS == 1) || (MG_ENABLE_OPENSSL == 1)struct mg_tls_opts opts = {//.ca = "ss_ca.pem", // Uncomment to enable two-way SSL.cert = "ss_server.pem", // Certificate PEM file.certkey = "ss_server.pem", // This pem contains both cert and key};mg_tls_init(c, &opts);
#endif} else if (ev == MG_EV_READ) {struct mg_iobuf *r = &c->recv;MG_INFO(("SERVER got data: %.*s", r->len, r->buf));mg_send(c, r->buf, r->len); // echo it back} else if (ev == MG_EV_CLOSE) {MG_INFO(("SERVER disconnected"));} else if (ev == MG_EV_ERROR) {MG_INFO(("SERVER error: %s", (char *) ev_data));}(void) fn_data;
}// Timer function - recreate client connection if it is closed
static void timer_fn(void *arg) {struct mg_mgr *mgr = (struct mg_mgr *) arg;if (c_res.c == NULL) {// connectc_res.i = 0;c_res.c = mg_connect(mgr, s_conn, cfn, &c_res);if (c_res.c == NULL)MG_INFO(("CLIENT cant' open a connection"));elseMG_INFO(("CLIENT is connecting"));}
}int main(void) {struct mg_mgr mgr; // Event managerstruct mg_connection *c;mg_log_set(MG_LL_INFO); // Set log levelmg_mgr_init(&mgr); // Initialize event managermg_timer_add(&mgr, 15000, MG_TIMER_REPEAT | MG_TIMER_RUN_NOW, timer_fn,&mgr); // Init timer for demo purposes, 15sc = mg_listen(&mgr, s_lsn, sfn, NULL); // Create server connectionif (c == NULL) {MG_INFO(("SERVER cant' open a connection"));return 0;}while (true)mg_mgr_poll(&mgr, 100); // Infinite event loop, blocks for upto 100ms// unless there is network activitymg_mgr_free(&mgr); // Free resourcesreturn 0;
}
下面从main
函数开始分析代码。
定义变量,struct mg_mgr
是用于保存所有活动连接的事件管理器,struct mg_connection
是单个连接描述符。
struct mg_mgr mgr; // Event managerstruct mg_connection *c;
设置 Mongoose 日志记录级别,设置等级为 MG_LL_INFO
。
mg_log_set(MG_LL_INFO); // Set log level
初始化一个事件管理器,也就是将上面定义的struct mg_mgr
变量 mgr
中的数据进行初始化。
mg_mgr_init(&mgr); // Initialize event manager
调用mg_timer_add
设置一个定时器,这会将其添加到事件管理器的内部定时器列表中。其中的参数15000
表示 15000 毫秒,也就是 15 秒;MG_TIMER_REPEAT | MG_TIMER_RUN_NOW
是定时器标志,其中MG_TIMER_REPEAT
表示定时重复调用函数,MG_TIMER_RUN_NOW
表示设置定时器后立即调用;timer_fn
是要调用的函数,&mgr
是要传递的参数。事件管理器将以参数 15 秒的时间间隔调用 timer_fn
函数,并将参数 &mgr
传递给它。
mg_timer_add(&mgr, 15000, MG_TIMER_REPEAT | MG_TIMER_RUN_NOW, timer_fn,&mgr); // Init timer for demo purposes, 15s
这个定时器函数的作用是如果客户端连接已关闭,则重新创建该连接。
下面我们先看下timer_fn
的实现:
如果c_res.c
的值为NULL
,则开始创建连接。将c_res.i
置 0,然后调用mg_connect
创建连接,s_conn
是要连接的 URL,cfn
是客户端事件处理函数,c_res
是要传入的参数。创建连接成功或者失败均会打印对应的日志。
// Timer function - recreate client connection if it is closed
static void timer_fn(void *arg) {struct mg_mgr *mgr = (struct mg_mgr *) arg;if (c_res.c == NULL) {// connectc_res.i = 0;c_res.c = mg_connect(mgr, s_conn, cfn, &c_res);if (c_res.c == NULL)MG_INFO(("CLIENT cant' open a connection"));elseMG_INFO(("CLIENT is connecting"));}
}
其中s_conn
是一个静态全局变量,默认参数如下:
static const char *s_conn = "tcp://localhost:8765"; // Connect to address
分析完timer_fn
的实现,回到main
函数。
通过 mg_listen
创建一个监听连接,监听地址s_lsn
,该参数为tcp://localhost:8765
,sfn
是服务器事件处理函数,传入的参数为NULL
。如果创建失败则打印失败日志并返回 0 结束程序。
c = mg_listen(&mgr, s_lsn, sfn, NULL); // Create server connectionif (c == NULL) {MG_INFO(("SERVER cant' open a connection"));return 0;}
其中s_lsn
是一个静态全局变量。
static const char *s_lsn = "tcp://localhost:8765"; // Listening address
接下来是事件循环,mg_mgr_poll
遍历所有连接,接受新连接,发送和接收数据,关闭连接,并为各个事件调用事件处理函数。设置参数100
,每次循环调用后阻塞 100 毫秒。
while (true)mg_mgr_poll(&mgr, 100); // Infinite event loop, blocks for upto 100ms// unless there is network activity
调用 mg_mgr_free
关闭所有连接,释放所有资源。
mg_mgr_free(&mgr); // Free resources
接下来看下服务器的事件处理函数sfn
的实现。
// SERVER event handler
static void sfn(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {
判断是否接收到MG_EV_OPEN
事件,收到MG_EV_OPEN
事件表示已创建连接,该事件在分配连接并将其添加到事件管理器之后立即发送。is_listening
表示监听连接。打印服务器正在监听的日志。
if (ev == MG_EV_OPEN && c->is_listening == 1) {MG_INFO(("SERVER is listening"));}
判断是否接收到MG_EV_ACCEPT
事件,表示已接受连接,服务器接受了一个来自客户端的连接。打印服务器接受连接的日志。如果代码被编译为支持 TLS,定义opts
参数,然后通过调用mg_tls_init
初始化 TLS 。其中cert
是服务器的证书,certkey
是证书的密钥。如果cert
为 NULL,则不进行身份验证。
} else if (ev == MG_EV_ACCEPT) {MG_INFO(("SERVER accepted a connection"));
#if (MG_ENABLE_MBEDTLS == 1) || (MG_ENABLE_OPENSSL == 1)struct mg_tls_opts opts = {//.ca = "ss_ca.pem", // Uncomment to enable two-way SSL.cert = "ss_server.pem", // Certificate PEM file.certkey = "ss_server.pem", // This pem contains both cert and key};mg_tls_init(c, &opts);
#endif}
判断是否接收到MG_EV_READ
事件,MG_EV_READ
表示从套接字socket接收到数据。当有从套接字socket接收到数据时,就会发送MG_EV_READ
事件。接收到的数据存放在c->recv
,将接收到的数据打印出来,然后通过mg_send
将接收到的数据发回给客户端。
} else if (ev == MG_EV_READ) {struct mg_iobuf *r = &c->recv;MG_INFO(("SERVER got data: %.*s", r->len, r->buf));mg_send(c, r->buf, r->len); // echo it back}
判断是否接收到MG_EV_CLOSE
事件,MG_EV_CLOSE
表示连接关闭。打印服务器断开连接的日志。
} else if (ev == MG_EV_CLOSE) {MG_INFO(("SERVER disconnected"));}
最后判断是否接收到MG_EV_ERROR
事件,表示有错误。打印出错误的日志。
} else if (ev == MG_EV_ERROR) {MG_INFO(("SERVER error: %s", (char *) ev_data));}
接下来看下客户端事件处理函数cfn
。
// CLIENT event handler
static void cfn(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {
定义变量i
,实际指向全局变量c_res
中的i
,用于后续的计数。
int *i = &((struct c_res_s *) fn_data)->i;
如果接收到MG_EV_OPEN
事件,表示已创建连接。打印客户端已经初始化的日志。
if (ev == MG_EV_OPEN) {MG_INFO(("CLIENT has been initialized"));}
如果接收到的是MG_EV_CONNECT
事件,表示连接已建立。打印客户端连接的日志。如果代码被编译为支持 TLS,定义opts
参数,通过调用mg_tls_init
初始化 TLS 。其中ca
表示证书颁发机构(Certificate Authority),用于验证另一端发送过来的证书,如果为 NULL,则禁用证书检查。最后将*i
的值置 1。
} else if (ev == MG_EV_CONNECT) {MG_INFO(("CLIENT connected"));
#if (MG_ENABLE_MBEDTLS == 1) || (MG_ENABLE_OPENSSL == 1)struct mg_tls_opts opts = {.ca = "ss_ca.pem"};mg_tls_init(c, &opts);
#endif*i = 1; // do something}
MG_EV_READ
事件表示从套接字socket接收到的数据。接收到的数据存放在c->recv
,定义指针变量r
指向它,然后将接收到的数据打印出来。
} else if (ev == MG_EV_READ) {struct mg_iobuf *r = &c->recv;MG_INFO(("CLIENT got data: %.*s", r->len, r->buf));}
如果接收到的是MG_EV_CLOSE
事件表示连接关闭。打印客户端断开连接的日志。将((struct c_res_s *) fn_data)->c
的值置NULL
,表示连接已结束,以便于下次在timer_fn
函数中判断c_res.c
的值时等于NULL
,重新创建客户端连接。
} else if (ev == MG_EV_CLOSE) {MG_INFO(("CLIENT disconnected"));// signal we are done((struct c_res_s *) fn_data)->c = NULL;}
如果接收到MG_EV_ERROR
事件表示有错误。打印出客户端的错误日志。
} else if (ev == MG_EV_ERROR) {MG_INFO(("CLIENT error: %s", (char *) ev_data));}
如果收到MG_EV_POLL
事件并且*i != 0
。其中MG_EV_POLL
由mg_mgr_poll
函数调用时发送。在MG_EV_CONNECT
事件中,客户端与服务器已建立连接后,会将*i
的值置 1。
当符合条件后,每次调用都会(*i)++
,每次调用间隔 100 毫秒。
当*i
的值等于 50 时,也就是连接建立 5 秒后,调用mg_send
函数发送消息Hi, there
给服务器,然后打印客户端发送数据的日志。
当*i
的值等于 100 时,也就是连接建立 10 秒后,将is_draining
的值置 1,发送剩余数据,然后关闭连接。
} else if (ev == MG_EV_POLL && *i != 0) {switch ((*i)++) {case 50: // 50 x 100ms = 5smg_send(c, "Hi, there", 9);MG_INFO(("CLIENT sent data"));break;case 100: // another 5s// send any possible outstanding data and close the connectionc->is_draining = 1;break;}}
tcp 的示例程序代码就都解析完了,下面实际运行一下 tcp 程序。
打开示例程序,编译并运行 tcp 示例程序:
pi@raspberrypi:~ $ cd Desktop/study/mongoose/examples/tcp/
pi@raspberrypi:~/Desktop/study/mongoose/examples/tcp $ make
cc ../../mongoose.c -I../.. -W -Wall -o example main.c
./example
2fa28 2 main.c:53:sfn SERVER is listening
2fa8c 2 main.c:19:cfn CLIENT has been initialized
2fa8c 2 main.c:86:timer_fn CLIENT is connecting
2fa8c 2 main.c:21:cfn CLIENT connected
2fa8c 2 main.c:55:sfn SERVER accepted a connection
运行 5 秒后,客户端发送数据Hi, there
给服务器,服务器收到消息后,给客户端回复Hi, there
消息。
30e1b 2 main.c:40:cfn CLIENT sent data
30e1c 2 main.c:66:sfn SERVER got data: Hi, there
30e1c 2 main.c:29:cfn CLIENT got data: Hi, there
运行 10 秒后,客户端与服务器断开连接。
3201a 2 main.c:31:cfn CLIENT disconnected
3201a 2 main.c:69:sfn SERVER disconnected
运行 15 秒后,客户端与服务器重新建立连接。
33537 2 main.c:19:cfn CLIENT has been initialized
33538 2 main.c:86:timer_fn CLIENT is connecting
33538 2 main.c:21:cfn CLIENT connected
33538 2 main.c:55:sfn SERVER accepted a connection
接下来重复这个过程
348c6 2 main.c:40:cfn CLIENT sent data
348c6 2 main.c:66:sfn SERVER got data: Hi, there
348c6 2 main.c:29:cfn CLIENT got data: Hi, there
35ac3 2 main.c:31:cfn CLIENT disconnected
35ac3 2 main.c:69:sfn SERVER disconnected
36fe0 2 main.c:19:cfn CLIENT has been initialized
36fe0 2 main.c:86:timer_fn CLIENT is connecting
36fe0 2 main.c:21:cfn CLIENT connected
36fe0 2 main.c:55:sfn SERVER accepted a connection
3836d 2 main.c:40:cfn CLIENT sent data
3836d 2 main.c:66:sfn SERVER got data: Hi, there
3836d 2 main.c:29:cfn CLIENT got data: Hi, there
3956a 2 main.c:31:cfn CLIENT disconnected
3956a 2 main.c:69:sfn SERVER disconnected
【参考资料】
examples/tcp
Documentation
本文链接:https://blog.csdn.net/u012028275/article/details/128745580