Californium 源码分析

1. Californium 项目简介

Californium 是一款基于Java实现的Coap技术框架,该项目实现了Coap协议的各种请求响应定义,支持CON/NON不同的可靠性传输模式。

Californium 基于分层设计且高度可扩展,其内部模块设计及接口定义存在许多学习之处;

值得一提的是,在同类型的 Coap技术实现中,Californium的性能表现是比较突出的,如下图:

更多的数据可以参考Californium-可扩展云服务白皮书

本文以框架的源码分析为主,其他内容不做展开。

2. 项目结构

目前Californium 项目稳定版本为 2.0.0-M2,项目的托管地址在:

https://github.com/eclipse/californium

模块说明

~.californium-core

californium 核心模块,定义了一系列协议栈核心接口,并提供了Coap协议栈的完整实现,

~.element-connector

从core模块剥离的连接器模块,用于抽象网络传输层的接口,使得coap可以同时运行于udp和tcp多种传输协议之上;

~.scandium-core

Coap over DTLS 支持模块,提供了DTLS 传输的Connector实现;

~.californium-osgi

californium 的osgi 封装模块;

~.californium-proxy

coap 代理模块,用于支持coap2coap、coap2http、http2coap的转换;

~.demo-xxx

样例程序;

其中,californium-core和element-connector是coap技术实现最关键的模块,后面的分析将围绕这两个模块进行。

3. 分层设计

Californiium 定义了三层架构

1 网络层,负责处理端口监听,网络数据收发;

2 协议层,负责Coap协议数据包解析及封装,实现消息的路由、可靠性传输、Token处理、观察者模型等等;

3 逻辑层,负责 Resource定义和映射,一个Resource 对应一个URL,可独立实现Coap 请求处理。

异步线程池

三层架构中都可以支持独立的线程池,其中网络层与协议层的线程池保持独立;

逻辑层可为每个Resource指定独立的线程池,并支持父级继承的机制,即当前Resource若没有定义则沿用父级Resource线程池;

若逻辑层未指定线程池,则默认使用协议层的线程池。

4. 包结构分析

4.1 californium-core

core 模块定义了协议栈相关的所有关键接口,根据功能职责的不同拆分为多个子 package;

根级 package定义的是Coap应用的一些入口类,如Client/Server实现、包括应用层CoapResource的定义。

4.1.1 package-coap

实现 coap协议 RFC7252 实体定义,包括消息类型、消息头、Observe机制等。

具体定义见下图

Coap 消息划分为Request/Response/EmptyMessage 三类;

MessageObserver 接口用于实现消息的状态跟踪,如重传、确认等。

4.1.2 package-network

network 是协议栈核心机制实现的关键模块,其涵盖了网络传输及协议层的定义及实现;

模块实现了一些关键接口定义,如将网络传输端点抽象为Endpoint,根据请求响应的关联模型定义了Exchange等。

协议栈的分层定义、消息编解码、拦截处理也由network包提供。

endpoins定义

Endpoint 定义为一个端点,通常与一个IP和端口对应,其屏蔽了client和server交互时的网络传输细节。

对于client来说,Endpoint代表通讯的服务端地址端口;而对于server来说则代表了绑定监听的地址及端口。

CoapEndpoint实现了Endpoint接口,通过RawDataChannel(见elements-connector部分)接口实现消息接收,通过Outbox接口实现消息发送。

通常CoapEndpoint 会关联一个Connector,以实现传输层的收发;

CoapStack对应了协议栈接口,用于处理CoapEndpoint上层的消息链路;

除此之外,CoapEndpoint 还应该包括消息编解码、拦截处理等功能。

exchange定义

Exchange描述了请求-响应模型,一个Exchange会对应一个Request,相应的Response,以及当前的Endpoint;

ExchangeObserver用于实现对Exchange状态的变更监听;

Exchange 通常存在于两种场景:

1 发送请求后初始化并存储,当接收到对应的响应之后变更为completed(执行清理工作)。

2 接收请求后初始化并存储,当发送响应时执行清理;

matcher定义

Matcher 是用于实现Exchange 生成及销毁的模块,提供了几个收发接口;

用于消息在进入协议栈CoapStack处理之前完成配对处理;

messagetool定义

MessageExchangeStore 实现了Exchange的查询、存储;

MessageIdProvider 用于提供Coap消息的MID,一个MID代表了一个唯一的消息(在消息生命周期内);

TokenProvider 用于提供Coap消息的Token,而Request及Response通过Token实现匹配;

network子模块

package-config

提供网络参数配置定义

package-deduplication

提供消息去重机制的实现

package-interceptors

提供消息传输拦截器定义

package-serialization

提供消息包的解析及编码实现

package-stack

提供协议栈分层定义及实现

4.1.3 package-server

应用层 server端实现的一些定义,包括Server接口、Resource定义。

CoapServer 可包含多个Endpoint,体现为一个Coap服务可架设在多个传输端口之上;

MessageDeliverer 是消息路由的接口,ServerMessageDelivery 实现了根据uri 查找Resource的功能;

ConcurrentCoapResource则为Resource 提供了一个独立线程池的执行方式。

4.1.4 package-observe

应用层 observe机制的定义,如下图

ObserveRelation 定义一个观察关系,对应一个观察者即观察目标Resource;

ObserveEndpoint 定义了一个观察者端点,并包含一个关系列表(一个观察者可以观察多个Resource);

ObserveManager 由CoapServer持有,用于管理观察者端点列表;

CoapResource 也会持有一个Relation集合以实现跟踪;其通过ObserveRelationFilter接口决定是否接受来自观察者的注册请求;

4.2 elements-connector

connector 模块由core模块剥离,用于实现网络传输层的抽象,这使得Coap协议可以运行于UDP、TCP、DTLS等多种协议之上。

Connector定义了连接器需实现的相关方法,包括启动停止、数据的收发;

RawData包含了网络消息包的原始字节数据,其解析和编码需要交由上层协议实现;

CorrelationContext 描述了上下文,用于支持传输协议的一些会话数据读写,如DTLS会话。

4.3. 核心接口

下面拟用一张关系图概括Californium 框架的全貌(部分内容未体现):

与分层设计对应,框架分为 transport 传输层、protocol 协议层、logic 逻辑层

transport 传输层,由Connector 提供传输端口的抽象,UDPConnector是其主要实现;

数据包通过RawData对象封装;该层还提供了CorrelationContext 实现传输层会话数据的读写支持。

protocol 协议层,提供了Coap 协议栈机制的完整实现;CoapEndpoint是核心的操作类,数据的编解码通过

DataSerializer、DataParser实现,MessageInterceptor提供了消息收发的拦截功能,Request/Response的映射处理

由 Matcher实现,Exchange 描述了映射模型;协议栈CoapStack 是一个分层的内核实现,在这里完成分块、重传等机制。

logic 逻辑层,定义了CoapClient、CoapServer的入口,包括消息的路由机制,Resource的继承机制;

Observe机制的关系维护、状态管理由ObserveManager提供入口。

5. 关键机制

5.1 协议栈;

californium-core 采用了分层接口来定义协议栈,其中CoapStack 描述整个栈对象,Layer则对应分层的处理;

这相当于采用了过滤器模式,分层的定义使得特性间互不影响,子模块可保持独立的关注点;

CoapStack定义如下:

public interface CoapStack {
// delegate to top
void sendRequest(Request request);
// delegate to top
void sendResponse(Exchange exchange, Response response);
...
// delegate to bottom
void receiveRequest(Exchange exchange, Request request);
// delegate to bottom
void receiveResponse(Exchange exchange, Response response);

接口包括了几个消息收发函数,而Layer也定义了一样的接口。

一个CoapUdpStack 包括的分层如下图:

CoapUdpStack 构造函数与此对应:

public CoapUdpStack(final NetworkConfig config, final Outbox outbox) {
...
Layer layers[] = new Layer[] {
new ExchangeCleanupLayer(),
new ObserveLayer(config),
new BlockwiseLayer(config),
reliabilityLayer };
setLayers(layers);
}

StackTopLayer和StackBottomLayer由基础类BaseCoapStack提供,实现了协议栈顶层和底层逻辑;

MessageDeliver是胶合应用层的接口,其从StackTopLayer收到Coap消息之后将继续分发到Resource;

StackBottomLayer则胶合了传输层,通过Inbox/Outbox接口实现与Connector的交互。

其他Layer的功能

ExchangeCleanLayer 提供Exchange清理功能,当取消请求时触发Exchange的清理功能;

ObserveLayer 提供Coap Observe机制实现;

BlockwiseLayer 提供Coap 分块传输机制实现;

ReliabilityLayer 提供可靠性传输,实现自动重传机制;

5.2 Exchange生命周期

Exchange对应于请求/响应模型,其生命周期也由交互模型决定,一般在响应结束之后Exchange便不再存活;

然而在Observe场景下例外,一旦启动了Observe请求,Exchange会一直存活直到Observe被取消或中断。

1 LocalExchange,即本地的Exchange, 对应于本地请求对方响应的交互。

BaseCoapStack.StackTopLayer实现了初始化:

public void sendRequest(final Request request) {
Exchange exchange = new Exchange(request, Origin.LOCAL);
...

当接收响应时进行销毁,observe类型的请求在这里被忽略:

    public void receiveResponse(final Exchange exchange, final Response response) {
if (!response.getOptions().hasObserve()) {
exchange.setComplete();
}

UdpMatcher 实现了销毁动作:

UdpMatcher--
public void sendRequest(final Exchange exchange, final Request request) {
exchange.setObserver(exchangeObserver);
exchangeStore.registerOutboundRequest(exchange);
if (LOGGER.isLoggable(Level.FINER)) {

这是在发送请求时为Exchange添加观察者接口,当exchange执行complete操作时触发具体的销毁工作:

UdpMatcher.ExchangeObserverImpl--
if (exchange.getOrigin() == Origin.LOCAL) {
// this endpoint created the Exchange by issuing a request
KeyMID idByMID = KeyMID.fromOutboundMessage(exchange.getCurrentRequest());
KeyToken idByToken = KeyToken.fromOutboundMessage(exchange.getCurrentRequest());
exchangeStore.remove(idByToken);
// in case an empty ACK was lost
exchangeStore.remove(idByMID);
...

值得一说的是,californium大量采用了观察者设计模式,这种方法在设计异步消息机制时非常有用.

此外,request的取消、中断操作(RST信号)、传输的超时都会导致exchange生命周期结束。

LocalExchange的生命周期如下图:

2 RemoteExchange,即远程的Exchange,对应于本地接收请求并返回响应的交互。

UdpMatcher实现了远程Exchange的初始化:

UdpMatcher--
public Exchange receiveRequest(final Request request) {
...
KeyMID idByMID = KeyMID.fromInboundMessage(request);
if (!request.getOptions().hasBlock1() && !request.getOptions().hasBlock2()) {
Exchange exchange = new Exchange(request, Origin.REMOTE);
Exchange previous = exchangeStore.findPrevious(idByMID, exchange);
if (previous == null) {
exchange.setObserver(exchangeObserver);
...

在发送响应时,Exchange被销毁,仍然由UdpMatcher实现:

UdpMatcher--
public void sendResponse(final Exchange exchange, final Response response) {
response.setToken(exchange.getCurrentRequest().getToken());
...
// Only CONs and Observe keep the exchange active (CoAP server side)
if (response.getType() != Type.CON && response.isLast()) {
exchange.setComplete();
}

注意到这里对response进行了last属性的判断,该属性默认为true,而ObserveLayer将其置为false,使得observe响应不会导致Exchange结束:

ObserveLayer--
public void sendResponse(final Exchange exchange, Response response) {
...
response.setLast(false);

连接中断(RST信号)、传输超时会导致Exchange的结束,此外由客户端发起的observe取消请求也会产生一样的结果。

RemoteExchange的生命周期如下图所示:

5.3 分块传输;

分块传输一般用于发送较大的请求体或接受较大的响应体,比如上传下载固件包场景,由于受到MTU的限制,需要实现分块传输;

Coap定义了分块传输的方式,采用Block1/Block2机制

Option选项

BlockOption是用于描述分块信息的选项类型,选项值为0-3个字节,编码包含了3个字段:当前分块编号;是否结束;当前分块大小。

为区分请求和响应的不同,分别有block1和block2 两个选项:

block1:用于发送POST/PUT请求时传输较大的内容体;

block2:用于响应GET/POST/PUT请求时传输较大的内容体;

size1:指示请求体的总大小;

size2:指示响应体的总大小;

配置选项

maxMessageSize:消息大小阈值,当发送的消息大于该阈值时需采用分块传输,该值必须小于MTU;

preferredBlockSize:用于指示分块的大小;

maxResourceBodySize:最大资源内容体大小,用于限制接收的请求或响应的总大小,若超过将提示错误或取消处理;

blockLifeTime:分块传输的生命周期时长,若超过该时长分块传输未完成则视为失败;

BlockwiseLayer实现了分块传输的完整逻辑,其中sendRequest的代码片段:

public void sendRequest(final Exchange exchange, final Request request) {
BlockOption block2 = request.getOptions().getBlock2();
if (block2 != null && block2.getNum() > 0) {
//应用层指定的分块..
} else if (requiresBlockwise(request)) {
//自动计算分块
startBlockwiseUpload(exchange, request);
} else {
//不需要分块
exchange.setCurrentRequest(request);
lower().sendRequest(exchange, request);
}
}
...
//实现分块阈值判断
private boolean requiresBlockwise(final Request request) {
boolean blockwiseRequired = false;
if (request.getCode() == Code.PUT || request.getCode() == Code.POST) {
blockwiseRequired = request.getPayloadSize() > maxMessageSize;
}
...
//startBlockwiseUpload实现了request分块逻辑,通过在请求的Option中加入Block1作为标识
private void startBlockwiseUpload(final Exchange exchange, final Request request) {
BlockwiseStatus status = findRequestBlockStatus(exchange, request);
final Request block = getNextRequestBlock(request, status);
block.getOptions().setSize1(request.getPayloadSize());
...
lower().sendRequest(exchange, block);
}

接收端检测Request的Block1选项,返回continue响应码,直到所有分块传输完成后进行组装交由上层处理:

private void handleInboundBlockwiseUpload(final BlockOption block1, final Exchange exchange, final Request request) {
//检查是否超过限制
if (requestExceedsMaxBodySize(request)) {
Response error = Response.createResponse(request, ResponseCode.REQUEST_ENTITY_TOO_LARGE);
error.setPayload(String.format("body too large, can process %d bytes max", maxResourceBodySize));
error.getOptions().setSize1(maxResourceBodySize);
lower().sendResponse(exchange, error);
} else {
...
if (block1.getNum() == status.getCurrentNum()) {
if (status.hasContentFormat(request.getOptions().getContentFormat())) {
status.addBlock(request.getPayload());
status.setCurrentNum(status.getCurrentNum() + 1); if ( block1.isM() ) {
//存在后面的block,返回Continue响应
Response piggybacked = Response.createResponse(request, ResponseCode.CONTINUE);
piggybacked.getOptions().setBlock1(block1.getSzx(), true, block1.getNum());
piggybacked.setLast(false);
exchange.setCurrentResponse(piggybacked);
lower().sendResponse(exchange, piggybacked);
} else {
...
//已经完成,组装后交由上层处理
Request assembled = new Request(request.getCode());
assembled.setSenderIdentity(request.getSenderIdentity());
assembleMessage(status, assembled);
upper().receiveRequest(exchange, assembled);
}

因此,一个请求体分块传输流程如下图所示:

响应体分块传输的逻辑与此类似,交互流程如下图:

5.4 消息重传;

Coap消息支持重传机制,当发送CON类型的消息时,要求接收端响应对应的ACK消息;如果在指定时间内没有收到响应,则进行重传。

基础消息重传由ReliabilityLayer实现,sendRequest 代码片段:

        if (request.getType() == null) {
request.setType(Type.CON);
}
if (request.getType() == Type.CON) {
prepareRetransmission(exchange, new RetransmissionTask(exchange, request) {
public void retransmit() {
sendRequest(exchange, request);
}
});
}
lower().sendRequest(exchange, request);

当发送CON类型消息时,通过 prepareRetransmission函数实现重传准备:

        int timeout;
if (exchange.getFailedTransmissionCount() == 0) {
timeout = getRandomTimeout(ack_timeout, (int) (ack_timeout * ack_random_factor));
} else {
timeout = (int) (ack_timeout_scale * exchange.getCurrentTimeout());
}
exchange.setCurrentTimeout(timeout);
ScheduledFuture<?> f = executor.schedule(task, timeout, TimeUnit.MILLISECONDS);
exchange.setRetransmissionHandle(f);

exchange.getFailedTransmissionCount() 返回0 代表第一次传输,采用的超时时间是:

timeout = random(ack_timeout, act_timeout*ack_random_factor)

//其中ack_timeout(超时起始值)、ack_random_factor(随机因子)由配置文件提供;

后续的重传时间将由上一次的timeout和ack_timeout_scale系数决定:

timeout = timeout * ack_timeout_scale

当接收ACK时,有必要取消重传处理,看看receiveResponse的实现:

    @Override
public void receiveResponse(final Exchange exchange, final Response response) {
exchange.setFailedTransmissionCount(0);
exchange.getCurrentRequest().setAcknowledged(true);
exchange.setRetransmissionHandle(null);
...

可以看到,接收到响应之后,将Request标记为ack状态,exchange.setRestransmissionHandler会导致上一次的重传schedu任务被取消。

最终重传任务由RetransmissionTask实现:

                int failedCount = exchange.getFailedTransmissionCount() + 1;
exchange.setFailedTransmissionCount(failedCount);
if (message.isAcknowledged()) {
return;
} else if (message.isRejected()) {
return;
} else if (message.isCanceled()) {
return;
} else if (failedCount <= max_retransmit) {
// Trigger MessageObservers
message.retransmitting();
// MessageObserver might have canceled
if (!message.isCanceled()) {
retransmit();
}
} else {
exchange.setTimedOut();
message.setTimedOut(true);
}

满足重传的条件

1 消息未被确认(收到ACK)或拒绝(收到RST)

2 消息未被取消;

3 消息未超过重传次数限制;

其中重传次数max_retransmit由配置提供,当超过该次数限制时消息将发生传输超时。

默认参数配置

ack_timeout=2s
ack_random_factor=1.5
ack_timeout_scale=2
max_retransmit=4

5.5 防止重复包;

由于存在重传机制,加上UDP传输的不稳定性,传输两端很可能会受到重复的消息包;

通常重复消息的检测要求实现消息容器以记录和匹配重复消息ID,然而执行时间越长,消息会越来越多,

因此消息容器必须具备清除机制,基于此点不同,californium 提供了两种实现机制:

5.5.1 标记清除

清除器维持一个消息容器,每个消息都保持一个初始的时间戳;

清除器定时进行扫描,发现太老的消息则将其清除。

SweepDeduplicator 提供了实现,清除代码片段:

private void sweep() {
final long oldestAllowed = System.currentTimeMillis() - exchangeLifetime;
final long start = System.currentTimeMillis();
for (Map.Entry<?, Exchange> entry : incomingMessages.entrySet()) {
Exchange exchange = entry.getValue();
if (exchange.getTimestamp() < oldestAllowed) {
incomingMessages.remove(entry.getKey());
}
}
...

其中incomingMessage采用了ConcurrentHashMap数据结构,这是一个并发性良好的线程安全集合;

然而从上面的代码也可以发现,sweep在这里是一个遍历操作,定时清除的老化时间默认为247s,假设1s内处理1000条消息,

那么每次清除时驻留的消息数量为247000,即需要遍历这么多的次数,对于CPU来说存在一定的开销。

采用这种方式,消息的存活时间基本上由exchangeLifetime参数和扫描间隔决定。

5.5.2 翻转清除

清除器维持三个消息容器,保持1、2、3三个索引分别指向相应消息容器,其中索引1、2代表了活动的消息容器,

索引3 代表老化的消息容器,如下图所示

消息索引首次会往 I1容器写入,同时也会往 I2容器存入拷贝;

查找消息时主要从I1 容器查找;

每个周期会执行一次翻转,几个容器指针发生置换(I1->I2,I2->I3,I3->I1),之后I3 指向的容器会被清理;

CropRotation 实现了翻转的逻辑,代码如下:

private void rotation() {
synchronized (maps) {
int third = first;
first = second;
second = (second+1)%3;
maps[third].clear();
}

基于上述的算法分析,I2容器的消息存活时间会小于一个周期,I1容器的消息则存活一个周期到两个周期之间,I3 容器则超过2个周期,是最老的容器;

基于这样的逻辑,翻转清除机制的消息存活时间是1-2个周期之间,而该机制相比标记清除的优点在于清除机制是整个容器一块清除,而不需要遍历操作,然而缺点是增加了存储开销。

JVM的垃圾回收机制也存在类似的设计,相信californium的开发者借鉴了一些思路。

至此,Californium框架的基本全貌已经分析完毕。如果希望对框架有更深入的理解,那么建议你直接在项目中直接使用它,并针对自己感兴趣的几个问题进行源码分析或调试,相信收获会更多。

6. 扩展阅读

RFC关于分块传输的定义

https://tools.ietf.org/html/draft-ietf-core-block-21

Hands on with Coap(需要翻墙)

https://docs.google.com/presentation/d/1dDZ7VTdjBZxnqcIt6qoX742d6dHbzap-D_H8Frf3LRE/edit#slide=id.p

Californium项目早期的介绍文档

https://people.inf.ethz.ch/mkovatsc/resources/californium/cf-thesis.pdf

Californium 项目源码

https://github.com/eclipse/californium


后记

往往我们在使用优秀开源框架的时候都是信手拈来,知其一则止步。

这或许跟环境有着极大的关系,试想如果公司让你天天陷于加班赶改的状态,项目上不合理分配资源,只要结果却不关心个人的成长。长此以往,谁还能回归到技术的路上?

然而,改变不了环境的结果只能改变自己,路漫漫其修远兮,无论你的选择如何,努力过的世界终究是精彩的。

californium 框架设计分析的更多相关文章

  1. 通讯框架 T-io 学习——给初学者的Demo:ShowCase设计分析

    前言 最近闲暇时间研究Springboot,正好需要用到即时通讯部分了,虽然springboot 有websocket,但是我还是看中了 t-io框架.看了部分源代码和示例,先把helloworld敲 ...

  2. Django框架----权限管理(设计分析以及具体细节)

    说起权限我们大家都知道,不一样的角色会有不一样的权限.比如就像学生管理系统一样,管理员,老师,学生之间的权限都是不一样的,那么展示的页面也是不一样的.所以,我们现在来看看具体操作. 目标:生成一个独立 ...

  3. Python学习---抽屉框架分析[数据库设计分析]180313

    基本的: models.py ####################################以下都是抽屉的代码#################################### fro ...

  4. Web API应用架构在Winform混合框架中的应用(1)

    在<Web API应用架构设计分析(1)>和<Web API应用架构设计分析(2)>中对WebAPI的架构进行了一定的剖析,在当今移动优先的口号下,传统平台都纷纷开发了属于自己 ...

  5. Web API应用架构设计分析(2)

    在上篇随笔<Web API应用架构设计分析(1)>,我对Web API的各种应用架构进行了概括性的分析和设计,Web API 是一种应用接口框架,它能够构建HTTP服务以支撑更广泛的客户端 ...

  6. Spring 框架的设计理念与设计模式分析

    转载地址:https://www.ibm.com/developerworks/cn/java/j-lo-spring-principle/ Spring 作为现在最优秀的框架之一,已被广泛的使用,并 ...

  7. RPC框架实现思路浅析

    第一部分,设计分析 远程调用要解决的主要问题: 1,序列化 : 如何将对象转化为二进制数据进行传输,如何将二进制数据转化对象 2,数据的传输(协议,第三方框架) 3,服务的注册/发现,单点故障,分布式 ...

  8. [转载] 多图详解Spring框架的设计理念与设计模式

    转载自http://developer.51cto.com/art/201006/205212_all.htm Spring作为现在最优秀的框架之一,已被广泛的使用,51CTO也曾经针对Spring框 ...

  9. Winform开发框架中工作流模块的表设计分析

    在较早博客随笔里面写过文章<Winform开发框架之简易工作流设计>之后,很久没有对工作流部分进行详细的介绍了,本篇继续这个主题,详细介绍其中的设计.实现及效果给大家,这个工作流在好几年前 ...

随机推荐

  1. Tsinsen-A1490 osu! 【数学期望】

    问题描述 osu!是一个基于<押忍!战斗!应援团><精英节拍特工><太鼓达人>等各种音乐游戏做成的一款独特的PC版音乐游戏.游戏中,玩家需要根据音乐的节奏,通过鼠标 ...

  2. SpringBoot JPA实现增删改查、分页、排序、事务操作等功能

    今天给大家介绍一下SpringBoot中JPA的一些常用操作,例如:增删改查.分页.排序.事务操作等功能.下面先来介绍一下JPA中一些常用的查询操作: //And --- 等价于 SQL 中的 and ...

  3. 【转】mysql-5..6.23-win64.zip安装及配置

    [强烈建议!!!!]把文件夹的名字也改成如下所说的,不然即使你什么环境配置都对,启动服务的时候依然会出现‘net’不是计算机内部或外部的命令这种令人很郁闷的问题了! 原文链接:http://jingy ...

  4. Leetcode 181. Employees Earning More Than Their Managers

    The Employee table holds all employees including their managers. Every employee has an Id, and there ...

  5. Win10 的虛擬桌面

    Win10 的虛擬桌面我覺得蠻多餘的,平常很少用,除非是像以前的 "切換老闆鍵" ,老闆來了,你不想讓他知道你在幹嘛,趕快切換另外一個桌面. 切換工作視窗:Alt + Tab 叫出 ...

  6. 如何在Eclipse下安装myeclipse插件

    来自http://www.blogjava.net/show911/archive/2008/04/27/86284.html 下载myeclipse插件 支持eclipse3.1.x, 具体安装步骤 ...

  7. margin的简单应用

    今晚学了盒模型的marg部分,简单仿下京东的官网首页部分 第一次制作,尽管看来实在惨不忍睹,毕竟娘不嫌儿丑,之后多加努力吧,这几天尽量加快学习进度,能单独制作一张精美的网页最好 附上代码 <!D ...

  8. localStorage eval script

    var globalEval =function(data) { (window.execScript || function(data){ window.eval.call(window,data) ...

  9. 使用YUIDoc生成JS文档

    其实YUIDoc主页已经写的比较清晰了,但有一些概念和细节再点出一些注意的地方. 目前最新的YUIDoc使用nodejs进行开发安装和使用都非常的方便. 我们只需要将我们的代码加上必要的注释,便可以很 ...

  10. Jq对象与dom对象的互相转换!

    JQ对象转化成dom对象 var a=$('div'); var b=a[0];//dom对象 转化成dom对象以后就可以使用dom方法了 dom对象转化成jq对象 var a=document.ge ...