写在前面的话:也没有做过源码分析,确实拿到了源码之后以前就只是看看源码里面坐着写的那些很给力的工具类之类的,比如以前看zermoq,czmq, lighthttpd这些大神级源码,我大都还是停留在看看他们写的工具类,比如:hash, map, list, arraylist等等这些工具集拿来然后用在我自己的工程里面,这么着确实给我的工作带来了很多的方便,但是也是凸显出我对很多的源码其实并未深入其中的。
这次来到上海了,工作不是很忙,所以还是想专心下来,看看所有的源码,提高自己的能力为主要目标,源码分析每个人有每个人的看源码的方式,其实我看源码的方式就比着网上的大神来说还是比较low的,希望看我的文档的不要嫌弃才对哈,呵呵,有些的不对的地方,或者不太符合各位的看源码习惯的同仁,希望给我提提意见,我后面也按照各位看源码的方式阅读试试,是不是更有效率一点。
我看源码其实就分3步:
1:看一下源码的大致的结构,这一步,我其实就看看,OK源码里面有工具类,有业务类,源码主次分的很清楚,就行了
2:把所有更业务相关的工具类全部通读一遍,这一步,我就只单纯的看源码里面的工具类,比如log, list等等,这些东西反正跟这个工程业务来说关系不是很大,拿出来以后我们自己可以在我们自己的工程里面也能使用。
3:有了上面工具类看完之后,开始看工程业务类的代码,OK这就是主线了,看完了基本上整个工程就相当于有了灵魂,把第二步的工具也都串联起来了。
这次源码分析我就先拿wget来下手了,为什么呢?随手从自己的Source Insight工程里面搞了一个出来了,也没什么目的,哈哈!
好了,废话不说了,那首先得说说wget的作用
1、下载整个http或者ftp站点。
2、断点续传。
3、批量下载。
4、选择性的下载。
5、密码和认证。
6、利用代理服务器进行下载。
目前分析的这一版的wget是1.9是支持ssl的,不用再使用curl去下载了。
下面正式开始源码分析过程;
先组织一下文件结构吧:
打开wget1.9/src目录,
工具类的.C文件如下
url.c+url.h : url工具类
hash.c+hash.h : hash表类
gnu-md5.c + gnu-md5.h : md5处理类
无依赖文件
Options.h, sysdep.h, log.c , snprintf.c , alloca.c
ansi2knr.c, cmpt.c
剩下的文件就是wget整个工程都会用到基础类库+业务处理类,这些文件之间大都会相互依赖,到了那些地方我再一一说明也不迟,先从最简单,无任何依赖的文件开始看,化繁为简。
全文就一个结构体,很清晰
struct options
{
int verbose;/* 冗长模式(这是缺省设置)? */
int quiet;/* 安静模式(没有输出) */ 是否安静
int ntry;/* 设定最大尝试链接次数(0 表示无限制). */
int retry_connrefused;/* 连接拒绝重试. */
int background;/* 是不是工作在后台*/
int kill_longer;/* 超过指定的content-length” 是够拒收消息*/
int ignore_length; /* 是否忽视“content-length”*/
int recursive;/* 是否递归*/
int spanhost;/*当递归时转到外部主机*/
int relative_only;/* 仅仅跟踪相对链接*/
int no_parent;/* 不要追溯到父目录*/
int reclevel;/* 最大递归深度 (inf 或 0 代表无穷).*/
int dirstruct;/* 强制创建目录*/
int no_dirstruct;/* 不创建目录*/
int cut_dirs;/* 忽略cut_dirs层远程目录*/
int add_hostdir;/* 是否增加主机目录*/
int noclobber;/* 不要覆盖存在的文件或使用.#前缀*/
char *dir_prefix;/* 将文件保存到目录 PREFIX/...*/
char *lfilename;/* 日志文件 */
char *input_filename;/* input文件名 */
int force_html;/* 把输入文件当作HTML格式文件对待? */
int spider;/* 不下载任何东西 */
char **accepts;/* 分号分隔的被接受扩展名的列表 */
char **rejects;/* 分号分隔的不被接受的扩展名的列表 */
char **excludes;/* 不被包含目录的列表*/
char **includes;/* 允许目录的列表 */
char **domains;/* 分号分隔的被接受域的列表 */
char **exclude_domains; /*分号分隔的不被接受的域的列表*/
int dns_cache;/* 是不是缓存dns */
char **follow_tags; /* 分号分隔的被跟踪的HTML标签的列表. */
char **ignore_tags; /* 分号分隔的被忽略的HTML标签的列表*/
int follow_ftp;/* 跟踪HTML文档中的FTP链接*/
int retr_symlinks;/* 在递归的时候,将链接指向文件(而不是目录)*/
char *output_document;/* documents被打印的输出文件*/
int od_known_regular;/* 输出文档是不是一个可操作的普通文件,i.e. not `-' or a device file. */
FILE *dfp;/* 输出文档的文件指针 */
int always_rest;/* Always use REST. */
char *ftp_acc;/* FTP 用户名 */
char *ftp_pass;/* FTP 密码 */
int netrc;/* 是否读取 .netrc文件 */
int ftp_glob;/* 打开或关闭文件名的 globbing机制*/
int ftp_pasv;/* 使用被动传输模式 (缺省值). */
char *http_user;/* HTTP 用户名 */
char *http_passwd;/* HTTP 密码 */
char *user_header;/* 在headers中插入字符串 STRING*/
int http_keep_alive;/* 是否关闭 HTTP活动链接 (永远链接).*/
int use_proxy;/* 是否使用代理 */
int allow_cache;/* 允许服务端缓存? */
char *http_proxy, *ftp_proxy, *https_proxy;
char **no_proxy;
char *base_href; /*将URL作为在-F -i参数指定的文件中出现的相对链接的前缀*/
char *progress_type;/* p设定进程条标记. */
char *proxy_user; /*设定代理的用户名为 USER*/
char *proxy_passwd; /*设定代理的密码为 PASS*/
double read_timeout;/* 读写 超时. */
double dns_timeout;/* DNS超时. */
double connect_timeout;/* 连接超时. */
int random_wait;/* 在下载之间等待0...2*WAIT秒*/
double wait;/* 两次尝试之间间隔SECONDS秒 */
double waitretry;/* 在重新链接之间等待1...SECONDS秒 */
int use_robots;/* 关注 robots.txt? */
long limit_rate;/* 限制下载速度 */
LARGE_INT quota;/* 下载和存储的最大文件大小 */
int numurls;/* 成功下载的url的数量*/
int server_response;/* 是否打印服务端的response? */
int save_headers;/* 保存HTTP头到文件 */
#ifdef ENABLE_DEBUG
int debug;/* Debugging on/off */
#endif
int timestamping;/* 不要重新下载文件除非比本地文件新 */
int backup_converted;/* 在转换文件X之前,将之备份为 X.orig*/
int backups;/* Are numeric backups made? */
char *useragent;/* 设定代理的名称为 AGENT而不是 Wget/VERSION. */
char *referer;/* 在HTTP请求中包含 `Referer: URL'头 */
int convert_links;/* 转换非相对链接为相对链接 */
int remove_listing;/* 是否不移走 `.listing'文件 */
int htmlify;/* Do we HTML-ify the OS-dependent
listings? */
char *dot_style;
long dot_bytes;/* How many bytes in a printing dot. */
int dots_in_line;/* How many dots in one line. */
int dot_spacing;/* How many dots between spacings. */
int delete_after;/* 文件下载后是否被删除 */
int html_extension;/* 将所有text/html文档以.html扩展名保存*/
int page_requisites;/* 下载显示HTML文件的所有图片 * 递归下载中的包含和不包含(accept/reject) */
char *bind_address;/* 指定本地使用地址(主机名或IP,当本地有多个IP或名字时使用). */
#ifdef HAVE_SSL
char *sslcadir;/* CA 目录 (hash files) */
char *sslcafile;/* CA File to use */
char *sslcertfile;/* 可选客户端证书. */
char *sslcertkey;/* 可选客户端证书的KEYFILE */
int sslcerttype;/* 0 = PEM / 1=ASN1 (DER) */
int sslcheckcert;/* 0 do not check / 1 check server cert */
char *sslegdsock; /* optional socket of the egd daemon */
int sslprotocol;/* 0 = auto / 1 = v2 / 2 = v3 / 3 = TLSv1 */
#endif /* HAVE_SSL */
int cookies;/*使用 cookies.*/
char *cookies_input;/*在开始会话前从文件 FILE中加载cookie*/
char *cookies_output;/*在会话结束后将 cookies保存到 FILE文件中*/
char *post_data;/* POST query string */
char *post_file_name;/* File to post */
enum {
restrict_unix,
restrict_windows
} restrict_files_os;/* file name restriction ruleset. */
int restrict_files_ctrl;/* non-zero if control chars in URLs
are restricted from appearing in
generated file names. */
int strict_comments;/* whether strict SGML comments are
enforced. */
};
extern struct options opt;
Sysdep.h文件这个文件主要定义了一些各种系统之间差别的东西,比如windows系统,aix,sunos, OS/2系统缺少或者应该定义的东西;比如Linux libc5没有MAP_FAILED这个宏的定义;SunOS系统没有提供strstr,strtok这个函数的定义等�O,还有一切比较旧的编译器Watcom需要自定义malloc.h文件之类的。
仅说明一下下面代码
#ifndef HAVE_U_INT32_T
# if SIZEOF_INT == 4
typedef unsigned int u_int32_t;
# else
# if SIZEOF_LONG == 4
typedef unsigned long u_int32_t;
# else
# if SIZEOF_SHORT == 4
typedef unsigned short u_int32_t;
# else
#error "Cannot determine a 32-bit type"
# endif
# endif
上面代码提供了有些平台不提供u_int32_t的定义。尽管关于整数的大小是跟平台无关的,一些代码也是需要32比特的整数类型的,这样的代码应该用u_int32_t(除了gnu-md5.[ch], 对于可移植性和跨平台它用自己的检测)
Safe-ctype.[ch]文件解析
这是一个标准库 <ctype.h>的兼容的可替代版本,有下面的特性:
- 实现了所有的C99要求的isxxx()函数
- 实现了一些解析类C语言非常有用的字符类
- 没有改变依赖于当前环境的特征
- 对于signed或者unsigned char之间的所有值表现都正确
为了避免冲突,头文件定义isxxx函数都是大写的,例如,ISALPHA == isalpha
Wget.h -- 工程头文件
这个文件包含了一些在其他地方不合适的定义,同时还有一些有用的include,比如讨厌的TIME_H
这个头文件还是挺重要的,下面我一面翻译然后一面相信解释一下,这个文件的理解对后面很多源码文件理解都带有很大的关联性。
/*查看是不是编译的时候打开了debug的开关*/
#ifndef ENABLE_DEBUG
# define NDEBUG
#endif
/**下面这个PARAMS我想很多人不是很清楚,简单说明一下
* 就简单记住,这个PARAMS宏是为了代码的可移植性
* 因为早期的C语言声明函数的时候都不办函参数,是直接写在实现中* 的, 肯定有人见过下面的函数实现
* int test()
* int a, b;
* { return a - b; }
* OK ,当然现在C语言编译器都没有这样的,这么仅仅为了兼容老的 *编译器
*/
#ifndef PARAMS
# if PROTOTYPES
# define PARAMS(args) args
# else
# define PARAMS(args) ()
# endif
#endif
/**
* 下面的宏判断仅仅是为了做程序的国际化和本地化使用
* 对就是这个函数gettext,有需要的可以自己查看一个linux
* 国际化和本地化的资料
*/
#ifdef HAVE_NLS
# define _(string) gettext (string)
# ifdef HAVE_LIBINTL_H
# include <libintl.h>
# endif /* HAVE_LIBINTL_H */
#else /* not HAVE_NLS */
# define _(string) string
#endif /* not HAVE_NLS */
下面的宏也是一个gettext的用于const字符串的无操作版本,其实就是直接返回了这个string
#define N_(string) (string)
OK ,下面是关于debug消息本地化的一个笔记
翻译:你会发现DEBUGP消息也是有意被标记为可翻译的,原因如下:
1:debug消息不是给用户看的,而是给开发者看的,因此debug消息更多的是应该是源码注释而不是直接程序里面输出
2:debug消息有很多,但大多数是随机而且琐碎的,会有很多无用功
3:最后,debug消息对我来说是wget的问题的一个线索,如果我得到了一个debug的消息是一个我都不理解的语言,那还是算了,语言都是一个问题了!
#define DO_NOTHING do {} while (0)
/* 你看看下面的宏定义,就是说,如果你打开了debug开关,你是用DEBUGP宏传入X参数,就会把X打印出来,相反啥也不做 */
#ifdef ENABLE_DEBUG
# define DEBUGP(x) do { if (opt.debug) { debug_logprintf x; } } while (0)
#else /* not ENABLE_DEBUG */
# define DEBUGP(x) DO_NOTHING
#endif /* not ENABLE_DEBUG */
/**
* 该__attribute__属性可以给被声明的函数加上类似printf或者scanf的* 特征,它可以使编译器检查函数声明和函数实际调用参数之间的格式化字* 符串是否匹配。该功能十分有用,尤其是处理一些很难发现的bug。
*/
#ifdef __GNUC__
# define GCC_FORMAT_ATTR(a, b) __attribute__ ((format (printf, a, b)))
#else /* not __GNUC__ */
# define GCC_FORMAT_ATTR(a, b)
#endif /* not __GNUC__ */
/*下面的枚举和函数都是来自log.c,但是因为它到处使用,所以定义在这了。我给大家框起来,这么好看*/
下面这个枚举说明了日志的级别 enum log_options { LOG_VERBOSE, LOG_NOTQUIET, LOG_NONVERBOSE, LOG_ALWAYS };
/** *<stdarg.h>这个头文件我觉得大家应该知道这家伙定义了什么* 了,对,就是,va_start, va_arg,va_end这三个可变参数宏,就是* 为了函数的可变参数存在的 * logprintf 第2个参数是格式化字符串,从第3个参数开始格式* 化参数 * debug_logprintf 第一个参数就是格式化字符串 */ #ifdef HAVE_STDARG_H void logprintf PARAMS ((enum log_options, const char *, ...)) GCC_FORMAT_ATTR (2, 3); void debug_logprintf PARAMS ((const char *, ...)) GCC_FORMAT_ATTR (1, 2); #else /* not HAVE_STDARG_H */ void logprintf (); void debug_logprintf (); #endif /* not HAVE_STDARG_H */
下面就是一些日志处理函数了
void logputs PARAMS ((enum log_options, const char *)); void logflush PARAMS ((void)); void log_set_flush PARAMS ((int)); int log_set_save_context PARAMS ((int)); |
/*下面的一些就是来自于utils.c里面的,这些也是到处使用,也给放到wget.h这个头文件里面来了*/
#ifndef DEBUG_MALLOC /*下面的宏是没有开启debug_malloc开关使用的内存操作*/ #define xmalloc xmalloc_real #define xrealloc xrealloc_real #define xstrdup xstrdup_real #define xfree free
void *xmalloc_real PARAMS ((size_t)); void *xrealloc_real PARAMS ((void *, size_t)); char *xstrdup_real PARAMS ((const char *));
#else /* DEBUG_MALLOC */ /*下面的宏是开启debug_malloc开关使用的内存操作,能看到宏后面都带上了debug字段*/ #define xmalloc(s) xmalloc_debug (s, __FILE__, __LINE__) #define xfree(p) xfree_debug (p, __FILE__, __LINE__) #define xrealloc(p, s) xrealloc_debug (p, s, __FILE__, __LINE__) #define xstrdup(p) xstrdup_debug (p, __FILE__, __LINE__)
void *xmalloc_debug PARAMS ((size_t, const char *, int)); void xfree_debug PARAMS ((void *, const char *, int)); void *xrealloc_debug PARAMS ((void *, size_t, const char *, int)); char *xstrdup_debug PARAMS ((const char *, const char *, int)); |
/* wget日志文件的名称 */
#define DEFAULT_LOGFILE "wget-log"
#define MD5_HASHLEN 16
OK, 写到这里了,系统有关的一些东西也差不多都解释了,下面就是一些很有用的宏定义,后面的代码中会用的到的,你可以把这些宏定义弄走,你以后写代码说不定也用上了,很多人都说宏定义不好,很多的C语言教程里面也说,宏和goto是很有争议性的东西,但是我觉得存在就有理由,很多大师级的任务写出来的宏定义看起来还是很爽的,比如说ttl模板,看起来很吃力,但是也比C++的boost宏模板生成写的要好得多了,呵呵,个人谬论了!
/* 看一个字符串是不是仅仅是一个连字符 */
#define HYPHENP(x) (*(x) == '-' && !*((x) + 1))
/* 最小值宏 */
#define MINVAL(x, y) ((x) < (y) ? (x) : (y))
/*16进制转10进制,X必须满足isxdigit*/
#define XDIGIT_TO_NUM(x) ((x) < 'A' ? (x) - '0' : TOUPPER (x) - 'A' + 10)
/*转换ascii码16进制X,Y的序列到0-255的数值*/
#define X2DIGITS_TO_NUM(h1, h2) ((XDIGIT_TO_NUM (h1) << 4) + XDIGIT_TO_NUM (h2))
/*把一个[0,16)范围的数转换成16进制的ascii表示,A-F大写*/
#define XNUM_TO_DIGIT(x) ("0123456789ABCDEF"[x])
/*把一个[0,16)范围的数转换成16进制的ascii表示,A-F小写*/
#define XNUM_TO_digit(x) ("0123456789abcdef"[x])
/* 返回已经初始化的数值元素的数量
For example:
static char a[] = "foo"; -- countof(a) == 4 (for terminating \0)
int a[5] = {1, 2}; -- countof(a) == 5
char *a[] = { -- countof(a) == 3
"foo", "bar", "baz"
}; */
#define countof(array) (sizeof (array) / sizeof (*(array)))
#define alloca_array(type, size) ((type *) alloca ((size) * sizeof (type)))
/*拷贝以beg开始,end接收的数据到一个重新分配的以’\0’结尾的内存区, place是out的*/
#define BOUNDED_TO_ALLOCA(beg, end, place) do {\
const char *BTA_beg = (beg);\
int BTA_len = (end) - BTA_beg;\
char **BTA_dest = &(place);\
*BTA_dest = alloca (BTA_len + 1);\
memcpy (*BTA_dest, BTA_beg, BTA_len);\
(*BTA_dest)[BTA_len] = '\0';\
} while (0)
/* Return non-zero if string bounded between BEG and END is equal to
STRING_LITERAL. The comparison is case-sensitive. */
/*比较beg开始,end结尾的字符串和string_literal的大小,返回非0相等,这种比较是大小写敏感的,是内存数据的比较*/
#define BOUNDED_EQUAL(beg, end, string_literal)\
((end) - (beg) == sizeof (string_literal) - 1\
&& !memcmp ((beg), (string_literal),\
sizeof (string_literal) - 1))
/* 这个跟上面的一样,就是比较的时候不区分大小写*/
#define BOUNDED_EQUAL_NO_CASE(beg, end, string_literal)\
((end) - (beg) == sizeof (string_literal) - 1\
&& !strncasecmp ((beg), (string_literal),\
sizeof (string_literal) - 1))
/* 其实就是strdup的宏实现 */
#define STRDUP_ALLOCA(ptr, str) do {\
(ptr) = (char *)alloca (strlen (str) + 1);\
strcpy ((ptr), (str));\
} while (0)
/* 如果你想避开任意大小的限制,又不需要一个全动态的数组,假设BASEVAR指向一个type类型的malloced数组,SIZEVAR是数组大小,下面的宏就是 realloc BASEVAR这个数组的,如果你传入的NEEDED_SIZE数值比SIZEVAR 大,为了确保每个元素摊还常量时间,重新分配的数组是以前的2倍,*/
#define DO_REALLOC(basevar, sizevar, needed_size, type)do\
{\
/* Avoid side-effectualness. */\
long do_realloc_needed_size = (needed_size);\
long do_realloc_newsize = 0;\
while ((sizevar) < (do_realloc_needed_size)) {\
do_realloc_newsize = 2*(sizevar);\
if (do_realloc_newsize < 32)\
do_realloc_newsize = 32;\
(sizevar) = do_realloc_newsize;\
}\
if (do_realloc_newsize)\
basevar = (type *)xrealloc (basevar, do_realloc_newsize * sizeof (type));\
} while (0)
/* 释放一个非空指针 */
#define FREE_MAYBE(foo) do { if (foo) xfree (foo); } while (0)
extern const char *exec_name;
/* Document type ("dt") flags */
enum
{
TEXTHTML = 0x0001,/* document is of type text/html
or application/xhtml+xml */
RETROKF = 0x0002,/* retrieval was OK */
HEAD_ONLY = 0x0004,/* only send the HEAD request */
SEND_NOCACHE = 0x0008,/* send Pragma: no-cache directive */
ACCEPTRANGES = 0x0010,/* Accept-ranges header was found */
ADDED_HTML_EXTENSION = 0x0020 /* added ".html" extension due to -E */
};
/*下面的枚举是基本错误类型,错误报告的细节一般没用应该被简化*/
typedef enum
{...} uerr_t;
typedef unsigned char boolean;
#ifndef FALSE
#define FALSE 0
#endif
#ifndef TRUE
#define TRUE 1
#endif
/* So we can say strcmp(a, b) == EQ rather than strcmp(a, b) == 0 or
the really awful !strcmp(a, b). */
#define EQ 0
/* 正无穷递归,文档写了一大堆,个人感觉然并卵,就记住它的定义就行了*/
#define INFINITE_RECURSION -1
#define CONNECT_ERROR(x) ((x) == ECONNREFUSED && !opt.retry_connrefused\
? CONREFUSED : CONERROR)
好了,wget.h这个文件我们就基本分析完了,其实我就个人理解加翻译了一下,总结一下wget.h这个头文件的总体:
1:定义了一些系统兼容性的问题
2:日志打印的声明
3:调试函数的声明
4:内存操作的声明
5:一些很有用的宏定义
6:一些后面会使用到的枚举定义
其实就是:宏,宏,宏,编译器和各种系统间兼容的问题(Sysdep.h)都是在这个头文件里面都包括了,这是第一次的源码剖析,如果有不恰当和说不明白的地方希望指出来