用灵丹妙药编写SSDP目录

2020-08-25 13:59:21

我过去把我所有的空闲时间都花在随意设计玩具项目上。随着时间的推移,可能是在工业界呆了几年之后,我开始花太多时间思考如何编写可维护的代码,以至于我认为我开始失去了编程的乐趣:探索新想法和学习如何做我以前从未做过的事情。我想重新发现那种快乐,要做到这一点,我需要停止太多的完美主义。

我认为,在办公室环境中,截止日期会迫使我向前看,要求完成工作,但在我的个人生活中,缺乏这种压力意味着我可以花很长时间来设计和重新设计同一段代码,直到它变得完美为止(它从来都不是完美的)。

为了解决这个问题,我想试试写博客!如果我能让自己兴奋地与其他人分享我的代码,尽管它是不完美和未完成的,那么也许我就可以开始忘记过去几年来一直困扰我的瘫痪。

首先,我只想简单介绍一下我几个月前编写的一个小程序。我想了解SSDP是如何工作的,所以我实现了一个SSDP目录!对于那些没有意识到的人来说,SSDP是90年代开始的一个相当简单的协议,用于促进网络服务的发现。如今,从智能电视到色调灯,它也被各种设备使用。

SSDP的关键是所谓的组播寻址。实质上,服务将它们的存在广播到专门指定的多播地址,然后网络上的任何其他人都能够监听这些存在通知,以便跟踪新服务的出现和消失。

Defmodule SSDPDirectory.MulticastChannel使用GenServer别名__模块__别名SSDPDirectory。{Discovery,Presence}@MulticeGroup{239,255,255,250}@Multical_port 1900 def start_link(opts\\[])do GenServer.start_link(__MODULE__,:OK,OPTS)end@spec Broadcast(GenServer.name(),iodata):OK def Broadcast(channel\\Multicic。%{socket:port}}def init(:OK)DO UDP_OPTIONS=[:BINARY,ACTIVE:TRUE,Add_Membership:{@MULTICK_GROUP,{0,0,0}},MULTICK_IF:{0,0,0,0},MULTICK_LOOP:FALSE,reuseaddr:TRUE]{:OK,Socket}=:GEN_udp.open(@MULTICK_PORT,UDP_OPTIONS){:OK,%{Socket:Socket}}end def Handle_CAST(。STATE)DO:OK=:GEN_UDP.SEND(state.socket,@组播_组,@组播_端口,数据包){:NOREPLY,STATE}End Task.Supervisor.start_child(SSDPDirectory.DecodingSupervisor,HANDLE_INFO({:udp,_Socket,_IP,_PORT,DATA},STATE)do def fn->;With{:OK,PACKET,REST}<;-:erlang.decode_Packet(:HTTP_bin,Data,[]),{:OK,HANDLER}<;-PACKET_HANDLER(PACKET),{:OK,DECODED}<;-handler.decode(REST)do:OK=handler.handle(已解码)end){:无回复,STATE}end defp PACKET_HANDLER({:HTTP_REQUEST,";NOTIFY";,_target,_version}),DO:{:OK,Presence}defp Packet_Handler({:HTTP_Response,_Version,200,";OK&34;}),DO:{:OK,Discovery.Response}Defp Packet_Handler(_Packet),DO::ErrorEnd。

大多数神奇之处都发生在init/1函数中。通过打开UDP套接字并将其加入到协议的多播组中,我们的进程现在能够接收广播到该组的数据包。该接收逻辑位于同一文件内的HANDLE_INFO/2函数中。

当接收到数据包时,我们会派生另一个负责处理该数据包的进程。此进程在Task.Supervisor下运行,以便将该进程的崩溃与MulticastChannel隔离。同样有趣的是,我们可以使用erlang.decode_Packet/3来解码传入的数据包。这是一个内置函数,允许我们逐个片段地解码各种数据包格式。在本例中,我们使用它将数据包解析为HTTP数据包。这也是Elixir的Mint解码HTTP响应的相同方式!

根据解码的数据包类型,PACKET_HANDLER/1然后将该数据包的处理委托给另一个模块。要么我们已经收到HTTP NOTIFY请求,并且我们正在处理在线状态通知,要么我们已经收到了对发现请求的响应。

让我们来看一下Presence案例。如果您很好奇,这里有一个在线状态通知示例:

Defmodule SSDPDirectory.Presence确实需要记录器别名__module__alias SSDPDirectory.HTTP@type命令::Presence.Alive.t()|Presence.ByeBye.t()@spec decode(Binary):Error|{:OK,command}def decode(Data)do case HTTP。decode_headers(data,[])do{:OK,Headers,_rest}->;Process_Headers(Headers。无法解码通知请求:";<;>;check(Data)end):Error End End@spec Handle(Command):OK def Handle(%Presence.Alive{}=command)do Presence.Alive.Handle(Command)end def Handle(%Presence.ByeBye{}=command)do Presence.ByeBye.Handle(命令)end defp process_Headers(Header)do_process_headers(Headers,%Presence.ByeBye.Handle(Command)end defp process_Headers(Headers)do_Process_Headers(Headers,%Presence.ByeBye{}=command)。SSDP:Alive";,USN:usn,type:type}When not is_nil(Usn)and not is_nil(Type)->;{:OK,%Presence.Alive{usn:usn,type:type,location:Map.get(args,:location)}}%{command:";ssdp:再见";,usn:usn,type:type}When not is_nil(USN)and not is。{:OK,%Presence.Bye{usn:usn,type:type}}_->;:错误end defp do_process_headers([{";nts";,command}|rest],args)do args=Map.put(args,:command,command)do_process_headers(rest,args)end defp do_process_headers([{";nt";,type}|rest],args)do args=Map.put(args,:type,type)do_process_headers(rest,args)end defp do_process_headers([{";usn";,usn}|rest],args)do args=Map.put(args,:usn,usn)do_process_headers(rest,args)end defp do_process_headers([{";al&#。,location}|rest],args)do args=Map.put(args,:location,location)do_process_headers(rest,args)end defp do_process_headers([{";location";,location}|rest],args)do args=Map.put(args,:location,location)do_process_headers(rest,args)end defp do_process_headers([_|rest],args)do_process。

看起来这里有很多事情要做,但实际上很简单。从decode/1开始,我们继续解码来自MulticastChannel的数据包。这一次我们感兴趣的是头文件,因此我们对其进行解码,然后对其进行处理,以确定我们正在处理的是哪种命令。

处理步骤只涉及递归头列表,并在映射中累加相关的头。一旦我们这样做了,我们只需构造相应的命令!

最后,命令处理程序根据正在处理的命令类型将任务委托给第三个模块。例如,在使用SSDP:AIVE命令的情况下:

Defmodule SSDPDirectory.Presence.Alive确实需要记录器别名__module__alias SSDPDirectory。{Cache,Service}@Enforce_Key[:usn,:type]defstruct[:location]++@@Enforce_Key@type t::%Alive{}@规范句柄(Alive.t()):OK def Handle(%Alive{}=command)do_=Logger.debug(fn->;";处理SSDP。检查(命令)结束)服务=%Service{usn:Command.usn,类型:命令。类型:命令类型,位置:命令。位置}:确定=缓存。插入(服务)结束。

在这里,我们只使用命令中的参数构造服务,然后将其存储在缓存中:

Defmodule SSDPDirectory.Cache确实使用GenServer需要记录器别名__module__alias SSDPDirectory.Service def start_link(opts\\[])do GenServer.start_link(Cache,:OK,opts)end def content(cache\\Cache)do:ets.tab2list(Cache)|>;Enum.into(%{})end def insert(cache\\cache,%Service{}=service)do GenServer.call(cache,{:insert,service})end def delete(cache\\cache,%Service{}=service)do GenServer.call(cache,{:delete,service})end def flush(cache\\Cache)do GenServer.call(cache,:flush)end def init(:OK)do table=:ets.new(Cache,[:%{table:table}}end def HANDLE_CALL({:INSERT,%Service{usn:usn}=service},_from,data)When not is_nil(Usn)do:ets.insert(data.table,{usn,service})_=Logger.debug(fn->;";缓存服务:";<;>;检查(USN)结束){:REPLY,:OK,DATA}END DEF HANDLE_CALL({:DELETE,%Service{USN:USN}},_FROM,Data)When Not is_nil(USN)do:ets.delete(data.table,usn)_=Logger.debug(FN-&>;&34;END_ELECTED SERVICE:";<;&GG。检查(USN)结束){:REPLY,:OK,DATA}END定义HANDLE_CALL(:FLUSH,_FROM,DATA)DO:ETs.DELETE_ALL_OBJECTS(data.table)_=Logger.debug(FN->;";已刷新缓存";END){:REPLY,:OK,DATA}ENDND。

对于缓存,我使用启用了READ_CONTURRENT的ETS表。调用Cache.content/1将返回表中存储的所有服务,这或多或少会将我们带回起点!协议中还有一些其他模块用于处理不同的命令,并用于发起发现请求,但在大多数情况下,它并不是一个非常复杂的应用程序!

就像我在编写Elixir代码时经常遇到的那样,我真的很惊讶这种语言可以如此轻松地完成一些事情,比如加入多播组,然后异步解码发送到套接字的任何数据包。我想我花了大约3个小时来编写整个应用程序,这更多地归功于Erlang VM给了我真正强大的工具,而不是我做其他任何事情。即使考虑到我令人难以置信的冗长的编码风格-我把类型和结构抛来抛去就像是Haskell一样-您也可能在100行左右的简短代码中做同样的事情。

在某种程度上,我更多的是为自己而不是任何东西写东西,我不知道其他人是否对这种博客帖子感兴趣,所以如果你已经读到这里,谢谢你继续关注我:)