zookeeper 客户端的实现
来源:互联网 发布:中国援助朝鲜 知乎 编辑:程序博客网 时间:2024/06/03 14:52
zookeeper 客户端的实现主要由以下三个类完成:
- org.apache.zookeeper.ZooKeeper
- org.apache.zookeeper.ClientCnxn
- org.apache.zookeeper.ClientCnxnSocketNIO
org.apache.zookeeper.ZooKeeper主要是一层api的封装,客户端程序用到一个Zookeeper实例就可以进行所有的操作
ZKWatchManager是在org.apache.zookeeper.ZooKeeper下的内部类,包含三个私有属性dataWatches、existWatches、childWatches, ZKWatchManager主要负责管理所有ClientCnxn从server集群上得到Watch事件
ClientCnxn是client端的核心实现,其中包含了两个轮寻的线程SendThread和EventThread,SendThread主要轮循从outgoingQueue队列中取得Zookeeper塞入的Packet包,通过ClientCnxnSocketNIO发送给服务器,并把发送的packet塞入pendingQueue队列中等待服务端的response,同时也从同服务端建立的管道中读取response把相应的packet移出pendingQueue,放入EventThrad负责处理的waitingEvents队列中,SendThread也负责和集群连接的建立、断开和session的ping连接,EventThread负责处理waitingEvent队列中packet,把packet中finished标识为true,使得阻塞的客户端函数返回并且取得packet中的response,根据不同的response调用不同的回调实现方法处理事件,其中waitingEvent队列采用LinkedBlockingQueue
ClientCnxnSocketNIO则是负责网络的通信,管道连接的建立,选择器的select操作,read和write的管道读写操作
二.构造函数
ZooKeeper有四种类型的构造函数,分别是:
- ZooKeeper(String connectString, int sessionTimeout, Watcher watcher)
- ZooKeeper(String connectString, int sessionTimeout, Watcher watcher, boolean canBeReadOnly)
- ZooKeeper(String connectString, int sessionTimeout, Watcher watcher, long sessionId, byte[] sessionPasswd)
- ZooKeeper(String connectString, int sessionTimeout, Watcher watcher, long sessionId, byte[] sessionPasswd, boolean canBeReadOnly)
可以分成两个大的类别,即设置sessionId和session密码的,与不设置这两个参数的。
三.构造函数过程
- 记录info级别的连接日志
- 将ZkWatchManager的默认watcher设置成传入的watcher
- 通过ConnectStringParser将传入的connectString,解析成多个或者一个服务器地址列表
- 通过服务器列表构建StaticHostProvider
- 初始化ClientCnxnSocket,可以通过
zookeeper.clientCnxnSocket
指定其实现,默认使用ClientCnxnSocketNIO。 - 通过StaticHostProvider及其它相关参数,创建ClientCnxn。如果提供了seesionId和sessionPassword,则将seenRwServerBefore置为true。然后启动
sessionId和sessionPassword用于重连的时候验证。
注意:由于客户端和ZooKeeper服务端连接的建立是异步的,因此构造函数调用结束,并不代表连接一定已经建立(虽然概率比较小)。
五.解析服务器地址:ConnectStringParser
ConnectStringParser用于解析传入的连接串,连接串是以逗号分隔的服务器:端口列表,如:
ip1:port1,ip2:port2
额外的,可以指定相对目录的地址,称为chroot,那么以后所有的目录都将以这个目录为基准。
比如:
ip:port1/root/,ip2:port2
那么后续的get操作,将以/root为相对目录,比如getData(“/yangqi/”),实际操作的路径为”/root/yangqi/”。
ConnectStringParser将结果解析为chroot,和使用InetSocketAddress表示服务器地址和端口的列表。
六.服务器连接的提供者:HostProvider
HostProvider接口定义了三个接口方法:
- size() hosts的大小,可能为0
- next(long delay) 下一个服务器地址,返回InetSocketAddress。参数delay指定,所有服务器都被轮询遍后,等待的时间。
- onConnected() 告诉provider,已经建立了一个成功的连接。
StaticHostProvider实现了接口HostProvider,它将传入的服务器列表随机,next()按照随机后的顺序返回。
七.观察者Watcher和观察到的事件WatchedEvent
WatchedEvent包含三类信息:
- KeeperState
- EventType
- path
KeeperState代表和ZK服务器的连接信息,包含Disconnected\SyncConnected\AuthFailed\ConnectedReadOnly\SaslAuthenticated\Expired等6种状态。
EventType代表发生的事件类型,包含五种状态:
- None
- NodeCreated
- NodeDeleted
- NodeDataChanged
- NodeChildrenChanged
其中后四种用于表示ZNode的状态或者数据变更,而None则用于会话的状态变更。
path则代表事件发生的ZNode路径。
八.ZKWatchManager和一次性观察
ZooKeeper中的watcher设置是一次性的,
ZKWatchManager实现了接口ClientWatchManager,ClientWatchManager只定义了一个方法
Set<Watcher> materialize(Watcher.Event.KeeperState state, Watcher.Event.EventType type, String path);
materialize方法返回一个Watcher(观察者)的集合。
ZKWatchManager 将Watcher分成了四大类,分别用DataWatcher\ExistsWatcher和ChildrenWatcher以及defaultWatcher表示。
private final Map<String, Set<Watcher>> dataWatches = new HashMap<String, Set<Watcher>>(); private final Map<String, Set<Watcher>> existWatches = new HashMap<String, Set<Watcher>>(); private final Map<String, Set<Watcher>> childWatches = new HashMap<String, Set<Watcher>>();
每当事件发生时,ZKWatchManager则将对应的Watcher对象从集合中删除(ZooKeeper的watch是一次性的),然后返回需要被通知的观察者集合。
a.defaultWatcher
defaultWatcher只会相应事件类型为None,代表连接状态发生变化的通知。
b.连接重置后watcher恢复
默认情况下,如果连接重连,那么之前的watcher将被自动恢复。
如果KeeperState的状态不为连接建立,并且zookeeper.disableAutoWatchReset设置为fasle,
那么在连接断开并恢复后,将重新恢复Watcher,否则将清空原有的Watcher。
c.WatchRegistration
抽象类WatchRegistration 用于将一个Watcher注册到一个ZNode 路径上。
因此WatchRegistration有两个字段,Watcher和path。
他有三个抽象方法:
1.根据状态码获得对应路径的对应Watcher
abstract protected Map<String, Set<Watcher>> getWatches(int rc);
2.根据状态码注册watcher
public void register(int rc)
3.判断状态吗判断是否需要增加watch
shouldAddWatch(int rc)
九.ClientCnxnSocket
ClientCnxnSocket被ClientCnxn用于客户端和服务端的socket通信。
十.ClientCnxn
ClientCnxn管理客户端和服务端的连接,并且在连接出现问题的时候做到透明的自动切换。
ZooKeeper初始化示意图
//ZooKeeper的构造函数
public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher,
boolean canBeReadOnly)
throws IOException
{
watchManager.defaultWatcher = watcher;
//默认的实现了process方法的watch
ConnectStringParser connectStringParser = new ConnectStringParser(
connectString);
//解析传入的hostsStr,用于指定chrootPath和生成多个InetSocketAddress集合
HostProvider hostProvider = new StaticHostProvider(
connectStringParser.getServerAddresses());
//提供InetSocketAddress的工具类
//其中的Collections.shuffle(this.serverAddresses)
//保证客户端请求集群中不同的机器,避免羊群效应
cnxn = new ClientCnxn(connectStringParser.getChrootPath(),
hostProvider, sessionTimeout, this, watchManager,
getClientCnxnSocket(), canBeReadOnly);
cnxn.start(); //启动线程sendThread和eventThread
}
//ClinetCnxn的构造函数
public ClientCnxn(String chrootPath, HostProvider hostProvider, int sessionTimeout,ZooKeeper zooKeeper,
ClientWatchManager watcher, ClientCnxnSocket clientCnxnSocket,
long sessionId, byte[] sessionPasswd, boolean canBeReadOnly) {
this.zooKeeper = zooKeeper;
this.watcher = watcher;
this.sessionId = sessionId; //初始为0
this.sessionPasswd = sessionPasswd; //初始为new byte[16]
this.sessionTimeout = sessionTimeout; //设置为3000ms
this.hostProvider = hostProvider;
this.chrootPath = chrootPath;
connectTimeout = sessionTimeout / hostProvider.size();
//连接的timeout设置为sessionTimeOut除以InetSockAddress集合大小
//size越大,连接timeout的值越小
readTimeout = sessionTimeout * 2 / 3;
//读的timeout设为sessionTimeout的三分之二
readOnly = canBeReadOnly;
sendThread = new SendThread(clientCnxnSocket);
eventThread = new EventThread();
}
在sendThread中States属性用于标识客户端与集群的连接状态,初始为NOT-CONNECTED,在线程的run方法中创建SocketChanel,并向服务端发送connect的请求消息,在read到服务端的response消息后将state修改为CONNECTED或者CONNECTEDREADONLY
//sendThread轮循的代码 if (!clientCnxnSocket.isConnected()) { //判断ClinetCnxnSocketNIO实现类中的管道选择建是否创建,第一次运行为空进入函数 if(!isFirstConnect){ //如果不是第一建立连接则休眠一定的时间 try { Thread.sleep(r.nextInt(1000)); } catch (InterruptedException e) { LOG.warn("Unexpected exception", e); } } // don't re-establish connection if we are closing if (closing || !state.isAlive()) { break; } startConnect(); //将state置为CONNECTING,表示连接进行中,并且通过hostProvider提供的InetSockAddress建立管道 //向selector中注册关心OP_CONNECT的管道 clientCnxnSocket.updateLastSendAndHeard(); //更新客户端发送和接收的时间搓 } if (state.isConnected()) { //...省略了zooKeeperSaslClient的部分代码to = readTimeout - clientCnxnSocket.getIdleRecv(); //IdleRecv表示上次收到消息和now的间隔 } else { to = connectTimeout - clientCnxnSocket.getIdleRecv(); } if (to <= 0) { //小于0表示间隔大于timeout则session失效,抛出异常重新进行连接 throw new SessionTimeoutException( "Client session timed out, have not heard from server in " + clientCnxnSocket.getIdleRecv() + "ms" + " for sessionid 0x" + Long.toHexString(sessionId)); } if (state.isConnected()) { int timeToNextPing = readTimeout / 2 - clientCnxnSocket.getIdleSend(); //在连接已经建立的条件下是否需要发送ping消息保持session if (timeToNextPing <= 0) { sendPing(); clientCnxnSocket.updateLastSend(); } else { if (timeToNextPing < to) { to = timeToNextPing; } } } // If we are in read-only mode, seek for read/write server if (state == States.CONNECTEDREADONLY) { long now = System.currentTimeMillis(); int idlePingRwServer = (int) (now - lastPingRwServer); if (idlePingRwServer >= pingRwTimeout) { lastPingRwServer = now; idlePingRwServer = 0; pingRwTimeout = Math.min(2*pingRwTimeout, maxPingRwTimeout); pingRwServer(); //由hostProvider得到集群中的另一个InetSockAddress直接建立sock得到outputStream发送‘isro’ //判断该地址是否是rw的服务器,在是的情况下抛出异常重新连接该地址rwServerAddress } to = Math.min(to, pingRwTimeout - idlePingRwServer); } clientCnxnSocket.doTransport(to, pendingQueue, outgoingQueue, ClientCnxn.this);//调用clientCnxnSocketNIO发送消息
clientCnxnSocketNIO中的doTransport主要完成选择建的select()操作获得准备好的通道进行相应的操作,doIo则负责通道的读和写,这也是完成网络通信的主要方法
void doTransport(int waitTimeOut, List<Packet> pendingQueue, LinkedList<Packet> outgoingQueue, ClientCnxn cnxn) throws IOException, InterruptedException { selector.select(waitTimeOut); //阻塞的等待相应的时间 Set<SelectionKey> selected; synchronized (this) { selected = selector.selectedKeys(); //获得准备好的SelectionKey集合 } // Everything below and until we get back to the select is // non blocking, so time is effectively a constant. That is // Why we just have to do this once, here updateNow(); //之所以在这更新now的时间是因为之前的所有操作都是非阻塞的 for (SelectionKey k : selected) { SocketChannel sc = ((SocketChannel) k.channel()); if ((k.readyOps() & SelectionKey.OP_CONNECT) != 0) { //第一次连接时设置的key仅关心连接 if (sc.finishConnect()) { updateLastSendAndHeard(); sendThread.primeConnection(); //添加conReq的Packet到outgoingQueue队列中等待下次发送 //并且enableReadWriteOnly,等待sendThread下一次调用doTransport,进而进入下面的doIO方法的调用 } } else if ((k.readyOps() & (SelectionKey.OP_READ | SelectionKey.OP_WRITE)) != 0) { doIO(pendingQueue, outgoingQueue, cnxn); //调用doIo发送或者读取消息 } } if (sendThread.getZkState().isConnected()) { //在连接的条件下保证outgoingQueue有数据时enableWrite synchronized(outgoingQueue) { if (findSendablePacket(outgoingQueue, cnxn.sendThread.clientTunneledAuthenticationInProgress()) != null) { enableWrite(); } } } selected.clear(); //清楚已经处理的建 } void doIO(List<Packet> pendingQueue, LinkedList<Packet> outgoingQueue, ClientCnxn cnxn) throws InterruptedException, IOException { SocketChannel sock = (SocketChannel) sockKey.channel(); if (sock == null) { throw new IOException("Socket is null!"); } if (sockKey.isReadable()) { //是否有可读的数据 int rc = sock.read(incomingBuffer); if (rc < 0) { throw new EndOfStreamException( "Unable to read additional data from server sessionid 0x" + Long.toHexString(sessionId) + ", likely server has closed socket"); } if (!incomingBuffer.hasRemaining()) { incomingBuffer.flip(); if (incomingBuffer == lenBuffer) { recvCount++; readLength(); //读取数据的长度,调用ByteBuffer重新分配incomingBuffer的长度 } else if (!initialized) { //在连接未建立时,initialized为false readConnectResult(); //读取response建立连接 enableRead(); if (findSendablePacket(outgoingQueue, cnxn.sendThread.clientTunneledAuthenticationInProgress()) != null) { // Since SASL authentication has completed (if client is configured to do so), // outgoing packets waiting in the outgoingQueue can now be sent. enableWrite(); } lenBuffer.clear(); incomingBuffer = lenBuffer; updateLastHeard(); initialized = true; //初始化完成 } else { sendThread.readResponse(incomingBuffer); //当连接建立时直接读取消息 lenBuffer.clear(); incomingBuffer = lenBuffer; updateLastHeard(); } } } if (sockKey.isWritable()) { //写入的管道可用 synchronized(outgoingQueue) { Packet p = findSendablePacket(outgoingQueue, cnxn.sendThread.clientTunneledAuthenticationInProgress()); //得到首个需要发送的Packet if (p != null) { updateLastSend(); // If we already started writing p, p.bb will already exist if (p.bb == null) { if ((p.requestHeader != null) && (p.requestHeader.getType() != OpCode.ping) && (p.requestHeader.getType() != OpCode.auth)) { p.requestHeader.setXid(cnxn.getXid()); //ping和auth的消息不需要发送xid } p.createBB(); } sock.write(p.bb); //写入数据 if (!p.bb.hasRemaining()) { sentCount++; outgoingQueue.removeFirstOccurrence(p); //当消息完全写入后将Packet从outgoingQueue中移除 if (p.requestHeader != null && p.requestHeader.getType() != OpCode.ping && p.requestHeader.getType() != OpCode.auth) { synchronized (pendingQueue) { pendingQueue.add(p); //如果不是ping和auth的消息则放入pendingQueue中 } } } } if (outgoingQueue.isEmpty()) { //判断outgoingQueue是否为空,空则disableWrite,反之亦然 disableWrite(); } else { enableWrite(); } } } }
readConnectResult方法最终会调用sendThread中的onConnected完成连接
void onConnected(int _negotiatedSessionTimeout, long _sessionId, byte[] _sessionPasswd, boolean isRO) throws IOException { negotiatedSessionTimeout = _negotiatedSessionTimeout; ... if (!readOnly && isRO) { //客户端设置是可读写的但是服务端仅是只读记入错误 LOG.error("Read/write client got connected to read-only server"); } readTimeout = negotiatedSessionTimeout * 2 / 3; //根据服务端回复的sessionTimeou重新设置这两个值 connectTimeout = negotiatedSessionTimeout / hostProvider.size(); hostProvider.onConnected(); sessionId = _sessionId; //客户端的sessionId设置为服务端分配的sessionId sessionPasswd = _sessionPasswd; //密码也设置为服务端提供的 state = (isRO) ? States.CONNECTEDREADONLY : States.CONNECTED; //根据服务端的是否可读写设置state的状态 seenRwServerBefore |= !isRO; KeeperState eventState = (isRO) ? KeeperState.ConnectedReadOnly : KeeperState.SyncConnected; eventThread.queueEvent(new WatchedEvent( //将事件放入waittingQueue中待EventThread线程处理 Watcher.Event.EventType.None, eventState, null)); }
客户端发送一个create请求
客户端程序通过调用Zookeeper的create函数发送create的Packet,函数等待Packet的完成
RequestHeader h = new RequestHeader();//请求的头消息 h.setType(ZooDefs.OpCode.create); //设置请求头消息的类型 CreateRequest request = new CreateRequest(); //请求的消息封装 CreateResponse response = new CreateResponse(); //返回消息的封装 request.setData(data); //塞入创建的数据 request.setFlags(createMode.toFlag());//创建节点的类型 request.setPath(serverPath); //服务端路径 if (acl != null && acl.size() == 0) { throw new KeeperException.InvalidACLException(); } request.setAcl(acl); //acl控制权限 ReplyHeader r = cnxn.submitRequest(h, request, response, null); //利用cnxn提交请求 public ReplyHeader submitRequest(RequestHeader h, Record request, Record response, WatchRegistration watchRegistration) throws InterruptedException { ReplyHeader r = new ReplyHeader(); //返回的头消息封装 Packet packet = queuePacket(h, r, request, response, null, null, null, null, watchRegistration); //封装好发送的Packet,往OutgoingQueue提交等待处理 synchronized (packet) { while (!packet.finished) { //调用函数等待直到服务端响应消息完成 packet.wait(); } } return r; }
接着进入上边sengThread提到的轮循处理的过程,待管道读到服务端的响应后进入sendThread.readResponse(incomingBuffer)方法,完成消息的响应的处理过程
void readResponse(ByteBuffer incomingBuffer) throws IOException { ByteBufferInputStream bbis = new ByteBufferInputStream( incomingBuffer); BinaryInputArchive bbia = BinaryInputArchive.getArchive(bbis); ReplyHeader replyHdr = new ReplyHeader(); replyHdr.deserialize(bbia, "header"); //反序列化得到返回的头消息 if (replyHdr.getXid() == -2) { // -2 is the xid for pings//-2 表示ping的消息回馈,再debug的情况下记录日志然后返回不进行其他操作 return; } if (replyHdr.getXid() == -4) { // -4 is the xid for AuthPacket if(replyHdr.getErr() == KeeperException.Code.AUTHFAILED.intValue()) { state = States.AUTH_FAILED; //向waittingQueue丢入授权失败的event eventThread.queueEvent( new WatchedEvent(Watcher.Event.EventType.None, Watcher.Event.KeeperState.AuthFailed, null) ); //将会从WathcerManager中得到所有的wathch进行处理 } return; } if (replyHdr.getXid() == -1) { // -1 means notification WatcherEvent event = new WatcherEvent(); event.deserialize(bbia, "response");... WatchedEvent we = new WatchedEvent(event); eventThread.queueEvent( we ); //该方法会从WatcherManager中得到所管理的响应的event //然后将event封装成WatcherSetEventPair丢入waittingQueue中等待EventThread的处理 return; } ... Packet packet; synchronized (pendingQueue) { if (pendingQueue.size() == 0) { throw new IOException("Nothing in the queue, but got " + replyHdr.getXid()); } packet = pendingQueue.remove(); //从pendingQueue中移除等待响应的Packet } /* * Since requests are processed in order, we better get a response * to the first request! */ try { if (packet.requestHeader.getXid() != replyHdr.getXid()) { //当请求的xid与服务端的xid不相等时,标识错误,抛出失去连接的错误 packet.replyHeader.setErr( KeeperException.Code.CONNECTIONLOSS.intValue()); throw new IOException("Xid out of order. Got Xid " + replyHdr.getXid() + " with err " + + replyHdr.getErr() + " expected Xid " + packet.requestHeader.getXid() + " for a packet with details: " + packet ); } packet.replyHeader.setXid(replyHdr.getXid()); //将返回的头消息放回等待的packet中 packet.replyHeader.setErr(replyHdr.getErr()); packet.replyHeader.setZxid(replyHdr.getZxid()); if (replyHdr.getZxid() > 0) { lastZxid = replyHdr.getZxid(); //更新最后的lastZxid } if (packet.response != null && replyHdr.getErr() == 0) { packet.response.deserialize(bbia, "response"); } } finally { finishPacket(packet); //调用该函数完成packet的最后一个步骤 } } private void finishPacket(Packet p) { if (p.watchRegistration != null) { p.watchRegistration.register(p.replyHeader.getErr()); //当返回的消息正确的情况下将watch放入WatcherManager中 } if (p.cb == null) { //如果Packet未设置回调函数则标识完成通知等待的线程 synchronized (p) { p.finished = true; p.notifyAll(); } } else { p.finished = true; //标识完成 eventThread.queuePacket(p); //将packet丢入waittingQueue中等待EventThread调用相应的回调方法 } }
最后在看一下EventThread对waittingQueue所做的操作
private void processEvent(Object event) { try { if (event instanceof WatcherSetEventPair) { //对于event的操作根据类型分为两类//第一是先前封装的WatcherSetEvetnPair针对返回的头消息是-4和-1所做的操作 //根据返回的WatchManager所管理的Watch分别调用各自的process函数处理 WatcherSetEventPair pair = (WatcherSetEventPair) event; for (Watcher watcher : pair.watchers) { try { watcher.process(pair.event); } catch (Throwable t) { LOG.error("Error while calling watcher ", t); } } } else { //第二种是包含cb所进行的回调处理 //根据Packet中设置的返回消息回调类型通过cb来完成 Packet p = (Packet) event; int rc = 0; String clientPath = p.clientPath; if (p.replyHeader.getErr() != 0) { rc = p.replyHeader.getErr(); } if (p.cb == null) { LOG.warn("Somehow a null cb got to EventThread!"); } else if (p.response instanceof ExistsResponse || p.response instanceof SetDataResponse || p.response instanceof SetACLResponse) { StatCallback cb = (StatCallback) p.cb; if (rc == 0) { if (p.response instanceof ExistsResponse) { cb.processResult(rc, clientPath, p.ctx, ((ExistsResponse) p.response) .getStat()); } else if (p.response instanceof SetDataResponse) { cb.processResult(rc, clientPath, p.ctx, ((SetDataResponse) p.response) .getStat()); } else if (p.response instanceof SetACLResponse) { cb.processResult(rc, clientPath, p.ctx, ((SetACLResponse) p.response) .getStat()); } } else { cb.processResult(rc, clientPath, p.ctx, null); } ... } } catch (Throwable t) { LOG.error("Caught unexpected throwable", t); } } }
针对Zookeeper客户端的实现逻辑和主要代码段介绍完了,看似简单但真正把代码都介绍完才慢慢体会到里边很多的细节,也算是真正意义上的看懂了,总觉着这篇文章的代码贴的太多,看得不是很舒服,下篇介绍Zookeeper实现的方式看看是否能换一种更好的方式写出来,很多事总要经历那么一个过程…
- zookeeper 客户端的实现
- ZooKeeper客户端--java实现
- 【zookeeper】客户端 底层实现
- zookeeper的python客户端
- Zookeeper的Java客户端
- ZooKeeper 客户端的使用
- zookeeper的java客户端
- Zookeeper客户端的使用
- 使用Node.js实现一个简单的ZooKeeper客户端
- 使用Node.js实现一个简单的ZooKeeper客户端
- ZooKeeper学习笔记:使用zookeeper的API实现增删查改以及客户端的观察者模式
- zookeeper的python客户端安装
- zookeeper 客户端编程的使用
- ZooKeeper客户端支持的语言
- ZooKeeper的客户端库Curator
- zookeeper的Java客户端API
- zookeeper的客户端操作命令
- 【zookeeper】简单的客户端命令
- hdu 4906 Our happy ending 。神奇的状态转移方程,记录下
- 黑马程序员——配置JDK环境变量
- Linux 标准目录结构
- 数据结构——二叉树的遍历
- IP地址详解
- zookeeper 客户端的实现
- CentOS6.3下apcupsd自动关机后 ssh登录报电源故障 POWER FAILURE
- python实现Stack和Queue
- Java多线程编程总结
- hdoj 1870 愚人节的礼物 【简单的栈应用】
- MyEclipse为选定语句快速添加大括号
- HDU 1698-Just a Hook(线段树 成段更新)
- linux下一个定时器的使用方法
- 类的 非主动使用不会触发类的初始化操作