cowboy源码分析

 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/433954.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

linux解压tz zip,TZ 文件扩展名: 它是什么以及如何打开它?

TZ 疑难解答常见的 TZ 打开问题Smith Micro StuffIt Deluxe 已删除尝试打开 TZ 文件时&#xff0c;您收到错误 “无法打开 TZ 文件类型”。 发生这种情况时&#xff0c;通常是由于 %%os%% 中缺少 Smith Micro StuffIt Deluxe。 操作系统不知道如何处理你的 TZ 文件&#xff0c;…

python table对象_(RPA学习):Python-docx 常用方法

原标题&#xff1a;(RPA学习)&#xff1a;Python-docx 常用方法**1.**引用库from docx import Document**2.**新建一个空 docxdocument Document()**3.**保存 docxdocument.save(‘c:/test2.docx’)**4.**打开指定的 docxdocument Document(‘c:/test.docx’)**5.**在末尾增加…

ubuntu13.10无法登陆

在启动时到了登录界面后&#xff0c;输入用户名和密码&#xff0c;系统黑一下屏又回到了登录界面&#xff0c;怎么也进不去&#xff1f; 这时&#xff0c;按CtrlAltF1可以进入普通用户shell&#xff0c;CtrlAltF2可以进入root用户shell&#xff0c;AltF7可以回到图形界面。 进…

无法定位程序输入点 except_软件测试中的功能测试点(三)

testkuaibao|软件测试自学公众号26.输入法半角全角检查再输入信息中&#xff0c;输入一个或连串空格&#xff0c;查看系统如何处理&#xff0c;如对于要求输入符点型数据的项中&#xff0c;输入全角的小数点(“。”或“.”&#xff0c;如4.5)&#xff1b;输入全角的空格等。 27…

ASP.NET站点跨子域名单点登陆(SSO)的实现

http://blog.csdn.net/jason_dct/article/details/8502075 ASP.NET站点跨子域名单点登陆&#xff08;SSO&#xff09;的实现 在MSDN的文档“配置跨应用程序的 Forms 身份验证&#xff08;http://msdn2.microsoft.com/zh-CN/library/eb0zx8fc.aspx&#xff09;” 中&#xff0c;…

linux实验三makefile,实验平台上Makefile详细的解释

作者&#xff1a;甘老师,华清远见嵌入式学院讲师。# CORTEX-A8 PERI DRIVER CODE# VERSION 2.0# ATHUOR www.linuxidc.com# MODIFY DATE#2013.03.28 Makefile/***(下面的解释将用这个的形式进行标注)写好的源文件,要编译成二进制文件.需要指定工具链的,这里指定我们的工具链是…

python esp8266模块_ESP8266使用笔记之常用固件

开发板使用的是NodeMCU开发板&#xff1a;目录1.学习使用ESP8266官方的SDK1.1使用SDK提供的AT固件1.2使用SDK Build固件2.学习使用NodeMCU固件(上层可使用Lua开发)和MicroPython固件(上层可使用MicroPython开发)学习使用ESP8266官方的SDK使用AT固件&#xff1a;下载开发包&…

基础C#总结

由于在学习c#这段视频是为了辅助设计模式的学习,这部分的内容也和VB的内容很大程度上是一样的.虽然在开始的 时候,有些困难.在接触了一些例子和实验后.理解起来变得顺畅了很多.下面是对c#基础内容的总结.很多内容都在VB中有 过接触,所以都是些基础知识.捋一捋,将这些时间脑子的…

e站app改内置hosts_米家踢脚线电暖器E评测:符合现代家居审美 全屋取暖“小钢炮”...

【科技犬】对于没有集中供暖的长江中下游地区居民而言&#xff0c;电暖器是不折不扣的"保命神器"。而在深秋的北方&#xff0c;昼夜温差较大&#xff0c;这种时候使用灵活、易于搬运的电暖器也成为更加明智的选择。在北方每年的冬季&#xff0c;室内温度就直接关系着…

asterisk libxml2

asterisk 在configure的时候&#xff0c;会去/usr/bin下检查有没有xml2-config这个可执行文件&#xff0c;还回去检查 编译的工具链中有没有libxml2.so这个库文件&#xff0c;即使是交叉编译&#xff0c;/usr/bin下应该也是上位机的xml2-config 而且xml2-config和libxml2.so的…

锐炬显卡可以linux吗,Intel Broadwell桌面CPU性能测试:Iris Pro 6200核显无敌了

Intel已经在Computex 2015发布了Broadwell-H桌面版处理器&#xff0c;分别有Core i7-5775C和Core i5-5765C两款&#xff0c;另有3款BGA封装的嵌入式型号。它们的特色是内置Intel史上最强Iris Pro 6200核显、以及较大的超频空间。这款处理器现在已经解禁&#xff0c;发布了评测&…

经纬度转化为xy坐标系_Arcgis添加经纬度矢量点

今天帮舍友作图&#xff0c;才体会到九边说的。学技术使人清醒&#xff0c;清醒的认识自己能力有限。少去网上怨天怨地&#xff0c;踏踏实实去提升实力。这是她给我的原始数据&#xff0c;一堆经纬度点。首先我们先进行数据去重&#xff0c;当然这一步可以根据自己数据考虑是否…

CSS中class优先级问题

CSS中class样式的优先级&#xff08;或者层叠效果&#xff09;是根据class在<style>或者css文件中定义的顺序比较&#xff0c;而不是根据class应用的顺序&#xff0c;定义越靠后优先级越高&#xff1b;如 <html> <style>.class1{color:red;}.class2{color:bl…

惊群现象

引用&#xff1a;http://www.cppblog.com/isware/archive/2011/07/20/151470.aspx-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------“据说”…

物联网课程学习目标_学习攻略|软件工程统计方法amp;amp;物联网

软件工程统计方法&&物联网任课老师&#xff1a;余松森&#xff0c;葛红课程特点及困难本课程的主要内容涉及统计机器学习方法&#xff0c;以及如何采用Python进行应用实现。同学们在学习中主要遇到以下问题&#xff1a;1、在课程内容方面&#xff0c;课本上的关于pytho…

笨办法学linux dhcp,iptables使用指南(上)

iptables-save用来把当前的规则存入一个文件里以备iptables-restore使用。它的使用很简单&#xff0c;只有两个参数&#xff1a;iptables-save [-c] [-t table]参数-c的作用是保存包和字节计数器的值。这可以使我们在重启防火墙后不丢失对包和字节的统计。带-c参数的iptables-s…

tdk怎么设置_你真的做好网站的标题、描述、关键词(TDK)设置了吗?

SEO其实是个苦活累活&#xff0c;大部分的工作都是在每日不断的坚持与重复。当然也是个细致活&#xff0c;很多的工作都是对一些细节问题的处理。可能平时你没留意到的地方&#xff0c;就是你的网站数据没能上来的原因。比如说SEO的基础设置&#xff1a;TDK。说到TDK(标题、描述…

流言终结者——C语言内存管理

写在前头&#xff1a; 我不能保证此文中&#xff0c;我的观点和理解全是对的&#xff0c;这也不是一篇教学贴&#xff0c;只是我偶尔突发奇想了几个特殊的场景&#xff0c;然后用实验得到结果&#xff0c;对结果进行分析&#xff0c;遂成此文。所以文中肯定存在错误&#xff0c…

mediastreamer2 的简介

原文&#xff1a;http://www.linphone.org/eng/documentation/dev/mediastreamer2.htmlMediastreamer2 是一个功能强大且小巧的流引擎&#xff0c;专门为音视频电话应用而开发。这个库为linphone中所有的接收、发送多媒体流提供处理&#xff0c;包括音/视频捕获&#xff0c;编码…

C# 监控字段_监控交换机选择:千兆/百兆/核心/PoE/光纤交换机选型指南

我们就交换机选型时的四个主要方面讲一下。01选择千兆还是百兆&#xff1f;视频监控系统的网络中&#xff0c;需要传输大量、持续的视频数据&#xff0c;这就要求交换机具有稳定转发数据的能力。交换机接入的摄像头数量越多&#xff0c;流经该交换机的数据量就会越大。我们可以…