一只简单的网络爬虫(基于linux C/C++)————socket相关及HTTP
2015-09-24 00:34
1046 查看
socket相关
建立连接网络通信中少不了socket,该爬虫没有使用现成的一些库,而是自己封装了socket的相关操作,因为爬虫属于客户端,建立套接字和发起连接都封装在build_connect中
[code]//建立连接 int build_connect(int *fd, char *ip, int port) { struct sockaddr_in server_addr; bzero(&server_addr, sizeof(struct sockaddr_in)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(port);//主机字节序转化为网络字节序 if (!inet_aton(ip, &(server_addr.sin_addr))) {//ip转化为网络字节序ip地址 return -1; } if ((*fd = socket(PF_INET, SOCK_STREAM, 0)) < 0) {//创建socket套接字 return -1; } if (connect(*fd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr_in)) < 0) {//连接 close(*fd); return -1; } SPIDER_LOG(SPIDER_LEVEL_DEBUG,"连接建立成功:%s",ip); return 0; }
build_connect(int *fd, char *ip, int port)中,ip和port都是通过url传递进去的,fd则是创建socket后通过指针传出来的。
可以使用下面的函数将socket设置为非阻塞模式
[code]void set_nonblocking(int fd) {//设置非阻塞模式 int flag; if ((flag = fcntl(fd, F_GETFL)) < 0) { SPIDER_LOG(SPIDER_LEVEL_ERROR, "fcntl getfl fail"); } flag |= O_NONBLOCK; if ((flag = fcntl(fd, F_SETFL, flag)) < 0) { SPIDER_LOG(SPIDER_LEVEL_ERROR, "fcntl setfl fail"); } }
核心便是使用了该函数fcntl,可以用O_NONBLOCK设置。
如果linux的内核在2.6.27以上,则有另一个方式,如下图所示
socket函数的原型是
[code] int socket(int domain, int type, int protocol);
这里的type参数一般可以取如下的值
linux2.6.27以上的内核支持SOCK_NONBLOCK与SOCK_CLOEXEC,意味着可以使用如下的方法创建一个非阻塞的套接字,一气呵成。
[code] int sockfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
发送请求
发送请求主要使用write函数向socket文件描述符写东西,而爬虫的发送主要就是发送http请求,以便获取我们想要的资源
[code]//发送http请求 int send_request(int fd, void *arg) { int need, begin, n; char request[1024] = {0}; Url *url = (Url *)arg; //打印下请求的信息 SPIDER_LOG(SPIDER_LEVEL_DEBUG,"url->path:%s",url->path); SPIDER_LOG(SPIDER_LEVEL_DEBUG,"发出的请求域名url->domain:%s",url->domain); //组成HTTP头部信息 sprintf(request, "GET /%s HTTP/1.0\r\n" "Host: %s\r\n" "Accept: */*\r\n" "Connection: Keep-Alive\r\n" "User-Agent: Mozilla/5.0 (compatible; Qteqpidspider/1.0;)\r\n" "Referer: %s\r\n\r\n", url->path, url->domain, url->domain); need = strlen(request); begin = 0; //向服务器发送请求 while(need) { n = write(fd, request+begin, need);//发送请求 if (n <= 0) { if (errno == EAGAIN) { //write buffer full, delay retry usleep(1000); continue; } SPIDER_LOG(SPIDER_LEVEL_WARN, "Thread %lu send ERROR: %d", pthread_self(), n); free_url(url); close(fd); return -1; } begin += n;//起始点指针的偏移 need -= n;//直到发送完 } return 0; }
http请求将在下面谈。我们这里是将http请求写入request数组中,然后使用
n = write(fd, request+begin, need);//发送请求
进行发送,n返回的是写入的长度,然后每次将长度更新直到写完了为止。如果返回EAGAIN则稍作延时继续写
EAGAIN
在Linux环境下开发经常会碰到很多错误(设置errno),其中EAGAIN是其中比较常见的一个错误(比如用在非阻塞操作中)。
从字面上来看,是提示再试一次。这个错误经常出现在当应用程序进行一些非阻塞(non-blocking)操作(对文件或socket)的时候。例如,以O_NONBLOCK的标志打开文件/socket/FIFO,如果你连续做read操作而没有数据可读。此时程序不会阻塞起来等待数据准备就绪返回,read函数会返回一个错误EAGAIN,提示你的应用程序现在没有数据可读请稍后再试。
又例如,当一个系统调用(比如fork)因为没有足够的资源(比如虚拟内存)而执行失败,返回EAGAIN提示其再调用一次(也许下次就能成功)。
接收消息
接收消息采用read函数,我们先预先分配一个1M的空间用来接收
[code]//一个HTML分配1M缓冲区 #define HTML_MAXLEN 1024*1024 void * recv_response(void * arg) {//epollin事件到来就调用该函数解析 begin_thread();//这个函数只是打印线程自身的id int i, n, trunc_head = 0, len = 0; char * body_ptr = NULL; evso_arg * narg = (evso_arg *)arg; Response *resp = (Response *)malloc(sizeof(Response)); resp->header = NULL; resp->body = (char *)malloc(HTML_MAXLEN); resp->body_len = 0; resp->url = narg->url; //regex_t 是一个结构体数据类型,用来存放编译后的正则表达式, //它的成员re_nsub 用来存储正则表达式中的子正则表达式的个数, //子正则表达式就是用圆括号包起来的部分表达式。 regex_t re; //int regcomp (regex_t *compiled, const char *pattern, int cflags) //pattern 是指向我们写好的正则表达式的指针 if (regcomp(&re, HREF_PATTERN, 0) != 0) {//compile error匹配错误 SPIDER_LOG(SPIDER_LEVEL_ERROR, "compile regex error"); } // SPIDER_LOG(SPIDER_LEVEL_INFO, "Crawling url: %s/%s", narg->url->domain, narg->url->path); while(1) { // typedef struct Response { // Header *header; // char *body;//内容 // int body_len;//长度 // struct Url *url;//相关联的url // } Response; // what if content-length exceeds HTML_MAXLEN? 超过则会一直读啊,读到没有数据为止 //读取后放到 resp->body + len n = read(narg->fd, resp->body + len, 1024);//得到返回数据 if (n < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) { // TODO: Why always recv EAGAIN? // should we deal EINTR //SPIDER_LOG(SPIDER_LEVEL_WARN, "thread %lu meet EAGAIN or EWOULDBLOCK, sleep", pthread_self()); usleep(100000); continue; } //strerror返回:指向错误信息的指针即错误的描述字符串 SPIDER_LOG(SPIDER_LEVEL_WARN, "Read socket fail: %s", strerror(errno)); break; } else if (n == 0) {//数据读完 // finish reading resp->body_len = len; if (resp->body_len > 0) {//匹配正则表达式,如果是新的会加入原始的队列 //编译好的正则表达式,反馈体,原来的url extract_url(&re, resp->body, narg->url);//该函数在url.cpp中 } // deal resp->body 处理响应体 for (i = 0; i < (int)modules_post_html.size(); i++) { SPIDER_LOG(SPIDER_LEVEL_WARN, "保存文件"); modules_post_html[i]->handle(resp);//此模块就是保存html文件的 } break; } else { //SPIDER_LOG(SPIDER_LEVEL_WARN, "read socket ok! len=%d", n); len += n;//更新已经读取的长度 resp->body[len] = '\0'; if (!trunc_head)//还没有截去头部 {//strstr() 函数搜索一个字符串在另一个字符串中的第一次出现。 //找到所搜索的字符串,则该函数返回第一次匹配的字符串的地址; //如果未找到所搜索的字符串,则返回NULL。 if ((body_ptr = strstr(resp->body, "\r\n\r\n")) != NULL) //头部于体相差两个\r\n { *(body_ptr+2) = '\0';//响应体 resp->header = parse_header(resp->body);//解析一下响应头,得到状态码还有类型 if (!header_postcheck(resp->header)) //用模块再次检测 {//这里经常出差 SPIDER_LOG(SPIDER_LEVEL_WARN, "goto leave"); goto leave; // modulues filter fail } trunc_head = 1; // cover header body_ptr += 4;//这部分对比网页的源码去看 for (i = 0; *body_ptr; i++) //保存内容 { resp->body[i] = *body_ptr; body_ptr++; } resp->body[i] = '\0'; len = i;//去除头部的操作应该是发生在第一次的 } continue; } } } leave: close(narg->fd); // close socket free_url(narg->url); // free Url object regfree(&re); // free regex object // free resp free(resp->header->content_type); free(resp->header); free(resp->body); free(resp); end_thread();//结束任务 return NULL; }
核心的接收函数如下:
[code] n = read(narg->fd, resp->body + len, 1024);//得到返回数据
n表示读取到的长度,n小于0表示错误,等于0表示数据读取完毕,读取完毕之后采用正则表达式解析页面,这个下次再谈。
HTTP
HTTP报文由从客户机到服务器的请求和从服务器到客户机的响应构成。请求报文格式如下:请求行 - 通用信息头 - 请求头 - 实体头 - 报文主体
请求行以方法字段开始,后面分别是 URL 字段和 HTTP 协议版本字段,并以 CRLF 结尾。SP 是分隔符。除了在最后的 CRLF 序列中 CF 和 LF 是必需的之外,其他都可以不要。有关通用信息头,请求头和实体头方面的具体内容可以参照相关文件。
应答报文格式如下:
状态行 - 通用信息头 - 响应头 - 实体头 - 报文主体
状态码元由3位数字组成,表示请求是否被理解或被满足。原因分析是对原文的状态码作简短的描述,状态码用来支持自动操作,而原因分析用来供用户使用。客户机无需用来检查或显示语法。有关通用信息头,响应头和实体头方面的具体内容可以参照相关文件。
HTTP请求
下图给出了请求报文的一般格式
我们这里的爬虫以下面的HTTP请求为例
[code]"GET /%s HTTP/1.0\r\n" "Host: %s\r\n" "Accept: */*\r\n" "Connection: Keep-Alive\r\n" "User-Agent: Mozilla/5.0 (compatible; Qteqpidspider/1.0;)\r\n" "Referer: %s\r\n\r\n", url->path, url->domain, url->domain)
HTTP响应
HTTP响应参考下面的例子
[code]HTTP/1.1 200 OK Date: Sat, 31 Dec 2005 23:59:59 GMT Content-Type: text/html;charset=ISO-8859-1 Content-Length: 122 <html> <head> <title>Wrox Homepage</title> </head> <body> <!-- body goes here --> </body> </html>
在接收函数中,有一个函数用于解析HTTP的头部,如下
[code]//解析反馈头 static Header * parse_header(char *header) { int c = 0; // typedef struct Header { // char *content_type;//文件类型 // int status_code;//状态码 // } Header; char *p = NULL; char **sps = NULL; char *start = header; Header *h = (Header *)calloc(1, sizeof(Header)); //找到\r\n第一次出现的地方 if ((p = strstr(start, "\r\n")) != NULL) {//第一行应该是HTTP/1.0 200 OK\r\n,'\r\n'只是一个字节 *p = '\0'; sps = strsplit(start, ' ', &c, 2);//按空格分隔字符串,分隔3次 if (c == 3) { h->status_code = atoi(sps[1]);//保存状态码 } else { h->status_code = 600; //给个自己的错误码 } start = p + 2; } while ((p = strstr(start, "\r\n")) != NULL) { *p = '\0'; sps = strsplit(start, ':', &c, 1);//以冒号分隔 if (c == 2) { if (strcasecmp(sps[0], "content-type") == 0)//查找内容的类型 { h->content_type = strdup(strim(sps[1])); } } start = p + 2; } return h; }
这里获取了状态码还有内容的类型,接着其实是接收体将HTTP头部覆盖了,因此最后的Response结构体的body其实只是保存了内容。
在学习HTTP相关的东西时,可以使用浏览器方便的查看一些必要的信息,例如
使用鼠标右键
点审查元素,可以看到很多的内容
Network——>可以看到头部等等的信息,这在学习的时候有助于我们更好地理解
更过的HTTP相关内容可以参考这里
相关文章推荐
- 设置TestCpp使用BOX2d
- 关于XMLHttpRequest对象的responseText属性
- 网络通讯http
- 一步一步分析odl【第二弹】openflow协议栈的运行流程之建立tcp连接
- 承接游戏UI美术外包【厦门巨游网络科技有限公司】
- 湖北省政府发文提出建立基金 奖励有突出贡献乡村教师
- HTTP协议中POST、GET、HEAD、PUT等请求方法以及一些常见错误
- HDU 5029 Relief grain (2014年广州赛区网络赛H题)
- nine-patch 转载制作方式http://2528.iteye.com/blog/1326647
- Xcode7 使用NSURLSession发送HTTP请求报错
- .9.png 转载地址http://www.cnblogs.com/lwbqqyumidi/p/3373070.html
- TCP/UDP编程
- HTTPS那些事(二)SSL证书
- 多核心Linux内核路径优化的不二法门之-多核心平台TCP优化
- https://github.com/yrs244742688/GeneratePemWithMoAndEx RSA加密
- HTTPS连接的前几毫秒发生了什么——Amazon HTTPS案例分析
- 多核心Linux内核路径优化的不二法门之-多核心平台TCP优化
- Android网络状态的监听
- TCP/IP 抓包分析
- 网络请求封装2