草庐IT

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

dadalaohua 2023-07-11 原文

【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 = 5s
        mg_send(c, "Hi, there", 9);
        MG_INFO(("CLIENT sent data"));
        break;
      case 100:  // another 5s
        // send any possible outstanding data and close the connection
        c->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) {
    // connect
    c_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"));
    else
      MG_INFO(("CLIENT is connecting"));
  }
}

int main(void) {
  struct mg_mgr mgr;  // Event manager
  struct mg_connection *c;

  mg_log_set(MG_LL_INFO);  // Set log level
  mg_mgr_init(&mgr);        // Initialize event manager
  mg_timer_add(&mgr, 15000, MG_TIMER_REPEAT | MG_TIMER_RUN_NOW, timer_fn,
               &mgr);                     // Init timer for demo purposes, 15s
  c = mg_listen(&mgr, s_lsn, sfn, NULL);  // Create server connection
  if (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 activity
  mg_mgr_free(&mgr);         // Free resources
  return 0;
}

下面从main函数开始分析代码。

定义变量,struct mg_mgr是用于保存所有活动连接的事件管理器,struct mg_connection是单个连接描述符。

  struct mg_mgr mgr;  // Event manager
  struct 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) {
    // connect
    c_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"));
    else
      MG_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 connection
  if (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 = 5s
        mg_send(c, "Hi, there", 9);
        MG_INFO(("CLIENT sent data"));
        break;
      case 100:  // another 5s
        // send any possible outstanding data and close the connection
        c->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

有关【Mongoose笔记】TCP 客户端与服务器的更多相关文章

  1. ruby - 使用 ruby​​ 和 savon 的 SOAP 服务 - 2

    我正在尝试使用ruby​​和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我

  2. ruby - 具有身份验证的私有(private) Ruby Gem 服务器 - 2

    我想安装一个带有一些身份验证的私有(private)Rubygem服务器。我希望能够使用公共(public)Ubuntu服务器托管内部gem。我读到了http://docs.rubygems.org/read/chapter/18.但是那个没有身份验证-如我所见。然后我读到了https://github.com/cwninja/geminabox.但是当我使用基本身份验证(他们在他们的Wiki中有)时,它会提示从我的服务器获取源。所以。如何制作带有身份验证的私有(private)Rubygem服务器?这是不可能的吗?谢谢。编辑:Geminabox问题。我尝试“捆绑”以安装新的gem..

  3. ruby-on-rails - 启动 Rails 服务器时 ImageMagick 的警告 - 2

    最近,当我启动我的Rails服务器时,我收到了一长串警告。虽然它不影响我的应用程序,但我想知道如何解决这些警告。我的估计是imagemagick以某种方式被调用了两次?当我在警告前后检查我的git日志时。我想知道如何解决这个问题。-bcrypt-ruby(3.1.2)-better_errors(1.0.1)+bcrypt(3.1.7)+bcrypt-ruby(3.1.5)-bcrypt(>=3.1.3)+better_errors(1.1.0)bcrypt和imagemagick有关系吗?/Users/rbchris/.rbenv/versions/2.0.0-p247/lib/ru

  4. ruby-on-rails - s3_direct_upload 在生产服务器中不工作 - 2

    在Rails4.0.2中,我使用s3_direct_upload和aws-sdkgems直接为s3存储桶上传文件。在开发环境中它工作正常,但在生产环境中它会抛出如下错误,ActionView::Template::Error(noimplicitconversionofnilintoString)在View中,create_cv_url,:id=>"s3_uploader",:key=>"cv_uploads/{unique_id}/${filename}",:key_starts_with=>"cv_uploads/",:callback_param=>"cv[direct_uplo

  5. ruby - 用 Ruby 编写一个简单的网络服务器 - 2

    我想在Ruby中创建一个用于开发目的的极其简单的Web服务器(不,不想使用现成的解决方案)。代码如下:#!/usr/bin/rubyrequire'socket'server=TCPServer.new('127.0.0.1',8080)whileconnection=server.acceptheaders=[]length=0whileline=connection.getsheaders想法是从命令行运行这个脚本,提供另一个脚本,它将在其标准输入上获取请求,并在其标准输出上返回完整的响应。到目前为止一切顺利,但事实证明这真的很脆弱,因为它在第二个请求上中断并出现错误:/usr/b

  6. ruby-on-rails - 在 Rails 中调试生产服务器 - 2

    您如何在Rails中的实时服务器上进行有效调试,无论是在测试版/生产服务器上?我试过直接在服务器上修改文件,然后重启应用,但是修改好像没有生效,或者需要很长时间(缓存?)我也试过在本地做“脚本/服务器生产”,但是那很慢另一种选择是编码和部署,但效率很低。有人对他们如何有效地做到这一点有任何见解吗? 最佳答案 我会回答你的问题,即使我不同意这种热修补服务器代码的方式:)首先,你真的确定你已经重启了服务器吗?您可以通过跟踪日志文件来检查它。您更改的代码显示的View可能会被缓存。缓存页面位于tmp/cache文件夹下。您可以尝试手动删除

  7. LC滤波器设计学习笔记(一)滤波电路入门 - 2

    目录前言滤波电路科普主要分类实际情况单位的概念常用评价参数函数型滤波器简单分析滤波电路构成低通滤波器RC低通滤波器RL低通滤波器高通滤波器RC高通滤波器RL高通滤波器部分摘自《LC滤波器设计与制作》,侵权删。前言最近需要学习放大电路和滤波电路,但是由于只在之前做音乐频谱分析仪的时候简单了解过一点点运放,所以也是相当从零开始学习了。滤波电路科普主要分类滤波器:主要是从不同频率的成分中提取出特定频率的信号。有源滤波器:由RC元件与运算放大器组成的滤波器。可滤除某一次或多次谐波,最普通易于采用的无源滤波器结构是将电感与电容串联,可对主要次谐波(3、5、7)构成低阻抗旁路。无源滤波器:无源滤波器,又称

  8. ruby - 我的 Ruby IRC 机器人没有连接到 IRC 服务器。我究竟做错了什么? - 2

    require"socket"server="irc.rizon.net"port="6667"nick="RubyIRCBot"channel="#0x40"s=TCPSocket.open(server,port)s.print("USERTesting",0)s.print("NICK#{nick}",0)s.print("JOIN#{channel}",0)这个IRC机器人没有连接到IRC服务器,我做错了什么? 最佳答案 失败并显示此消息::irc.shakeababy.net461*USER:Notenoughparame

  9. ruby - Rails 开发服务器、PDFKit 和多线程 - 2

    我有一个使用PDFKit呈现网页的pdf版本的Rails应用程序。我使用Thin作为开发服务器。问题是当我处于开发模式时。当我使用“bundleexecrailss”启动我的服务器并尝试呈现任何PDF时,整个过程会陷入僵局,因为当您呈现PDF时,会向服务器请求一些额外的资源,如图像和css,看起来只有一个线程.如何配置Rails开发服务器以运行多个工作线程?非常感谢。 最佳答案 我找到的最简单的解决方案是unicorn.geminstallunicorn创建一个unicorn.conf:worker_processes3然后使用它:

  10. ruby - Dropbox 类似 git 的服务——没有 rsync 和 inotify - 2

    关于如何使用git设置类似Dropbox的服务,您有什么建议吗?您认为git是解决此问题的合适工具吗?我在考虑使用git+rush解决方案,你觉得怎么样? 最佳答案 检查这个开源项目:https://github.com/hbons/SparkleShare来自项目的自述文件:Howdoesitwork?SparkleSharecreatesaspecialfolderonyourcomputer.Youcanaddremotelyhostedfolders(or"projects")tothisfolder.Theseprojec

随机推荐