嵌入式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脚本进行处理的功能。
(南通大学电子信息学院 付康为 刘德靖 孙玲 施佺)