【Mongoose笔记】TCP 客户端与服务器

news/2024/11/29 3:43:20/

【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:8765sfn是服务器事件处理函数,传入的参数为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_POLLmg_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


http://www.ppmy.cn/news/18590.html

相关文章

Python版本PathPlanning运动规划库中RotationToWorldFrame函数内部计算过程分析

本文主要对Python版本PathPlanning运动规划库中RotationToWorldFrame函数的内部计算过程分析,包括相关必备python基础和计算过程分析两部分,并给出了等效的MATLAB版本计算过程程序,方便分析对比。 (注:RotationToWorld…

模拟实现一个简单的命令行解释器(shell)

目录 前言 环境变量与本地变量 和环境变量相关的命令 获取环境变量的三种方法 第一种 第二种 第三种 进程地址空间 页表 为什么存在进程地址空间 第一 第二 第三 进程控制 进程的产生 进程终止 进程等待 进程替换 模拟实现一个shell 前言 我们通过各种指令来实现…

Java 23种设计模式(2.创建者模式-单例设计模式)

1. 创建者模式 创建型模式分为: 单例模式工厂方法模式抽象工程模式原型模式建造者模式 什么是创建者模式? 创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是“将对象的创建与使用分离”。 这样可以降低系统的耦合度…

CSDN竞赛24期题解

总结 本次竞赛的主要考点在于模拟,而且需要考虑的情况还蛮多,平时复杂点的模拟很少做,这次AC也花了挺长时间,后面还是需要夯实一下基础啊。 题目列表 1.计数问题 题目描述 试计算在区间 1 到 n 的所有整数中,数字…

【Linux_】环境变量

【Linux_】环境变量 心有所向,日复一日,必有精进专栏:《Linux_》作者:沂沐沐目录 【Linux_】环境变量 什么是环境变量 常见变量 查看环境变量方法 环境变量相关的命令 通过系统调用获取或设置环境变量 环境变量通常是具有全…

LeetCode 1802. 有界数组中指定下标处的最大值(C++)

思路: 首先根据题目要求,相邻数字的差距不能大于1,所以数组中的元素分布一定是以最大元素位置为塔顶,向两边发散的金字塔状,最小值为1,这样的结构能保证数组元素和一定是最小的(只有1是重复元素…

Allegro如何输出第三方网表操作指导

Allegro如何输出第三方网表操作指导 在做PCB设计的时候,会需要输第三方网表,Allegro支持快速输出第三方网表,如下图 具体操作如下 选择File选择Export

TVM: End-to-End Optimization Stack for Deep Learning论文阅读

摘要 很多目前最为流行的深度学习框架,如 TensorFlow、MXNet、Caffe 和 PyTorch,支持在有限类型的服务器级 GPU 设备上获得加速,这种支持依赖于高度特化、供应商特定的 GPU 库。然而,专用深度学习加速器的种类越来越多&#xff0…