前言

Mongoose是一个开源web服务器程序,仅有mongoose.c和 mongoose.h两个文件,虽然小巧,但具备基本的web服务器的功能,同时Mongoose是用C编写的,因此非常适合应用到嵌入式设备中。
目前Mongoose可以支持嵌入到C/C++、Python、C#。本文主要介绍下如何在Linux环境下将Mongoose代码嵌入到C应用程序中,并完成服务器的部署工作。
为了更快地熟悉web服务器的特性,在介绍部署服务器之前,有必要先介绍下服务器和客户端之间网络通讯的基础传输技术。


TCP连接过程

浏览器与Web服务器之间的请求和响应是使用HTTP协议进行的,但HTTP协议只是应用层使用的协议,传输层进行的数据通信则是通过TCP协议来保证的。
TCP协议是面向连接的协议,总体包括建立连接、数据传输和关闭连接这三个过程,建立连接采用“三次握手”方式来完成,连接成功后即可发送数据,在关闭连接之前,为确保数据正确传递完毕,需要采用“四次挥手”方式关闭连接。下面来分析下“三次握手”和“四次挥手”的过程。

1、客户端(192.20.1.12)“三次握手”连接服务器(192.20.1.122)
图片描述

  • 第一次握手:客户端发送连接请求报文段至服务器
SYN(flag)=1,表示客户端要求建立连接
Seq=x,随机产生序号
  • 第二次握手:服务器收到报文后确认连接请求,向客户端返回应答报文
SYN(flag)=1
ACK(flag)=1,表示服务器确认请求
ACK(num) =x+1(客户端Seq+1)
Seq=y,随机产生序号
  • 第三次握手:客户端收到响应后检查ACK(num)和ACK(flag)值,并向服务器给出确认
ACK(flag)=1,检查正确,客户端发送确认
ACK(num) =y+1(服务器Seq+1),客户端发送确认号
Seq=x+1
  • 完成三次握手,客户端与服务器开始传送数据

2、客户端(192.20.1.12)与服务器(192.20.1.122)“四次挥手”断开连接。客户端与服务器均可主动发起断开TCP连接请求,此处以客户端发起请求为例。
图片描述

  • 第一次挥手:客户端发送释放请求报文段至服务器,并不在发送数据
FIN(flag)=1,客户端要求终止连接
ACK(num)=z,上包数据Seq值
Seq=x,上包数据ACK(num)值
  • 第二次挥手:服务器收到释放连接请求后向客户端返回应答报文。此时连接处于半关闭状态,即客户端不再向服务器发送数据,但如果服务器仍有数据要发送给客户端,仍可以发送,客户端只要正确收到数据,仍应向服务器发送确认
ACK(flag)=1,表示服务器确认请求
ACK(num) =x+1(客户端Seq+1)
Seq=y,随机产生序号
  • 第三次挥手:若服务器不再向客户端发送数据,则服务器发送连接释放应答至客户端,关闭反方向连接
FIN(flag)=1,服务器要求终止连接
ACK(flag)=1,服务器发送确认
ACK(num) =x+1,与第二次挥手一致
Seq=y,与第二次挥手一致
  • 第四次挥手:客户端收到响应后向服务器给出确认,释放从服务器至客户端方向的连接。
ACK(flag)=1,检查正确,客户端发送确认
ACK(num) =y+1(服务器Seq+1),客户端发送确认号
Seq=x+1
  • 完成四次挥手,客户端与服务器全部连接完全释放

图片描述

也许有人会有疑惑,为什么建立连接协议是“三次握手”,而关闭连接却是“四次挥手”呢?由于TCP连接是全双工的,当关闭连接时,服务器收到客户端的FIN报文通知,仅仅表示客户端没有数据发送给服务器,但未必服务器所有的数据都发送给了客户端,可能还需要发送一些数据后再关闭连接。所以ACK报文和FIN报文分开发送形成了“四次挥手”。


Socket编程

在网络编程中不可避免地会使用到Socket编程,Socket将TCP/IP协议封装成API接口,
使得程序员更方便地使用TCP/IP协议栈。
Socket是以"打开—读/写—关闭"模式实现,以TCP协议通讯的Socket为例,交互流程大致如下,这里从服务器的角度对流程进行介绍:

图片描述

  • 创建socket()。socket()根据指定的协议族(IPv4/IPv6)、通信类型(TCP/UDP)在sockfs文件系统中分配一个使用socket描述字关联的资源,进程可以像访问一个已经打开的文件一样访问socket资源。
  • 绑定bind()。bind()在这个socket上绑定一个指定的端口号和IP地址。网络通信归根结底可视为不同计算机上的进程间通讯,IP地址只能确定进程所在的计算机,而结合端口号可以唯一确定整个网络中的一个网络进程。当IP地址为INADDR_ANY(0)时,表示本地计算机的默认IP地址。
  • 监听listen()。服务器socket端口一直处于等待状态,监听网络中所有客户端对端口号请求,随时准备接收客户端发来的连接
  • 接受accept()。服务器socket监听到客户端请求之后,将请求放在等待队列中,accept()从等待队列中提取连接请求,创建一个新的socket进行操作,而原来所监听的socket不受影响。
  • 读/写write()/read()。发送内容即向socket写入内容,读取内容即从socket获取内容
  • 关闭close()。close()关闭socket。

MakeFile

C程序的编译过程中,依次要进行预处理、编译、汇编、链接四个阶段,在Linux平台shell输入gcc命令完成上述步骤,但gcc在编译一个包含许多源文件的工程时,需要将其中的每个源文件都编译一遍,然后再全部连接起来,这样效率非常低,尤其当用户只是修改了其中某一个文件的时候,许多已经生成的目标文件是不会改变的,完全没有必要将每个文件都重新编译一遍。
为解决gcc编译的低效问题,Windows平台上的集成开发环境(IDE)提供了工程管理器,用户只需要点击一个“make”按钮就可以启动工程管理器对整个程序进行自动编译。
在Linux平台上,make工具和makefile提供了简单有效的工程管理方式。makefile决定编译工程的规则,通过make命令可以启动make工具根据makefile文件中的编译规对程序进行编译和链接,最终生成可执行文件。
Mongoose官网下载的源程序例程中提供了makefile文件,可以直接使用。

文件一:
PROG = simplest_web_server
MODULE_CFLAGS=-DMG_DISABLE_DAV_AUTH -DMG_ENABLE_FAKE_DAVLOCK
include ../examples.mk

文件二:
SUBDIRS = $(sort $(dir $(wildcard ./*/)))
SUBDIRS:=$(filter-out ./ ./CC3200/ ./ESP32_IDF/ ./ESP8266_RTOS/ ./mbed/ ./MSP432/ ./nRF51/ ./nRF52/ ./NXP_K64/ ./NXP_LPC4088/ ./PIC32/ ./STM32F4_CC3100/ ./TM4C129/ ./WinCE/, $(SUBDIRS))

ifeq ($(OS), Windows_NT)
  SUBDIRS:=$(filter-out ./netcat/ ./raspberry_pi_mjpeg_led/ ./captive_dns_server/, $(SUBDIRS))
endif

.PHONY: $(SUBDIRS)

all: $(SUBDIRS)

$(SUBDIRS):
    @$(MAKE) -C $@
clean:
    for d in $(SUBDIRS) ; do $(MAKE) -C $$d clean ; done

此处为了更好地说明makefile文件的功能,简单地编写了生成可执行文件的编译规则。将simplest_web_server.c mongoose.h编译成中间文件simplest_web_server.o,mongoose.c mongoose.h编译成中间文件mongoose.o,再将.o文件文件和所需的库文件链接成最终的可执行文件.all

all : simplest_web_server.o mongoose.o
    gcc -o all simplest_web_server.o mongoose.o 
simplest_web_server.o :  simplest_web_server.c mongoose.h
    gcc -c simplest_web_server.c    
mongoose.o :  mongoose.c mongoose.h
    gcc -c mongoose.c


Linux平台运行

Mongoose开源程序(官网下载6.9版本)提供了多个功能的例程,这里选用simplest_web_server(基础web服务器程序)的main函数作为主应用程序。

图片描述

创建Socket、绑定、监听等流程在以下函数中实现:

nc = mg_bind(&mgr, s_http_port, ev_handler);

循环mg_mgr_poll()遍历所有Socket,接受到新连接后进行数据收发,同时调用Socket相应事件处理函数。

for (;;) { 
mg_mgr_poll(&mgr, 1000); 
}

由用户实现的事件处理接口如下所示,可在该接口中实现服务器后台的业务逻辑功能:

static void ev_handler(struct mg_connection *nc, int ev, void *p) {
  if (ev == MG_EV_HTTP_REQUEST) {
    mg_serve_http(nc, (struct http_message *) p, s_http_server_opts);
  }
}

此处示例在mg_serve_http()中通过Linux文件系统查找index.html文件并返回给浏览器:

  if (opts.index_files == NULL) {
    opts.index_files = "index.html,index.htm,index.shtml,index.cgi,index.php";
  }

将mongoose.h、mongoose.c和simplest_web_server.c拷贝到自己的程序目录下,同时将index.html也放置到该目录下。

图片描述

在Linux的Shell命令行输入make命令,make工具会自动寻找当前目录下的“makefile”文件进行解析,并生成最终的可执行文件.all,运行.all可启动web服务器程序。

图片描述
图片描述

在服务器本机的浏览器中输入http://localhost:1200/来访问web服务器,如下图所示:

图片描述

通过另一台电脑的浏览器输入http://192.168.20.1:1200/,如下图所示:

图片描述


元宵大师
140 声望130 粉丝

Python资深数据分析师、高级项目管理师,致力于推动人工智能、大数据分析在金融量化交易领域中的应用。