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
下载 boa0.94.13.tar.gz到虚拟机,然后执行解压:
tar xzf boa‐0.94.13.tar.gz
1.2.2 安装依赖包
boa需要安装工具bison,flex
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 (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

1.2.6 Boa的配置
修改boa/src目录下的boa.conf文件
(1)Group的修改
修改Group nobody为Group 0
(2)User的修改
修改User nobody为User 0
(3)ScriptsAlias的修改
修改ScriptAlias /cgbin/ /usr/lib/cgibin/ 为ScritpAlias /cgibin/ /www/cgibin/ (4)DocumentRoot的修改
修改DocumentRoot /var/www 为DocumentRoot /www
(5)ServerName的设置
修改#ServerName www.your.org.here为ServerName www.your.org.here
(6)AccessLog的设置
修改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文件里面保存
由网络页面输入的IP,netmask,gateway配置
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之后可以在浏览器看到对应三个输入框:
输入对应IP,Netmask,Gateway之后,发送到对应的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函数里面,可能会注意到%[09,.]这种不同于传统%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客户端选择
在这里采用netkitftp0.17.tar.gz,解压
3.2.2 打补丁
在源码包检索目录下,可以看到netkitftp0.17cross.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文件,内容如下:
ftp‐data 20/tcp
ftp‐data 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码转译为对应的特殊符号。
tip:system不能调用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 socket中TCP的握手建立连接

从图中可以看出,当客户端调用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 socket中TCP的握手释放连接

某个应用进程首先调用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编程的学习让我了解了服务器和用户是怎么完成网络通信的。以及了解了阿里云的工作原理,为以后从事相关专业打下了坚实的基础。今天是最后一次交作业了,借此机会,在此非常感谢老师的谆谆教导。本次课程收益匪浅,如果有幸从事相关行业,老师绝对是职业之路的引路人。最后再次表达感谢!