如圖所示, 在hadoop中客戶端需要和服務端通信 。 首先我們看一下需求是啥。
舉一個例子,在客戶端想要往hadoop集群中寫數據的時候,它需要先和namenode通信,以便獲得 諸一個blockID。
這時 ,我們希望在客戶端可以做到 諸如 調用一個方法 如 getBlockID() 則就獲得了服務端的發過來的ID ,如果調用本地方法一樣。
需求搞定,我們看現實有的條件 服務端通信我們有的能力為socket,這個是已經封裝在linux內核之中, JAVA對linux內核通信又進行了封裝,有了自己的
Socket ServerSocket 通信, 同時在JAVA Nio中又提出了 異步方式的IO。
好,我們有的資源和需要達到的目標都已經有了,下面是實現中間件來彌補兩者之間的鴻溝。
首先從客戶端來看。 客戶端調用服務端的服務,肯定需要底層通信處理,而且這些通信處理需要集中處理,不能每次遠程調用,都需重新處理一遍底層連接。
有什么方法可以達到這個目的么 ? 動態代理。
-
public Object invoke(Object proxy, Method method, Object[] args)
-
throws Throwable {
-
-
ObjectWritable value = (ObjectWritable)
-
client.call(new Invocation(method, args), remoteId);
-
-
return value.get();
-
}
一般我們看到的動態代理的invoke()方法中總會有 method.invoke(ac, arg); 這句代碼。而上面代碼中卻沒有,這是為什么呢?其實使用 method.invoke(ac, arg); 是在本地JVM中調用;
在客戶端這邊並沒有Proxy對象,我們需要到服務找到對應的對象然后調用相應的方法。在hadoop中,是將數據發送給服務端,服務端將處理的結果再返回給客戶端,所以這里的invoke()方法必然需要進行網絡通信。
到這里我們可以再一次從圖形來表示一下我們需要達到的目標。
下面這句代碼就是服務端真正的調用相應方法的語句, 其中的instance對象,是運行在服務端的對象,call是客戶端傳遞過來的參數。 通過反射機制,進行方法調用。
-
Object value = method.invoke(instance, call.getParameters());
上面我們把大體的框架搭建起來了,下面一步步進行細節分析。
在上面所示的invok()方法中,最終調用的方法為
-
client.call(new Invocation(method, args), remoteId);
-
value.get();
我們需求分析 Client端是如何通過 這兩個方法 調用了遠程服務器的方法,並且獲取返回值得。
需要解決的三個問題是
- 客戶端和服務端的連接是怎樣建立的?、
- . 客戶端是怎樣給服務端發送數據的?
- 客戶端是怎樣獲取服務端的返回數據的?
-
public Writable call(Writable param, ConnectionId remoteId)
-
throws InterruptedException, IOException {
-
Call call = new Call(param); //將傳入的數據封裝成call對象
-
Connection connection = getConnection(remoteId, call); //獲得一個連接
-
connection.sendParam(call); // 向服務端發送call對象
-
boolean interrupted = false;
-
synchronized (call) {
-
while (!call.done) {
-
try {
-
call.wait(); // 等待結果的返回,在Call類的callComplete()方法里有notify()方法用於喚醒線程
-
} catch (InterruptedException ie) {
-
// 因中斷異常而終止,設置標志interrupted為true
-
interrupted = true;
-
}
-
}
-
if (interrupted) {
-
Thread.currentThread().interrupt();
-
}
-
-
if (call.error != null) {
-
if (call.error instanceof RemoteException) {
-
call.error.fillInStackTrace();
-
throw call.error;
-
} else { // 本地異常
-
throw wrapException(remoteId.getAddress(), call.error);
-
}
-
} else {
-
return call.value; //返回結果數據
-
}
-
}
-
}
網絡通信有關的代碼只會是下面的兩句了:
-
Connection connection = getConnection(remoteId, call); //獲得一個連接
-
connection.sendParam(call); // 向服務端發送call對象
先看看是怎么獲得一個到服務端的連接吧,下面貼出ipc.Client類中的getConnection()方法。
-
private Connection getConnection(ConnectionId remoteId,
-
Call call)
-
throws IOException, InterruptedException {
-
if (!running.get()) {
-
// 如果client關閉了
-
throw new IOException("The client is stopped");
-
}
-
Connection connection;
-
//如果connections連接池中有對應的連接對象,就不需重新創建了;如果沒有就需重新創建一個連接對象。
-
//但請注意,該//連接對象只是存儲了remoteId的信息,其實還並沒有和服務端建立連接。
-
do {
-
synchronized (connections) {
-
connection = connections.get(remoteId);
-
if (connection == null) {
-
connection = new Connection(remoteId);
-
connections.put(remoteId, connection);
-
}
-
}
-
} while (!connection.addCall(call)); //將call對象放入對應連接中的calls池,就不貼出源碼了
-
//這句代碼才是真正的完成了和服務端建立連接哦~
-
connection.setupIOstreams();
-
return connection;
-
}
下面貼出Client.Connection類中的setupIOstreams()方法:
-
private synchronized void setupIOstreams() throws InterruptedException {
-
???
-
try {
-
???
-
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)); //將輸出流裝飾成DataOutputStream
-
writeHeader();
-
// 跟新活動時間
-
touch();
-
//當連接建立時,啟動接受線程等待服務端傳回數據,注意:Connection繼承了Tread
-
start();
-
return;
-
}
-
} catch (IOException e) {
-
markClosed(e);
-
close();
-
}
-
}
再有一步我們就知道客戶端的連接是怎么建立的啦,下面貼出Client.Connection類中的setupConnection()方法:
-
private synchronized void setupConnection() throws IOException {
-
short ioFailures = 0;
-
short timeoutFailures = 0;
-
while (true) {
-
try {
-
this.socket = socketFactory.createSocket(); //終於看到創建socket的方法了
-
this.socket.setTcpNoDelay(tcpNoDelay);
-
???
-
// 設置連接超時為20s
-
NetUtils.connect(this.socket, remoteId.getAddress(), 20000);
-
this.socket.setSoTimeout(pingInterval);
-
return;
-
} catch (SocketTimeoutException toe) {
-
/* 設置最多連接重試為45次。
-
* 總共有20s*45 = 15 分鍾的重試時間。
-
*/
-
handleConnectionFailure(timeoutFailures++, 45, toe);
-
} catch (IOException ie) {
-
handleConnectionFailure(ioFailures++, maxRetries, ie);
-
}
-
}
-
}
終於,我們知道了客戶端的連接是怎樣建立的了,其實就是創建一個普通的socket進行通信。
問題2:客戶端是怎樣給服務端發送數據的?
第一句為了完成連接的建立,我們已經分析完畢;而第二句是為了發送數據,呵呵,分析下去,看能不能解決我們的問題呢。下面貼出Client.Connection類的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);
-
}
-
}
問題3:客戶端是怎樣獲取服務端的返回數據的?
,當連接建立時會啟動一個線程用於處理服務端返回的數據,我們看看這個處理線程是怎么實現的吧,下面貼出Client.Connection類和Client.Call類中的相關方法吧:
-
方法一:
-
public void run() {
-
???
-
while (waitForWork()) {
-
receiveResponse(); //具體的處理方法
-
}
-
close();
-
???
-
}
-
-
方法二:
-
private void receiveResponse() {
-
if (shouldCloseConnection.get()) {
-
return;
-
}
-
touch();
-
try {
-
int id = in.readInt(); // 阻塞讀取id
-
if (LOG.isDebugEnabled())
-
LOG.debug(getName() + " got value #" + id);
-
Call call = calls.get(id); //在calls池中找到發送時的那個對象
-
int state = in.readInt(); // 阻塞讀取call對象的狀態
-
if (state == Status.SUCCESS.state) {
-
Writable value = ReflectionUtils.newInstance(valueClass, conf);
-
value.readFields(in); // 讀取數據
-
//將讀取到的值賦給call對象,同時喚醒Client等待線程,貼出setValue()代碼方法三
-
call.setValue(value);
-
calls.remove(id); //刪除已處理的call
-
} else if (state == Status.ERROR.state) {
-
???
-
} else if (state == Status.FATAL.state) {
-
???
-
}
-
} catch (IOException e) {
-
markClosed(e);
-
}
-
}
-
-
方法三:
-
public synchronized void setValue(Writable value) {
-
this.value = value;
-
callComplete(); //具體實現
-
}
-
protected synchronized void callComplete() {
-
this.done = true;
-
notify(); // 喚醒client等待線程
-
}
客戶端的代碼分析就到這里,我們可以發現 ,客戶端使用 普通的socket 連接把客戶端的方法調用 名稱 參數 (形參 和實參) 傳遞到服務端了。
下面分析服務端的代碼。
對於ipc.Server,我們先分析一下它的幾個內部類吧:
Call :用於存儲客戶端發來的請求
Listener : 監聽類,用於監聽客戶端發來的請求,同時Listener內部還有一個靜態類,Listener.Reader,當監聽器監聽到用戶請求,便讓Reader讀取用戶請求。
Responder :響應RPC請求類,請求處理完畢,由Responder發送給請求客戶端。
Connection :連接類,真正的客戶端請求讀取邏輯在這個類中。
Handler :請求處理類,會循環阻塞讀取callQueue中的call對象,並對其進行操作。
你會發現其實ipc.Server是一個abstract修飾的抽象類。那隨之而來的問題就是:hadoop是怎樣初始化RPC的Server端的呢?Namenode初始化時一定初始化了RPC的Sever端,那我們去看看Namenode的初始化源碼吧:
-
private void initialize(Configuration conf) throws IOException {
-
???
-
// 創建 rpc server
-
InetSocketAddress dnSocketAddr = getServiceRpcServerAddress(conf);
-
if (dnSocketAddr != null) {
-
int serviceHandlerCount =
-
conf.getInt(DFSConfigKeys.DFS_NAMENODE_SERVICE_HANDLER_COUNT_KEY,
-
DFSConfigKeys.DFS_NAMENODE_SERVICE_HANDLER_COUNT_DEFAULT);
-
//獲得serviceRpcServer
-
this.serviceRpcServer = RPC.getServer(this, dnSocketAddr.getHostName(),
-
dnSocketAddr.getPort(), serviceHandlerCount,
-
false, conf, namesystem.getDelegationTokenSecretManager());
-
this.serviceRPCAddress = this.serviceRpcServer.getListenerAddress();
-
setRpcServiceServerAddress(conf);
-
}
-
//獲得server
-
this.server = RPC.getServer(this, socAddr.getHostName(),
-
socAddr.getPort(), handlerCount, false, conf, namesystem
-
.getDelegationTokenSecretManager());
-
-
???
-
this.server.start(); //啟動 RPC server Clients只允許連接該server
-
if (serviceRpcServer != null) {
-
serviceRpcServer.start(); //啟動 RPC serviceRpcServer 為HDFS服務的server
-
}
-
startTrashEmptier(conf);
-
}
-
this.serviceRpcServer = RPC.getServer(this, dnSocketAddr.getHostName(),
-
-
dnSocketAddr.getPort(), serviceHandlerCount,
這里面我們需要重點關注的是這個上面這個方法, 可以看到這里面傳遞過去的第一個參數是this .我們在前面說服務端最終是需要調用在服務端的某個對象來實際運行方法的。
現在這個this對象,及namenode對象就是服務端的相應對象。我們就有疑問,那么客戶端有那么多接口 ,namenode都實現了相應的對象么?是的都實現了。這也好理解,客戶端
會調用什么方法,肯定都是服務端和客戶端事先約定好的,服務端肯定把相應的對象創建好了來等待客戶端的調用。我們可以看一下namenode實現的端口,就很明晰了。
-
public class NameNode implements ClientProtocol, DatanodeProtocol,
-
NamenodeProtocol, FSConstants,
-
RefreshAuthorizationPolicyProtocol,
-
RefreshUserMappingsProtocol {
下面我們來分析服務端是如何處理請求的。
分析過ipc.Client源碼后,我們知道Client端的底層通信直接采用了阻塞式IO編程。但hadoop是單中心結構,所以服務端不可以這么做,而是采用了java NIO來實現Server端,那Server端采用java NIO是怎么建立連接的呢?分析源碼得知,Server端采用Listener監聽客戶端的連接,下面先分析一下Listener的構造函數吧:
-
public Listener() throws IOException {
-
address = new InetSocketAddress(bindAddress, port);
-
// 創建ServerSocketChannel,並設置成非阻塞式
-
acceptChannel = ServerSocketChannel.open();
-
acceptChannel.configureBlocking(false);
-
-
// 將server socket綁定到本地端口
-
bind(acceptChannel.socket(), address, backlogLength);
-
port = acceptChannel.socket().getLocalPort();
-
// 獲得一個selector
-
selector= Selector.open();
-
readers = new Reader[readThreads];
-
readPool = Executors.newFixedThreadPool(readThreads);
-
//啟動多個reader線程,為了防止請求多時服務端響應延時的問題
-
for (int i = 0; i < readThreads; i++) {
-
Selector readSelector = Selector.open();
-
Reader reader = new Reader(readSelector);
-
readers[i] = reader;
-
readPool.execute(reader);
-
}
-
// 注冊連接事件
-
acceptChannel.register(selector, SelectionKey.OP_ACCEPT);
-
this.setName("IPC Server listener on " + port);
-
this.setDaemon(true);
-
}
在啟動Listener線程時,服務端會一直等待客戶端的連接,下面貼出Server.Listener類的run()方法:
-
public void run() {
-
???
-
while (running) {
-
SelectionKey key = null;
-
try {
-
selector.select();
-
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
-
while (iter.hasNext()) {
-
key = iter.next();
-
iter.remove();
-
try {
-
if (key.isValid()) {
-
if (key.isAcceptable())
-
doAccept(key); //具體的連接方法
-
}
-
} catch (IOException e) {
-
}
-
key = null;
-
}
-
} catch (OutOfMemoryError e) {
-
???
-
}
下面貼出Server.Listener類中doAccept ()方法中的關鍵源碼吧:
-
void doAccept(SelectionKey key) throws IOException, OutOfMemoryError {
-
Connection c = null;
-
ServerSocketChannel server = (ServerSocketChannel) key.channel();
-
SocketChannel channel;
-
while ((channel = server.accept()) != null) { //建立連接
-
channel.configureBlocking(false);
-
channel.socket().setTcpNoDelay(tcpNoDelay);
-
Reader reader = getReader(); //從readers池中獲得一個reader
-
try {
-
reader.startAdd(); // 激活readSelector,設置adding為true
-
SelectionKey readKey = reader.registerChannel(channel);//將讀事件設置成興趣事件
-
c = new Connection(readKey, channel, System.currentTimeMillis());//創建一個連接對象
-
readKey.attach(c); //將connection對象注入readKey
-
synchronized (connectionList) {
-
connectionList.add(numConnections, c);
-
numConnections++;
-
}
-
???
-
} finally {
-
//設置adding為false,采用notify()喚醒一個reader,其實代碼十三中啟動的每個reader都使
-
//用了wait()方法等待。因篇幅有限,就不貼出源碼了。
-
reader.finishAdd();
-
}
-
}
-
}
當reader被喚醒,reader接着執行doRead()方法。
下面貼出Server.Listener.Reader類中的doRead()方法和Server.Connection類中的readAndProcess()方法源碼:
-
方法一:
-
void doRead(SelectionKey key) throws InterruptedException {
-
int count = 0;
-
Connection c = (Connection)key.attachment(); //獲得connection對象
-
if (c == null) {
-
return;
-
}
-
c.setLastContact(System.currentTimeMillis());
-
try {
-
count = c.readAndProcess(); // 接受並處理請求
-
} catch (InterruptedException ieo) {
-
???
-
}
-
???
-
}
-
-
方法二:
-
public int readAndProcess() throws IOException, InterruptedException {
-
while (true) {
-
???
-
if (!rpcHeaderRead) {
-
if (rpcHeaderBuffer == null) {
-
rpcHeaderBuffer = ByteBuffer.allocate(2);
-
}
-
//讀取請求頭
-
count = channelRead(channel, rpcHeaderBuffer);
-
if (count < 0 || rpcHeaderBuffer.remaining() > 0) {
-
return count;
-
}
-
// 讀取請求版本號
-
int version = rpcHeaderBuffer.get(0);
-
byte[] method = new byte[] {rpcHeaderBuffer.get(1)};
-
???
-
-
data = ByteBuffer.allocate(dataLength);
-
}
-
// 讀取請求
-
count = channelRead(channel, data);
-
-
if (data.remaining() == 0) {
-
???
-
if (useSasl) {
-
???
-
} else {
-
processOneRpc(data.array());//處理請求
-
}
-
???
-
}
-
}
-
return count;
-
}
-
}
獲得call對象
下面貼出Server.Connection類中的processOneRpc()方法和processData()方法的源碼
-
方法一:
-
private void processOneRpc(byte[] buf) throws IOException,
-
InterruptedException {
-
if (headerRead) {
-
processData(buf);
-
} else {
-
processHeader(buf);
-
headerRead = true;
-
if (!authorizeConnection()) {
-
throw new AccessControlException("Connection from " + this
-
+ " for protocol " + header.getProtocol()
-
+ " is unauthorized for user " + user);
-
}
-
}
-
}
-
方法二:
-
private void processData(byte[] buf) throws IOException, InterruptedException {
-
DataInputStream dis =
-
new DataInputStream(new ByteArrayInputStream(buf));
-
int id = dis.readInt(); // 嘗試讀取id
-
Writable param = ReflectionUtils.newInstance(paramClass, conf);//讀取參數
-
param.readFields(dis);
-
-
Call call = new Call(id, param, this); //封裝成call
-
callQueue.put(call); // 將call存入callQueue
-
incRpcCount(); // 增加rpc請求的計數
-
}
處理call對象
你還記得Server類中還有個Handler內部類嗎?呵呵,對call對象的處理就是它干的。下面貼出Server.Handler類中run()方法中的關鍵代碼:
-
while (running) {
-
try {
-
final Call call = callQueue.take(); //彈出call,可能會阻塞
-
???
-
//調用ipc.Server類中的call()方法,但該call()方法是抽象方法,具體實現在RPC.Server類中
-
value = call(call.connection.protocol, call.param, call.timestamp);
-
synchronized (call.connection.responseQueue) {
-
setupResponse(buf, call,
-
(error == null) ? Status.SUCCESS : Status.ERROR,
-
value, errorClass, error);
-
???
-
//給客戶端響應請求
-
responder.doRespond(call);
-
}
-
}
終於看到了call 方法 我們下面看看服務端實際的call方法是怎么執行的吧
-
public Writable call(Class<?> protocol, Writable param, long receivedTime)
-
throws IOException {
-
try {
-
Invocation call = (Invocation)param;
-
if (verbose) log("Call: " + call);
-
-
Method method =
-
protocol.getMethod(call.getMethodName(),
-
call.getParameterClasses());
-
method.setAccessible(true);
-
-
long startTime = System.currentTimeMillis();
-
Object value = method.invoke(instance, call.getParameters());
最后一句我們發現實際上是用了反射。 反射中的那個實際對象 instance 就是在namenode起來的時候創建的namenode對象。