关键词:嵌入式HTTP服务器 Linux 控制网络
引言
控制网络一般指以控制“事物对象”为特征的计算机网络系统,简称Infranet(infrastructure network),它处在企业网(Intranet)的底层,构成了整个企业网的基础。近些年来,在控制网络中采用了现场总线(Field Bus)和工业以太网(Ethernet)两种技术。出现这种情况主要有两个原因:第一,目前存在多种现场总线标准,不同的标准采用完全不同的通信协议,也就是说现场总线的开放性不够;第二,以太网虽然能够解决开放性的问题,并具备成本低廉、技术成熟等优点,但由于以太网最初是被设计用于以资源共享为目的的计算机局域网,因此在实时性和可靠性上暂时还不能完全满足工业控制的要求。事实上,目前一个较大规模的控制网络往往综合采用了这两种技术。在现场层,由于强调实时性、可靠性和安全性,常采用现场总线技术;在监控管理层,考虑到采用不同标准的控制网段之间的集成以及与高层企业信息网络的集成,一般采用以太网技术。现场层的现场总线控制系统FCS(Field Control System)或其它设备级轻质网络通过网关或嵌入式HTTP服务器与高层以太网相连。这样,不仅采用不同协议的控制网段能够实现互联,而且各个控制网段能方便地与高层企业信息网互通,从而最终实现企业网的管控一体化和对现场设备的Internet远程监控。通过以上分析可以看出,控制网络中的网关或嵌入式HTTP服务器起着连接现场层和监迭管理层的作用,因此它是整个控制网络的关键设备。
网关或嵌入式HTTP服务器与传统的嵌入式系统相比,有一些不同的特性。传统的嵌入式系统是面向应用、有一些不同的特性。传统的嵌入式系统是面向应用、面向产品的,强调成本和高效设计系统,因此本质上不具备通用性和可移植性。网关或嵌入式HTTP服务器由于处在现场层和监控管理层的中间,因此它与具体应用和产品是一种弱耦合的关系。同时,技术发展的趋势是:硬件成本越来越低,功能越来越强,越来越多的芯片和板卡具备“平台”的特点,适用于多种应用场合。嵌入式实时操作系统(Embedded Real Time Operationg System)的发展更是为嵌入式软件提供了一个通用的软件平台。综上所述,在网关或嵌入式HTTP服务器设计中,考虑通过选用适合的硬件和嵌入式实时操作系统,使整个系统具备相当的通用性和可移植性。对于连接不同的设备级轻质网络或不同的应用,只需要通过更换硬件模块和对代码作最小的修改即可实现。
1 基于Linux的嵌入式HTTP服务器的结构
为了实现设计目标,嵌入式HTTP服务器一般应采用功能较强的能用PC、工业PC、或高档MPU作为硬件平台,嵌入式实时操作系统作为软件平台进行平发。硬件平台应具备以太网口和一个或多个通信模块,比如RS232、RS485、CAN通信卡等。嵌入式实时操作系统实现了TCP/IP等网络协议,并提供实时任务、进程管理、内存管理、文件系统、API等功能。
Linux操作系统是一种多进程,多用户的通用操作。由于它具备免费、源码公开、内核可裁减、支持多线程、网络功能强大、设计精巧、性能稳定的特点,因此近年它也被广泛用到嵌入式系统的设计中。一个应用于嵌入式系统的Linux经过裁减和重新编译后只包括进程管理、内存管理、文件系统、若干个驱动程序和实用的函数等。
下面以本人参与的转子秤控制系统为例来说明一个基于Linux的嵌入式HTTP服务器的结构。转子秤是水泥工业中的关键计量喂料设备,一条大型的生产线需要许多台转子秤,对转子秤的控制涉及到重量、转速、温度、一氧化碳含量等若干个参量。由于现场环境的高噪声、高粉尘、高电磁干扰,无法在现场配备键盘、显示器、触摸屏等人机交互设备,无法在现场实现对设备的监控和维护。同时,一条生产线有多台转子秤,为每台转子秤配备人机交互设备也是不经济的。为此,考虑为整个系统设计一个嵌入式HTTP服务器,各转子秤控制器与嵌入式HTTP服务器用CAN总线相连。通过嵌入式HTTP服务器实现对整个系统的在线监控和远程监控。在嵌入式HTTP服务器的设计中,选用研祥公司PC104总线的486X嵌入式CPU卡作为硬件平台,该板卡是具有128MB的在板ROM、CF卡接口和以太网接口等。选择该板卡的原因是PC104总线的功能扩展模块非常丰富,通过选择不同的模块很容易就支持多种总线。软件平台方面,选用Linux2.0内核并对它作适当裁减。整个嵌入式HTTP服务器的结构简图如图1所示。
2 基于Linux的嵌入式HTTP服务器的设计
工控领域的嵌入式HTTP服务器应该具备如下基本功能。
①实时数据发布。实时数据主要包括系统运行过程中设备的各种状态信息。嵌入式HTTP服务器将实时数据以网页形式发布到Internet上,且动态实时刷新。客户可以通过浏览器访问这些实时信息。
②参数设置。参数包括运行参数和设备状态参数,如各种初始值、常数等。嵌入式HTTP服务器接收到客户提交的参数设备请求后,执行参数写入操作。
③远程实时控制。远程实时控制允许远程用户在线地控制系统中的相应执行机构,比如电机、电磁阀等。嵌入式HTTP服务器接收到远方客户提交的控制操作请求后,将下发控制命令驱动监控系统中相应的执行机构。
④访问级别设置和权限认证。只有权限不低于要求访问级别的客户,经嵌入式HTTP服务器认证后,方可进行其权限范围内的监控操作。
3 主要实现技术
3.1 超文本传输协议
HTTP协议是一个面向事务、无状态的应用层协议。在传输层,HTTP协议使用请求(request)/响应(response)模型。一次简单的HTTP事务包括以下过程。首先,客户(浏览器)发起和建立一条到服务器的TCP连接。然后,客户发送一个HTTP请求到服务器,请求包含方法、URI、协议版本和一个类MIME报文。服务器解析HTTP请求后,给出相应的HTTP响应,响应包括协议版本、状态码、解释状态码的简短短语和一个类MIME报文。最后,释放TCP连接。Linux操作系统为用户提供了称为BSD Socket的网络编程接口。利用其中的TCP套接口函数,可以非常方便地实现HTTP协议。
HTTP1.0为每一次HTTP请求/响应建立一条新的TCP连接,由于建立一条TCP连接要经历3次握手,因此效率不高。HTTP1.1提出了可持续性连接的概念。HTTP1.1只建立一次TCP连接,而重复地使用它传送一条素的请求/响应消息,减少了额外开销。在嵌入式HTTP服务器中,一般使用HTTP1.1协议。HTTP1.1协议的细节请参考RFC2616。
3.2 通用网关接口CGI
参数设置和远程控制功能都是通过CGI(通用网关接口)程序和表单实现的。CGI使用HTML表单向Web服务器发送信息。基本语法如下:
<FORM METHOD=get/post ACTION=URL></FORM>
其中,METHOD属性指定将数据传送到Web服务器的方法。输入方法有两种:GET和POST。ACTION属性定义要对表单数据进行处理的CGI脚本的URL。
CGI的工作流程是首先由浏览器将用户输入的数据传递给Web服务器,Web服务器根据接收到的数据设置环境变量并启动CGI脚本,CGI脚本从环境变量中读取所需要的数据并进行相应处理,最后使用STDOUT输出HTML形式的结果文件,经Web服务器送回浏览器,最终显示给用户。传统的CGI程序与服务器代码分开,是一个符号CGI标准的可执行文件,并储存在CF卡等存储设备上,一般用脚本语言编写。考虑到嵌入式HTTP服务器要求速度快,功能和代码都尽可能精简的特点,可以把原先由可执行文件完成的功能用C函数实现,放在服务器代码内部,并直接从HTTP请求报文接收数据。与传统CGI程序相比,这种方法具备如下特点:
*不需要标准输入,CGI函数可以直接获取到浏览器送来的信息;
*不需要标准输出,CGI函数可以直接将数据送回给浏览器;
*不需要环境变量,CGI和Web服务器在同一程序中实现,不需要环境变量来交换信息。
3.3 自定义标记
要在网页中显示工控系统中大量的实时数据,常规方法是将HTML代码直接集成到程序代码中,或者反之将C程序代码集成到HTML标记语言中。这两种方法均要求开发人员对HTML标记语言的语法细节非常熟悉。网页或程序结构的单方面调整都将导致整个系统全盘修改,系统不具备灵活性与可扩展性。HTML的精髓在于该语言的“标记”性,各种不同标记的具体含义是由服务器和浏览器进行解析。因此,当现有标记不能满足新的应用需求时,可以自行定义新的标记,只需服务器将自定义标记解析为标准标记,然后传送给浏览器即可。在本项目中,主要的实时数据转速、重量、一氧化碳含量等状态信息,可以定义相应的标记。服务器中解析相应标记的函数同样用C语言来实现。运行时,当客户端发出查看某实时网页的请求后,嵌入式HTTP服务器将相应的网页文件从电子盘加载到内存进行逐项解析。当辨识出自定义标记后,就调用相应的函数。该函数返回该标记对应的当前值,并置换HTML文件流中的自定义标记。最后,嵌入式HTTP服务器将解析结果发送给客户端。实时网页的设计与相应的HTTP服务器处理程序得以分离,处于一种弱耦合关联状态。这样,网页界面的调整不会影响HTTP服务器的程序设计,HTTP服务器程序的修改也与网页界面设计无关,整个嵌入式HTTP服务器具备灵活性和可扩展性。
3.4 多线程
最初的进程定义包含程序、资源及其执行三部分,其中程序通常指代码,资源通常包括 内存资源、I/O资源、信号处理等,而程序的执行指执行上下文,这一部分后来发展为线程。在线程的概念出现以前,为了减小进程切换的开销,操作系统设计者逐渐修改正进程的概念,允许将进程所占有的资源从其主体剥离出来,允许某些进程共离享一部分资源,例如文件、信号、数据内存、甚至代码,这就是轻质进程的概念。Linux内核的2.0.x版本就已经实现了轻质进程。应用程序可以通过一个统一的clone()系统调用接口,用不同的参数指定创建轻质进程还是普通进程。在内核中,clone()调用经过参数传递和解释后会调用do_fork(),这个核内函数同时也是fork、vfork()系统调用的最终实现。在do_fork()中,不同的flone_flags将导致不同的行为。在LinuxThreads中,使用(CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND)参数调用clone()创建“线程”,表示共享内存、共享文件系统、共享文件描述符表,以及共享信号处理方式。Linux操作系统下,已经实现基于轻质进程的符号POSIX1003.C标准的线程库LinuxThreads。
在传统的Unix服务器程序设计中,为了使服务器具备并发处理连接的能力,通常采用父进程处理连接,并调用fork()创建子进程来处理用户请求的方法。这种方法的缺点是进程创建慢,耗费资源,进程切换开销大,进程之间通信比较困难等,不适用对资源、速度有要求的嵌入式系统。因此,在嵌入式HTTP服务器的开发中使用线程的方法。利用LinuxThreads提供的pthread_create()等函数派生出线程,也即轻质进程来处理多个HTTP请求。
4 工作流程和代码设计
4.1工作流程
嵌入式HTTP服务器程序开始运行时,主进程首先创建一个接口,并和主机地址绑定到一起,随后置为被动监听状态,等待客户端连接请求的到来。分别用函数socket()创建一个接口,bind()绑定地址,listen()监听,accept()接收来完成。当建立一个TCP连接后,函数accept()返回一个新的套接口描述符,主进程就创建一个新的子线程(轻质进程)处理这个新的连接。
子线程用于处理每具体的HTTP请求。子线程首先解析用户的HTTP请求。当用户请求一个网页时,子线程查找文件系统。如果该网页文件存在,且通过权限认证,就把它从CF卡读入内存并扫描,发现有自定义标记则调用相应函数进行处理,最后把结果返回给浏览器;否则给一个简单的出错消息。当用户是上传数据时,子线程调用相应函数读取数据进行处理,并返回处理结果给浏览器。
4.2 代码设计
在嵌入式HTTP服务器的代码设计中,考虑到代码的移植性和扩展性,利用C语言实现了面向对象风格的代码结构。代码主要由两上数据结构request_inf和response_inf以及其上一组操作函数组成。
结构request_inf和response_inf分别用来保存HTTP请求报文和响应报文的所有信息。在结构定义时,应根据具体应用特点设计结构包含的成分。
嵌入式HTTP服务器的函数包括通用函数、CGI函数和自定义标记处理函数等,其中通用函数是一些与HTTP1.1协议有关的函数。
(1)通用函数
*void prase_request_line(char *,struct *request_inf)
该函数用来解析HTTP请求报文的请求行(Request_Line),并把相应信息存放在结构request_inf中。其中,对请求行中URI部分的解析包括两种情况。如果用户请求一个网页,则获取文件路径、文件类型;如果用户要求上传数据,则把数据放在一个字符数组中。然后将文件路径和类型,或者指向该数组的指针、方法、版本号信息都放入结构request_inf中。
*void prease_general_header(char*,struct*request_inf)
该函数用来解析HTTP请求报文的调用首部(General_Header)。之所以把此函数与函数prase_request_line()分开,是考虑到程序的修植性和扩展性。请求行和通用首部是请求报文中的不同部分,不不同的场合下,要求解析的信息可能存在差导师。同时,这样也能使程序结构更清楚。比如,本项目要从通用首部解析字段Keep_Alive。该字段指明一个最长的时间或最大请求数目,在此范围内可以保持TCP连接不被释放(即前文提到的HTTP1.1的持续连接特性,persistent connection)。
*void prase_request_header(char*,struct*request_inf)
void prase_entity_header(char*,struct*request_inf)
HTTP请求报文的请求头部用来说明浏览器的一些信息,实体头部则用来说明请求报文中可能存在的实体主体信息。本项目实际上并不需要使用这两个函数来获取相关信息,但考虑到程序的扩展性和移植性,此处仍然把它列出来,它们是两个空函数。
*send_status_line(int fd,struct *response_inf)
此函数用来产生一个HTTP响应报文的状态行(Status_line)。状态行包括三部分内容,即HTTP版本、状态码以及解释状态码的简单短语。这些信息预先放在结构response_inf中。
*send_general_header(int fd,struct*response_inf)
send_response_header(int fd,struct*response_inf)
send_entity_header(int fd,struct*response_inf)
这三个函数分别用来产生HTTP响应报文的通用首部、响应首部(Response_header)和实体首部。嵌入式HTTP服务器是一个瘦服务器,功能非常简单。因此HTTP响应报文的通用首部、响应首部和实体首部中的可选字段许多是不需要的,还有许多是固定不变的,例如Last_modified和Content_type字段。Last_modified字段指出资源上次被修改的时间并由接收方解释。如果接收方已有此资源的拷贝,但此拷贝比Last-Modified域所指定的要旧,那该拷贝就是过期的。由于网页文件中含有自定义标记,具有实时性,所以此字段根本没有含有Content_type字段指出实体的媒体类型,本项目中的嵌入式HTTP服务器被设计成只支持HTML类型,因此该字段的内容总是Content_type=text/html。有关服务器和资源的所有标题域信息都被放入结构response_inf中。
*send_white_line(int fd)
此函数用于实体首部和实体之间传送一个空白行。
*void send_entity_body(int fd,char *buff_file)
此函数用来传递实体主体,实体主体实际上是一个处理后的网页文件,它被放在指针buff_file指向的缓冲区内。
*void zero_request_inf(struct*request_inf)
void zero_response_inf(struct*response_inf)
这两个函数用于结构request_inf和response_inf清零。
*void get_file(struct*request_inf,struct * response_inf,char*buff_file,void*,void*)
该函数用来处理用户HTTL请求。首先,函数会检查request_inf结构,判断用户是请求一个网页文件还是上传数据。当用户请求网页文件时,函数将根据request_inf结构中的文件路径信息,在文件系统录找此文件。如果文件不存在或不具备权限,则函数将状态码和解释短语写入结构response_inf,然后直接返回;否则读取文件并调用自定义标记处理函数,对标记进行处理,处理过的网页文件被放入buff_file指向的缓冲区内,并把状态码、解释短路和与实体有关的一些信息写入结构response_inf。当用户上传数据时,该函数调用CGI处理函数向CAN总线网络发送帧,然后将状态码和解释短路写入结构response_inf。利用状态码和解释短语只能用“200,OK”或“500,Internal Server Error”等,简单反映执行情况。用户要获取详细信息,可待一段合适的时间后请求网页文件。函数中两个void指针分别指向自定义标记处理函数和CGI处理函数,或者对应的函数指针数组。
(2)自定义标记处理函数和CGI处理函数
自定义标记处理函数用于对自定义的处理,每一类自定义标记对对应一种自定义标记处理函数,同一类自定义标记的不同数据点利用参数来区分,比如转子秤1的重量标记可以用weight1来表示。所有的自定义标记处理函数被放在一起,构成一个函数指针数组。自定义标记处理函数向CAN总线网络发送远程帧和接收数据帧,获取相应的状态信息。CGI总线网络发送远程帧和接收数据帧,获取相应的状态信息。CGI处理函数用变量名来区分,同一类变量对应一种CGI处理函数。与自定义标记处理函数类似,所有的CGI处理函数也被放在一起,构成一个函数指针数组。由于自定义标记函数和CGI处理函数类型众多,这里就不列举了。
结语
我们设计的嵌入式HTTP服务器具备良好的通用性和可移植性。通过更换或增加PC104通信模块,该服务器能够支持不同的现场总线,或同时连接几种不同的设备级轻质网络。同时在服务器代码设计中,用C语言实现了面向对象风格的代码结构。这样,如果要求服务器端具备更多的特性,只需要简单修改结构request_inf、response_inf、操作函数和网页文件即可达到目的。这种设计思路不仅适用于嵌入式HTTP服务器,随着硬件技术尤其是嵌入式操作系统技术的发展,它同样能够应用到其它嵌入式产品的开发中。