zoukankan      html  css  js  c++  java
  • 一只简单的网络爬虫(基于linux C/C++)————socket相关及HTTP

    socket相关

    建立连接
    网络通信中少不了socket,该爬虫没有使用现成的一些库,而是自己封装了socket的相关操作,因为爬虫属于客户端,建立套接字和发起连接都封装在build_connect中

    //建立连接
    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设置为非阻塞模式

    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函数的原型是

      int socket(int domain, int type, int protocol);

    这里的type参数一般可以取如下的值
    这里写图片描述

    linux2.6.27以上的内核支持SOCK_NONBLOCK与SOCK_CLOEXEC,意味着可以使用如下的方法创建一个非阻塞的套接字,一气呵成。

     int sockfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);

    发送请求
    发送请求主要使用write函数向socket文件描述符写东西,而爬虫的发送主要就是发送http请求,以便获取我们想要的资源

    //发送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
    "
                "Host: %s
    "
                "Accept: */*r
    "
                "Connection: Keep-Alive
    "
                "User-Agent: Mozilla/5.0 (compatible; Qteqpidspider/1.0;)
    "
                "Referer: %s
    
    ", 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的空间用来接收

    //一个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] = '';
    
                if (!trunc_head)//还没有截去头部
                {//strstr() 函数搜索一个字符串在另一个字符串中的第一次出现。
                 //找到所搜索的字符串,则该函数返回第一次匹配的字符串的地址;
                 //如果未找到所搜索的字符串,则返回NULL。
                    if ((body_ptr = strstr(resp->body, "
    
    ")) != NULL) //头部于体相差两个
    
                    {
                        *(body_ptr+2) = '';//响应体
                        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] = '';
                        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;
    }

    核心的接收函数如下:

     n = read(narg->fd, resp->body + len, 1024);//得到返回数据

    n表示读取到的长度,n小于0表示错误,等于0表示数据读取完毕,读取完毕之后采用正则表达式解析页面,这个下次再谈。

    HTTP

    HTTP报文由从客户机到服务器的请求和从服务器到客户机的响应构成。请求报文格式如下:
    请求行 - 通用信息头 - 请求头 - 实体头 - 报文主体
    请求行以方法字段开始,后面分别是 URL 字段和 HTTP 协议版本字段,并以 CRLF 结尾。SP 是分隔符。除了在最后的 CRLF 序列中 CF 和 LF 是必需的之外,其他都可以不要。有关通用信息头,请求头和实体头方面的具体内容可以参照相关文件。
    应答报文格式如下:
    状态行 - 通用信息头 - 响应头 - 实体头 - 报文主体
    状态码元由3位数字组成,表示请求是否被理解或被满足。原因分析是对原文的状态码作简短的描述,状态码用来支持自动操作,而原因分析用来供用户使用。客户机无需用来检查或显示语法。有关通用信息头,响应头和实体头方面的具体内容可以参照相关文件。
    HTTP请求
    下图给出了请求报文的一般格式
    这里写图片描述
    我们这里的爬虫以下面的HTTP请求为例

    "GET /%s HTTP/1.0
    "
                "Host: %s
    "
                "Accept: */*
    "
                "Connection: Keep-Alive
    "
                "User-Agent: Mozilla/5.0 (compatible; Qteqpidspider/1.0;)
    "
                "Referer: %s
    
    ", url->path, url->domain, url->domain)

    HTTP响应
    HTTP响应参考下面的例子

    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的头部,如下

    //解析反馈头
    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));
        //找到
    第一次出现的地方
        if ((p = strstr(start, "
    ")) != NULL) 
        {//第一行应该是HTTP/1.0 200 OK
    ,'
    '只是一个字节
            *p = '';
            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, "
    ")) != NULL) 
        {
            *p = '';
            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相关内容可以参考这里

  • 相关阅读:
    Oracel基础知识
    64位系统运行32位Oracle程序解决方案
    Oracle 级联删除
    string转DateTime(时间格式转换)
    vs2013 内置IIS Express相关问题
    小马哥课堂-统计学-置信区间(2)
    小马哥课堂-统计学-置信区间
    小马哥课堂-统计学-标准误差
    小马哥课堂-统计学-中心极限定理
    python之histogram
  • 原文地址:https://www.cnblogs.com/sigma0-/p/12630466.html
Copyright © 2011-2022 走看看