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相关内容可以参考这里

  • 相关阅读:
    如何在Ubuntu Server 18.04上安装Microsoft的Procmon
    如何在Ubuntu 20.04上安装Wine 5.0
    如何在Kali Linux 2020中启用SSH服务
    如何在Ubuntu 20.04 LTS Focal Fossa上安装Apache Groovy
    如何使用命令在Ubuntu 20.04 Linux上安装Vmware Tools
    在Ubuntu 20.04 LTS Focal Fossa上安装Zabbix Agent
    hdu 2089 不要62
    hdu 2093 成绩排名
    hdu 2104 hide handkerchief
    leetcode147对链表进行插入排序
  • 原文地址:https://www.cnblogs.com/sigma0-/p/12630466.html
Copyright © 2011-2022 走看看