从0开始造一个轮子(方的)

前言

俗话说「不要重复造轮子」,可是我以为经过研究大神造的轮子,而后本身去尝试造一个简陋版的,对于提高本身的软件构思是颇有帮助的。
回归正题,最近在作一个做业,和计算机网络相关的,笔者选择了用Java开发一个简陋版的HTTP客户端,因而笔者去拜读了Square公司开源的OkHttp,参照了Okhttp的设计思想,开发了Yohttpjava

这里给出Github地址:YoHttp,欢迎你们一块儿学习探讨。git

软件架构

笔者将软件大概设计成五大模块:github

  1. 请求信息
    这部分即对应上图的Request,用于用户构建请求信息,如URLmethod、请求头等。这部分是用户能够操做的。
  2. Yohttp客户端
    用户建立一个YoHttp,而后将请求信息注入到Yohttp便可以开始使用请求功能,请求包括同步请求和异步请求,其中一个YoHttp包含一个调度中心、一个链接池,因此对于一个项目来讲,维护着一个YoHttp客户端就足以。
  3. 处理链
    这里是请求的具体实现操做,笔者将一个一个操做封装成一个拦截器,如把获取Socket链接的操做封装成链接拦截器、把Socket流的读写封装成收发拦截器,而后咱们请求须要用到哪些操做,便可把这些拦截器一个一个拼接起来组合成一个处理链(Chain),一个处理链对应着一个请求。执行处理链中的一个个拦截器,直到执行完全部的拦截器,也对应着一个请求的完成。这也是为何咱们须要将收发拦截器放在最后,由于一个请求的最后一个操做确定是进行Socket流的写和读。
    笔者认为这样将一个一个操做封装成拦截器,而后组合拦截器拼凑成处理链,最后执行处理链便可达到执行操做,极大的解耦了请求过程,同时也提升了扩展性。

  1. 调度中心
    调度中心在使用异步请求的时候用到,调度中心维护着一个请求队列和一个线程池,请求队列里面存储的是处理链Chain。线程池负责执行队列中的处理链。
    笔者认为这里使用线程池能提升队列的处理效率,毕竟如今PC都是多核心的,充分利用CPU提升效率仍是不错的。缓存

  2. 链接池
    每一个请求都是去链接池获取Socket链接,若是链接池中存在IPPORT相同的链接则直接返回,不然建立一个Socket链接存储到链接池而后返回,而链接池中的链接闲置时间超过最大容许闲置的时间后就会被关闭
    笔者认为经过使用链接池能减小链接建立销毁的开销,在请求较多、请求频率较高的场景下能提升效率。bash

介绍完了架构,咱们看看怎么使用咱们的HTTP客户端:网络

  1. 同步请求
Request request = new Request.Builder()
        .url("www.baidu.com")
        .get()
        .build();
YoHttpClient httpClient = new YoHttpClient();
Response response = httpClient.SyncCall(request).executor();
System.out.println(response.getBody());
复制代码

第一步新建个请求信息Request,填写请求的URL、请求方法、请求头等信息。
第二步新建个YoHttp客户端,选择同步请求并将请求信息注入,执行请求。架构

  1. 异步请求
Request request = new Request.Builder()
        .url("www.baidu.com")
        .get()
        .build();
YoHttpClient httpClient = new YoHttpClient();
httpClient.AsyncCall(request).executor(new CallBack() {
    @Override
    public void onResponse(Response response) {
        System.out.println(response.getBody());
    }
});
复制代码

第一步新建个请求信息Request,填写请求的URL、请求方法、请求头等信息。
第二步新建个YoHttp客户端,选择异步请求并将请求信息注入,执行请求,当请求有响应的时候,会经过回调异步请求的onResponse方法来反馈响应内容。并发

说完了架构还有使用方法,接下来笔者介绍各个模块的具体实现。异步

请求信息

在实现Request的时候,笔者使用的是Builder模式,即构造者模式,在Request中添加个静态内部类Builder,用于构造Request。ide

YoHttpClient

在YoHttp客户端中有一个调度中心和一个链接池,调度中心是使用异步请求的时候用上的,链接池则是在请求获取Socket链接的时候使用。

  1. 构造方法
    笔者设置了两个构造方法:
public YoHttpClient() {
    this(5, TimeUnit.MINUTES);
}

public YoHttpClient(int keepAliveTime, TimeUnit timeUnit) {
    this.dispatcher = new Dispatcher();
    this.connectionPool = new ConnectionPool(keepAliveTime, timeUnit);
}
复制代码

一个是无参构造方法,一个是指定链接池中链接最大闲置时间的构造方法,若是用户使用了无参构造方法,默认设置链接池中的链接最大闲置时间是5分钟。

  1. 同步请求方法SynchCall
public SyncCall SyncCall(Request request) {
    return new SyncCall(this, request);
}

// SyncCall.java
@Override
public Response executor() {
    synchronized (this) {
        if (this.executed)
            throw new IllegalStateException("Call Already Executed");
        this.executed = true;
    }
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.add(new ConnectionInterceptor(yoHttpClient, request));
    interceptors.add(new CallServerInterceptor(request));
    Chain chain = new Chain(interceptors, null);
    Response response = chain.proceed();
    chain = null;
    return response;
}

//Chain.java
public Response proceed() {
    Response response = new Response();
    for (int i = 0; i < interceptors.size(); i++) {
        response = interceptors.get(i).proceed(response);
    }
    return response;
}
复制代码

建立一个SynchCall同步请求,SynchCall里面有个executor方法,这个方法建立一个存储拦截器Interceptor的List,咱们把请求中须要用到的操做(拦截器)存入到List中,例如咱们用到了链接拦截器(ConnectionInterceptor)、收发拦截器(CallServerInterceptor),而后将List封装成一个处理链(Chain),最后调用处理链的proceed方法遍历List中的拦截器并执行,这样便可达到执行一个请求的全部操做,这里是同步请求,因此阻塞处处理链执行完成返回response以后才return。

  1. 异步请求AsyncCall
public AsyncCall AsyncCall(Request request) {
    return new AsyncCall(this, request);
}

//AsyncCall.java
public void executor(CallBack callBack) {
    synchronized (this) {
        if (this.executed)
            throw new IllegalStateException("Call Already Executed");
        this.executed = true;
    }
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.add(new ConnectionInterceptor(yoHttpClient, request));
    interceptors.add(new CallServerInterceptor(request));
    Chain chain = new Chain(interceptors, callBack);
    yoHttpClient.getDispatcher().addChain(chain);
}
复制代码

异步请求中,一样是在executor方法构造好所需的拦截器,将拦截器封装成处理链,区别的地方在这里并非立刻调用处理链的proceed方法,而是将处理链添加到调度中心的请求队列中,而后立刻返回了,调度中心的具体实如今后文介绍。

处理链

处理链在上文的YoHttpClient介绍的差很少了,这里补充一下拦截器的设计。
全部的拦截器都实现Interceptor这个接口,这个接口很简单,只有一个方法proceed,只须要将具体的操做写到这个方法便可。例如链接拦截器ConnectionInterceptor的实现以下。

@Override
public Response proceed(Response response) {
    Address address = request.getAddress();
    Connection connection = yoHttpClient.getConnectionPool().getConnection(address);
    request.setConnection(connection);
    return response;
}
复制代码

第一步是获取请求信息中的IPPORT(笔者将这二者封装成了Address)
第二步是使用这个address去链接池中获取链接。

这个proceed方法是提供给处理链中执行的。

调度中心

调度中心在异步请求中使用到,调度中心维护着一个请求队列和一个线程池。笔者采用的是阻塞队列(考虑到并发问题)和可缓存线程池,这个线程池的特色:核心线程数是0,线程数最大是Integer.MAX_VALUE,线程闲置时间最大容许为60秒。
调度中心有2个内部类,一个是CallRunnable,这个内部类的做用是将处理链Chain封装成Runnable公线程执行。另外一个是ChainQueue,这个内部类维护着一个阻塞队列,控制着请求的入队和出队。

private void executor() {
    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            while (true) {
                while (chainQueue.size() > 0) {
                    executorService.submit(new CallRunnable(chainQueue.pollChain()));
                }
            }
        }
    });
    thread.start();
}

//CallRunnable内部类
private final class CallRunnable implements Runnable {
    private Chain chain;

    CallRunnable(Chain chain) {
        this.chain = chain;
    }
    @Override
    public void run() {
        Response response = chain.proceed();
        chain.getCallBack().onResponse(response);
        chain = null;
    }
}
复制代码

在调度中心开启了一个线程,经过遍历阻塞队列,若是阻塞队列中有请求,则交给线程池去处理,线程经过调用处理链的proceed方法来遍历处理链中的拦截器,这个和同步请求中的同样的,当执行完后才能经过回调将响应返回给客户端。

链接池

笔者将Socket链接封装成一个Connection,而链接池维护的则是一个存储Connection的HashMap。

  1. 获取链接
public Connection getConnection(Address address) {
    return tryAcquire(address);
}

private Connection tryAcquire(Address address) {
    if (connections.containsKey(address)) {
        connections.get(address).setTime(System.currentTimeMillis());
        return connections.get(address);
    }

    synchronized (address) {
        cleanUpConnection();
        if (!connections.containsKey(address)) {
            Connection connection = new Connection(address);
            connection.setTime(System.currentTimeMillis());
            connections.put(address, connection);
            return connection;
        } else {
            connections.get(address).setTime(System.currentTimeMillis());
            return connections.get(address);
        }
    }
}
复制代码

经过调用getConnection方法便可获取到一个链接,而getConnection的实现是经过调用私有方法tryAcquire,获取的流程以下:
第一步先判断链接池中是否存在address相同的链接,有则则更新线程的活跃时间而后直接返回,没有则执行第二步。
第二步锁住address,目的是防止多个线程同时建立同一个链接,锁住以后再次判断链接池是否存在链接了,没有则进行建立而后返回。

  1. 清理超过闲置时间的链接
private void cleanUpConnection() {
    for (Map.Entry<Address, Connection> entry: connections.entrySet()) {
        if (System.currentTimeMillis() - entry.getValue().getTime() <= keepAliveTime) {
            try {
                connections.get(entry.getKey()).getSocket().close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            connections.remove(entry.getKey());
        }
    }
}
复制代码

这个cleanUpConnection方法在每次获取链接的时候都会执行一次,遍历链接池中的链接,若是链接池中的链接超过容许的闲置时间则关闭这个链接而后将链接移除Map。

总结

这个项目仅是学习使用,请勿用于生产环境
目前仅实现了GETPOSTDELETEPUT方法,但愿后面会完善更多功能还有把IO改为NIO提升性能。
但愿各位前辈看完以后能给点意见或者留下个赞~
最后再附上Github地址:YoHttp,欢迎你们一块儿学习探讨。

原文地址:ddnd.cn/2019/04/12/…

相关文章
相关标签/搜索