HadoopRPC源码解析
来源:互联网 发布:迅龙数据恢复有用吗 编辑:程序博客网 时间:2024/05/28 05:14
HadoopRPC源码阅读学习
背景
RPC在Hadoop生态中的作用
HadoopRPC源码解析
ipc.RPC源码分析
ipc.Server源码分析
ipc.Client源码分析
与其他RPC框架对比分析来提炼通用模型
参考资源
一、背景
Hadoop是一个大数据处理平台,目前在大数据领域应用也非常广泛,刚好最近我们BI组在进行把底层数据仓库迁移到Hadoop平台,所以在工作之余开始去深入了解下hadoop内部实现以更好地应用它,在遇到问题的时候有更好的解决思路。本篇分享先介绍Hadoop领域中RPC框架的实现原理,后续会继续分析HDFS及MapReduce及Hive实现原理跟大家分享(hadoop版本1.0,JDK版本1.6)。
- public Object invoke(Object proxy, Method method, Object[] args)
- throws Throwable {
- final boolean logDebug = LOG.isDebugEnabled();
- long startTime = 0;
- if (logDebug) {
- startTime = System.currentTimeMillis();
- }
- ObjectWritable value = (ObjectWritable)
- // 一次远程RPC调用, 传入客户端调用的方法名,方法参数类型,方法参数值
- client.call(new Invocation(method, args), remoteId);
- // 返回远程方法调用返回值
- return value.get();
- }
同样,为了对Client类有个初步的了解,我们也先罗列几个我们感兴趣的内部类:
Call :该类封装了一个RPC请求,它包含五个成员变量,分别是唯一标识id,函数调用信息param、函数执行返回值value,异常信息error和执行完成标识done。由于HadoopRPCServer采用了异步方式处理客户端请求,这使得远程过程调用的发生顺序与结果返回顺序无直接关系,而Client端正是通过id识别不同的函数调用。当客户端向服务端发送请求时,只需要填充id和param这两个变量,而剩下的三个变量:value,error,done,则由服务端根据函数执行情况填充.
Connection :用Client与每个Server之间维护一个通信连接。该连接相关的基本信息及操作被封装到Connection类中,其中基本信息主要包括:通信连接唯一标识remoteId,与Server端通信的Socket,网络输入流in,网络输出流out,保存RPC请求的哈希表calls等.
ConnectionId :唯一确定一个连接
(1)请求建立连接
下面开始分析客户端是怎么跟服务端建立连接的,客户端向服务端发起RPC请求时,首先会请求服务端建立连接,核心代码如下:
- public Writable call(Writable param, ConnectionId remoteId)
- throws InterruptedException, IOException {
- Call call = new Call(param); // 创建一个可序列化的Call对象,封装了调用方法名、方法参数类型、方法参数值, 并且随机生成一个标识调用Call的唯一ID
- Connection connection = getConnection(remoteId, call); // 获取此次调用的连接对象, 并把调用Call放入此连接的calls调用队列中
- connection.sendParam(call); // 发起一个远程RPC调用
- boolean interrupted = false;
- synchronized (call) {
- while (!call.done) {
- try {
- call.wait(); // 阻塞, 等待远程服务器返回结果 【这里注意下,mysqlJDBC驱动的Connection数据库库连接因为是线程安全的,所以可以直接阻塞线程等待mysql的响应,但是这里hadoop rpc是可以再多线层环境下调用rpc接口的, 所以才以这种方式处理】
- } catch (InterruptedException ie) {
- interrupted = true;
- }
- }
- if (call.error != null) { // 如果RPC服务端异常则直接抛出异常
- if (call.error instanceof RemoteException) {
- throw call.error; // 直接抛出服务端异常
- } else {
- throw wrapException(connection.getRemoteAddress(), call.error);
- }
- } else {
- return call.value; // 返回远程服务器返回值
- }
- }
- }
- private Connection getConnection(ConnectionId remoteId, Call call)
- throws IOException, InterruptedException {
- if (!running.get()) {
- throw new IOException("The client is stopped");
- }
- Connection connection;
- do {
- synchronized (connections) {
- connection = connections.get(remoteId); // 根据ConnectionId获取Connection对象
- if (connection == null) {
- connection = new Connection(remoteId); // 创建Connection连接对象, 这个连接对象就代表一个远程服务器一个服务的连接, 可能包含多个RPC方法
- connections.put(remoteId, connection); // 往客户端Client对象中添加一个Connection对象
- }
- }
- } while (!connection.addCall(call)); // 往连接Connection中添加Call, 并唤醒Connection线程接收远程服务器响应
- connection.setupIOstreams(); // 建立与远程服务器的连接, 创建Socket对象, 只初始化一次, 并且做一些验证工作及初始化写一次ConnectionHeader头信息, 并且启动Connectin线程获取该连接上的响应
- return connection; // 返回连接对象
在Connection的setupIOstreams方法中会去建立和服务端的连接,本质会去创建一个Socket对象,建立一个TCP长连接,并且封装相关输入输出流,核心代码如下:
- private synchronized void setupIOstreams() throws InterruptedException {
- if (socket != null || shouldCloseConnection.get()) { // 已经建立连接直接返回
- return;
- }
- try {
- short numRetries = 0;
- final short maxRetries = 15;
- Random rand = null;
- while (true) {
- // 与远程服务器建立连接, 创建一个Socket对象
- setupConnection();
- // 获取输入流
- InputStream inStream = NetUtils.getInputStream(socket);
- // 获取输出流
- OutputStream outStream = NetUtils.getOutputStream(socket);
- // 发送RPC Header信息给RPC服务器, 这里RPC服务器正常接收后不会响应, 因为只会验证客户端和服务端RPC程序版本是否匹配, 但是验证没通过后会响应失败状态, 并且服务端会关闭连接
- // 这里RPC服务器正常接收后不会响应
- writeRpcHeader(outStream);
- // 包装输入流
- this.in = new DataInputStream(new BufferedInputStream
- (new PingInputStream(inStream)));
- // 包装输出流
- this.out = new DataOutputStream
- (new BufferedOutputStream(outStream));
- // 写入Header信息, 在该连接的第一次RPC请求时一起随参数发送, 主要发送的是接口名
- writeHeader();
- // 启动Connection线程, 用于接收远程服务器的响应
- start();
- return;
- }
- } catch (Throwable t) {
- close();
- }
- }
(2)发送请求数据
建立好和服务端的连接之后,就可以和服务端通信了,客户端发起RPC请求时,会先去把请求相关的调用方法参数等序列化成字节流发送给服务端,核心代码如下:
- public void sendParam(Call call) {
- if (shouldCloseConnection.get()) { // 如果连接已经关闭则直接返回
- return;
- }
- DataOutputBuffer d = null;
- try {
- synchronized (this.out) { // 对于同一个OutputStream必须同步发送RPC调用, 因为在同一个连接上的多个调用Call必须在同步下进行RPC请求
- d = new DataOutputBuffer();
- d.writeInt(call.id); // callId
- call.param.write(d); // Invocation对象, 实现了Writable接口, 把Invocation对象序列化成二进制数据, 在服务端又会把二进制数据反序列化成Invocation对象
- byte[] data = d.getData(); // 获取字节数据
- int dataLength = d.getLength();
- out.writeInt(dataLength); // 1.写入CallId和调用参数(方法名、方法参数类型、方法参数值)的长度, 4个字节
- out.write(data, 0, dataLength);// 2.写入CallId和序列化后的调用参数(方法名、方法参数类型、方法参数值)
- /** (数据长度4字节 + callId4个字节 + Invocation对象字节流) */
- out.flush(); // 刷新发送, 在该连接上的RPC调用第一次肯定会发送(完整ConnectionHeader头信息+调用参数), 之后都只会发送调用参数
- }
- } catch(IOException e) {
- markClosed(e);
- } finally {
- IOUtils.closeStream(d);
- }
- }
(3)获取服务端响应
发起请求时候,客户端会阻塞等待服务端响应数据,响应完之后业务线程会被唤醒,核心代码如下:
- public void run() {
- if (LOG.isDebugEnabled())
- // 这里Hadoop-RPC通过一个线程去接收响应, 因为是在同一个连接上只能这样处理
- while (waitForWork()) {
- receiveResponse(); // 单线程方式接收远程RPC服务器的响应, 并不能保证每个调用是按序返回的,因为这得取决于Server的响应
- }
- close(); // 关闭连接及连接上的Call
- }
- private void receiveResponse() {
- if (shouldCloseConnection.get()) {
- return;
- }
- touch();
- try {
- int id = in.readInt(); // 获取CallId 这里可能会阻塞, 如果服务端验证Header时抛异常断开连接则这里会直接抛异常
- Call call = calls.get(id); // 从此连接的调用队列中取出对于CallId的Call对象
- int state = in.readInt(); // read call status 获取RPC服务端状态status
- if (state == Status.SUCCESS.state) { // 1.RPC调用成功
- Writable value = ReflectionUtils.newInstance(valueClass, conf); // 创建一个ObjectWritable对象, 用来反序列化服务端返回值
- value.readFields(in); // read value 读取RPC远程调用返回值
- call.setValue(value); // 设置此次远程RPC调用的返回值到Call, 并且唤醒此个RPC业务线程, 返回调用结果
- calls.remove(id); // 响应成功则从连接的调用队列中把Call对象移除
- } else if (state == Status.ERROR.state) { // 2.如果当前调用在服务端出现调用异常, 则此调用业务线程抛出异常即可, 不必关闭连接
- call.setException(new RemoteException(WritableUtils.readString(in),
- WritableUtils.readString(in))); // 设置异常和完成标志,最后唤醒业务线程抛出异常
- calls.remove(id); // 把当前调用从此连接的调用队列中移除
- } else if (state == Status.FATAL.state) { //3.第一次服务端RPC-Header验证失败, 则此连接要被关闭
- // 验证没通过直接关闭连接
- markClosed(new RemoteException(WritableUtils.readString(in), // 设置shouldCloseConnection为true, 并且设置异常
- WritableUtils.readString(in)));
- }
- } catch (IOException e) { // 4.第二次服务端Header验证失败, 则此连接要被关闭
- markClosed(e); // 第二次服务端Header验证失败, 则此连接要被关闭
- }
- }
- public synchronized void setValue(Writable value) {
- this.value = value; // 设置响应
- callComplete(); // 唤醒业务线程
- }
- protected synchronized void callComplete() {
- this.done = true; // 完成RPC调用
- this.notify(); // 唤醒业务线程
- }
HadoopRPC-Client端请求处理流程具体序列图如下:
Listener : 请求监听类,用于监听客户端发来的请求.
Connection :连接类,真正的客户端请求读取逻辑在这个类中.
Reader : 当监听器监听到用户请求,便让Reader读取用户请求数据.
Call :用于封装客户端发来的请求.
Handler :请求处理类,会循环阻塞读取callQueue中的call对象,并对其进行操作.
Responder :响应RPC请求类,请求处理完毕,由Responder发送给请求客户端.
该阶段的主要任务是接收来自各个客户端的RPC请求,并将它们封装成固定的格式(Call对象)放到一个共享阻塞队列callQueue中,以便进行后续处理。该阶段内部又分为两个子阶段:请求接收和请求读取,分别有两种线程完成:Listener和Reader请求接收线程Listener初始化源码如下,整个Server只有一个Listener线程,统一负责监听来自客户端的连接请求,一旦有新的请求到达,它会采用轮训的方式从线程池中选择一个Reader线程进行处理。Listener的run() 方法中会阻塞等待客户端请求建立连接,Listener的run()方法的核心代码如下:
- public void run() {
- LOG.info(getName() + ": starting");
- SERVER.set(Server.this);
- while (running) {
- SelectionKey key = null;
- try {
- selector.select(); // 如果Selector中注册的ServerSocketChannel没有新的Socket请求的话, 就阻塞在这里
- 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 (Exception e) {
- closeCurrentConnection(key, e);
- }
- cleanupConnections(false);
- }
- }
紧接着具体的请求接收处理是在Listener的doAccept()方法中处理的,获取连接后会往Reader线程中的多路复用器Selector注册连接,Listener的doAccept方法的核心代码如下:
- void doAccept(SelectionKey key) throws IOException, OutOfMemoryError {
- Connection c = null;
- ServerSocketChannel server = (ServerSocketChannel) key.channel(); // 拿到ServerSocketchannel
- SocketChannel channel; // 拿到Socketchannel
- while ((channel = server.accept()) != null) { // 非阻塞的拿到SocketChannel
- channel.configureBlocking(false); // 把SocketChannel设置为非阻塞模式
- channel.socket().setTcpNoDelay(tcpNoDelay);
- Reader reader = getReader(); // 随机轮询获取一个Rearder线程
- try {
- reader.startAdd(); // 开始添加SocketChannel,先阻塞Reader线程,唤醒Reader线程中的Selector
- SelectionKey readKey = reader.registerChannel(channel); // 往reader线程中的Selector注册刚刚连接的SocketChannel的OP_READ事件
- c = new Connection(readKey, channel, System.currentTimeMillis()); // 传入SocketChannel创建Connection连接, 每个SocketChannel都有一个Connection对象
- readKey.attach(c); // 为每个SelectionKey(每个SocketChannel)绑定一个Connection,当做attachment
- synchronized (connectionList) {
- connectionList.add(numConnections, c); // 往Server端的连接队列connectionList中添加一个Connection
- numConnections++;
- }
- } finally {
- reader.finishAdd(); // 完成添加SocketChannel并唤醒Reader线程
- }
- }
- }
客户端和服务端连接建立成功之后,服务端的Reader线程中维护了连接,有了连接就可以传输数据,Reader线程的run方法中就是阻塞去等待客户端的请求数据,一旦该连接上有可读数据,该Reader线程就会被唤醒,紧接着会去解析字节流序列化请求数据,封装成Call对象,塞到callQueue阻塞队列,Reader的run()方法的核心代码如下:
- public void run() {
- LOG.info("Starting SocketReader");
- synchronized (this) {
- while (running) {
- SelectionKey key = null;
- try {
- readSelector.select(); // 如果Selector中注册的SocketChannel中都没有可读数据的话, 就阻塞在这里
- while (adding) { // 如果Selector需要注册新的Channel,则这里需阻塞掉,等待Channel注册完被唤醒
- this.wait(1000);
- }
- Iterator<SelectionKey> iter = readSelector.selectedKeys().iterator(); // 获取OP_READ事件的SocketChannel的SelectionKey
- while (iter.hasNext()) {
- key = iter.next();
- iter.remove();
- if (key.isValid()) {
- if (key.isReadable()) { // SocketChannel有可读数据
- doRead(key);
- }
- }
- key = null;
- }
- } catch (IOException ex) {
- LOG.error("Error in Reader", ex);
- }
- }
- }
- }
- public void doRead(SelectionKey key) throws InterruptedException {
- Connection c = (Connection)key.attachment(); // 拿到之前SocketChannel关联绑定的Connection连接
- if (c == null) {
- return;
- }
- c.setLastContact(System.currentTimeMillis()); // 最后被访问时间
- try {
- count = c.readAndProcess(); // 从SocketChannel中读取数据并处理
- } catch (InterruptedException ieo) {
- throw ieo;
- } catch (Exception e) { // 这里一般是服务端验证RPCHeader时或验证调用Header时抛的异常, 则需要解绑SelectionKey并且关闭连接
- count = -1; //so that the (count < 0) block is executed
- }
- if (count < 0) {
- closeConnection(c); // Reader线程读取PRC-Header协议验证失败后或请求Header验证失败后会关闭连接
- c = null;
- }
- else {
- c.setLastContact(System.currentTimeMillis());
- }
- }
- public int readAndProcess() throws IOException, InterruptedException {
- while (true) {
- int count = -1;
- if (dataLengthBuffer.remaining() > 0) {
- count = channelRead(channel, dataLengthBuffer); // 读取协议的第一个字段,协议的长度, 4个字节(可能是hrpc或协议长度或数据长度)
- if (count < 0 || dataLengthBuffer.remaining() > 0)
- return count;
- }
- // 首先收到无需响应的客户单RPC-Header信息
- if (!rpcHeaderRead) { // 第一次RPC-Header验证
- if (rpcHeaderBuffer == null) {
- rpcHeaderBuffer = ByteBuffer.allocate(2);
- }
- count = channelRead(channel, rpcHeaderBuffer); // 读取客户端程序版本4及授权方式80, 共2个字节
- if (count < 0 || rpcHeaderBuffer.remaining() > 0) {
- return count;
- }
- int version = rpcHeaderBuffer.get(0); // 读取客户端程序版本4, 共1个字节
- byte[] method = new byte[] {rpcHeaderBuffer.get(1)}; // 读取授权方式80, 共1个字节
- authMethod = AuthMethod.read(new DataInputStream(
- new ByteArrayInputStream(method)));
- dataLengthBuffer.flip();
- dataLengthBuffer.clear(); // 清空dataLengthBuffer
- if (authMethod == null) {
- throw new IOException("Unable to read authentication method");
- }
- rpcHeaderBuffer = null;
- rpcHeaderRead = true; // 头信息已经读取标识
- continue; // RPC-Header正常读取后直接continue, 可能直接返回, 也可能读取第一次RPC请求
- }
- if (data == null) {
- dataLengthBuffer.flip();
- dataLength = dataLengthBuffer.getInt(); // 获取协议长度或数据长度
- if (dataLength == Client.PING_CALL_ID) { // 如果没有信息了则直接返回
- if(!useWrap) { //covers the !useSasl too
- dataLengthBuffer.clear();
- return 0; //ping message // 返回
- }
- }
- data = ByteBuffer.allocate(dataLength); // 根据dataLength创建一个dataLength大小的缓冲区, 用来读数据
- }
- count = channelRead(channel, data); // 读取第一次请求Header信息或请求数据
- if (data.remaining() == 0) {
- dataLengthBuffer.clear(); // 清空dataLengthBuffer
- data.flip();
- if (skipInitialSaslHandshake) {
- data = null;
- skipInitialSaslHandshake = false;
- continue;
- }
- boolean isHeaderRead = headerRead; // 是否已经读取第一次请求Header信息
- if (useSasl) {
- saslReadAndProcess(data.array());
- } else {
- processOneRpc(data.array()); // 处理rpc请求,把封装好的请求信息Call塞到callQueue阻塞队列
- }
- data = null;
- if (!isHeaderRead) { // 读取第一次RPC请求Header之后会再continue, 继续读取请求数据
- continue;
- }
- }
- return count; // 一个正常RPC请求读取完后会直接返回
- }
- }
- private void processOneRpc(byte[] buf) throws IOException,
- InterruptedException {
- if (headerRead) {
- processData(buf); // 处理RPC请求
- } else {
- processHeader(buf); // 读取头信息,随第一次RPC请求时带过来,解析出此连接上的客户端请求接口的完全限定名
- headerRead = true;
- }
- }
- private void processData(byte[] buf) throws IOException, InterruptedException {
- DataInputStream dis =
- new DataInputStream(new ByteArrayInputStream(buf)); // 获取Invocation对象的字节流封装成DataInputStream
- int id = dis.readInt(); // try to read an id 前四个字节是客户端调用Call ID
- Writable param = ReflectionUtils.newInstance(paramClass, conf);//read param 创建一个Invocation对象, 实现了Writable接口, 用来反序列化客户端调用参数
- param.readFields(dis); // 读取二进制流反序列化成客户端调用参数封装对象Invocation(方法名+方法参数类型+方法参数值)
- Call call = new Call(id, param, this); // 把请求封装成Call(调用id+请求参数+此连接Connection对象), 塞到阻塞队列中
- callQueue.put(call); // 把Call塞到callQueue阻塞队列中, queue the call; maybe blocked here
- incRpcCount(); // Increment the rpc count RPC请求计数加1
- }
- public void run() {
- LOG.info(getName() + ": starting");
- SERVER.set(Server.this);
- ByteArrayOutputStream buf =
- new ByteArrayOutputStream(INITIAL_RESP_BUF_SIZE);
- while (running) {
- try {
- // 从阻塞队列中获取Call,如果阻塞队列为空,则当前线程阻塞
- final Call call = callQueue.take();
- String errorClass = null;
- String error = null;
- Writable value = null; // Server端RPC方法的返回结果值ObjectWritable
- CurCall.set(call);
- try {
- if (call.connection.user == null) {
- value = call(call.connection.protocol, call.param,
- call.timestamp);
- } else {
- value =
- call.connection.user.doAs
- (new PrivilegedExceptionAction<Writable>() {
- @Override
- public Writable run() throws Exception {
- // 反射调用对应服务,返回结果ObjectWritable, 传入Connection中接口的Class对象, 是在建立连接之后第一次客户端请求带过来的
- return call(call.connection.protocol,call.param, call.timestamp);
- }
- }
- );
- }
- } catch (Throwable e) {
- errorClass = e.getClass().getName(); // 服务端RPC方法抛出的异常类信息, java.lang.IOException
- error = StringUtils.stringifyException(e); // 服务端RPC方法抛出的异常堆栈信息,封装成字符串的形式传送给客户端
- }
- CurCall.set(null);
- synchronized (call.connection.responseQueue) { // 同一个连接上的多个响应必须在同步下进行
- setupResponse(buf, call,
- (error == null) ? Status.SUCCESS : Status.ERROR,
- value, errorClass, error); // 生成返回给客户端的数据包,包含(客户端调用ID+状态status+RPC方法返回值),设置到Call对象中
- if (buf.size() > maxRespSize) {
- LOG.warn("Large response size " + buf.size() + " for call " +
- call.toString());
- buf = new ByteArrayOutputStream(INITIAL_RESP_BUF_SIZE);
- }
- responder.doRespond(call); // 响应请求,调用Responder的doRespond方法, 同一个连接上的多个响应必须在同步下进行RPC响应
- }
- }
- }
- }
- public Writable call(Class<?> protocol, Writable param, long receivedTime)
- throws IOException {
- try {
- // 获取客户端封装的调用参数Invocation, 是在Reader线程中反序列化成Invocation的
- Invocation call = (Invocation)param;
- if (verbose) log("Call: " + call);
- // 根据反射获取Method对象
- Method method = protocol.getMethod(call.getMethodName(),call.getParameterClasses());
- // 设置可见性
- method.setAccessible(true);
- long startTime = System.currentTimeMillis();
- // 通过反射调用服务端方法
- Object value = method.invoke(instance, call.getParameters());
- rpcMetrics.addRpcQueueTime(qTime);
- rpcMetrics.addRpcProcessingTime(processingTime);
- rpcMetrics.addRpcProcessingTime(call.getMethodName(), processingTime);
- if (verbose) log("Return: "+value);
- // 返回服务端方法返回值, 把返回值value和返回值类型包装成ObjectWritable进行序列化
- // 服务端返回值包装类ObjectWritable
- return new ObjectWritable(method.getReturnType(), value);
- }
- }
- public void run() {
- LOG.info(getName() + ": starting");
- SERVER.set(Server.this);
- long lastPurgeTime = 0;
- while (running) {
- try {
- // Responder线程阻塞在这里,需要在Handler线程中唤醒
- // 如果Selector需要注册新的Channel,则这里需阻塞掉,等待Channel注册完被唤醒
- waitPending();
- writeSelector.select(PURGE_INTERVAL);
- Iterator<SelectionKey> iter = writeSelector.selectedKeys().iterator();
- while (iter.hasNext()) {
- SelectionKey key = iter.next();
- iter.remove();
- try {
- if (key.isValid() && key.isWritable()) {
- doAsyncWrite(key); // 写数据
- }
- } catch (IOException e) {
- LOG.info(getName() + ": doAsyncWrite threw exception " + e);
- }
- }
- long now = System.currentTimeMillis();
- if (now < lastPurgeTime + PURGE_INTERVAL) {
- continue;
- }
- }
- }
- private void doAsyncWrite(SelectionKey key) throws IOException {
- Call call = (Call)key.attachment(); // 从SelectionKey中获取Call对象, 【其实用的是Call对象中的Connection对象的responseQueue响应队列】
- if (call == null) {
- return;
- }
- if (key.channel() != call.connection.channel) {
- throw new IOException("doAsyncWrite: bad channel");
- }
- synchronized(call.connection.responseQueue) {
- if (processResponse(call.connection.responseQueue, false)) { // 往SocketChannel中写数据, 如果此连接上还有响应没发送完则返回false, 继续监听此SocketChannel上的写事件
- try {
- key.interestOps(0); // 此连接上的响应如果已经全部发送完,则从Responder线程中的Selector解绑SelectionKey
- } catch (CancelledKeyException e) {
- LOG.warn("Exception while changing ops : " + e);
- }
- }
- }
- }
- HadoopRPC源码解析
- HadoopRPC机制分析系列之一: 动态代理
- HadoopRPC框架-----模拟NameNode和Client通信
- 源码解析
- 源码解析
- 【JDk源码解析之一】ArrayList源码解析
- 【源码解析】-- ArrayList的源码解析
- EventBus源码解析(史上最全的源码解析)
- 【源码】Vector、Stack源码解析
- Sping源码解析-源码下载
- <Android源码>IntentService源码解析
- JAVA源码解析-String源码
- JAVA源码解析-ArrayList源码
- JAVA源码解析-LinkedList源码
- Spark源码-SparkContext源码解析
- Jboss源码解析
- 网页病毒源码解析
- strlen源码解析
- ATM系统
- 10.11笔试时遇到的知识点总结
- mac下maven的安装
- 黑马程序员——OC中的block:代码块
- XML入门
- HadoopRPC源码解析
- hdu 5428 The Factor(数论)
- The martian 2015 火星救援百度网盘下载真正可用的。2015-10-11更新
- php启用zend guard loader扩展问题
- sys.argv使用方法及shell读文本并执行python文件
- 10.08NOIP模拟赛
- Linux学习首页
- git 详解
- 我摘录的无损截取html(我不是作者不会打原创标签)