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

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

【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是要连接的 URLcfn是客户端事件处理函数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

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: go服务器