/****************************************************************************
*
* tinyhttpd-0.1.0_hacking
*
* 1.这是tinyhttpd-0.1.0版本中httpd.c(主程序)的源码,源码不到500行(除去注释).
* 2.通过分析、阅读该源码,可以一窥web服务器的大致工作机制.
* 3.知识量:
* 1.C语言;
* 2.Unix或类Unix系统编程;
* 3.微量的http协议(请求行、消息头、实体内容);
* 4.如何阅读别人的代码( 从main函数开始 :) );
* 4.tinyhttpd-0.1.0 文件结构如下:
* .
* |-- Makefile -------->makefile 文件
* |-- README -------->说明文档
* |-- htdocs -------->程序会到该文件夹下找对应html、cgi文件
* | |-- README -------->说明文档
* | |-- check.cgi -------->cgi 程序
* | |-- color.cgi ----^
* | `-- index.html -------->默认的 web 首页文件
* |-- httpd.c -------->你接下来要阅读的文件
* `-- simpleclient.c -------->没发现该文件有任何用处 @_@
* 5.如何阅读该文档:
* 1.linux下使用vi/vim配和ctags,windows下使用Source Insight,当然你也
* 可以用其他文本编辑器看.
* 2.先找到main函数,然后就可以开始阅读了,遇到对应的函数,就去看对应的
* 函数.
* 3.对于有些函数,本人没有添加注释,或者说本人觉得没必要.
* 4.祝您好运. :)
*
* 6.tinyhttpd-0.1.0版本下载url: http://sourceforge.net/projects/tinyhttpd/
*
* 如果您对本文有任何意见、提议,可以发邮件至zengjf42@163.com,会尽快回复.
* 本文的最终解释权归本人(曾剑锋)所有,仅供学习、讨论.
*
* 2015-3-1 阴 深圳 尚观 Var
*
***************************************************************************/ /* J. David's webserver */
/* This is a simple webserver.
* Created November 1999 by J. David Blackstone.
* CSE 4344 (Network concepts), Prof. Zeigler
* University of Texas at Arlington
*/
/* This program compiles for Sparc Solaris 2.6.
* To compile for Linux:
* 1) Comment out the #include <pthread.h> line.
* 2) Comment out the line that defines the variable newthread.
* 3) Comment out the two lines that run pthread_create().
* 4) Uncomment the line that runs accept_request().
* 5) Remove -lsocket from the Makefile.
*/
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <ctype.h>
#include <strings.h>
#include <string.h>
#include <sys/stat.h>
#include <pthread.h>
#include <sys/wait.h>
#include <stdlib.h> #define ISspace(x) isspace((int)(x)) #define SERVER_STRING "Server: jdbhttpd/0.1.0\r\n" void accept_request(int);
void bad_request(int);
void cat(int, FILE *);
void cannot_execute(int);
void error_die(const char *);
void execute_cgi(int, const char *, const char *, const char *);
int get_line(int, char *, int);
void headers(int, const char *);
void not_found(int);
void serve_file(int, const char *);
int startup(u_short *);
void unimplemented(int); /**
* accept_request 函数说明:
* 1.获取请求方式,目前只支持GET、POST请求;
* 2.在本程序中所有的POST请求、带参数的GET请求都都被定义为访问cgi程序;
* 3.从带参数的GET请求中分离出请求参数;
* 4.如果没有指定需要访问的文件,使用index.html文件作为默认访问文件;
* 5.检查需要访问的文件是否存在,以及其是否具有对应的权限;
* 6.根据是否是cgi程序访问,来执行对应的任务.
*/
void accept_request(int client)
{
/**
* 局部变量说明:
* 1.buf : buffer缩写,主要用于暂存从socket中读出来的数据;
* 2.numchars : 用于保存每次从socket中读到的字符的个数;
* 3.method : 用于保存请求方式,目前该软件只支持GET、POST这两种方式;
* 4.url : 用于保存访问文件信息,有些地方叫uri;
* 5.path : 用于保存文件路径;
* 6.i, j : 处理数据时的下标;
* 7.st : 在判断文件类型、是否存在的时候用到;
* 8.cgi : 是否调用cgi程序的标志.
*/
char buf[];
int numchars;
char method[];
char url[];
char path[];
size_t i, j;
struct stat st;
int cgi = ; /* becomes true if server decides this is a CGI
* program */
char *query_string = NULL; /**
* 判断程序是否是GET、POST请求两种的其中一种,如果不是则报错.
*/
numchars = get_line(client, buf, sizeof(buf));
i = ; j = ;
while (!ISspace(buf[j]) && (i < sizeof(method) - ))
{
method[i] = buf[j];
i++; j++;
}
method[i] = '\0'; if (strcasecmp(method, "GET") && strcasecmp(method, "POST"))
{
unimplemented(client);
return;
} /**
* 该程序把POST请求定义为cgi请求.
*/
if (strcasecmp(method, "POST") == )
cgi = ; /**
* 获取当前url,这里的url不过括网址,而是除去网址之后的东西,
* 如浏览器中输入:http://127.0.0.1:8080/example/index.html
* 得到的url:/example/index.html
* 在有些地方不称这个为url,称之为uri
*/
i = ;
while (ISspace(buf[j]) && (j < sizeof(buf)))
j++;
while (!ISspace(buf[j]) && (i < sizeof(url) - ) && (j < sizeof(buf)))
{
url[i] = buf[j];
i++; j++;
}
url[i] = '\0'; /**
* 每次运行的时候都会出现2次这个,目前还不知道是什么原因导致的原因,
* 这是本人在源代码的基础上添加的调试输出.
* url: /favicon.ico
* url: /favicon.ico
*/
printf("url: %s\n", url); /**
* 如果是GET请求,如果带了请求参数,那么也是cgi请求,并且从url中分离出请求参数
*/
if (strcasecmp(method, "GET") == )
{
query_string = url;
while ((*query_string != '?') && (*query_string != '\0'))
query_string++;
if (*query_string == '?')
{
cgi = ;
*query_string = '\0';
query_string++;
}
} /**
* 所有的需要的html文件、cgi程序都在htdocs文件夹中,
* 如果没有指定html文件,或者cgi程序,那么使用默认的index.html文件
* 作为目标输出文件.
*/
sprintf(path, "htdocs%s", url);
if (path[strlen(path) - ] == '/')
strcat(path, "index.html"); /**
* 检查要访问的文件的状态,如:
* 1.是否存在;
* 2.是否是一个文件夹;
* 3.如果是cgi程序,是否用于对应的权限.
* 当然如果执行stat时就出错了,那么,直接将socket中的数据读完,
* 然后返回没有找到相关内容的信息提示.
*/
if (stat(path, &st) == -) {
while ((numchars > ) && strcmp("\n", buf)) /* read & discard headers */
numchars = get_line(client, buf, sizeof(buf));
not_found(client);
}
else
{
if ((st.st_mode & S_IFMT) == S_IFDIR) /* 如果是一个文件夹 */
strcat(path, "/index.html");
if ((st.st_mode & S_IXUSR) ||
(st.st_mode & S_IXGRP) ||
(st.st_mode & S_IXOTH) ) /* 权限问题 */
cgi = ; /**
* 通过cgi变量来判断是执行cgi程序,还是仅仅是返回一个html页面.
*/
if (!cgi)
serve_file(client, path); /* 向客户端返回一个html文件 */
else
execute_cgi(client, path, method, query_string); /* 执行一个cgi程序 */
} close(client);
} void bad_request(int client)
{
char buf[]; sprintf(buf, "HTTP/1.0 400 BAD REQUEST\r\n");
send(client, buf, sizeof(buf), );
sprintf(buf, "Content-type: text/html\r\n");
send(client, buf, sizeof(buf), );
sprintf(buf, "\r\n");
send(client, buf, sizeof(buf), );
sprintf(buf, "<P>Your browser sent a bad request, ");
send(client, buf, sizeof(buf), );
sprintf(buf, "such as a POST without a Content-Length.\r\n");
send(client, buf, sizeof(buf), );
} /**
* 主要完成将resource指向的文件内容拷贝输出到客户端浏览器中
*/
void cat(int client, FILE *resource)
{
char buf[]; fgets(buf, sizeof(buf), resource);
while (!feof(resource))
{
send(client, buf, strlen(buf), );
fgets(buf, sizeof(buf), resource);
}
} void cannot_execute(int client)
{
char buf[]; sprintf(buf, "HTTP/1.0 500 Internal Server Error\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "Content-type: text/html\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "<P>Error prohibited CGI execution.\r\n");
send(client, buf, strlen(buf), );
} void error_die(const char *sc)
{
perror(sc);
exit();
} void execute_cgi(int client, const char *path,
const char *method, const char *query_string)
{
/**
* 局部变量说明:
* 1.buf : buffer缩写;
* 2.cgi_output : 用于保存输出管道的文件描述符;
* 3.cgi_input : 用于保存输入管道的文件描述符;
* 4.pid : 进程pid,最后父进程退出之前,等待子进程先退出,
* 并回收相关的资源,这部分工作主要由waitpid()来完成;
* 5.status : 在waitpid()中用于保存子进程的退出状态,本程序没有具体使用;
* 6.i : 计数器;
* 7.c : POST读取请求参数时,读取到的字符保存在这里;
* 8.numchars : 读取的字符个数;
* 9.conten_length : 内容实体的字符数;
*/
char buf[];
int cgi_output[];
int cgi_input[];
pid_t pid;
int status;
int i;
char c;
int numchars = ;
int content_length = -; /**
* 在本程序中,GET请求的消息头没有任何用处,直接处理掉就行了,
* 而如果是POST请求,需要的消息头中的获取实体的大小,也就是Content-Length:后面跟的数字
*/
buf[] = 'A'; buf[] = '\0';
if (strcasecmp(method, "GET") == )
while ((numchars > ) && strcmp("\n", buf)) /* read & discard headers */
numchars = get_line(client, buf, sizeof(buf));
else /* POST */
{
numchars = get_line(client, buf, sizeof(buf));
while ((numchars > ) && strcmp("\n", buf))
{
buf[] = '\0';
if (strcasecmp(buf, "Content-Length:") == )
content_length = atoi(&(buf[]));
numchars = get_line(client, buf, sizeof(buf));
}
if (content_length == -) {
bad_request(client);
return;
}
} /**
* 返回返回行信息.
*/
sprintf(buf, "HTTP/1.0 200 OK\r\n");
send(client, buf, strlen(buf), ); /**
* 父子进程通过管道通信.
*/
if (pipe(cgi_output) < ) {
cannot_execute(client);
return;
}
if (pipe(cgi_input) < ) {
cannot_execute(client);
return;
} /**
* 创建子进程,用于执行cgi程序,父进程接受子进程的结果,并返回给浏览器
*/
if ( (pid = fork()) < ) {
cannot_execute(client);
return;
}
if (pid == ) /* child: CGI script */
{
char meth_env[]; //cgi 请求方式环境变量
char query_env[]; //cgi GET请求参数环境变量
char length_env[]; //cgi POST请求参数内容大小环境变量 /**
* 重定向标准输入输出,并设置好对应的环境变量.
*/
dup2(cgi_output[], );
dup2(cgi_input[], );
close(cgi_output[]);
close(cgi_input[]);
sprintf(meth_env, "REQUEST_METHOD=%s", method);
putenv(meth_env);
if (strcasecmp(method, "GET") == ) {
sprintf(query_env, "QUERY_STRING=%s", query_string);
putenv(query_env);
}
else { /* POST */
sprintf(length_env, "CONTENT_LENGTH=%d", content_length);
putenv(length_env);
}
/* 执行对应的程序 */
execl(path, path, NULL);
exit();
} else { /* parent */
close(cgi_output[]);
close(cgi_input[]);
/**
* 对于POST请求,将实体中的请求参数通过管道传送到cgi程序中
*/
if (strcasecmp(method, "POST") == )
for (i = ; i < content_length; i++) {
recv(client, &c, , );
write(cgi_input[], &c, );
}
/**
* 读取cgi程序的执行结果,返回给浏览器
*/
while (read(cgi_output[], &c, ) > )
send(client, &c, , ); close(cgi_output[]);
close(cgi_input[]);
/**
* 等待子进程运行结束,并回收子进程的资源,
* 防止出现孤儿进程
*/
waitpid(pid, &status, );
}
} int get_line(int sock, char *buf, int size)
{
/**
* 局部变量说明:
* 1.i : 数组下标计数,不能大于size;
* 2.c : 每次读到的字符保存在这里面;
* 3.n : 每次读到的字符个数.
*/
int i = ;
char c = '\0';
int n; /**
* 一直读到buf满了,或者遇到了'\n'为止.
*/
while ((i < size - ) && (c != '\n'))
{
n = recv(sock, &c, , );
/* DEBUG printf("%02X\n", c); */
if (n > )
{
/**
* 读到'\r'也算是结束,通过判断后面有没有跟'\n'来判断是否要将下
* 一个字符取出来,并且无论'\r'后面跟不跟'\n',都将'\r'换成'\n'.
*/
if (c == '\r')
{
n = recv(sock, &c, , MSG_PEEK);
/* DEBUG printf("%02X\n", c); */
if ((n > ) && (c == '\n'))
recv(sock, &c, , );
else
c = '\n';
}
buf[i] = c;
i++;
}
else
c = '\n';
}
buf[i] = '\0'; /* 字符串结尾 */ return(i);
} void headers(int client, const char *filename)
{
char buf[];
(void)filename; /* could use filename to determine file type */ strcpy(buf, "HTTP/1.0 200 OK\r\n");
send(client, buf, strlen(buf), );
strcpy(buf, SERVER_STRING);
send(client, buf, strlen(buf), );
sprintf(buf, "Content-Type: text/html\r\n");
send(client, buf, strlen(buf), );
strcpy(buf, "\r\n");
send(client, buf, strlen(buf), );
} void not_found(int client)
{
char buf[]; sprintf(buf, "HTTP/1.0 404 NOT FOUND\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, SERVER_STRING);
send(client, buf, strlen(buf), );
sprintf(buf, "Content-Type: text/html\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "<HTML><TITLE>Not Found</TITLE>\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "<BODY><P>The server could not fulfill\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "your request because the resource specified\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "is unavailable or nonexistent.\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "</BODY></HTML>\r\n");
send(client, buf, strlen(buf), );
} void serve_file(int client, const char *filename)
{
/**
* 局部变量说明:
* 1.resource : 打开的文件的文件指针;
* 2.numchars : 每次读到的字符个数;
* 3.buf : buffer的缩写.
*/
FILE *resource = NULL;
int numchars = ;
char buf[]; /**
* 在本程序中消息头对于纯GET请求没有什么用,直接读取丢掉.
*/
buf[] = 'A'; buf[] = '\0';
while ((numchars > ) && strcmp("\n", buf)) /* read & discard headers */
numchars = get_line(client, buf, sizeof(buf)); resource = fopen(filename, "r");
if (resource == NULL)
not_found(client);
else
{
/* 发送消息头 */
headers(client, filename);
/* 发送内容实体 */
cat(client, resource);
}
fclose(resource);
} /**
* startup 函数完成内容:
* 1.获取一个作为服务器的socket;
* 2.绑定服务器端的socket;
* 3.通过判断参数port的值,确定是否需要动态分配端口号;
* 4.服务器开启监听;
* 5.返回服务器段的socket文件描述符.
*/
int startup(u_short *port)
{
/**
* 局部变量说明:
* 1.httpd : 保存服务器socket描述符,并作为返回值返回;
* 2.name : 用于保存服务器本身的socket信息,创建服务器.
*/
int httpd = ;
struct sockaddr_in name; httpd = socket(PF_INET, SOCK_STREAM, );
if (httpd == -)
error_die("socket"); memset(&name, , sizeof(name));
name.sin_family = AF_INET;
name.sin_port = htons(*port);
name.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < )
error_die("bind"); if (*port == ) /* if dynamically allocating a port */
{
int namelen = sizeof(name);
if (getsockname(httpd, (struct sockaddr *)&name, &namelen) == -)
error_die("getsockname");
*port = ntohs(name.sin_port);
} if (listen(httpd, ) < )
error_die("listen");
return(httpd);
} void unimplemented(int client)
{
char buf[]; sprintf(buf, "HTTP/1.0 501 Method Not Implemented\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, SERVER_STRING);
send(client, buf, strlen(buf), );
sprintf(buf, "Content-Type: text/html\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "<HTML><HEAD><TITLE>Method Not Implemented\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "</TITLE></HEAD>\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "<BODY><P>HTTP request method not supported.\r\n");
send(client, buf, strlen(buf), );
sprintf(buf, "</BODY></HTML>\r\n");
send(client, buf, strlen(buf), );
} /**********************************************************************/ int main(void)
{
/**
* 局部变量说明:
* 1.server_sock : 服务器端的socket描述符;
* 2.port : 服务器端的socket端口号,如果是0的,startup()将会采用
* 自动生成的方式生成新的端口号供使用;
* 3.client_sock : 客户端连接进来产生的客户端socket描述符;
* 4.client_name : 用于保存客户端连接进来的socket信息;
* 5.client_name_len : struct sockaddr_in结构体的大小,在accpet的时候
* 需要用到,这个参数必须传,否则会出错;
* 6.newthread : 用于保存新创建的线程的ID.
*/
int server_sock = -;
u_short port = ;
int client_sock = -;
struct sockaddr_in client_name;
int client_name_len = sizeof(client_name);
pthread_t newthread; /**
* startup 函数完成内容:
* 1.获取一个作为服务器的socket;
* 2.帮定服务器断的sockt;
* 3.通过判断参数port的值,确定是否需要动态分配端口号;
* 4.服务器开启监听.
*/
server_sock = startup(&port);
printf("httpd running on port %d\n", port); while ()
{
/**
* 等待客户端的连接,使用client_name保存客户端socket信息,
* client_name_len是client_name对应结构体的长度.
*/
client_sock = accept(server_sock,
(struct sockaddr *)&client_name,
&client_name_len);
if (client_sock == -)
error_die("accept");
/**
* 创建一个新的线程来处理任务,并把客户端的socket描述符作为参数传给accept_request,
* accept_request 函数说明:
* 1.获取请求方式,目前只支持GET、POST请求;
* 2.在本程序中所有的POST请求、带参数的GET请求都都被定义为访问cgi程序;
* 3.从带参数的GET请求中分离出请求参数;
* 4.如果没有指定需要访问的文件,使用index.html文件作为默认访问文件;
* 5.检查需要访问的文件是否存在,以及其是否具有对应的权限;
* 6.根据是否是cgi程序访问,来执行对应的任务.
*/
if (pthread_create(&newthread , NULL, accept_request, client_sock) != )
perror("pthread_create");
} /**
* 不知道为什么,这条语句在while外边,竟然会影响到程序的关闭 :(
* 这行代码注释掉才能连续访问,不注释,只能访问一次,所以直接注释了
* 反正程序停止都使用ctrl+c,不影响程序的运行.
*/
//close(server_sock); return();
}

tinyhttpd-0.1.0_hacking的更多相关文章

  1. ZAM 3D 制作简单的3D字幕 流程(二)

    原地址:http://www.cnblogs.com/yk250/p/5663907.html 文中表述仅为本人理解,若有偏差和错误请指正! 接着 ZAM 3D 制作简单的3D字幕 流程(一) .本篇 ...

  2. ZAM 3D 制作3D动画字幕 用于Xaml导出

    原地址-> http://www.cnblogs.com/yk250/p/5662788.html 介绍:对经常使用Blend做动画的人来说,ZAM 3D 也很好上手,专业制作3D素材的XAML ...

  3. 微信小程序省市区选择器对接数据库

    前言,小程序本身是带有地区选着器的(网站:https://mp.weixin.qq.com/debug/wxadoc/dev/component/picker.html),由于自己开发的程序的数据是很 ...

  4. osg编译日志

    1>------ 已启动全部重新生成: 项目: ZERO_CHECK, 配置: Debug x64 ------1> Checking Build System1> CMake do ...

  5. tinyhttpd源码分析

    我们经常使用网页,作为开发人员我们也部署过httpd服务器,比如开源的apache,也开发过httpd后台服务,比如fastcgi程序,不过对于httpd服务器内部的运行机制,却不是非常了解,前几天看 ...

  6. tinyhttpd服务器源码学习

    下载地址:http://sourceforge.net/projects/tinyhttpd/ /* J. David's webserver */ /* This is a simple webse ...

  7. HTTP服务器的本质:tinyhttpd源码分析及拓展

    已经有一个月没有更新博客了,一方面是因为平时太忙了,另一方面是想积攒一些干货进行分享.最近主要是做了一些开源项目的源码分析工作,有c项目也有python项目,想提升一下内功,今天分享一下tinyhtt ...

  8. Tinyhttpd 代码学习

    前阵子,参加了实习生面试,被面试官各种虐,问我说有没有读过一些开源的代码.对于只会用框架的我来说真的是硬伤啊,在知乎大神的推荐下在EZLippi-浮生志找了一些源代码来阅读,于是从小型入手,找了Tin ...

  9. Tinyhttpd阅读笔记

    1.简介 tinyhttpd是一个开源的超轻量型Http Server,阅读其源码,可以对http协议,微型服务器有进一步的了解. 源码链接: 参考博客:tinyhttpd源码分析 2.笔记 ---- ...

随机推荐

  1. H-The Cow Lineup(POJ 1989)

    The Cow Lineup Time Limit: 1000MS   Memory Limit: 30000K Total Submissions: 5367   Accepted: 3196 De ...

  2. 绝对应当收藏的10个实用HTML5代码片段(转)

    HTML5绝对是一个流行元素,受到如此多的公司组织的追捧,作为极客来说,岂能错过呢?在今天这篇文章中,我们将分享一些超实用的HTML5的代码片段,相信大家一定会喜欢! 正确的嵌入flash 如果你经常 ...

  3. XAML-1

    1.XAML Extension Application Marked Language,是WPF技术中专门用来设计UI的语言.XAML是从XML派生出来的,是一种声明式语言,当你看到一个标签,就是声 ...

  4. 原创:整理编辑jQuery全部思维导图【附下载地址】

    主图 全部图已经打包:下载地址 2. 3. 4. 5. 6. 附上一点简单说明 Dom对象和jquer对象之间的转化 如何将一个jquery对象转换为DOM对象? test是一个span元素 var ...

  5. 为什么要使用 F#?

      对于小部分 .NET 程序员来说,学习一门 .NET Framework 函数化语言无疑将使自己在编写功能强大软件方面前进一大步.而对其他程序员来说,学习 F# 的理由就因人而异了.F# 能为开发 ...

  6. layer.js定制弹窗

    <button>点击</button> <div class="order" id="order"> 定制 </div ...

  7. 使用Matrix控制图像或组件变换的步骤

    1.获取Matrix对象,该Matrix对象既可新创建,也可直接获取其他对象内封装的Matrix(例如Transformation对象内部) 2.调用Matrix的方法进行平移.旋转.缩放.倾斜等. ...

  8. 利用开源框架Volley来下载文本和图片。

    Android Volley是Android平台上很好用的第三方开源网络通信框架.使用简单,功能强大. 下载连接地址:http://download.csdn.net/detail/zhangphil ...

  9. 常见JS(JavaScript)冲突解决方法

    1.一般JS冲突解决办法 a.最容易出现的就是js的命名冲突 ①.变量名冲突 变量有全局变量和局部变量当全局变量变量和局部变量名称一致时,就会js冲突,由于变量传递数值或地址不同就会产生JavaScr ...

  10. 入門必學NO.1 Android 初學特訓班(第四版) 目錄

    第 01 章 敲開 Android 的開發大門工欲善其事,必先利其器,要學習 Android 應用程式,先取得功能強大的開發工具,就可讓學習事半功倍. 1.1 Android 是啥米?1.2 建構 A ...