1.远程过程调用(RPC)
即允许程序调用位于其他机器上的过程:比如A是调用方,B是被调用方,A将信息传给B,然后挂起等待B反馈信息给A,最简单的过程调用是方法调用,典型的控制流同步模型(C/S模型在不同进程之间交互)就是一种RPC。
RPC通过接口定义语言(IDL)描述远程调用的接口信息,通过调用IDL编译器之后会生成三个文件包括头文件(包含接口函数的定义),客户存根(可以理解为客户端),服务器骨架(可以理解为服务器)
2.Java远程方法调用(RMI)
即允许一个虚拟机上的java进程调用不同虚拟机上对象的方法,RMI提供了标准的Stub/Skeleton机制,其中Stub对象(保存远程对象的接口和方法列表)可以被客户端引用的远程对象,Stub对象将调用请求通过RMI基础结构转发到远程对象,当接受到调用请求时,服务器端的Skeleton对象会处理有关调用远方对象的所有细节,并调用Skeleton对象
RMI使用过程:
1) 定义远程调用接口,其中远程调用接口需要满足以下条件
l 必须定义为public
l 必须继承于java.rmi.Remote
l 对于远程调用接口中的每个方法,必须将java.rmi.RemoteException声明在throws中
l 在远程方法声明中,作为参数或者返回值或者包含在其他非远程对象中的远程对象,必须声明为其对应的远程接口
2) 定义远程对象类,必须继承自UnicastRemoteObject,并实现相应的远程接口,允许远程对象类中有额外的成员函数,但只有远程接口中的函数才能被客户端访问,并启动注册表服务rmiregistry,启动服务器RMIQueryStatusServer。
3) 远程对象绑定到注册服务表上(客户端需要根据注册服务表创建本地存根对象)
服务器骨架创建远程对象后
LocateRegistry.createRegistry(12090)//注册端口
Naming.rebind(UMI_URL,object)//object指远程对象,UMI_URL与上述注册端口对应的url资源,即远程对象绑定了注册服务表
4) 客户端通过注册服务表查找并创建远程对象的本地引用
Naming.lookup(RMIQueryStatusServer.RMI_URL)
5)通过本地引用调用远程对象方法,客户端RMIQueryStatueClient工作依赖于RMI存根,存根又是基于Java的代理机制实现。Rmic工具可以根据远程调用类的字节码.class生产相应的存根,如果使用-keep选项,则会保存中间结果.java
3.Java动态代理(java.lang.reflect.Proxy)
代理对象(Proxy)往往实现与目标对象(Target)相同的接口,并作为目标对象的代替,接收用户的调用,并将全部或者部分调用转发给目标对象。Proxy用于创建动态代理类和实例的静态方法
Proxy的静态方法getProxyClass()//用于获得代理类的java.lang.Class对象(只会创建一次对象),此代理类的构造函数一定包含一个InvocationHandler实例
Hadoop IPC中一般用newProxyInstance()返回一个或者多个指定接口的代理类实例,在接口上的方法电泳会指派到对应的调用处理程序上
调用转发:由java.lang.reflect.InvocationHandler实现,其中唯一的接口函数invoke(proxy,method,args);//proxy(代理对象),method(用户调用代理对象上的方法),args传给该方法的参数,并将结果返回给调用者
Java中使用动态代理(Proxy)的基本步骤:
(1) 创建动态代理监控的接口
(2) 创建目标类实现上述接口
(3) 创建调用转发器实现InvocationHandler接口中的invoke(),一般成员变量为上述目标类对应的目标对象,最简单的实现就是直接由目标对象执行method
(4) 创建动态代理对象proxy,Proxy.newInstance(classLoader,interfaces,handler);
其中classLoader为目标对象对应的类加载器,interfaces为代理对象proxy监控的接口,handler为上述调用转发器的实例
(5) 当proxy调用接口函数时,会触发调用转发器的invoke(),由目标对象进行相关处理并返回结果
Java中套接字的使用的基本步骤:
Server端:
ServerSocket listen = new ServerSocket(); //创建监听套接字
listen.bind(new InetSocketAddress("127.0.0.1",6666));//绑定监听端口
Socket socket=listen.accept();
InputStream inputStream=socket.getInputStream();//读数据
OutputStream outputStream=socket.getOutputStream();//写数据
DataInputStream dataInputStream=new DataInputStream(inputStream);
String str=dataInputStream.readUTF();
System.out.println("server receive message is"+str);
DataOutputStream dataOutputStream=new DataOutputStream(outputStream);
dataOutputStream.writeLong(4096);
dataOutputStream.flush();
System.out.println("server send message is"+4096);
dataInputStream.close();
dataOutputStream.close();
inputStream.close();
outputStream.close();
socket.close();
listen.close();
客户端:
Socket socket=new Socket();
socket.connect(new InetSocketAddress("127.0.0.1",6666));
InputStream inputStream=socket.getInputStream();
OutputStream outputStream=socket.getOutputStream();
DataInputStream dataInputStream=new DataInputStream(inputStream);
DataOutputStream dataOutputStream=new DataOutputStream(outputStream);
dataOutputStream.writeUTF("hi server");
dataOutputStream.flush();
System.out.println("client is saying hi server");
Long msg=dataInputStream.readLong();
System.out.println("client receive msg is"+msg);
dataOutputStream.close();
dataInputStream.close();
outputStream.close();
inputStream.close();
socket.close();
非阻塞I/O(选择器(Selector)和通道(Channel)),java.nio.Buffer本质上是个数组,Channel使用Buffer实例(块)来传递数据
Buffer包含4个索引:
1) capacity:缓冲区Buffer中元素的总数
2) position:缓冲区的位置,下一个要读取或者写入元素的索引
3) limit:缓冲区的限制,第一个不应该读取或者写入元素的索引
4) mark:缓冲区的位置设置标记,通过mark()方法设置一个位置后,利用reset()方法可将position重置为mark()所在位置
0<=mark<=position<=limit<=capacity
Buffer提供了put()/get()方法从而能放入/取出缓冲区中的数据
Buffer提供clean()方法清空缓冲区,position=0,limit=capacity
Buffer提供filp()将缓冲区准备为数据传出状态,limit置于position之后且position=0,读模式->写模式
Buffer提供rewind()将position置0,但不改变limit
Buffer提供compact()将position和limit间的数据复制到缓冲区开始位置,position设置为数据长度,limit=capacity,写模式->读模式
Channel是NIO,代表与一个设备的连接,通过它可以进行输入输出操作。(数据交换的基本单位是Buffer),具有以下特点
1) SocketChannel创建后,可以通过connect()连接到远程机器,并通过close()关闭连接
2) ServerSocketChannel对象无bind()方法,accept()会返回一个SocketChannel
3) SocketChannel的connect()是非阻塞的,用户必须通过isConnected()判断连接是否建立
Selector(选择器)的基本使用步骤:
1) 通过静态的工厂方法创建Selector实例
2) 通过Channel的注册方法(register())与Selector实例关联(多对多关联),关联信息保存在SelectionKey的实例中,选择器注册标记SelectionKey维护了一个通道Channel感兴趣的事件类型信息,包括4种类型:
l OP_READ(通道有数据可读)
l OP_WRITE(通道已经可写)
l OP_CONNECT(通道连接已建立)
l OP_ACCEPT(通道上有连接请求)
3) Selector实例调用select()轮询等待已注册通道上出现感兴趣I/O事件的个数,selectedKeys()会返回可进行I/O操作的通道,两次调用select()之间必须手工清空
4) 对于accpet()操作获得的SocketChannel对象,需将信道设置为非阻塞模式,并将其注册到选择器,并设置感兴趣的选择为OP_READ/ OP_WRITE
5) 创建connect对象,conn为关联的附加信息,若可读/可写则可以conn.handleRead()/conn.handleWrite()处理通道的读写操作
4.Hadoop远程过程调用
IPC(进程间通信·)的使用基本步骤
1) IPC接口必须继承VersionedProtocol接口,其中VersionedProtocol接口包括唯一的接口函数getProtocolVersion(Stringprotocol,long clientVersion);//protocol协议接口名,clientVersion客户端版本,返回服务器段的接口实现的版本
2) IPC接口的实现必须实现IPC接口函数和getProtocolVersion()
3) IPC服务器根据IPC接口的实现创建IPC对象,RPC.getServer(queryService,”0.0.0.0”,IPC_PORT,new Configuration());//IPC对象,监听地址,监听端口,配置信息创建一个服务器实例,并通过调用start()和stop()来控制IPC服务器的开关。
4) IPC客户端根据IPC接口的class对象创建IPC客户端RPC.getProxy(IPCQueryStatus.class,IPCQueryServer.IPC_VER,addr,newConfiguration());//IPC接口的类对象,IPC接口服务器端的版本信息,服务器端地址,配置信息,IPC客户端对象(proxy)调用IPC接口方法,RPC.stopProxy(query)关闭IPC客户端。
IPC的代码结构
Hadoop中与IPC相关的代码全在org.apache.hadoop.ipc中,一共7个文件,其中最为重要的是Client.java,Server.java,RPC.java三个文件
1) Client.java:其内部类主要有Client.Connection,Client.ConnectionId(IPC连接相关);Client.Call,Client.ParallelCall(远程调用Call相关)
2) Server.java:其内部类主要有Server.Connection(IPC连接相关);Server.Call(远程调用Call相关),对远程调用Call的处理由Linstener,Handler,Responder配合完成
3) RPC.java:其内部类主要包括与客户端相关的RPC.ClientCache,RPC.Invoker,RPC.Invocation;与服务器相关的内部类RPC.Server
1. Connection
分为Client.Connection和Server.Connection各自保存客户端/服务器的IPC连接信息,IPC连接建立在TCP连接之上
(1) ConnectionId:为了唯一区分一条IPC连接以及方便IPC连接复用,包括address(服务器地址),ticket(用户组),IPC接口的实例;
(2) ConnectionHeader:IPC连接建立后发送的第一个消息,包括ticket(用户组),IPC接口的实例,用于检查服务器是否实现IPC接口
2. Call
Clent.ParallelCall是Client.Call调用的子类,客户端并行调用多个远程IPC服务器上的IPC接口方法,并等待所有响应
Client.ParallelResults保存已返回的部分结果
IPC客户端接口实例的方法调用->RPC.Invoker捕获调用请求并创建RPC.Invocation对象->RPC.Invoker调用Client.call()根据RPC.Invocation对象创建远程调用Client.Call对象->通过IPC连接发送至服务器,Client.call()等待远程调用结果才返回
3. 服务器处理器
主要包括监听器(Listener)、处理器(Handler)和应答器(Responder),处理客户端的远程调用请求
Listener:监听客户端的IPC连接请求和连接之后的数据请求,并调用服务器连接对象上的相关方法。连接对象主要工作是接收客户端的远程调用请求帧,反序列化后放入阻塞队列中,有Handler处理
Handler:根据远程调用Call的上下文,调用IPC接口的实现类,完成过程调用,并将嗲用结果序列化后,在链接的应答队列为空时返回给客户端。
Responder:客户端较忙,应答队列不为空时,Handler将调用结果放入响应队列,并通过IPC连接发送回客户端。
5 HadoopIPC连接相关过程
(1) Client.Connection的成员变量
分为与TCP相关的成员信息,与IPC相关的成员信息,与远程调用相关的成员信息
与TCP相关的成员变量有IPC服务器的地址server,TCP连接对应的Socket对象socket、以及socket对象上的输入流对象in和输出流对象out
与IPC连接相关的成员变量:连接消息头header、连接标识remoteId
与维护IPC连接和关闭IPC连接相关的成员变量:lastActivity(最后一次I/O发生时间),shouldCloseConnection(连接关闭标记),closeException(导致IPC连接关闭的异常)
与远程调用相关的成员变量:calls(保存目前IPC连接上的所有远程调用)
(2) Server.Connection的成员变量
与TCP相关的成员变量:channel(套接字通道),buffer(缓冲区)
与IPC连接相关的成员变量:versionRead(检查客户端IPC版本与服务器版本是否一致),headerRead(连接头检查,检查服务器是否实现了客户端需要的IPC接口以及客户端用户是否有权限使用这些接口),连接头header,远程接口protocol,客户端用户user,authFailedCall(用户鉴权失败后对客户端的应答和具体使用方法),autoFailedResponse(用户授权失败后对客户端的应答和具体使用方法),lastContact(最后一次收到客户端数据的时间)
与远程调用相关的成员变量:responseQueue(IPC调用应答队列),rpcCount(该IPC连接上正在处理的RPC数)
建立IPC连接
客户端:
Client.getConnection(addr,protocol,ticket,call)://只有在IPC调用时才建立,如果有合适的connection则采用连接复用,否则创建一个新连接,其中Client中存在一个connections的HashTable成员变量,用于记录ConnectionId与Connection的关系
Client.Connection.setupIOstreams()//获得connection之后调用此方法首先会建立Socket连接,然后调用writeHeader()发送头信息包括IPC连接魔数,协议版本号,ConnectionHeader,调用touch()更新最后一次I/O发生的时间,启动线程读取并等待响应数据
服务器端:
Listener在其构造函数张打开服务器的端口,创建Selector进行监听,其参数为backlogLength(指定在监听端口上排队的请求的最大长度)
Listener.run()调用select()方法处理事件doAccept()//接受客户端的连接请求、注册Socket到选择器,并创建Server.Connection对象;doRead()//通过Server.Connection.readAndProcess读取并处理数据(如果顺利读取到IPC链接魔数和协议版本号并完成版本检查,则设置rpcHeaderRead为true然后进入连接头检查,若通过则设置headerRead为true,服务器开始处理IPC请求)连接头检查的包括两个步骤(1.processHeader()调用读入ConnectionHeader,保证服务器实现了IPC接口和获取用户信息2.Service.authorize()保证用户有权限访问远程接口)
数据分帧和读写
客户端到服务器的通信采用显长方式,即先采用定长接收数据,然后采用显长接收数据,在IPC连接建立后,当客户端调用IPC接口时,需要发送Client.Call对象到服务器(通过sendParam(Call call)封装到数据输出缓冲区中然后读取缓冲区消息的长度和内容发送到服务器);服务器端调用readAndProcess()读取消息并处理;而服务器往客户端写数据通过Server.setupResponse();客户端读服务器的数据通过Connection.receiveResponse()。
维护IPC连接(心跳信息)
客户端长时间没有远程调用时,会向服务器发送心跳信息,从而维IPC连接。sendPIng()会判断当前时间和lastActivity的差值是否超过某个值,从而发送心跳信息给服务器(注意sendPing()只能被PingInputStream的handleTimeout()调用,而handleTimeout()只被中的read()调用,而超时时间在setupIOstream()中通过Socket的setSoTimeout()设置)
关闭IPC连接(调用Client.stop(),IPC连接错误,IPC连接长时间没有IPC调用)
Client.stop()关闭客户端,设置running=false,调用interrupt()唤醒IPC连接线程并执行关闭操作
IPC长时间没有IPC调用的判断发生在waitForWork()中,只有在连接正常(!shouldCloseConnection),客户端处于工作状态(running),远程调用正在处理的情况下,waitForWork才会返回true,否则会关闭连接
Connection.close()首先会在连接列表中移除当前连接,关闭输入/输出流,调用Connection.cleanupCalls()清理未完成的远程调用。
HadoopIPC方法调用过程
与IPC调用相关的三个抽象类包括RPC.Invocation,Client.Call,Server.Call
RPC.Invocation:当客户端接口实例调用时被Invoker句柄捕获,创建RPC.Invocation实例对象,其成员变量包括方法名(methodName),形式参数(parameterClasses),实际参数(parameters)
Client.Call:RPC.Invocation对象需要被封装成Client.Call对象之后才能通过IPC连接传给服务器,其成员变量包括:远程调用ID(id),参数(param实际上就是被封装的RPC.Invocation对象),调用结果(正常返回则返回值赋值给value,异常返回则将异常包装在RemoteException中并赋值给error),状态标识(done标识调用是否完成)
Server.Call:服务器端会创建Server.Call与Client.Call对应,其中成员变量包括远程调用ID(id),参数(param实际上就是被封装的RPC.Invocation对象),应答结果(response),连接对象(connection,Server.Call需要利用connection发送回客户端),时间戳(timestamp则是进行超时检查,接受请求和调用结束后都会再次更新,若超时则清除应答)
客户端方法调用过程:
1)RPC.getProxy(ICP接口.class,IPC_VER,addr,conf)获得动态代理
2)Proxy调用接口方法被Invoker(在Invoker实例创建时会初始化client成员变量)捕获,client.call(invocation,addr,method.getDeclaringClass(),ticket)
3)进入call()执行体
3.1)创建Call实体
3.2)创建IPC连接
3.3)connection.sendParam(call)//发送call至服务器
3.4)等待服务器的应答call.wait()
4)调用Call.callComplete(),设置done=true(标志调用完成),同时调用notify()通知call停止等待
5)执行receiveResponse()会判断调用是否成功,若Status.SUCCESS,则call.setValue()保存正常的应答值,若Status.ERROR,则call.setException()保存异常,若Status.FATAL,则markClosed()强制关闭连接。
服务器端方法调用过程
Listener:在调用doRead()方法读取数据,而在Connection.readAndProcess()中恢复数据帧,然后调用processData()处理一帧数据,构造Server.Call实例对象,并将该对象发送阻塞队列callQueue中。
Handler:
1)在run()中循环处理每个请求,callQueue.take()
2)每个请求call通过Subject.doAs(call.connection.user,newPrivilegedExceptionAction
3)Server.call()完成服务器端的方法调用,其中call(protocol,param,receiveTime)//protocol接口名称,param方法信息,而call()的具体实现在RPC.Server中
4)执行RPC.Server中的call()方法,首先得到接口方法method,然后利用反射机制IPC夫妻实现对象调用相应的方法,完成远程过程调用
5)若调用正常结束,则将结果放入ObjectWritable对象中,否则只能抛出IOException
Responder:Handler调用Responder的doRespond()方法将处理完的结果交给Responder
1)responder.doRespond(call),即将call放入应答队列responseQueue中(特殊情况当应答队列中元素个数为1时,直接调用processResponse()发送应答,否则会执行Responder.run())
2)创建Selector实例selector绑定channel,执行select()等待通道可写,执行doAsyncWrite(key)输出远程调用结果
3)根据key得到Server.Call的实例,然后执行processResponse()操作,若返回值为true,则说明该通道上没有等待的数据,调用SelectionKey.interestOpt()方法清除兴趣操纵集
4)在processResponse(responseQueue,inHandler)中,调用channelWrite(channel,call.response)发送应答至客户端
5)清理工作doPurge(call,now)//关闭connection连接