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为空,然后取得锁,此时仍进行了实例的创建。也就不满足单例模式了。所以第二次的再次判空时必要的。只有这样才能保证即使多线程也能创建唯一的实例。

posted @ 2020-07-08 23:46  smalllll  阅读(1027)  评论(0编辑  收藏  举报