Hadoop RPC源码解析——Client类

来源:互联网 发布:唐泽寿明 知乎 编辑:程序博客网 时间:2024/06/05 14:41

Hadoop RPC主要由三大部分组成:Client、Server和RPC,如下表所示。

内部类

功能

Client

连接服务器、传递函数名和相应的参数、等待结果

Server

主要接受Client的请求、执行相应的函数、返回结果

RPC

外部编程接口,主要是为通信的服务方提供代理


这三部分在Hadoop RPC架构中的位置如下图所示:


本文主要解析Client类。Client类主要包含了以下几个内部类

内部类

功能

Call

封装Invocation对象,作为VO写到服务端,也存储从服务端返回的数据

Connection

处理远程连接对象,继承了Thread

ConnectionID

唯一确定一个连接


Call内部类:它封装了一个RPC请求,它包含了五个成员变量:唯一标识id、函数调用信息param、函数执行返回值value、出错或异常信息error和执行完成标识符done。由于HadoopRPC Server采用了异步方式处理客户端请求,因此远程过程调用的发生顺序与结果返回顺序无直接关系,而Client端正是通过id识别不同的函数调用。当客户端向服务器端发送请求时,只需填充id和param两个变量,剩下三个变量由服务器端根据函数执行情况填充。

Connection内部类:Client与每个Server之间维护的一个通信连接。该连接相关的基本信息及操作被封装到了Connection中。其中基本信息主要包括:通信连接唯一标识remoteId,与Server端通信的Socket socket,网络输入数据流in,网络输出数据流out,保存RPC请求的哈希表calls。

ConnectionID内部类:该类封装了address、ticket、protocol,用来唯一标志一个从客户端到服务器的连接


Client类主要用来发送远程过程调用信息并接受执行结果。它涉及的类关系图下图3-9所示



为了更好的读懂代码,在分析具体的代码之前我们要先了解一下以上三个内部类之间的关系。

一个客户端(Client)可以有多个连接(connection),保存在connections(类型为Hashtable<ConnectionId, Connection>)成员变量中,每个连接由connectionID唯一标识。同时每个连接维护自己的一个call队列。Call是对Invocation的进一步封装,其中记录了要调用的方法,参数,返回值,错误信息等,用一个id来标记每个call,这个id的值等于client中单调递增的成员变量counter的值。以上的关系可用下图来表示


接下来我们真正进行源码分析,首先我们来查看Client类中的call()方法,其代码如下

//代码二//Client#call/**    * 建立一个调用,将参数(即RPC中的invocation类的对象)传递给remoteId指定的那个IPC服务器,并返回value   * */  public Writable call(Writable param, ConnectionId remoteId)                         throws InterruptedException, IOException {  //将传入的参数封装成一个call对象    Call call = new Call(param);    //获得一个连接,Connection是一个新线程    Connection connection = getConnection(remoteId, call);    //向服务器端发送call对象    connection.sendParam(call);                 // send the parameter    boolean interrupted = false;    synchronized (call) {      while (!call.done) {        try {        //等待结果的返回,在call类的callComplete()方法中有notify()方法用于唤醒该线程          call.wait();                                  } catch (InterruptedException ie) {          // save the fact that we were interrupted          interrupted = true;        }      }      if (interrupted) {        // set the interrupt flag now that we are done waiting        Thread.currentThread().interrupt();      }      if (call.error != null) {        if (call.error instanceof RemoteException) {          call.error.fillInStackTrace();          throw call.error;        } else {           throw wrapException(connection.getRemoteAddress(), call.error);        }      } else {        return call.value;      }    }  }

当调用call函数执行某个远程方法时,Client端需要进行如下几个步骤,如下图所示。

步骤一 创建一个Connection对象,并将远程方法调用信息封装成Call对象,放到Connection对象的哈希表calls中。

步骤二 调用Connection类的sendParam()方法将当前Call对象发送给Server。

步骤三 服务器端处理完RPC后将结果通过网络返回给Client端,Client端通过receiveResponse()方法获得。

步骤四 Client端检查结果处理状态,并将对应的Call对象从哈希表calls中删除。


下面我们将从“客户端和服务端怎样建立连接?”,“客户端怎样发送数据到服务端?”,“客户端怎样获取服务端返回的数据?”三方面来分析Client类中的call()方法。首先解决第一个问题,对应于代码二中的getConnection()方法。

//代码三//Client#getConnection/**   * 从Connection线程池中取一个线程或者创建一个新的加入线程池,   * 同一个ConnectionID对应到Connection线程是重用的*/   private Connection getConnection(ConnectionId remoteId,                                   Call call)                                   throws IOException, InterruptedException {    ……    Connection connection;  //但请注意,这里的连接对象只是存储了remoteId的信息,其实还并没有和服务端建立连接。      do {      synchronized (connections) {        connection = connections.get(remoteId);        if (connection == null) {          connection = new Connection(remoteId);          connections.put(remoteId, connection);        }      }    } while (!connection.addCall(call));    //这里才真正完成了和服务器的连接    connection.setupIOstreams();    return connection;  }


查看connection.addCall()方法

//代码四//Client.Connection#addCall/**     * 将一个调用加入到对应的Connection的call队列中并唤醒一个监听者*/    private synchronized boolean addCall(Call call) {      if (shouldCloseConnection.get())        return false;      calls.put(call.id, call);      notify();      return true;}


再返回到代码三,我们来查看setupIOstream()方法。

//代码五//Client.Connection#setupIOstream/**      * 此方法用于连接到服务器并且设置IO流。然后发送一个头给服务器     * 然后启动一个连接线程用于等待服务器的响应     */    private synchronized void setupIOstreams() throws InterruptedException {      ……        while (true) {          setupConnection(); //建立连接          InputStream inStream = NetUtils.getInputStream(socket);//获得输入流          OutputStream outStream = NetUtils.getOutputStream(socket);//获得输出流          writeRpcHeader(outStream);         ……          this.in = new DataInputStream(new BufferedInputStream              (new PingInputStream(inStream)));//将输入流装饰城DataInputStream          this.out = new DataOutputStream          (new BufferedOutputStream(outStream));//将输出流装饰城DataOutStream          writeHeader();          // update last activity time更新活动时间          touch();          // start the receiver thread after the socket connection has been set up          //当连接建立时,启动接受线程等待服务端传回消息          start();          return;        }      } catch (Throwable t) {        if (t instanceof IOException) {          markClosed((IOException)t);        } else {          markClosed(new IOException("Couldn't set up IO streams", t));        }        close();      }    }

查看setupConnection()方法。

//代码六//Client.Connection#setupConnectionprivate synchronized void setupConnection() throws IOException {      short ioFailures = 0;      short timeoutFailures = 0;      while (true) {        try {          //通过socketFactory创建一个socket          this.socket = socketFactory.createSocket();          this.socket.setTcpNoDelay(tcpNoDelay);          if (UserGroupInformation.isSecurityEnabled()) {            KerberosInfo krbInfo =               remoteId.getProtocol().getAnnotation(KerberosInfo.class);            if (krbInfo != null && krbInfo.clientPrincipal() != null) {              String host =                 SecurityUtil.getHostFromPrincipal(remoteId.getTicket().getUserName());              // If host name is a valid local address then bind socket to it              InetAddress localAddr = NetUtils.getLocalInetAddress(host);              if (localAddr != null) {                this.socket.bind(new InetSocketAddress(localAddr, 0));              }            }          }          // connection time out is 20s设置连接超时时间为20s          NetUtils.connect(this.socket, server, 20000);          if (rpcTimeout > 0) {            pingInterval = rpcTimeout;  // rpcTimeout overwrites pingInterval          }          this.socket.setSoTimeout(pingInterval);          return;        } catch (SocketTimeoutException toe) {          if (updateAddress()) {            timeoutFailures = ioFailures = 0;          }              handleConnectionFailure(timeoutFailures++, 45, toe);        } catch (IOException ie) {          if (updateAddress()) {            timeoutFailures = ioFailures = 0;          }          handleConnectionFailure(ioFailures++, ie);        }      }    }

在这里它通过socketfactory创建了一个普通的socket并绑定到特定的端口进行通信。至此我们就知道了客户端到服务器的连接是怎样建立的了。建立完连接自然就是要发送数据了,也就是要解决第二个问题了。我们回到代码二中来具体查看connection.sendParam(call);代码如下:

//代码七//Client.Connetion#sendParam/**     * 通过将参数发送给远端的服务器来初始化一个调用     */    public void sendParam(Call call) {      if (shouldCloseConnection.get()) {        return;      }      DataOutputBuffer d=null;      try {        synchronized (this.out) {          if (LOG.isDebugEnabled())            LOG.debug(getName() + " sending #" + call.id);          d = new DataOutputBuffer();//首先创建一个缓冲区          d.writeInt(call.id);          call.param.write(d);//向缓冲区中写入数据          byte[] data = d.getData();          int dataLength = d.getLength();          out.writeInt(dataLength);      //首先写入数据的长度          out.write(data, 0, dataLength);//向服务端写数据          out.flush();        }      } catch(IOException e) {        markClosed(e);      } finally {        IOUtils.closeStream(d);      }    }

以上的代码就解释了客户端如何发送数据给服务端,其实就是socket发送数据的一般过程。接下来解第三个问题——分析客户端是怎样获取服务端传回的数据的。我们回到代码五,其中有个start()方法。我们知道,Connection继承了Thread类,所以可以调用它的start方法来启动一个新的线程,当启动以后该线程就会独自去执行自身的run()方法。接下来我们开查看Connection类中的run()方法。

//代码八//Client.Connetion#runpublic void run() {      if (LOG.isDebugEnabled())        LOG.debug(getName() + ": starting, having connections "             + connections.size());      //等待工作,直到有需要读的数据或者关闭连接      while (waitForWork()) {//wait here for work - read or close connection      //具体的处理方法        receiveResponse();      }      close();      if (LOG.isDebugEnabled())        LOG.debug(getName() + ": stopped, remaining connections "            + connections.size());    }

这段代码的重点是receiveResponse()方法,也就是该方法对收到的响应进行了具体的处理,下面来具体查看该函数。

//代码九//Client.Connetion#receiveResponse/**     * 接收一个响应,因为只有一个接受者,所以不用同步处理     */    private void receiveResponse() {      if (shouldCloseConnection.get()) {        return;      }      touch();      try {        int id = in.readInt();              //阻塞读取call的id        if (LOG.isDebugEnabled())          LOG.debug(getName() + " got value #" + id);        Call call = calls.get(id);//根据id在calls池中找到该响应对应的发送者        int state = in.readInt();     //阻塞读取call对象的状态        if (state == Status.SUCCESS.state) {          Writable value = ReflectionUtils.newInstance(valueClass, conf);          value.readFields(in);                 //读取数据          call.setValue(value);    //将读取到的值赋给call对象,同时唤醒client中的等待线程          calls.remove(id);        //删除已处理的id        } else if (state == Status.ERROR.state) {          ……        } else if (state == Status.FATAL.state) {         ……        }      } catch (IOException e) {        markClosed(e);      }    }

接着查看call.setValue()方法。

//代码十//Client.Call#setValue/**     * 若没有错误则将值赋给value变量,     * 同时唤醒等待在该调用上的调用者线程     */    public synchronized void setValue(Writable value) {      this.value = value;      callComplete();    }

接着查看callComplete()方法。

代码十一//Client.Call#callComplete/** *当调用完成并有返回值时自动唤醒相应的调用*/    protected synchronized void callComplete() {      this.done = true;      notify();                                 //唤醒调用者    }

至此,客户端接收服务端发回的响应也就完成了。总结一下就是:启动一个Connection处理线程,读取从服务端传来的call对象,将call对象读取完毕后唤醒client等待在该call上的处理线程。

       将客户端涉及的三个问题的具体调用流程用下图进行概括表示。







0 0
原创粉丝点击