Ranch模块分析

原创
2019/08/23 14:38
阅读数 100

ranch_listener_sup模块

介绍

此模块是ranch调用的一个模块,用来处理所有的监听和网络连接中心。
该模块的创建时由ranch:start_listener函数启动的基于监督者进程ranch_sup的子进程

功能函数介绍

  • erlang的map结构 #{},#{name =>"test"} 注意区分两种表达式: =>(可以用来更新映射和创建新的映射) :=(只能更新映射,在键不存在时会抛出异常)
  • start_link:创建supervisor进程,并且将创建这个进程的参数保存到ranch_server中。
    1.Ref 监听者实例别名
    2.Transport socket进程的创建者
    3.protocol 实际的功能执行者
  • init:ranch_listener_sup进程创建成功的调用函数。
    主要做了下面的事情:
    1.创建子进程ranch_conns_sup,传入的参数有Ref,Transport,Protocol三个。这个进程的功能是管理所有的外部连接。
    2.创建子进程ranch_acceptors_sup,传入参数是Ref,Transport(可选两种:tcp,ssl).这个进程就是实际的对外端口监听者
    3.将该进程的{Ref,self()(自己的进程pid)}存入到ranch_server
    4.进程是以reset_for_one的方式启动这两个进程,则表明这两个进程是有依赖关系的。如果ranch_conns_sup挂了,则一定也会挂掉ranch_acceptors_sup,然后重新一次重启这两个进程
    5.实际中,ranch_acceptors_sup的启动是依赖ranch_conns_sup进程的,所以,必须ranch_conns_sup进程必须先启动

ranch_conns_sup模块

介绍

接受socket连接的进程,创建Protocol进程,并且监控这些进程的退出

功能函数介绍

  • start_link:这个进程必须的,因为该进程是以supervisor创建的子进程,必须有此函数。
    该进程不是gen_server的标准,而是采用原始的proc_lib:start_link创建的进程,传入的参数包括 父进程id,Ref,Transport,Protocol
  • init:通过start_link函数调用本模块的init函数。
    该函数做了一下的功能:
    1.设置process_flag(trap_exit,true),标识是可以接受link模块的退出信息
    2.设置{Ref,self()}到ranch_server保存数据
    3.从ranch_server和TransOpts中获取相应的初始化信息,并保存到loop循环的#state中
    4.proc_lib:init_ack(Parent, {ok, self()}) 这是是内部的proc_lib的实现,标识告诉父进程,进程创建成功了,并会返回给父进程一个{ok,Pid(当前创建进程的pid)}
  • loop:执行主要的逻辑
    1.{?MODULE,start_protocol, To, Socket}:socket收到连接,发送该进程,创建Protocol进程。在此进程创建的时候需要注意,实例进程创建进程的返回值一定要注意,{ok, Pid}表示自己对自己进程关闭负责,而{ok,SupPid,ProtocolPid}则表示,该进程是有一个conns_sup进程来管理这些启动的进程。此处ranch是提供了两种方法来处理。
    2.{?MODULE,active_connections,To,Tag}:获取该进程激活的连接数
    3.{remove_connection, Ref,Pid}:移除连接,此处注意,当前只是计数减一,并没有真正的关闭进程 4.{set_max_conns}:重新设置最大的连接数,如果sleeper里面有数据,则会将这些链接重新启用 5.{‘EXIT’,Parent,Reason}:此处Parent指的是ranch_listener_sup进程,如果父进程关闭,则无条件的关闭启动的所有连接进程
    6.{'EXIT',Pid,Reason}:link的进程关闭了,则清理pid.此处要注意,该进程link的pid不能设置process_flag(trap_exit,true),负责就不会收到此断开信息。如果有睡眠的连接进程等待,则激活这些连接进程。
    7.{system,From,Request}:调用系统的指令.gen_server的terminate,code_change函数都和此有关系。目前看到的支持3中大类,1.suspended 挂起 2.running 获取一些模块的信息 3.terminating 关闭进程
    8.{'$gen_call', {To, Tag}, Request(which_children,count_children)}:To:标识是From,即发起者的进程pid,Tag=erlang:monitor(process,Process)即发起者监听接受方的MRef信息,在收到信息后,取消monitor,并返回信息。并且此种请求时通过同步的方式请求。具体的调用方法是:gen_server:call(SupPid,which_children)的方式请求
  • start_protocol:此方法由socket进程发起,同步创建实例,执行loop的第一种情况
  • active_connections:同步请求活跃的连接数,和loop的第8中情况相似,也可以已第8种情况替代
  • handshake:此方法大调用于loop的第1中情况,将启动的实例进程pid和socket进行绑定。并且验证,启动实例。如果当前的连接数已满,则阻塞住socket进程。如果条件满足,则确认绑定成功。
  • terminate(#stat{shutdown=brutal_kill},Reason,):关闭此进程,此函数是有system触发的,system_terminate()函数调用terminate,直接结束。brutal_kill标识是直接kill掉。
  • terminate(#state{shutdown=integer()}):表示是非直接关闭,等待进程的自我了断。
  • kill_children:直接杀死进程,杀死前unlinke(Pid),避免收到进程杀死的信息
  • shutdown_children,wait_children:这两个函数时相互配合使用的,在exit(P,shutdown)成功时,会收到wait_children的{‘DOWN,,process,Pid,’}的信息,显示的知道有多少个进程被中断。
  • system_continue,system_terminate,system_code_change:这三个函数,是配合system的命令在sys模块默认调用的,目前来看,主要有3个功能,system_continue:用来获取一些进程的信息;system_terminate:用来系统中断进程;system_code_change:用来进程一些数据的更新处理
  • report_error:对错误写日志记录

ranch_acceptors_sup 连接接受监督者模块

介绍

设置监听socket,将此socket同时分发给ranch_accpetor 接受者,监听连接

函数分析

  • start_link:启动进程的入口,使用supervisor的模式启动进程
  • init:1.获取连接监督者,获取Transport的配置信息,更具配置的监听连接数,通过one_for_one的方式创建一定数量的监听连接进程,来监听外部连接
  • recevie 的机制
%% 通过设置receive after 0,优先处理{alarm,X}的消息。因为在超时时间为0的receive中,会立即触发一个超时,但是在此之前,
系统会尝试对邮箱进行模式匹配,所以此方法,可以对消息优先处理,可以用于清空邮箱中的所有消息。
1.优先匹配例子
priority_receive()->  
    receive
      {alarm,X}->
    after 0->
            receive
        Any->
         Any
      end
    end.
2.清空邮箱所有消息
flush(Logger) ->
	receive Msg ->
		ranch:log(warning,
			"Ranch acceptor received unexpected message: ~p~n",
			[Msg], Logger),
		flush(Logger)
	after 0 ->
		ok
	end.
  • 拓展:为什么在call一个进程时,timeout了,但是call 的进程也会执行完消息处理?
节选自gen.erl
do_call(Process, Label, Request, Timeout) ->
    try erlang:monitor(process, Process) of
	Mref ->
	    %% If the monitor/2 call failed to set up a connection to a
	    %% remote node, we don't want the '!' operator to attempt
	    %% to set up the connection again. (If the monitor/2 call
	    %% failed due to an expired timeout, '!' too would probably
	    %% have to wait for the timeout to expire.) Therefore,
	    %% use erlang:send/3 with the 'noconnect' option so that it
	    %% will fail immediately if there is no connection to the
	    %% remote node.

	    catch erlang:send(Process, {Label, {self(), Mref}, Request},
		  [noconnect]),
	    receive
		{Mref, Reply} ->
		    erlang:demonitor(Mref, [flush]),
		    {ok, Reply};
		{'DOWN', Mref, _, _, noconnection} ->
		    Node = get_node(Process),
		    exit({nodedown, Node});
		{'DOWN', Mref, _, _, Reason} ->
		    exit(Reason)
	    after Timeout ->
		    erlang:demonitor(Mref, [flush]),
		    exit(timeout)
	    end
    catch
	error:_ ->
	    %% Node (C/Java?) is not supporting the monitor.
	    %% The other possible case -- this node is not distributed
	    %% -- should have been handled earlier.
	    %% Do the best possible with monitor_node/2.
	    %% This code may hang indefinitely if the Process 
	    %% does not exist. It is only used for featureweak remote nodes.
	    Node = get_node(Process),
	    monitor_node(Node, true),
	    receive
		{nodedown, Node} -> 
		    monitor_node(Node, false),
		    exit({nodedown, Node})
	    after 0 -> 
		    Tag = make_ref(),
		    Process ! {Label, {self(), Tag}, Request},
		    wait_resp(Node, Tag, Timeout)
	    end
    end.

从上面代码中可以看到,是在本地进程monitor了被call的进程,并且erlang:send的方式发送信息,此时再receive一个阻塞等待结果返回,然后再TimeOut的时候,如果还没有返回,则返回给进程exit(timeout)的错误信息。所以,就会出现,虽然timeout了,被调的进程还是会处理完,而不能当作被调进程没有收到处理。
如何避免?:1,从源头避免,确保call的进程处理信息足够简单,不超时 2.采用cast,或 ! 替代处理
参考网址:Erlang中带超时的receive

ranch_acceptor模块

简介

此模块用来接收外部的socket连接。并通知ranch_conns_sup通知业务进程启动并且绑定到该socket进程

函数介绍

  • start_link:次数采用原始的spawn_link函数启动一个进程,返回{ok, Pid}

  • loop:1.通过Transport:accept阻塞进程,直到接收到一个连接进来,然后绑定该连接的socket先到connsSup上,然后调用ranch_conns_sup:start_protocol启动一个实例进程,进行绑定。
    {ok, CSocket}:标识连接成功,并且创建了一个连接的socket,进行启动和绑定操作
    {error,emfile}:大量的并发操作调用,导致操作系统的文件描述符数量被瞬间用完,抛出emfile.此处应为会同时创建多个连接进程,和多个外部的socket连接,则必然会出现此种情况。在晚上看到过,有人在使用了1024个连接,https之后,就会出现问题,导致连接不成功。贴上网址如下:http://erlang.org/pipermail/erlang-questions/2015-January/082446.html
    {error,closed}:表明listening socket 关闭了
    ?MODULE:loop(_):此方法是可以保证,在版本更新时,总是调用到最新的模块函数

  • flush:每一次循环是,都清空掉该进程接收到的其他非accepotr消息

  • 关于连接出现{error,emfile}的问题:
    1.此错误标识同时创建了多个链接进程,导致操作系统的文件描述符数量被瞬间用完导致。
    2.有两种I/O处理,一个异步和一个同步。异步I/O下,这个会比较容易实现,需要给予一定的过载保护,防止过分压榨底层系统的性能。
    3.两种方式:1、设置 ulimit -n 10480 2.修改 /etc/security/limit.conf文件的句柄数量

        limit.conf 
        #<domain>      <type>  <item>         <value>
        #
    
        #*               soft    core            0
        #*               hard    rss             10000
        #@student        hard    nproc           20
        #@faculty        soft    nproc           20
        #@faculty        hard    nproc           50
        #ftp             hard    nproc           0
        #@student        -       maxlogins       4
    
        * soft nproc 65535
        * hard nproc 65535
        * soft nofile 65535
        * hard nofile 65535
        # End of file
    

4.参考网址:
[erlang-questions] {error,emfile}
从EMFILE和ENFILE说起,fd limit的问题(一)
EMFILE,too many open files的解决方案

此处记录问题:

  • 1.如果设置cert密匙,客户端是如何发送的,并且服务器又是如何验证的
展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部