嵌入式web技术因其跨平台的特点得到了广泛的应用[1]。用户只需要登录浏览器即可实现对嵌入式设备状态的查看与控制。随着物联网技术的发展,网络地址的需求量剧增,未来ipv6将在嵌入式领域发挥巨大的作用[2]。然而,目前ipv4技术还无法完全被新的ipv6技术所取代,这使得现有的应用程序必须同时兼容ipv4地址与ipv6地址。如何在嵌入式web服务器中同时使用ipv4地址和ipv6地址则成为了嵌入式领域中的一个重要问题[3]。本文从实际应用出发,设计了一个能够同时支持ipv4与ipv6双协议栈的嵌入式web服务器。
基本原理
嵌入式web服务器的基本原理是:用户在浏览器中输入嵌入式设备的ip地址,随后浏览器向嵌入式web服务器发出http请求,嵌入式web服务器针对该请求作出http响应,最后浏览器对响应的内容进行解析,以网页的形式呈现给用户。嵌入式web服务器原理如图1所示。
http请求和响应的报文是通过网络进行传输的。浏览器向web服务器请求网页数据的具体流程如图2所示[4]。
浏览器和web服务器之间是通过tcp协议进行通信的,tcp协议是一种面向连接的、可靠的、基于字节流的传输层通信协议。web服务器监听特定的网络端口,当浏览器向web服务器发出请求时,两者之间通过tcp协议建立连接,然后传输http请求报文和http响应报文。web服务器实际上也是一个tcp服务器,典型的tcp服务器的架构如图3所示。
针对现代农业物联网技术的应用需求,为了使系统中的嵌入式web服务器在支持ipv4地址访问的基础上,还能支持ipv6地址的访问,本文按照图3所示的典型tcp服务器架构设计了一个同时支持ipv4地址与ipv6地址访问请求的嵌入式web服务器,具体实现过程如下。
设计实现
为了进行浏览器与web服务器之间的通信,首先就要建立网络连接,采用的方式为socket通信。socket又称为套接字,应用程序通常情况下通过套接字向网络发出请求或者应答网络请求[5]。web服务器需要为每一个与其连接的客户端分配一个socket套接字,作为相互通信的基础。传统的ipv4网络服务器建立socket描述符的代码如下所示:
structsockaddr_inserver_addr;/*服务器端ip地址*/
structsockaddr_inclient_addr;/*客户端ip地址*/
sockfd=socket(af_inet,sock_stream,0);
bzero(&server_addr,sizeof(structsockaddr_in));
server_addr.sin_family=af_inet;
server_addr.sin_addr.s_addr=htonl(inaddr_any);
server_addr.sin_port=htons(80);
上述代码中,第一行和第二行分别定义了服务器和客户端的套接字地址变量。第三行代码的作用为服务器端建立socket描述符,af_inet表明服务器使用的是ipv4协议族,而sock_stream表明使用的是tcp协议。第四行代码是为了清空sockaddr_in结构体变量,为填充内容做好准备。第五行是为sockaddr_in结构体变量填入ipv4协议族。第六行填入inaddr_any表明该服务器可以接收任意ip地址的数据,即绑定到所有的ip地址。第七行是为sockaddr_in结构体变量填入80端口号,80端口号为web服务器中的htto专用的端口号。
参照ipv4服务器建立socket描述符的过程,为了实现对ipv6地址的支持,对上述代码进行如下修改:
structsockaddr_in6server_addr;/*服务器端ip地址*/
structsockaddr_in6client_addr;/*客户端ip地址*/
server_socket=socket(pf_inet6,sock_stream,0));
bzero(&server_addr,sizeof(structsockaddr_in6));
server_addr.sin6_family=pf_inet6;
server_addr.sin6_addr=in6addr_any;
server_addr.sin6_port=htons(8080);
新的web服务器代码将sockaddr_in结构体更改为sockaddr_in6结构体,而sockaddr_in6结构体的成员如下所示:
structsockaddr_in6{
sa_family_tsin6_family;
in_port_tsin6_port;
structin6_addrsin6_addr;
……
};
成员sin6_family表明所使用的地址协议族,pf_inet6表明使用的是ipv6协议族;sin6_addr为web服务器监听的ip地址,将其设为in6addr_any是要接收任意ip地址发送的数据,即“inaddr_any”的ipv6版本;成员sin6_port则表明了web服务器所使用的端口,使用8080端口而
不是80端口的原因是为了防止与嵌入式linux设备上现有的web服务器相冲突。用ipv6建立服务器端的话,即使客户端仍用ipv4的socket连接也可以正常通信,ipv4的地址会被转换成这种地址“::ffff:ipv4地址”,即ipv4映射地址。
图4给出了浏览器向web服务器发送的http请求报文的格式,其中,url是用户所需的资源。例如,当用户在浏览器地址栏输入“192.168.1.1:8080/index.html”时,http请求报文的请求行为“get/index.htmlhttp/1.1”。从该行中即可得到用户所需的资源信息。设计的get_user_url(unsignedchar*url,unsignedchar*request)函数则可以获得浏览器所需的url。随后,将根据该url搜索相应的资源,并为组合http响应报文做好准备。
web服务器的主要工作就是组合http响应报文,然后将其发送给请求网页的浏览器。http响应报文的格式如图5所示。
http请求报文和响应报文的头部字段主要有content-length、content-type等。为了实现http响应报文的组合,本文设计了函数response_by_source(unsignedchar*source,intclient_socket)。该函数首先将构造http响应头部,然后和http响应报文的内容即用户请求的资源进行组合。函数代码如下所示:
strcpy(response_buf,“http/1.0200ok\r\n”);
get_mime_type(mime_type,source);
strcat(response_buf,mime_type);
sprintf(response_tmp,“content-length:%ld\r\n”,file_size);
strcat(response_buf,response_tmp);
strcat(response_buf,“\r\n”);
第1行的作用为构造http响应报文的状态行,向请求的服务器回应“http/1.0200ok”,表明请求已成功,请求的响应头或数据体将随此响应返回。第2、3行的作用是为了构造头部字段content-type,函数get_mime_type(mime_type,source)的主要作用就是通过用户请求的url得出请求资源的类型。第4行关键字content-length指的是用户请求的资源大小。第5行的作用是把http响应报文头部内容填入数据发送缓冲区中,web服务器将会把数据发送缓冲区中的内容发送至浏览器。第6行为数据发送缓冲区中的内容添加一个空行,因为http响应报文的头部与内容要用一个换行符隔开。
报文头部content-type表明了http响应报文的内容类型,浏览器将根据内容的类型来进行相应的处理。get_mime_type(unsignedchar*mime_type,unsignedchar*source)的代码如下所示:
/*功能:根据客户端的请求确定应答的mime类型*/
voidget_mime_type(unsignedchar*mime_type,unsignedchar*source)
{
unsignedchar*pchar=null;/*字符指针*/
unsignedchartype[20]={0};/*存放source字符串中的type信息*/
pchar=strrchr(source,‘.’);/*寻找source中最后一个‘.’号
*/
strcpy(type,pchar);
if(strncmp(type,“.html”,strlen(type))==0)
{
strcpy(mime_type,“content-type:text/html\r\n”);
}
elseif(strncmp(type,“.jpg”,strlen(type))==0)
{
strcpy(mime_type,“content-type:image/jpeg\r\n”);
}
elseif(strncmp(type,“.png”,strlen(type))==0)
{
strcpy(mime_type,“content-type:image/png\r\n”);
}
return;
}
上述代码目前可以对html、jpg和png
格式的文件进行处理。如果需要对其他类型的文件进行处理,可以再进行适当修改。
content-length为http响应报文中内容的长度,可以用如下代码进行计算:
fseek(fp,0l,seek_end);
file_size=ftell(fp);
fseek(fp,0l,seek_set);
计算响应报文内容长度的原理是将文件指针移到文件尾,然后计算出文件尾距离文件头的距离,即是文件的大小;计算结束后还原文件指针的位置。
在对http响应报文的头部构造完成后,可以先将其进行发送,发送代码如下所示:
write(client_socket,response_buf,http_header_len);
这样就可以把http响应报文的头部发送给浏览器。接下来,就要对报文的内容进行发送。发送报文内容部分的代码对发送大文件进行了特殊的处理,首先从文件中读取一定数量的内容,然后将其发送至浏览器。循环往复,直到读到文件尾为止,最后对文件进行关闭操作。代码如下所示:
do{
unsignedinti=0;/*用于计数的变量*/
/*从文件中读取20000个数据项,每个数据项的大小为1个字
节,即读取20000字节的内容,返回实际读到的字节数*/
read_count=fread(response_content_buf,1,20000,fp);
for(i=0;i
{
response_buf[i]=response_content_buf[i];
}
/*分批发送http应答报文中的内容*/
if(write(client_socket,response_buf,read_count)==-1)
{
fprintf(stderr,“writeerror:%s\n”,strerror(errno));
exit(1);
}
memset(response_buf,0,sizeof(response_buf));
memset(response_content_buf,0,sizeof(response_content_buf));
}while(read_count!=0);fclose(fp);
为了能对多个浏览器同时进行服务,该web服务器还增加了多线程的机制。每当一个浏览器与之建立连接时,web服务器会产生一个线程为其进行服务,确保了服务的实时性。多线程的代码如下所示:
pthread_ta_thread;
void*thread_result=null;
pthread_create(&a_thread,null,server_thread,(void
*)&client_socket);/*创建服务器线程*/
整个web服务器处理的流程如图6所示。
系统测试
在嵌入式linux平台下,输入命令“ifconfig”,即可得到当前设备的ip地址,如图7所示。由图可见,该设备的ipv4地址为“192.168.1.106”,ipv6地址则为“fe80::c23f:eff:fef4:394b”。
在嵌入式linux设备中启动web服务器程序,并在后台运行。在浏览器中输入web服务器的ipv4地址,即使用ipv4地址访问web服务器,如图8所示。得到web服务器反馈的网页如图9所示。由图9可见,web服务器能够输出html网页以及png格式的图片。在网页中输入web服务器的ipv6地址,即用ipv6地址来访问web服务器,如图10所示,得到如图11所示的web服务器反馈网页。
同时使用其他浏览器访问web服务器也会得到同样的响应结果,说明本文设计的web服务器能够同时支持ipv4与ipv6地址进行访问。
本文完成了一个支持ipv4与ipv6地址同时进行访问的嵌入式web服务器设计,但目前也仅仅实现了输出网页内容的功能,还无法对cgi脚本进行处理,并与用户进行交互。后续将不断完善系统功能,增加对cgi脚本进行处理的功能。
(南通大学电子信息学院 付康为 刘德靖 孙玲 施佺)