参见 :http://learnyousomeerlang.com/buckets-of-sockets
为了加深理解,自译如下,若理解有误或更好的建议,请帮忙指出, :)
Buckets of Sockets
目前为止,我们做了一些关于Erlang本身很有趣的事,但是却极少与外面的世界交互,最多也就是从某个地方读写文件。
尽管和自己的联系越多越有趣,但是,现在是时候走出舒适区,来一起谈谈剩下的世界了。
这一章会包含三个使用sockets部件:IO lists,UDP sockets 和TCP sockets. IO lists并不算严格意义的一个主题,IO lists只是
使发送字符串到其它sockets或其它Erlang drivers更加效率。
IO Lists
我在前面部分提到过:我们可以使用字符串(整数的列表)或二进制(使用二进制数据结构来保存数据)来处理文本, 使用”Hello World”或<<”Hello World”>>发送信息,会得到相类似的符号,类似的结果。
不同在于:你是如何组织信息:一个字符串就是一个整数的列表,每一个字母都映射到列表中的一个元素,如果你想在列表中间或结尾增加一个元素,你不得不遍历到你要增加的元素位置,这并不是你想要的,但是:
A = [a] B = [b|A] = [b,a] C = [c|B] = [c,b,a]
在上面这个例子中,A,B,C都是不需要重写(复制)的,C等价于[c,b,a],[c|B],或[c,|[b|[a]]],在最后,[c,|[b|[a]]],里面的[a]等价于A,[C|B]里面的B就是第二行的B.那么我们来看看下面这需要复制的有什么不同?
A = [a] B = A ++ [b] = [a] ++ [b] = [a|[b]] C = B ++ [c] = [a|[b]] ++ [c] = [a|[b|[c]]]
你看出来上面赋值时发生的重写(rewrite)了么?
当我们创建B时,必须要去rewrite A;当创建C时,必须要rewrite B(包括[a]),如果我们再在用类似的方法创建D,我们不得不rewrite C…
所以操作很长的字符串是非常没有效率的做法,同时还会创建大量的垃圾数据给Erlang VM处理.
但是使用二进制就不会这么糟糕:
A = <<"a">> B = <<A/binary, "b">> = <<"ab">> C = <<B/binary, "c">> = <<"abc">>
在上面这个情况下:binaries知道自己数据的长度,所以增加一个元素消耗是固定的,这比lists好多了,同时binaries非常紧密(compact,节省空间),由于这2个原因,我们以后会坚持使用binaries来处理文本(text).
当然也有一些缺点,binaries意味着处理数据只有这一种方式,但操作binaries(修改或分离)还是会有一些消耗,因此我们会在代码里面个别的使用string与binaries的转换。但非常不推荐频繁转换这2个类型。
在上面的情况里,IO lists是我们的救世主,但IO lists也是数据结构里面非常奇怪的一种类型,他们是字节,(整数0~255),binaries,或其它IO lists 组成的一个”列表“。这就意味着函数可以接受Io lists 也可以
接受如[$H, $e, [$l, <<"lo">>, " "], [[["W","o"], <<"rl">>]] | [<<"d">>]]的格式,当真的遇到这种情况时,Erlang VM 会把list 扁平化成<<”Hello World”>> .
什么样的函数可以处理IO lists? 可以查看
1. 相关模块 io, file; 2. TCP 和UDP sockets也可以处理; 3. 一些库函数也可,比如一些使用unicode 或者 正则表达式 re 的,都可以处理他们。
你可以使用下面语句来做试试:
IoList = <<"Hello World">>,
io:format("~s~n", [IoList]).
总的来说:使用binaries来替换string,可以避免数据结构改变时产生大量地动态创建垃圾。
TCP and UDP: Bro-tocols
UDP protocol 是我们要谈到的第一种socket,基于UDP协议,UDP是基于协议IP层,提供类似于端口少量抽象接口。UDP被认为是一个缺少维护状态的协议。从UDP端口得到的数据会被分割成从多小块,没有标记,没有会话标识,不能保证你收到的数据和发送过来的数据是一致的。
实际中,别人发一个packet给接收者,接收者有可能收不到这个packet,由于以上原因,人们倾向于在packets很小,丢失一小部分packets也没关系,且没有太多的复杂的数据交换的场景使用UDP(最典型就是视频下载啦)
与UDP相对的还有保证送达机制的TCP协议,TCP会负责处理那些丢弃的packets,重新发送它们,使用独立的会话来保证多对发送和接收者。
TCP为保证可靠的信息传递,不得不牺牲了效率,变得比UDP传输慢且数据冗余大;UDP快,便是不可靠;所以请根据你的具体场景选择使用哪一个。
无论如何,在Erlang使用UDP非常简单,我们只需要指定端口并设定一个socket,那么这个socket就可以收发数据;
For a bad analogy, this is like having a bunch of mailboxes on your house (each mailbox being a port) and receiving tiny slips of paper in each of them with small messages. They can have any content, from "I like how you look in these pants" down to "The slip is coming from inside the house!". When some messages are too large for a slip of paper, then many of them are dropped in the mailbox. It's your job to reassemble them in a way that makes sense, then drive up to some house, and drop slips after that as a reply. If the messages are purely informative ("hey there, your door is unlocked") or very tiny ("What are you wearing? -Ron"), it should be fine and you could use one mailbox for all of the queries. If they were to be complex, though, we might want to use one port per session, right? Ugh, no! Use TCP!
一个不怎么恰当的类比:这就像你的房子边上有一大堆的信箱(相当于端口),每个端口可以用于接受很多小容量信件,这些信件可以携带任何内容,比如:“我喜欢你看裤子的样子”,“这纸片是来自房子内部的!”… 如果一些消息太大,会被切割为多个信件放到信箱中,你的职责就是使用有意义的规则分配它们,并把它们放到对应的房子里面作为一个回应。如果一个信息非常有用(hey,你的门没锁)或者非常小(Ron,你今天穿了什么)时,你可以使用一个信箱来处理所有的信息,但是如果信息非常复杂,我们可能需要对每组会话都使用专门的一个端口,这样显然非常丑陋!这时你应该用TCP.
TCP协议是有状态的(stateful),基于连接(connection-based)的一种协议.在发出信息之前,你必须要先握手,这意味着:某人想发信件到对应的信箱时,必须要先打个招呼“hey,我是IP:94.25.12.37,可以聊会么?”然后你应答”当然,把你的消息用数字N打上一个标签,每发一句就递增这个数字N”.从这以后,只要你想和IP92.25.12.37互相交流的内容,都有可能把检查到对方没有收的内容,对些进行重新发送。
这种方式,我们就可以使用一个信箱(端口)来保持交流,这对于TCP来说是一件非常优雅的事,虽然会增加一些负担,便是保证了所有的信息都按顺序被合更的分配到对应的地方。
如果你对以上的分析不感兴趣,不要太失望,我们马上就会讨论怎么在Erlang里使用TCP和UDP scokets,这些都很简单。
UDP Sockets
以下有一些基本的UDP操作:
1. 建立socket(setting up a socket); 2. 发信息(sending mesasge); 3. 收信息(receiving message ); 4. 关闭连接(closeing a connection);
第一个操作,不管你要干什么,首先你要使用 gen_udp:open/1-2
打开一个socket,最简单的示例如下:
{ok, Socket} = gen_udp:open(PortNumber).
PortNumer取值范围1~65535:
%%0~1023被认为是系统端口,大多数情况下,你的操作系统会不让你监听系统端口,除非你给了相应的权限; %%1024~49151是注册端口,通常这些端口都没有被限制,可以自由使用;(有时不通常是因为 registered to well known services); %%49152~65535就是动态或私有(dynamic or private)端口,他们常用于短暂使用( ephemeral ports);
下面我们的实验使用的是一些安全未被使用过的端口,比如:8789.
但是在此这前要说明一点:如果你使用 gen_udp:open/2,这里的第二个参数是一个选项的列表,比如会指定我们要接收的数据是列表还是二进制;我们接收方式{active, true}还是{active, false};
还有一些选项指定连接网络类型是IPv4或IPv6;可不可以使用UDP的socekt进行广播({broadcast, true | false}), 缓冲区的大小等等;
还有非常多有用的选项可以使用,但是我们此次实际只用一个简单的连接来做,其它部分你要去学习TCP,UDP协议后再自己折腾吧。
所以我们在Eralng shell中找开一个socket:
1> {ok, Socket} = gen_udp:open(8789, [binary, {active,true}]). {ok,#Port<0.676>} 2> gen_udp:open(8789, [binary, {active,true}]). {error,eaddrinuse}
这第一个命令,使用接收二进制,{active,true}的方式打开了8789端口,你可以看到一个如#Port<0.676>的返回值,这代表着我们刚打开的端口,你可以类似使用Pid一样使用这个端口,你甚至可以把它们与进程link起来,便于sockect 崩溃时,对应进程可以处理;
第二个命令还想再次打开同一个8789端口,因为第一个命令打开的端口没有释放掉,返回一个错误{error,eaddiinuse}该地址已被使用了。
不管怎样,我们再开一个Erlang shell终端,使用一个不同的端口找开第二个UDP socket:
1> {ok, Socket} = gen_udp:open(8790). {ok,#Port<0.587>} 2> gen_udp:send(Socket, {127,0,0,1}, 8789, "hey there!"). ok
哈哈,看到一个新的函数:gen_udp:send/4 ,从名称上就可以看出是用来发信息,参数:
gen_udp:send(OwnSocket, RemoteAddress, RemotePort, Message).
RemoteAddress 可以是一个字符串,原子结构(包括”example.org”这样的域名),一个IPv4(4元素数字的元组)或IPv6(8元素数字的元组);
RemotePort就是对应收信的端口,
Message就是信息本身,可以是字符串,二进制,或一个IO list.
刚发出去的信息,对方收到了么,你可以在你的第一个shell里面刷新看下:
3> flush(). Shell got {udp,#Port<0.676>,{127,0,0,1},8790,<<"hey there!">>} ok
妙哉!二个shell就这样可以通讯啦… 打开socket的进程会收到一个格式如下的消息:
{udp, Socket, FromIp, FromPort, Message}.
这个消息包含了它从哪来,内容是什么,所以我们可以使用active 模块来收发信息。
那么什么是passive模式呢,为了说明passive 模式,我们需要关闭第一个shell里面的socket,然后再开一个新的socket端口:
4> gen_udp:close(Socket). ok 5> f(Socket). ok 6> {ok, Socket} = gen_udp:open(8789, [binary, {active,false}]). {ok,#Port<0.683>}
在上面,我们关闭了socket,解绑了Socket变量,然后再把Socket绑定为passive模式,然后再试试下面的命令:
7> gen_udp:recv(Socket, 0).
这时你的shell会被阻塞,recv/2是用来等待一个passive socket把信息发进来的函数,0表示我们希望接受消息的长度,有趣的是,这个长度对于gen_udp来说是被忽略的(没用);gen_tcp也有一个类似的函数,在那里面,这个参数才会生效。
不管怎么,如果我们不发消息给这个进程,recv/2就会永远等待不返回;
现在我们去第二个shell里面给第一个进程发一个新的消息看看:
3> gen_udp:send(Socket, {127,0,0,1}, 8789, "hey there!").
ok
第二个shell里面就打印出
{ok,{{127,0,0,1},8790,<<"hey there!">>}}
如果你不想一直阻塞在这里面:你可以使用
8> gen_udp:recv(Socket, 0, 2000).
{error,timeout}
以上几乎是大部分的UDP内容了,真的只有这么多,不骗你哦!
TCP Sockets
TCP sockets的大部分接口都和UDP sockets相似,但他们的工作原理是非常不一样的,其中最大的一点就是客户端和服务器是完全不同的2个东西,一个客户端行为如下:
而服务器则是下面这种模式:
huh?, 看上去很怪异么?客户端表现有点类似于gen_udp:你连接一个端口,收发信息,然后关闭socket;
对于服务器端,我们加入一个新的模式:listening。这是因为TCP是通过建立会话来工作的。
首先,我们要新开一个shell终端,使用 gen_tcp:listen(Port, Options),
监听:
1> {ok, ListenSocket} = gen_tcp:listen(8091, [{active,true}, binary]). {ok,#Port<0.661>}
这个监听的端口负责管理使用connection连接上来的请求,你可以看到我们使用了类似于gen_udp上面的参数,其实大部分的选项对于IP sockets都是相似的,TCP有一些特殊的选项:
a connection backlog ({backlog, N}
), keepalive sockets ({keepalive, true | false}
), packet packaging ({packet, N}
,N指每一个packet的头在解析时会被自动去掉……
一旦监听socket打开,任何进程都可以使用这个端口,进入到一个accepting状态,阻塞在这里面直到某客户端上来和它说点什么。
2> {ok, AcceptSocket} = gen_tcp:accept(ListenSocket, 2000). ** exception error: no match of right hand side value {error,timeout} 3> {ok, AcceptSocket} = gen_tcp:accept(ListenSocket). ** exception error: no match of right hand side value {error,closed}
哇,我们timeout后崩溃掉了,这个监听的socket被关闭掉了,那么我们得重新来过!这次把超时设置为2s(2000ms)
4> f(). ok 5> {ok, ListenSocket} = gen_tcp:listen(8091, [{active, true}, binary]). {ok,#Port<0.728>} 6> {ok, AcceptSocket} = gen_tcp:accept(ListenSocket).
这个进程已阻塞在等待交流。非常好!那么我们再开一个shell终端:
1> {ok, Socket} = gen_tcp:connect({127,0,0,1}, 8091, [binary, {active,true}]). {ok,#Port<0.596>}
这个和上面gen_udp的选项相同,在最后面加了一上Timeout选项,如果你不加,就会一直等待,回头看看第一个Shell里面,他返回的是{ok,SocketNumber}.从这以后,这个accept socket和客户端的socket就可以一对一地通讯了,类似于gen_udp.使用第二个Shell给第一个Shell发信息:
3> gen_tcp:send(Socket, "Hey there first shell!").
ok
你可以在第一个shell终端上看到:
7> flush(). Shell got {tcp,#Port<0.729>,<<"Hey there first shell!">>} ok
两边的sockets都可以用同样的方式发消息,也可以使用gen_tcp:close(Socket)来关闭socket.不过要注意:关闭一个accept socket只会关闭本身的socket;关闭一个listen socket就会关闭自身和与之建立连接的accept sockets,如果关闭后再去连接就会返回{error,closed}.
这就是Erlang中TCP sockets的大部分内容,你信么?
虽然以上是大部分内容,但是还是有很多要注意的,如果你已亲自体验了上面这些sockets,或许会发现里面有关于sockets的所属权问题。
By this, I mean that UDP sockets, TCP client sockets and TCP accept sockets can all have messages sent through them from any process in existence, but messages received can only be read by the process that started the socket:
我还想说明:UDP sockets ,TCP 客户端和TCP accept sockets都可以被任何进程用于发消息,但是收消息只能被启动的它的进程所收到:
这一点都不实用?这意味着我们必须要保持启动sockets的进程一直存在,可不可以搞得更加灵活一点呢?
1. Process A starts a socket 2. Process A sends a request 3. Process A spawns process B with a socket 4a. Gives ownership of the 4b. Process B handles the request socket to Process B 5a. Process A sends a request 5b. Process B Keeps handling the request 6a. Process A spawns process C 6b. ... with a socket ...
进程A负责管理(running a bunch of queries),但是每个新启动的进程会负责等待回应,处理对应的消息。A把任务分派给新的进程是非常明智!关键在于怎么把socket的所有权(ownership)也转移给新的进程:
gen_tcp,gen_udp都可以使用controlling_process(Socket,Pid)来转移sokcet的所有权。调用这个函数会告诉Erlang:”我不想再管这个socket了,把这socket给Pid来管理吧,我退出!”
从这以后,Pid就可以使用此socket来收发信息啦!That's it.
接下来,还讲了inet,什么的,
看到这里,总觉得原英文写得有趣生动,翻译过来,又生硬,又渣,完全没有勇气再糟蹋下去了。所以强烈推荐你还是看原文吧:http://learnyousomeerlang.com/buckets-of-sockets
Oh NO~~~~~~~~~~~~~
《哆啦A梦》里一直有个镜头:大雄去上学或者出发去各种地方,哆啦A梦站在家门口挥手微笑说:我等你的好消息。