嵌入式系统开发流程实验三

首页-达尔闻    基础应用    嵌入式系统开发流程实验三

Part.1 移植web_server

 

1.下载源码包

 

1.1 Boa简介

 

本次移植的Web服务器是Boa

 

Boa是一种非常小巧的Web服务器,其可执行代码只有大约60KB左右。作为一种单任务Web服务器,Boa只能依次完成用户的请求,而不会fork出新的进程来处理并发连接请求。但Boa支持CGI,能够为CGI程序fork出一个进程来执行。Boa的设计目标是速度和安全。

 

 

1.2 移植流程

 

1.2.1 下载源码包

 

最新发行版本:0.94.13

 

下载 boa­0.94.13.tar.gz到虚拟机,然后执行解压:

tar xzf boa‐0.94.13.tar.gz

1.2.2 安装依赖包

 

boa需要安装工具bisonflex

sudo apt‐get install bison flex

否则会爆出:​​​​​​

make: yacc:command not found
make: *** [y.tab.c] error 127 
make: lex:command not found
make: *** [lex.yy.c] error 127

1.2.3 修改文件

 

(1)修改src/compat.h

 

找到

#define TIMEZONE_OFFSET(foo) foo##‐>tm_gmtoff

修改成

#define TIMEZONE_OFFSET(foo) (foo)‐>tm_gmtoff

否则会出现错误:

 

util.c:100:1: error: pasting "t" and "‐>" does not give a valid preproces sing token make: *** [util.o] error 1

 

(2)修改src/log.c

注释掉

if (dup2(error_log, STDERR_FILENO) == ‐1) {
 DIE("unable to dup2 the error log");
 }

为:

/*if (dup2(error_log, STDERR_FILENO) == ‐1) {
DIE("unable to dup2 the error log");
 }*/

 

否则会出现错误:

log.c:73 unable to dup2 the error log:bad file descriptor

(3)修改src/boa.c

 

注释掉下面两句话:

if (passwdbuf == NULL) {
DIE(”getpwuid”);
}
if (initgroups(passwdbuf‐>pw_name, passwdbuf‐>pw_gid) == ‐1) {
DIE(”initgroups”);
}

#if 0
if (passwdbuf == NULL) {
DIE(”getpwuid”);
}
if (initgroups(passwdbuf‐>pw_name, passwdbuf‐>pw_gid) == ‐1) {
DIE(”initgroups”);
}
#endif

否则会出现错误:

boa.c:211 ‐ getpwuid: No such file or directory

注释掉下面语句:

if (setuid(0) != ‐1) {
 DIE(”icky Linux kernel bug!);
}

#if 0
if (setuid(0) != ‐1) {
DIE(”icky Linux kernel bug!);
}
#endif

否则会出现问题:

boa.c:228 ‐ icky Linux kernel bug!: No such file or directory

 

1.2.4 生成Makefile脚本

 

cd /your boa path/src
./configure

修改生成的Makefile脚本编译器选项:

CC = arm‐linux‐gcc

 

 

1.2.5 编译

make

之后会得到boa的执行文件

 

1.2.6 Boa的配置

 

修改boa/src目录下的boa.conf文件

 

1Group的修改

 

修改Group nobodyGroup 0

 

2User的修改

 

修改User nobodyUser 0

 

3ScriptsAlias的修改

 

修改ScriptAlias /cg­bin/ /usr/lib/cgi­bin/ ScritpAlias /cgi­bin/ /www/cgi­bin/ 4DocumentRoot的修改

 

修改DocumentRoot /var/www DocumentRoot /www

 

5ServerName的设置

 

修改#ServerName www.your.org.hereServerName www.your.org.here

6AccessLog的设置

 

修改AccessLog /var/log/boa/access_log #AccessLog /var/log/boa/access_log

 

修改上述配置完成后,在开发板建立对应目录:

mkdir /etc/boa
mkdir /www
mkdir /www/cgi‐bin

之后在开发板上将文件拷入到对应目录夹下:

cp boa /etc/boa
cp boa.conf /etc/boa

 

最后再把网页文件index.html拷入到/www目录下,在浏览器输入IP地址即可看到对应网页

 

 

Part.2 网络访问页面配置IP, netmask, gateway

 

设计思路:通过cgi程序修改rcS文件,在rcS文件里面保存

 

由网络页面输入的IPnetmaskgateway配置

 

2.1 网络页面index.html

 

 

<form action="/cgi‐bin/ip.cgi" method="GET" target="nm_iframe">
<p>please input IP address, Netmask, Gateway</p>
<label>IP Address:<input name="address" size="30" maxlength="40"> </label><br>
<label>Netmask:<input name="netmask" size="30" maxlength="40"></label><br>
<label>Gateway:<input name="gateway" size="30" maxlength="40"></label><br>
<input type="SUBMIT" value="Send">
</form>

上面是我的网页文件index.html的完整内容,关于HTML文件的书写规范这里不做详细介绍.

输入ip之后可以在浏览器看到对应三个输入框:

输入对应IPNetmaskGateway之后,发送到对应的cgi程序处理,后面会对cgi具体内容做介绍。

 

 

2.2 rcS文件介绍

 

rcS文件是开发板启动后自动执行的脚本文件,用户可以在里面添加自定义的一些配置。

 

这里我主要添加IP的配置语句:

ifconfig eth0 192.168.100.102 netmask 255.255.255.0
route add default gw 192.168.100.101

 

 

2.3 cgi程序设计

 

这里我用c语言设计了一个cgi程序,接收网页发来的数据,并完成对应的操作。

char str3[30] = "route add default gw ";

char tab[20] = "\n";

char line0[150];

char line1[150];

int i;

int j;

printf("%s%c%c\n", "Content‐Type:text/html;charset=iso‐8859‐1", 13, 10);

printf("<TITLE>IP Configuration</TITLE>\n");

printf("<H3>IP Configuration</H3>\n");

data = getenv("QUERY_STRING");

if(data == NULL)

printf("<P>ERROR:no input setup or transmission meet with troubles!");

else if(sscanf(data, "address=%[0‐9,.]&netmask=%[0‐9,.]&gateway=%[0‐ 9,.]", address, netmask, gateway)!=3)

printf("<P>ERROR:input is invalid!");

else{

//printf("%s%c%c\n", "Content‐Type:text/plain;charset=iso‐8859‐1",13,10);

strcat(strcat(strcat(strcat(strcpy(line0, str0), address), str1), netma sk), tab);

strcat(strcat(strcpy(line1, str3), gateway), tab);

fp = fopen(ip_config, "r+");

for(i=0; i<9; i++)

{

fgets(buff[i], 255, (FILE*)fp);

}

fclose(fp);

fp = fopen("/etc/init.d/rcS", "w+");

strcpy(buff[6], line0);

strcpy(buff[7], line1);

for(j=0; j<8; j++)

{

fputs(buff[j], fp);

}

fclose(fp);

}

return 1;

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#define ip_config "/etc/init.d/rcS"

int main(void) 

{

char *data;

char address[100]; 

char netmask[100]; 

char gateway[100]; 

FILE *fp = NULL;

char buff[10][150];

char str0[30] = "ifconfig eth0 ";

 char str1[30] = " netmask ";

char address[100];

 char netmask[100]; 11 char gateway[100];

 FILE *fp = NULL;

char *data;

{

 

char str1[30] = " netmask ";

}

 

前面<form>块中数据发送的方式是"GET",这里我们通过:getenv("QUERY_STRING")来去接受数据并放在地址data上。通过sscanf函数完成数据的分配。之后打开文件,写入对应行,即可完成IP配置的操作。tips:sscanf函数里面,可能会注意到%[0­9,.]这种不同于传%s,%d 的数据模式。这是因为特殊字符会经过URL码,会带来数据接受的错误,而上述模式可以解决这个问题。

 

 

Part.3 页面设置文件传输功能

 

我在开发板移植了FTP程序,然后在主机安装vsftpd程序,在主机打开vcstpd程序,然后在通过页面选择开发板上输入主机的IP地址,是上传还是下载文件,输入文件的名字即可通过TCP协议完成文件的传输。

 

3.1 网络页面配置

 

<form action="/cgi‐bin/ftp.cgi" method="GET" target="nm_iframe">
<p>please input address, transmission type, file path</p>
<label>IP Address:<input name="address" size="30" maxlength="40"> </label><br>
<label>Transmission Type:<input type="radio"name="trans_type"value="download">Download<br>
<input type="radio" name="trans_type" value="upload">Upload</label><br>
<label>File Path:<input name="file_path"></label><br> 7 <input type="SUBMIT" value="Send">
</form>

3.2 ftp安装(开发板)

 

使用busybox制作的根文件系统,没有ftp客户端工具,无法登录到其他的ftp服务器,需要自己编译ftp指令。

 

3.2.1 ftp客户端选择

 

在这里采用netkit­ftp­0.17.tar.gz解压

 

3.2.2 打补丁

 

在源码包检索目录下,可以看到netkit­ftp­0.17­cross.patch的补丁文件,在源码目录下导入:

patch ‐p1< netkit‐ftp‐0.17‐cross.patch

3.2.3 指定交叉编译工具

./configure ‐‐with‐c‐compiler=arm‐linux‐gcc

3.2.4 编译

 

直接编译会遇到以下错误:

make
glob.c: In function 'ftpglob':
glob.c:126:15: error: 'ARG_MAX' undeclared (first use in this function)
glob.c:126:15: note: each undeclared identifier is reported only once for each function it appears in
glob.c: In function 'ginit':
glob.c:167:11: error: 'ARG_MAX' undeclared (first use in this function)
glob.c: In function 'Gcat':
glob.c:582:32: error: 'ARG_MAX' undeclared (first use in this function)
make[1]: *** [glob.o] error 1
make[1]:leaving directory `/home/am335x/package/netkit‐ftp‐0.17/ftp'
make: *** [ftp.build] eooro 2

解决方法:

修改glob.c文件,在#include "glob.h"这一行下添加

#define ARG_MAX 16380 //数值可任意

之后重新make就可以得到ftp指令,复制到开发板即可。

但是开发板运行ftp会遇到:

ftp: ftp/tcp: unknown service

我们还需要在开发板上建立/etc/services文件,内容如下:

ftpdata 20/tcp
ftpdata 20/udp
ftp 21/tcp
ftp 21/udp

 

3.3 vsftpd安装(主机)

 

安装并启动vsftpd

 

sudo apt‐get install vsftpd
service vsftpd start

 

3.3.4 shell脚本编写

 

因为此次FTP传输需要通过网页完成,所以我这里写了两

 

shell脚本文件(ftp_upload.sh, ftp_download.sh),由网页读取目标主机IP地址,然后根据传输需要,最后通过cgi程序读取文件名称执行不同的传输操作。

 

ftp_upload.sh:

#!/bin/sh
ftp ‐v ‐n $1<<EOF
user edision soc2017
binary
cd /home/edision/Download
lcd uftp
prompt
put $2
bye
EOF
echo "commit to ftp successfully"

 

$1参数为目标主机的IP地址,$2为上传文件名

ftp_download.sh:

#!/bin/sh
ftp ‐v ‐n $1<<EOF
user edision soc2017
 binary
cd /home/edision/Download
 lcd uftp
prompt
get $2
bye
EOF
echo "download from ftp successfully"

$1参数为目标主机的IP地址,$2为下载文件名

 

 

3.3.5 cgi程序设计

 

#include <stdio.h>
 #include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define BURSIZE 2048
int hex2dec(char c);
void urldecode(char url[]);
int main(void)
{
char *data;
char address[100];
char trans_type[10];
char file_path[100];
printf("%s%c%c\n", "Content‐Type:text/html\n\n;charset=iso‐8859‐1", 13, 10);
printf("<TITLE>FTP Transmission</TITLE>\n");
printf("<H3>FTP Transmission</H3>\n");
data = getenv("QUERY_STRING");
if(data == NULL)
printf("<P>ERROR:no input setup or transmission meet with troubles!");
else if(sscanf(data, "address=%[0‐9,.]&trans_type=%[a‐z]&file_path=%[a‐ z0‐9A‐Z,.]", address, trans_type, file_path)!=3)
printf("<P>ERROR:input is invalid!");
else{
urldecode(file_path);
if (strcmp("upload", trans_type) == 0){
//system("./www/cgi‐bin/ftp_upload.sh " + address + ' ' + file_path);
if(fork()==0){
if(execl("/www/cgi‐bin/ftp_upload.sh", "ftp_upload.sh", address, file_p ath, NULL)<0)
perror("Error on upload!\n");
}
}
else if (strcmp("download", trans_type) == 0){
//system("./www/cgi‐bin/ftp_download.sh " + address + ' ' + file_path);
if(fork()==0){
if(execl("/www/cgi‐bin/ftp_download.sh", "ftp_upload.sh", address, file _path, NULL)<0)
perror("Error on upload!\n");
}
}
else
printf("Error:unknow instructions!\n");
}
}
int hex2dec(char c)
{
if ('0' <= c && c <= '9')
{
return c ‐ '0';}
else if ('a' <= c && c <= 'f')
{
return c ‐ 'a' + 10;
}
else if ('A' <= c && c <= 'F')
{
return c ‐ 'A' + 10;
}else
{
return ‐1;
}
}
void urldecode(char url[])
{
int i = 0;
int len = strlen(url);
int res_len = 0;
char res[BURSIZE];
for (i = 0; i < len; ++i)
{
char c = url[i];
if (c != '%')
{
res[res_len++] = c;
}
else
{
char c1 = url[++i];
char c0 = url[++i];
int num = 0;
num = hex2dec(c1) * 16 + hex2dec(c0);
res[res_len++] = num;
}
}
res[res_len] = '\0';
strcpy(url, res);
}

cgi程序接收到IP地址和文件名后,根据传输类型的不同,选择对应的脚本文件,然后执行。这里我的文件传输通过多线程是支持多用户连接的,可以极大提高传输效率。然后定义了一个URL译码程序,在用户传输特殊字符(这里主要是路径斜线),可以将传输的URL码转译为对应的特殊符号。

 

tipsystem不能调用shell脚本,必须使用execl。具体原因猜测应该是system运行权限的问题。

 

 

Part.4 云计算demo

 

4.1 什么是Socket

 

网络中的进程是通过socket来通信的,那什么是socket呢?socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭

 

close”模式来操作。我的理解就是Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)。

 

4.2 socket服务器端与客户端通信过程

 

4.2.1 socketTCP的握手建立连接

从图中可以看出,当客户端调用connect时,触发了连接请求,向服务器发送了SYN J包,这时connect进入阻塞状态;服务器监听到连接请求,即收到SYN J包,调用accept函数接收请求向客户端发送SYN K ACK J+1,这时accept进入阻塞状态;客户端收到服务器的SYN K ACK J+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立。

4.2.2 socketTCP的握手释放连接

某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M

另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;

一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个 FIN N

接收到这个FIN的源发送端TCP对它进行确认。

 

4.3 程序设计

 

4.3.1 服务器端:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<unistd.h>
#include<sys/un.h>
#include<sys/socket.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<netdb.h>
#include<signal.h>
#include<ctype.h>
#define PORT 5000
//获取子进程退出信号 在退出时给出提示信息
void sig_handler(int signo)
{
pid_t pid;
int stat;
pid = waitpid(1,&stat,WNOHANG);
while(pid>0){
printf("child process terminated (PID: %ld)\n",(long)getpid());
pid = waitpid(1,&stat,WNOHANG);
}
return;
}
typedef struct{
double a;
double b;
char s[10];
char echo_buf[1024];
}Mes;
int main(int argc ,char *argv[])
{
socklen_t clt_addr_len;
int listen_fd;
int com_fd;
int i;
static char send_buf[1024];
int len;
int port;
pid_t pid;
struct sockaddr_in clt_addr;
struct sockaddr_in srv_addr;
if(argc!=1)
{
printf("Usage: %s port\n",argv[0]);
exit(1);
}
if(signal(SIGCHLD,sig_handler) < 0){
perror("can not set the signal\n");
exit(1);
}
listen_fd = socket(PF_INET,SOCK_STREAM,0);
if(listen_fd < 0){
perror("can not create listening socket\n");
exit(1);
}
memset(&srv_addr,0,sizeof(srv_addr));
srv_addr.sin_family = AF_INET;
srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
srv_addr.sin_port = htons(PORT);
int t=bind(listen_fd,(struct sockaddr*)&srv_addr,sizeof(srv_addr));
if(t==‐1){
perror("can not bind server socket\n");
exit(1);
}
t=listen(listen_fd,20);
if(t==‐1){
perror("can not listen the client connect request\n");
close(listen_fd);
}
memset(send_buf,0,1024);
while(1)
{
len = sizeof(clt_addr);
com_fd = accept(listen_fd,(struct sockaddr*)&clt_addr,&len);
if(com_fd < 0){
if(errno == EINTR){
continue;
}
else{
perror("can not accept client connect request\n");
close(listen_fd);
exit(1);
}
}
pid = fork();
if(pid < 0){
perror("can not create the child process\n ");
close(listen_fd);
exit(1);
}else if(pid == 0){
Mes mes;
int n=sizeof(mes);
while((len = read(com_fd,&mes,n))>0){
if(strcmp(mes.s,"echo")!=0){
printf("Message from client(%d):%s %.2lf %.2lf\n",len,mes.s,mes.a,mes.b);
if(send_buf[0]=='@'){
break;
}
double result = 0;
if(strcmp(mes.s,"add")==0){
printf("加法\n");
result = mes.a+mes.b;
}else if(strcmp(mes.s,"sub")==0){
printf("减法\n");
result = mes.a mes.b;
}else if(strcmp(mes.s,"mul")==0){
printf("乘法\n");
result = mes.a * mes.b;
}else if(strcmp(mes.s,"div")==0){
printf("除法\n");
if(mes.b == 0){
printf("除数为0\n ");
result == 0;
}else{
result = mes.a / mes.b;
}
}else{
printf("不识别的操作\n");
}
printf("结果:result = %.2lf\n",result);
if(mes.b == 0 && (strcmp(mes.s,"div") == 0)){
sprintf(send_buf,"%s","除数为0 无意义\n");
}else{
sprintf(send_buf,"%.2lf",result);
}
}else{
printf("客户端的消息:%s\n",mes.echo_buf);
printf("回复的消息");
fgets(send_buf,1024,stdin);
}
write(com_fd,send_buf,strlen(send_buf));
}
close(com_fd);
return 0;
}else{
close(com_fd);
}
}
return 0;
}

 

设计思路:

 

本次设计的程序需要支持多用户连接,因此我设计了多线程连接的思想,来为每一个客户的访问申请一个子线程。所以首先定义了sgi_handler函数用于获取子进程退出信号,在退出时给出提示信息。然后在用户输入

 

add/sub/mul/div op1 op2来去执行运算以获得返回结果。

 

程序主体:

首先定义了一个结构体,存储操作符和数据。

子进程退出提示信号函数。

创建socket()描述符用于标示唯一的socket,然后赋予其协议域,socke类型以及指定协议。

bind()函数把ipv4地址和端口号赋予socket

listen()函数监听这个socket,如果客户端调用connect()发出连接请求,服务器就可以接收到这个请求。

TCP服务器端监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。

之后通过fork()函数创建子进程,通过read函数读取内容进行处理,并通过write函数将结果写回到客户端,并等待下一条命令的输入。

 

4.3.2 客户端程序设计

 

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<sys/un.h>
#include<unistd.h>
#include<netdb.h>
#include<errno.h>
#include<arpa/inet.h>
#define PORT 5000
typedef struct{
double a;
double b;
char s[10];
char echo_buf[1024];
}Mes;
int main(int argc,char *argv[])
{
int connect_fd;
int ret;
char recv_buf[1024];
int i;
int port;
int len;
static struct sockaddr_in srv_addr;
if(argc!=1){
printf("Usage: %s server_ip_address port \n",argv[0]);
exit(1);
}
connect_fd = socket(PF_INET,SOCK_STREAM,0);
if(connect_fd < 0){
perror("can not create communication socket\n");
exit(1);
}
memset(&srv_addr,0,sizeof(srv_addr));
srv_addr.sin_family = AF_INET;
srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
srv_addr.sin_port = htons(PORT);
if((connect(connect_fd,(struct
sockaddr*)&srv_addr,sizeof(srv_addr)))==‐1){
perror("can not to the server\n");
close(connect_fd);
exit(1);
}
memset(recv_buf,0,1024);
while(1){
Mes mes;
int n=sizeof(mes);
printf("请输入表达式:\n");
scanf("%s",mes.s);
if(strcmp(mes.s,"echo")==0){
fgets(mes.echo_buf,1024,stdin);
mes.a = 0;
mes.b = 0;
}else{
scanf("%lf%lf",&mes.a,&mes.b);
sprintf(mes.echo_buf,"%s","NULL");
}
write(connect_fd,&mes,n);
len = read(connect_fd,recv_buf,n);
if(strcmp(mes.s,"echo")==0){
printf("服务端回复的消息:%s\n",recv_buf);
}else if(len > 0){
double result;
sscanf(recv_buf,"%lf",&result);
printf("计算的结果: %.2lf\n",result);
if(recv_buf[0] == '@')
break;
}
}
close(connect_fd);
return 0;
}

 

设计思路:

 

发送add/sub/mul/div op1 op2指令到服务器端,获得返回的结果。结构相对服务器端不用考虑多线程的问题,结构较为简单。

 

首先定义了一个结构体,存储操作符和数据。

创建socket()描述符用于标示唯一的socket,然后赋予其协议域,socke类型以及指定协议。

bind()函数把ipv4地址和端口号赋予socket

listen()函数监听这个socket,如果客户端调用connect()发出连接请求,服务器就可以接收到这个请求。

客户端向服务端发送连接请求

连接成功之后向服务器发送命令并等待返回结果。

 

运行示例:

 

服务器端:

用户端:

感受体会:

这次实验首先是服务器的移植,让我了解了网页的制作。然后明白浏览器是怎么通过IP地址来去访问服务器,真是深感神奇,第一次做出来了自己期望的网页界面(虽然很简陋),从网页的学习我现在有明白了之前爬虫爬下来网页的内容的含义,对我以后制作自动下载的脚本帮助甚大。第二个作业是要在页面能够配置IP。我思索了整整两天,最开始是希望通过能够在网页上书写javascript来去修改本地的rcS文件,但是这个想法真的是纯属异想天开,最终选择放弃。后来经过仔细调研最终选择了通过cgi程序来去和浏览器完成数据的交互,这给我开启了一个新世界的大门,如果可以这样的话,我可以通过网页发送期望的数据来完成和开发板的交互。在此思路下,第三个实验室通过TCP协议完成文件的传输。原本我想的是在开发板上配置vsftpd,然后通过远程ftp访问,但是发现面临无法调用用户端的脚本文件的问题,因此转换了一下思路,在目标主机上安装vsftpd,在开发板上运行ftp,这种思维转为又给我带来了新的灵感。然后shell脚本也会编写了,嵌入式真是一门综合性的学科。最后socket编程的学习让我了解了服务器和用户是怎么完成网络通信的。以及了解了阿里云的工作原理,为以后从事相关专业打下了坚实的基础。今天是最后一次交作业了,借此机会,在此非常感谢老师的谆谆教导。本次课程收益匪浅,如果有幸从事相关行业,老师绝对是职业之路的引路人。最后再次表达感谢!

2019年7月19日 17:52
收藏