2013-01-21 by 谢鸿锋  

原创文章,转载请注明:转载自Erlang云中漫步

目录

=================================

一、概述

二、ranch源码分析

三、cowboy源码分析

1、Request调度规则

2、http协议实现分析

3、http协议之chunked编码

4、http协议之long_polling

5、http协议之websocket

6、http协议之rest-api

=================================

cowboy 越来越让人舒服了,改版之后的cowboy分为两大application,将TCP拆分出来,成了ranch application,cowboy成了基于TCP(ranch)的一个cowboy_protocol(http实现)。不仅如此,cowboy还给出了rest-api、websocket、chunked、long-polling的支持,相当之完美!

一、概述

cowboy是一个小型、快速,模块化,采用Erlang开发的HTTP服务器。

ranch 是一个socket acceptor pool,TCP协议类型。

cowboy的特点:

1.代码少。
2.速度快。
3.模块化程度高,transport和protocol都可轻易替换。
4.采用二进制语法实现http服务,更快更小。
5.极易嵌入其它应用。
6.有dispatcher,可以嵌入FastCGI PHP 或者是 Ruby.
7.没有进程字典,代码干净。

总体来讲cowboy的特点在于分层架构及模块化设计,即把网络层的套接字管理和应用层协议实现,以及对消息的处理,这三层几乎完全解藕。

cowboy application详细介绍见:https://github.com/extend/cowboy/

{application, cowboy, [

{id, "Cowboy"},

{description, "Small, fast, modular HTTP server."},

{sub_description, "Cowboy is also a socket acceptor pool, "

"able to accept connections for any kind of TCP protocol."},

{vsn, "0.7.0"},

{applications, [

kernel,

stdlib,

ranch,

crypto

]},

{mod, {cowboy_app, []}},

ranch application详细介绍见:https://github.com/extend/ranch/

{application, ranch, [

{id, "Ranch"},

{description, "Socket acceptor pool for TCP protocols."},

{sub_description, "Reusable library for building networked applications."},

{vsn, "0.6.0"},

{mod, {ranch_app, []}},

二、ranch源码分析

1、看下效果

a) 启动ranch应用 application:start(ranch).

b)启动例子tcp_echo应用 application:start(tcp_echo).

c)客户端连接测试

d)处理客户端请求

e)客户端断开

下面对关键代码执行轨迹进行分析

2、启动ranch应用

> application:start(ranch).

代码执行轨迹关注点见下面几张图红线框住部分

启动了监督进程ranch_sup,以one_for_one方式监督启动ranch_server工作进程

此时进程监督树如下:

关注数据:etsranch_server,此时为空,

               ranch_server进程状态数据state,此时为空,

下面跟踪客户端连接进来时,这两个数据的变化情况。

3、启动例子tcp_echo应用

> application:start(tcp_echo).

代码执行轨迹关注点见下面几张图红线框住部分

It will have a pool of 1 acceptors, use a TCP transport and forward connections to the “echo_protocol” handler.

动态生成ranch_sup的子进程{ranch_listener_sup, Ref},类型为supervisor。Ref值为tcp_echo。

结束此进程可以调用ranch:stop_listener(tcp_echo)。ranch_sup在application:start(ranch)时已经产生。

各参数说明见其注释:

Start a listener for the given transport and protocol.

A listener is effectively a pool of NbAcceptors acceptors. Acceptors accept connections on the given Transport and forward connections to the given Protocol handler. Both transport and protocol modules can be given options through the TransOpts and the ProtoOpts arguments. Available options are documented in the listen transport function and in the protocol module of your choice.

All acceptor and connection processes are supervised by the listener.

It is recommended to set a large enough number of acceptors to improve performance. The exact number depends of course on your hardware, on the protocol used and on the number of expected simultaneous connections.

The Transport option max_connections allows you to define the maximum number of simultaneous connections for this listener. It defaults to 1024. See ranch_listener for more details on limiting the number of connections.

Ref can be used to stop the listener later on.

This function will return {error, badarg}` if and only if the transport module given doesnt appear to be correct.

此时进程状态数据及ets表数据如下:

ranch_server进程monitor进程<0.41.0>、<0.44.0>。<0.41.0>为listener,<0.44.0>为acceptor。<0.42.0>为connections 管理者,客户端连接进程由其以simple_one_for_one方式监控。此时客户端连接数为0。

ets表及进程状态数据生成跟踪代码轨迹:

至此,application(ranch)、application(tcp_echo)启动完成,

进程监督树产生/监督类型及关键代码轨迹分析完毕。

进程状态数据及ets表数据生成及关键代码轨迹也已分析完毕。

下面跟踪有客户端连接时的情况。

4、客户端连接

接收到客户端连接,从下面几个方面进行分析

A、 生成ConnsSup子进程,controlling_process(CSocket, ConnPid)绑定接收的客户端socket。

上图<0.42.0>为ConnsSup,<0.78.0>为ConnPid

B、 ConnPid生成,代码轨迹如下

C、 更新表ranch_server客户端连接数

59行ranch_listener:add_connection(ListenerPid,ConnPid)更新客户端连接数

D、 max_connections最大连接数处理:超过最大连接数将不再进行accept,轮询检测连接数,当小于最大连接数时,才开始accept接受客户端的连接。

5、处理客户端请求

客户端发的数据,服务器端收到后,原样响应传回给客户端。

6、客户端断开。

到这里,ranch的源码分析就完成了。

不足之处:acceptor接收客户端连接,用了一个临时的进程来中转,中转完毕此进程即销毁。

上图<0.79.0>即为临时进程,其功能完全可以合并到<0.44.0>中来进行。何必“创建->中转->销毁”多此一举呢?hotwheels的处理就是如此,干脆利落,堪称优雅。hotwheels源码分析见博主另外一篇原创文章: http://www.cnblogs.com/poti/archive/2012/11/06/hotwheels.html。

三、cowboy源码分析

cowboy application实现了http协议,给出了rest-api、websocket、chunked、long-polling的支持,相当完美。

1、Request调度规则

见cowboy_dispatcher:match(Dispatch, Host, Path)

-spec match(Dispatch::dispatch_rules(), Host::binary() | tokens(), Path::binary())

-> {ok, module(), any(), bindings(),

HostInfo::undefined | tokens(), PathInfo::undefined | tokens()}

| {error, notfound, host} | {error, notfound, path}

| {error, badrequest, path}.

-type tokens() :: [binary()].

-type match_rule() :: '_' | <<_:8>> | [binary() | '_' | '...' | atom()].

-type dispatch_path() :: [{match_rule(), module(), any()}].

-type dispatch_rule() :: {Host::match_rule(), Path::dispatch_path()}.

-type dispatch_rules() :: [dispatch_rule()].

示例说明:

match_test_() ->

Dispatch = [

{[<<"www">>, '_', <<"ninenines">>, <<"eu">>], [

{[<<"users">>, '_', <<"mails">>], match_any_subdomain_users, []}

]},

{[<<"ninenines">>, <<"eu">>], [

{[<<"users">>, id, <<"friends">>], match_extend_users_friends, []},

{'_', match_extend, []}

]},

{[<<"ninenines">>, var], [

{[<<"threads">>, var], match_duplicate_vars,

[we, {expect, two}, var, here]}

]},

{[<<"erlang">>, ext], [

{'_', match_erlang_ext, []}

]},

{'_', [

{[<<"users">>, id, <<"friends">>], match_users_friends, []},

{'_', match_any, []}

]}

],

%% {Host, Path, Result}

Tests = [

{<<"any">>, <<"/">>, {ok, match_any, [], []}},

{<<"www.any.ninenines.eu">>, <<"/users/42/mails">>,

{ok, match_any_subdomain_users, [], []}},

{<<"www.ninenines.eu">>, <<"/users/42/mails">>,

{ok, match_any, [], []}},

{<<"www.ninenines.eu">>, <<"/">>,

{ok, match_any, [], []}},

{<<"www.any.ninenines.eu">>, <<"/not_users/42/mails">>,

{error, notfound, path}},

{<<"ninenines.eu">>, <<"/">>,

{ok, match_extend, [], []}},

{<<"ninenines.eu">>, <<"/users/42/friends">>,

{ok, match_extend_users_friends, [], [{id, <<"42">>}]}},

{<<"erlang.fr">>, '_',

{ok, match_erlang_ext, [], [{ext, <<"fr">>}]}},

{<<"any">>, <<"/users/444/friends">>,

{ok, match_users_friends, [], [{id, <<"444">>}]}},

{<<"ninenines.fr">>, <<"/threads/987">>,

{ok, match_duplicate_vars, [we, {expect, two}, var, here],

[{var, <<"fr">>}, {var, <<"987">>}]}}

],

[{lists:flatten(io_lib:format("~p, ~p", [H, P])), fun() ->

{ok, Handler, Opts, Binds, undefined, undefined}

= match(Dispatch, H, P)

end} || {H, P, {ok, Handler, Opts, Binds}} <- Tests].

match_info_test_() ->

Dispatch = [

{[<<"www">>, <<"ninenines">>, <<"eu">>], [

{[<<"pathinfo">>, <<"is">>, <<"next">>, '...'], match_path, []}

]},

{['...', <<"ninenines">>, <<"eu">>], [

{'_', match_any, []}

]}

],

Tests = [

{<<"ninenines.eu">>, <<"/">>,

{ok, match_any, [], [], [], undefined}},

{<<"bugs.ninenines.eu">>, <<"/">>,

{ok, match_any, [], [], [<<"bugs">>], undefined}},

{<<"cowboy.bugs.ninenines.eu">>, <<"/">>,

{ok, match_any, [], [], [<<"cowboy">>, <<"bugs">>], undefined}},

{<<"www.ninenines.eu">>, <<"/pathinfo/is/next">>,

{ok, match_path, [], [], undefined, []}},

{<<"www.ninenines.eu">>, <<"/pathinfo/is/next/path_info">>,

{ok, match_path, [], [], undefined, [<<"path_info">>]}},

{<<"www.ninenines.eu">>, <<"/pathinfo/is/next/foo/bar">>,

{ok, match_path, [], [], undefined, [<<"foo">>, <<"bar">>]}}

],

[{lists:flatten(io_lib:format("~p, ~p", [H, P])), fun() ->

R = match(Dispatch, H, P)

end} || {H, P, R} <- Tests].

2、http协议分析

http协议说明见:http://www.cnblogs.com/poti/articles/2851330.html

从http_SUITE: echo_body/1例子开始分析http协议解析。

命令行代码执行:

1> Config = [{priv_dir,"D:/eclipse/workspace/cowboy/test/priv"}].

2> http_SUITE:init_per_suite(Config).

3> Client = http_SUITE:init_per_group(http, Config).

4> http_SUITE:echo_body(Client).

{ok,<<"aaa">>,

{client,request,[],#Port<0.1848>,ranch_tcp,5000,<<>>,

keepalive,

{1,1},

undefined}}

调度规则:

Dispatch =

[

{[<<"localhost">>], [

{[<<"echo">>, <<"body">>], http_handler_echo_body, []},

]}

]

测试代码:

请求处理模块

以上代码完成了下面功能:

A、客户端http请求

B、服务器http响应

cowboy_protocol.erl代码分析

parse_method:解析method。 例子中是POST

parse_uri_path:解析path。   /echo/body

parse_version:解析version。  HTTP/1.1

parse_uri_query:解析query,url中$?分割部分。 例子中为空。

parse_uri_fragment:解析fragment,url中$#分割部分。 例子中为空。

parse_header:解析header。

Headers = [{<<"connection">>,<<"close">>},

{<<"user-agent">>,<<"Cow">>},

{<<"host">>,<<"localhost:33080">>},

{<<"content-length">>,<<"3">>}]

parse_host:解析host。 { <<"localhost">>, 33080 }

request:开始处理request。

cowboy_router.erl作用:根据Reqeust从Dispatch中找到handler模块及handle_opts。

cowboy_handler.erl作用:执行handler模块,带上参数handler_opts。

接下来,看Handler:init/3、Handler:handle/2,这里Handler为http_handler_echo_body.erl。

init/3的结果HandlerState这里为undefined会传给handle/2参数State。

到这里,客户端http请求处理完毕,

服务器http响应完成后将根据request中connection字段的值进行相应处理。

如果connection=close则Transport:close(Socket)断开连接;

如果connection=keep-alive则保持连接,处理下一个请求;如果超时未收到请求,则断开连接。

3、http协议chunked编码

chunked编码是HTTP/1.1 RFC里定义的一种编码方式,

协议说明见:http://www.cnblogs.com/poti/articles/2822159.html

从http_SUITE: chunked_response/1例子开始进行分析

客户端http请求:

服务器http响应:

服务器http响应,分下面几个数据包进行:

a)cowboy_req:chunked_reply(200, Req) -> chunked协议http头部发送

关键代码轨迹如下:

b)cowboy_req:chunk("chunked_handler\r\n", Req2) -> 第1个chunk数据包发送。

发送chunk数据包chunked_handler\r\n

关键代码轨迹如下:

c)cowboy_req:chunk("works fine!", Req2) -> 第2个chunk数据包发送。

发送chunk数据包works fine!。分析同b),不赘述。

d)cowboy_req:ensure_response(#http_req{socket=Socket, transport=Transport,resp_state=chunks}, _) -> 最后一个chunk数据包发送。

发送last-chunk数据包<<"0\r\n\r\n">>

4、http协议long_polling

long-polling的服务,其客户端是不做轮询的,客户端在发起一次请求后立即挂起,一直到服务器端有更新的时候,服务器才会主动推送信息到客户端。 在服务器端有更新并推送信息过来之前这个周期内,客户端不会有新的多余的请求发生,服务器端对此客户端也啥都不用干,只保留最基本的连接信息,一旦服务器有更新将推送给客户端,客户端将相应的做出处理,处理完后再重新发起下一轮请求。

从http_SUITE: check_status/1例子开始进行分析

/long_polling 处理模块为http_handler_long_polling

定时器动作5次后给客户端响应状态码102。下面分析下服务器代码处理过程:

erlang:send_after/3、erlang:start_timer/3说明见:http://www.cnblogs.com/poti/articles/2823209.html

cowboy_handler:handler_init/4执行后返回结果为

 

erlang:hibernate/3用途:使当前进程进入休眠状态,当有消息发送给进程时,激活进程调用Module:Function(Args)。

这里Module为cowboy_protocol,Function为resume。

这个例子中激活进程的消息从何而来?两个地方:

激活进程后执行方法:cowboy_protocol:resume/6

这里Module为cowboy_handler,Function为handler_loop。接下来的代码执行轨迹如下:

当前进程再次进入休眠状态,这里Module为cowboy_handler,Function为handler_loop。

激活进程后执行方法:cowboy_handler: handler_loop

直到计数器为0

以上就是cowboy中long-polling的处理过程。

小结一下此例子中cowboy的long-polling处理过程:

a)  处理模块init方法启动一个定时器,同时返回结果: {loop, Req, state(), timeout(), hibernate}

b)  当前进程进入休眠状态

c)  定时器发送消息,激活休眠的进程,回调处理模块的方法info/3

d)  如果服务器响应客户端的条件符合,则服务器给客户端响应结果,到这里,客户端的长连接就处理完毕了

e)  否则,info/3中重新启动一个定时器,goto 到b)继续执行。

cowboy中实现long-polling的三种方式:

init/3 -> {loop, Req, state(), hibernate}

当前进程不创建定时器,有休眠状态。不限时的休眠方式长轮询。

init/3 -> {loop, Req, state(), timeout()}

当前进程创建定时器,无休眠状态。限时的无休眠方式长轮询。

init/3 -> {loop, Req, state(), timeout(), hibernate}

当前进程创建定时器,有休眠状态。限时且有休眠方式的长轮询。

hibernate的作用:进程进入休眠状态,消耗服务器资源最小化,直到有消息到达时被激活。

5、http协议websocket

5.1、websocket草案00协议 见:http://www.cnblogs.com/poti/articles/2828392.html

cowboy中websocket版本00的实现过程如下:

a)  握手协议,建立websocket连接通道

客户端发送消息:

请求中的“Sec-WebSocket-Key1”,“Sec-WebSocket-Key2”和最后的“8字节的Key3”都是随机的,

服务器端会用这些数据来构造出一个16字节的应答。

其中:8字节的Key3为请求的内容,其它的都是http请求头。

判断当前请求是否WEBSOCKET,主要是通过请求头中的Connection是不是等于Upgrade以及Upgrade是否等于WebSocket。

服务器响应消息:

把请求的第一个Key中的数字除以第一个Key的空白字符的数量,而第二个Key也是如此,然后把这两个结果与请求最后的8字节字符串连接起来,然后进行MD5构造产生16字节的加密数据。

b)  消息传送

客户端和服务端发送非握手文本消息,消息以0x00开头,0xFF结尾。

c)  连接断开

客户端发送<<0xFF, 0x00>>,服务器回复<<0xFF, 0x00>>。

下面对关键代码进行分析

a 握手协议

a.1 客户端发送:

a.2 服务器处理:

处理模块websocket_handler.erl:init/3返回结果

{upgrade, protocol, cowboy_websocket}.

http请求头部验证:下图红线框住部分必须要有。

服务器给客户端发送:

status(101) -> <<"101 Switching Protocols">>;切换协议。

a.3 客户端收到后,如下处理:

101状态检查。http响应头部验证:下图红线框住部分必须要有。

接着,客户端往服务器发送一个随机的8个字节的字符串给服务器。

a.4 服务器收到此数据,作为key3,与前面的

Sec-Websocket-Key1: Y\" 4 1Lj!957b8@0H756!i

Sec-Websocket-Key2: 1711 M;4\\74  80<6

一起生成Challenge(16个字节的加密KEY),加密KEY算法:

Sec_WebSocket-Key1的产生方式:
(1)提取客户端请求的Sec_WebSocket-Key1中的数字字符组成字符串k1
(2)转换字符串k1为8个字节的长整型intKey1
(3)统计客户端请求的Sec_WebSocket-Key1中的空格数k1Spaces
(4)intKey1/k1Spaces取整k1FinalNum
(5)将k1FinalNum转换成字节数组再反转最终形成4个字节的Sec_WebSocket-Key1

Sec_WebSocket-Key2的产生方式:
(1)提取客户端请求的Sec_WebSocket-Key2中的数字字符组成字符串k2
(2)转换字符串k2为8个字节的长整型intKey2
(3)统计客户端请求的Sec_WebSocket-Key2中的空格数k2Spaces
(4)intKey2/k2Spaces取整k2FinalNum
(5)将k2FinalNum转换成字节数组再反转最终形成4个字节的Sec_WebSocket-Key2

Sec_WebSocket-Key3的产生方式:
客户端握手请求的最后8个字节

将Sec_WebSocket-Key1、Sec_WebSocket-Key2、Sec_WebSocket-Key3合并成一个16字节数组
再进行MD5加密形成最终的16个字节的加密KEY

服务器生成Challenge后,发送给客户端,

a.5 客户端收到此消息后,握手协议到此就算完成。

b 握手协议完成后,进行消息的发送接收:

客户端和服务端发送非握手文本消息,消息以0x00开头,0xFF结尾。

服务器端:

客户端:

c 断开连接

客户端发送<<0xFF, 0x00>>,服务器回复<<0xFF, 0x00>>。

客户端:

服务器:

5.2、websocket草案10协议 见:http://www.cnblogs.com/poti/articles/2828378.html

cowboy中websocket版本7、8、13的实现过程:

a)  握手协议,建立websocket连接通道

客户端发送消息:

1)Sec-WebSocket-Key后面的长度为24的字符串是客户端随机生成的,我们暂时叫他cli_key,服务器必须用它经过一定的运算规则生成服务器端的key,暂时叫做ser_key,然后把ser_key发回去,ser_key后面会介绍;

2)把http头中Upgrade的值由"WebSocket"修改为了"websocket";

3)把http头中的"Origin"修改为了"Sec-WebSocket-Origin";

4)增加了http头"Sec-WebSocket-Accept",用来返回原来草案00服务器返回给客户端的握手验证,原来是以body的形式返回,现在是放到了http头中。

服务器响应消息:

服务器端制作秘钥ser_key:

1)服务器端将cli_key(长度24)截取出来dGhlIHNhbXBsZSBub25jZQ==

用它和自定义的一个字符串(长度36):258EAFA5-E914-47DA-95CA-C5AB0DC85B11 连接起来,像这样:dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11

2)然后把这一长串经过SHA-1算法加密,得到长度为20字节的二进制数据,再将这些数据经过Base64编码,最终得到服务端的密钥,也就是ser_key:s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

3)然后将ser_key发送给客户端

至此,算是握手成功了!

b)  消息传送

消息格式:

各字段详细说明见:http://www.cnblogs.com/poti/articles/2828378.html

cowboy中将按这个规则对数据进行编码及解码。下面对Opcode做个说明:

Opcode:4位操作码,定义有效负载数据,以下是定义的操作码:

*  %x0 表示连续消息片断
      *  %x1 表示文本消息片断
      *  %x2 表未二进制消息片断
      *  %x3-7 为将来的非控制消息片断保留的操作码
      *  %x8 表示连接关闭
      *  %x9 表示心跳检查的ping
      *  %xA 表示心跳检查的pong
      *  %xB-F 为将来的控制消息片断的保留操作码

c)  心跳消息:

ok = gen_tcp:send(Socket, << 1:1, 0:3, 9:4, 0:8 >>), %% ping

{ok, << 1:1, 0:3, 10:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000), %% pong

d)  连接断开

ok = gen_tcp:send(Socket, << 1:1, 0:3, 8:4, 0:8 >>), %% close

{ok, << 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000),

6、HTTP协议之rest-api

   未完待续

转载于:https://www.cnblogs.com/poti/archive/2013/01/21/2870302.html

cowboy源码分析相关推荐

  1. Cowboy 源码分析(十八)

    在上一篇中,我们整理了下cowboy_http_protocol:header/3函数,在文章的末尾留下2个没有讲到的函数,今天,我们先看下cowboy_http_protocol:error_ter ...

  2. Cowboy 源码分析(一)

    首先,今天0点<暗黑破坏神3>就要正式开服了,但是我把晚上献给了erlang,经过前几天的努力,我已经看完了 Erlang OTP设计原则,在这里非常感谢,翻译成中文的作者 Shining ...

  3. Cowboy 源码分析(十一)

    上一篇,我们使用debugger和HttpFox很方便了,查看了方法中的变量,不得不说,debugger 断点调试还是比较好用的.这一篇,我们仍将使用这些工具来帮助我们了解代码,好了,接着上一篇继续来 ...

  4. ranch 源码分析(完)

    接上 ranch 源码分析(三) 在上一次,根据ranch源码把大概流程理了一遍,下面我们将一些细节解释一下. ranch只是一个服务的框架,它提供了传输层协议代码(ranch_tcp 和ranch_ ...

  5. 【Golang源码分析】Go Web常用程序包gorilla/mux的使用与源码简析

    目录[阅读时间:约10分钟] 一.概述 二.对比: gorilla/mux与net/http DefaultServeMux 三.简单使用 四.源码简析 1.NewRouter函数 2.HandleF ...

  6. SpringBoot-web开发(四): SpringMVC的拓展、接管(源码分析)

    [SpringBoot-web系列]前文: SpringBoot-web开发(一): 静态资源的导入(源码分析) SpringBoot-web开发(二): 页面和图标定制(源码分析) SpringBo ...

  7. SpringBoot-web开发(二): 页面和图标定制(源码分析)

    [SpringBoot-web系列]前文: SpringBoot-web开发(一): 静态资源的导入(源码分析) 目录 一.首页 1. 源码分析 2. 访问首页测试 二.动态页面 1. 动态资源目录t ...

  8. SpringBoot-web开发(一): 静态资源的导入(源码分析)

    目录 方式一:通过WebJars 1. 什么是webjars? 2. webjars的使用 3. webjars结构 4. 解析源码 5. 测试访问 方式二:放入静态资源目录 1. 源码分析 2. 测 ...

  9. Yolov3Yolov4网络结构与源码分析

    Yolov3&Yolov4网络结构与源码分析 从2018年Yolov3年提出的两年后,在原作者声名放弃更新Yolo算法后,俄罗斯的Alexey大神扛起了Yolov4的大旗. 文章目录 论文汇总 ...

最新文章

  1. iphone无线充电充电测试软件,无线充电哪家强?5款Qi无线充电板横向测评
  2. 工作流引擎 Activiti 万字详细进阶
  3. Codeforces Round #615 (Div. 3) A-F
  4. 40种Javascript中常用的使用小技巧【转】
  5. SQL的各种使用方法
  6. (转)ORACLE之常用FAQ V1.08
  7. Windows 8 DirectX 开发学习笔记(十五)使用Billboard实现树木贴图
  8. SCCM2012SP1---资产管理和远程管理
  9. smtp邮件服务器配置,配置电子邮件通知和指定 SMTP 服务器
  10. PCWorld评出的2010年世界杀毒软件排名
  11. 如何求解最大公约数和最小公倍数
  12. 华为/荣耀 笔记本 HiboardDataReport.exe应用程序错误
  13. 在Windows系统上部署DHCP服务器
  14. shopNC注册后无法登陆的问题
  15. 导入tkinter出错
  16. 自己建网站时要注意哪些细节
  17. AE502 112种创意视频字幕动画呼出线框文字标题效果包括PR预设与扩展脚本ae模板
  18. Html网页设计-羽毛球网站设计
  19. c++ 拼数 (sort 快排)
  20. ibatis java.util.Map作为parameterClass和resultClass(转)

热门文章

  1. error 1307 (HY000):Failed to create procedure
  2. C# 淘宝商品微信返利助手开发-(三)返利助手开发(1)API介绍
  3. mysql数据库root密码在哪个文件中_mysql - 本地数据库忘记了root用户的密码
  4. Easyui笔记:jquery执行append后input的验证失效解决方案
  5. 国内比较好用的5款测试管理工具
  6. Oracle常见错误
  7. sql 简单加密函数
  8. c语言两个字符串比较,将两个字符串s1和s2比较,如果s1s2,数组编程:将2个字符串s1和s2比较。若s1s2输出1;若s1=s2,输出0;若s1s2,输出-1(不能用strcmp函数)...
  9. 使用layer.tips实现鼠标悬浮时触发事件提示消息实现
  10. 使用ArrayList时设置初始容量的重要性