zoukankan      html  css  js  c++  java
  • webMagic学习系列:downloader模块学习

    摘要:

    本篇主要剖析webmagic的downloader模块,对于httpclient模块涉及到的http相关的知识,例如:Request、Response以及重定向策略进行一定的分析。首先梳理了本模块的结构、然后对于执行流程进行了分析,最后对于其中涉及的设计模式:单例模式和相关算法进行了代码分析。

    0x00:downloader的模块结构

    downloader涉及到的类和接口主要如下表所示:

    类名称 作用 方法说明 备注
    Downloader 定义downloader接口规范 downloade(r:Request,t:Task):Page 接口
    AbstractDownloader 定义downloader状态接口 onSuccess(),onError(),@Overdide:downloade() 抽象类,
    HttpClientDownloader 具体的下载接口 继承自AbstractDownloader 具体类
    CustomRedirectStrategy 定义重定向策略
    HttpClientGenerator 配置httpCliet的辅助类 getHttpClient(s:Site):HttpClient
    HttpClientRequestContext 数据类 存储requestcontext和clinetcontext
    HttpUriRequestConverter 配置Request的辅助类 convert(r:Request,s:Site,p:Proxy):Request

    ox01:downloade的具体执行逻辑

    首先来看具体的downloade代码:

        @Override
        public Page download(Request request, Task task) {
            if (task == null || task.getSite() == null) {
                throw new NullPointerException("task or site can not be null");
            }
            CloseableHttpResponse httpResponse = null;
            CloseableHttpClient httpClient = getHttpClient(task.getSite());
            Proxy proxy = proxyProvider != null ? proxyProvider.getProxy(task) : null;
            HttpClientRequestContext requestContext = httpUriRequestConverter.convert(request, task.getSite(), proxy);
            Page page = Page.fail();
            try {
                httpResponse = httpClient.execute(requestContext.getHttpUriRequest(), requestContext.getHttpClientContext());
                page = handleResponse(request, request.getCharset() != null ? request.getCharset() : task.getSite().getCharset(), httpResponse, task);
                onSuccess(request);
                logger.info("downloading page success {}", request.getUrl());
                return page;
            } catch (IOException e) {
                logger.warn("download page {} error", request.getUrl(), e);
                onError(request);
                return page;
            } finally {
                if (httpResponse != null) {
                    //ensure the connection is released back to pool
                    EntityUtils.consumeQuietly(httpResponse.getEntity());
                }
                if (proxyProvider != null && proxy != null) {
                    proxyProvider.returnProxy(proxy, page, task);
                }
            }
        }
    
    

    可以看到主要的代码流程还是很清晰的,首先得到配置好的httpClient,这是通过getClient()方法得到的,这个方法具体涉及到设计模式中的单例,我们稍后再详细讲,然后根据传递过来的Request得到RequestContext和ClinetContext,根据执行httlClient的execute方法,这个方法就是具体的向服务端发送资源请求的方法,该方法会将服务器的资源封装到Response对象中。最后将Request和Response封装到Page中去,供后续的PageProcessor使用。

    下面个用伪代码描述上面的流程:

    fun download(r:Requst,t:Task):Page
        httpClient = getClient(t.site())
        context = convert(r,t.site(),proxy)
        response = httpClient.execute(context.requestContext,context.clinetContext)
        page = handle(r,response)
        return page
    

    可以看到downloade函数实际上关键的核心代码就是httpClinet的execute方法,其他的代码统一都可以抽象成准备工作。

    0x02:初始化策略

    httpClient初试化实际上涉及了一系列的参数配置,包括使用到的socket的参数配置,以及http一些连接配置,由于涉及到的参数非常多,对于socket的参数配置和httpClinet均使用到了Builder模式。具体的代码代码如下:

       private CloseableHttpClient generateClient(Site site) {
            HttpClientBuilder httpClientBuilder = HttpClients.custom();
            
            httpClientBuilder.setConnectionManager(connectionManager);
            if (site.getUserAgent() != null) {
                httpClientBuilder.setUserAgent(site.getUserAgent());
            } else {
                httpClientBuilder.setUserAgent("");
            }
            if (site.isUseGzip()) {
                httpClientBuilder.addInterceptorFirst(new HttpRequestInterceptor() {
    
                    public void process(
                            final HttpRequest request,
                            final HttpContext context) throws HttpException, IOException {
                        if (!request.containsHeader("Accept-Encoding")) {
                            request.addHeader("Accept-Encoding", "gzip");
                        }
                    }
                });
            }
            //解决post/redirect/post 302跳转问题
            httpClientBuilder.setRedirectStrategy(new CustomRedirectStrategy());
    
            SocketConfig.Builder socketConfigBuilder = SocketConfig.custom();
            socketConfigBuilder.setSoKeepAlive(true).setTcpNoDelay(true);
            socketConfigBuilder.setSoTimeout(site.getTimeOut());
            SocketConfig socketConfig = socketConfigBuilder.build();
            httpClientBuilder.setDefaultSocketConfig(socketConfig);
            connectionManager.setDefaultSocketConfig(socketConfig);
            httpClientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(site.getRetryTimes(), true));
            generateCookie(httpClientBuilder, site);
            return httpClientBuilder.build();
        }
    
    

    可以看到实际上就是根据站点来配置client参数的过程,也就是说,我们可以将一些自定义参数放置到Site实例中,这样就可以将参数填入了。这实际上也是我么常用的初始化策略,当参数众多时,我们抽象出相关的配置类,这样可以将参数集中管理起来,实现代码的结构化。

    ox03:单例模式

    在第一节中我们提到,httpClinet使用了单例模式,下面我们看具体的实现过程:

        private CloseableHttpClient getHttpClient(Site site) {
            if (site == null) {
                return httpClientGenerator.getClient(null);
            }
            String domain = site.getDomain();
            CloseableHttpClient httpClient = httpClients.get(domain);
            if (httpClient == null) {
                synchronized (this) {
                    httpClient = httpClients.get(domain);
                    if (httpClient == null) {
                        httpClient = httpClientGenerator.getClient(site);
                        httpClients.put(domain, httpClient);
                    }
                }
            }
            return httpClient;
        }
    

    可以看到代码的关键部分如下:

    if(httpClient == null) {
        synchronized(this) {
            if(httpClinet == null) {
                htttpClinet = httpClinetGenerator.getClinet();
            }
        }
    }
    

    也就是代码判断了两次单例是否为空,第一次判断为空,然后加锁进行单例的判断,这个比较容易理解,但是第二次再次判断是为什么呢,我们设想如下情况:

    当前单例未被创建,所以httpClient为null,线程一判断结果为空后还未加锁,此时进行了线程的切换,线程2得到了执行权,此时由于线程1为创建实例,所以线程2会创建一个实例出来。然后再切回线程1执行,由于之前线程1判断了httpClient为空,然后取得锁,此时仍进行了实例的创建。也就不满足单例模式了。所以第二次的再次判空时必要的。只有这样才能保证即使多线程也能创建唯一的实例。

  • 相关阅读:
    MariaDB · 版本特性 · MariaDB 的 GTID 介绍
    stm8s 中断重复进入
    PCB积累
    链表的创建、增加、删除、改数据、遍历
    百度文库文字下载工具指引
    防倒灌的开关电路
    AD快速从原理图查找pcb中元件
    三目运算符填坑
    嵌入式结构化分层思想
    原码,反码,补码
  • 原文地址:https://www.cnblogs.com/zhangshoulei/p/13270044.html
Copyright © 2011-2022 走看看