1992 October 20
作者 / Cynosure (Dave Richards)
少量更新于 1994 Sept 16 由 Robocoder (Anthon Pang)
翻译 / Jjgod Jiang (2002 Jul 6-8, Nov 24)
在 MudOS 0.8.14 和 0.9.0 中的一个增强就是 Internet socket 功能被包含
进来了。MudOS 与 TMI 研究者一直希望使网络上 MUD 的更紧密地通过通信集
成在一起, Socket efun (或者 LPC socket) 依靠允许 LPC 开发者写作基于
Internet socket 的应用程序提供了第一级的 MUD 集成性。 例如,已经存在
用于 telnet、远程 MUD finger、远程 MUD tell、互联 MUD 邮件递送以及参
与 MUDWHO 系统的 LPC 物件。
之所以要写这么一份文档, 就是要把它作为一个指导你如何使用 LPC socket
进行基于网络的互联编程的指南。 它的定位将是那些有一定经验的 LPC 程序
员,他们已经理解 LPC 编程的大部分基础内容,但希望写一些基于网络的LPC
服务。
Socket 模式
-----------
一共有五种不同的通信模式或者叫做套接字(socket)模式:
MUD、STREAM、DATAGRAM、STREAM_BINARY,和 DATAGRAM_BINARY。这五种模式
的定义可以在 mudlib include 的 <socket.h> 中找到。
MUD 模式
--------
MUD 模式是一个面向连接的通信模式, 这种模式中 LPC 数据类型可以由网络
从一个 MUD 传送到另一个 MUD。例如,在 MUD 模式中你可以发送结构数据—
—如数组和映射——穿过网络到另一个也使用 MUD 模式 socket 的 MUD 中。
除 object 以外的所有的 LPC 数据类型都可以由 MUD 模式发送和接收。
jjgod 注:无法传送物件造成了穿越 mud 的功能, 比如一直比较热的分站漫
游功能受到一些阻碍,我们需要较为麻烦的获得一个物件身上所有
的变量(比如一个 USER_OB, 需要查找 /clone/user/user.c 以及
所有直接和间接继承——可以用 deep_inherit_list 得到——的
的文件中的变量),然后一个一个的发送和接收。 这样就造成兼容
性和可扩展性很成问题。
STREAM 模式
-----------
STREAM 模式也是一个面向连接的通信模式,和 MUD 模式不同的是,所有的数
据都是以字符串形式发送和接收的。所以,你可以通过 STREAM 模式从其他的
网络端(比如你自己写的一个 MUD 客户端)发送到 MUD 中。由于无法直接发送
和接收所有的 LPC 数据类型,STREAM 模式会显得不那么强大,但很多应用程
序,像 telnet,用不着发送整数、数组这样的数据, 而都是以各个方向的字
符流的形式浏览的。
MUD 模式 socket 其实就是使用特殊的代码以通过 STREAM 模式来实现发送和
接收 LPC 数据类型。因此,如果应用程序需要额外的数据提取,更适合用MUD
模式。 但 MUD 模式由于其固有的限制,将比 STREAM 模式更慢,也将使用更
多的内存。 注意,STREAM 模式是无法确保发送的字符串能够全部立即到达,
实际上它们是一份一份的送到后, 再将之重新组合在一起(每一份将按顺序到
达)。
DATAGRAM 模式
-------------
和 MUD 与 STREAM 模式不同,DATAGRAM 模式是无连接的。你不需要确立一个
连接就可以在 MUD 间传输数据了。因此,每份数据是采用一种叫“datagram”
的数据包发送到目的地的,每个这种数据包都自带寻址信息,能够自觉地从网
络的一端行走到另一端。
因为 DATAGRAM 模式没有一个确定的连接,所以发出的 datagram 包有可能在
网络中就这么丢失了,没有任何一个 MUD 能收到它。 例如 TMI 用 DATAGRAM
模式发送的一个到 Portals 的数据包如果在网络中丢失了, Portals 可能永
远收不到它,而且完全不知道曾经发送过这么一个数据包。 而且 TMI 也无法
得知这个数据包是否丢失了,因为就算丢失了也不会收到任何错误信息。
TCP 和 UDP
----------
在 MUD 模式和 STREAM 模式中,MUD 间将建立一个 TCP 连接,TCP 是一种能
够保证数据被正确发送的协议,如果它发现数据包丢失了,就会尝试重新发送
它,直到正确收到为止:它通过特定的算法来发送数据,这种算法使其能够估
算数据要花多长时间才能到达,如果过了那段时间还没收到回答,它就重新发
送一个数据包,直到收到确认信息为止。TCP 协议也确保了数据包能够按顺序
到达,而且也不会收到两个同样的数据包。(这是 TCP 协议的一点很肤浅的描
述,但也算简要地说明了为什么这种协议使得数据传输变为可信赖的。)
DATAGRAM socket 则不然。 UDP 是一种面向 datagram 的协议,它在 MUD 间
发送数据包时是完全和“连接”、“重新发送”等这些词沾不上边的。但是,
既然 DATAGRAM 模式是不可靠的,为什么有人会用到它呢?当然,TCP 显得更
好:它通过重新发送确保了数据能够正确到达、它考虑到了网络中所有可能出
现的恶劣情况……而实际上,不少应用程序却不在乎所有的数据是否正确到达
了另一端。你可能要问了,既然不在乎还发送它干什么?好吧好吧,这是个不
错的问题,但现在就回答它太早了, 你只要记住有些情况是得用到 DATAGRAM
模式的,我们会在后边详细的解释一下,别着急。
jjgod 注:例如 InterMud 这种松散集成的程序,就完全不用考虑数据包是否
正确发到了, 你的 MUD 有的时候甚至仅仅是毫无目的的在网上寻
找是否有能够互联的 MUD, 定时的 ping 也不期望别的 MUD 就一
定能收到——收不到就收不到呗,最多不过是少了一个与之互联的
MUD 而已,对自己没什么伤害。 相反,如果你与数百个 MUD 互联,
而和这些 MUD 之间传送信息都需要一次次的确认、 确认再确认,
网络负担就很可观了。
创建 Socket
-----------
好的,让我们从创建一个 MUD 模式 socket 开始, 我们可以写这样一个物件
来实现:
#include <socket.h>
void create()
{
int s;
s = socket_create(MUD, "close_callback");
if (s < 0)
{
write("socket_create: " + socket_error(s) + "\n");
return;
}
write("Created socket descriptor " + s + "\n");
socket_close(s);
}
void close_callback(int s)
{
write("socket " + s + " has closed\n");
}
让我们分解一下这个程序来看清楚一个 socket 是如何创建的。不过首先要说
的是,到我们能够用 socket 来发送数据还有一个漫长的路程要走,创建仅仅
是第一步呢。所以给点耐心吧,弄明白每个例子以后再开始做。
我们做的第一件事情就是 #include <socket.h>,所有的 socket 定义都在这
个 socket.h 里面。首先是定义,例如 MUD、STREAM 和 DATAGRAM,虽然每个
名字都对应着一个数字,但写得好的程序应该用宏定义来代替数字:因为某天
你可能要修改这些定义(假如有一天 MUD 和 STREAM 代表的数字调换了,就不
必在代码中一处一处的修改数字), 也是因为这样可以让人一看就明白你打算
做什么。
我们定义了一个整数变量 s, 很多 socket 应用程序中 s 都作为 socket 的
缩写出现。然后我们以两个参数调用 socket_create(),第一个参数是socket
的模式(我们上面讨论的那些),注意我们使用宏定义 MUD 来代表 MUD 模式。
例子中第二个参数叫做关闭回叫函数, 当这个连接被关闭时就会由 MudOS 呼
叫此函数。 回叫(callback)常常被用在 LPC socket efun 中,以通知物件重
要网络事件的发生。注意,我们还可以以 STREAM 或 DATAGRAM 为第一个参数
来创建 STREAM 或 DATAGRAM socket。
所有的 socket efun 都会返回一个退出状态或者返回一个特定的值。 这个值
代表了此函数完整的状态。我们规定负值代表错误或者警告,当返回一个错误
代码时,应用程序得决定如何回答它。 有时假如 MUD 管理员不修改本地的配
置文件或者 MudOS 的话,根本没有可能获得成功的返回代码, 那怎么办呢?
比如说上面的例子,如果返回了错误(s 小于 0),我们就使用socket_error()
这个 efun 来在屏幕上显示出一段错误原因的信息,这在调试时是很有用的,
但在实际运行的时候,我们可能不希望所有的用户都看到这些杂乱的信息,所
以那时还是换成 log_file() 来把错误记录下来,以备以后改正。
如果 socket_create() 成功了,它将返回一个大于等于 0 的整数,这个整数
代表了一个 socket、一个 socket 描述符(descriptor), 或者一个文件描述
符,这三个名字都源于 UNIX 术语。 如果 socket_create() 返回负值,就说
明有错误发生,没有创建出什么 socket。 但这并不是获得一个错误的理由,
实际上, 最常见的错误往往是因为指定错了 socket 模式造成的(如果你采用
socket.h 中的定义来指定,通常不那么容易出错,因为每个宏定义都很明显),
或者是 socket 溢出了。MUD 管理员可以通过配置 MudOS 能使用的 socket数
目来避免这个问题,默认这个数目是 16, 但你最好配置一下以适应自己 MUD
的需求,增加这个数字就可以使你能用到更多的 LPC socket 了。注意,每个
到达的 LPC socket 都将占用一个用于玩家登录和打开文件的 socket。 若你
需要同时保证大量的玩家和大量打开的 socket, 就得考虑增加这个最大值以
保证此进程能够打开足够多的文件描述符。
所有的 socket 物件都得小心不要丢失了 socket,socket 物件不像其他的 L
PC 物件,它们的数目是有限的。 所以,如果我们在上面的例子中成功的创建
了一个 socket,后面一定要记住关闭它。 我们知道,丢失资源的轨迹称为“
泄漏”,socket 泄漏发生在当一个物件创建了 socket,使用了一会儿却在不
再用到它的时候忘了关闭它,结果这个物件就一直占用着一个 socket, 别的
物件只能看着干着急。不过如果一个物件被析构了,它那里的所有LPC socket
都会被自动关闭的。另一个值得注意的是,每个 socket 都有它独一无二的 s
ocket 描述符(或者 socket 编号),因此如果某物件创建了一个 socket, 另
一个物件创建了第二个 socket, 这两个物件不可能收到相同的 socket 描述
符。我们可以利用这一点来实现一些功能,例如以 socket 描述符来作为一个
mapping 的索引(index)以记下每个打开的 socket 的信息。 但记住,如果一
个 socket 被关闭了,它就可以被 socket_create() 重新用到来作为新 soc-
ket 的描述符。
jjgod 注:现在的 ES2 mudlib 中这些头文件似乎都移到了 /include/net 中。
如 socket 模式定义在 <net/socket.h> 中,但 socket 错误的信
息定义在 <socket_err.h> 中。
客户端/服务器模型
-----------------
在继续介绍剩下的 socket efun 之前, 现在我们最好停下来复习一些基本的
网络概念。面向连接的通信常常是以客户端/服务器端模型构成的, 在这个模
型中,任何一个连接要么是客户端,要么就是服务器端。由客户端发起连接、
(向服务器端)请求一些服务。服务器端则是在等待客户端的连接请求,如果等
到了的话就提供相应的服务。例如 FTP 就是这么工作的。 用户通过连接到服
务器发出一个请求,服务器就会满足客户端的请求。服务器端和客户端不同之
处在于,它可能同时在为多个客户提供服务。
MUD 和 STREAM 就用到了客户端/服务器端模型, 客户端和服务器端采用些微
有点不同的方式来确立连接。后面我们还会讨论到使用 DATAGRAM 模式的点对
点(peer-to-peer)模型, 这种模型和客户端/服务器端模型也略微有点不同,
例如在确立连接时。
你也可以同时启用多个服务,每个服务都以一个“众所周知”或者说是“约定
俗成”的端口来区分。端口是从 1 到 65535 之间的一个整数,尽管如此,大
多数的 MUD 都采用 1025 到 65535 这个范围内的端口号,因为前 1023 个端
口都是为 telnet、ftp 这些已经成为标准的应用保留的。 要让客户端和服务
器端之间能够进行通讯, 服务器端得先创建一个 socket,将之帮定到一个约
定的端口上,然后监听连接的请求。另一方面,客户端也必须创建一个 sock-
et,然后连接到那个约定的端口上。也就是说客户端将连接到服务器端所绑定
和监听着的那个端口。这就是为什么称之为“众所周知”端口的原因了,——
客户端得先知道有这么个端口,才能去连接。约定的端口号往往定义在 Inte-
rnet RFC 文档中,以一个大家认可的标准形式存在。
我们将在下面讨论如何通过执行 socket efun 来建立一个客户端与服务器端
的连接。不过在客户端能够发起一个连接请求之前,服务器端还需要做一些准
备工作,所以,先让我们启动服务器。
绑定到一个端口
--------------
在服务器端用 socket_create() 创建了一个 socket 之后(而且当然返回值大
于等于 0),下一个合理的步骤是绑定到一个端口,这个工作是由 socket_bi-
nd() 完成的。让我们在上面的例子里添加一点代码试试。 首先,声明一个新
的整数来记录呼叫错误,也就是那个 socket_bind() 的返回值啦。
int error;
// 现在让我们添加一个到 socket_bind() 的呼叫:
error = socket_bind(s, 12345);
if (error != EESUCCESS)
{
write("socket_bind: " + socket_error(error) + "\n");
socket_close(s);
return;
}
这段内容要加在 socket_close(s) 之前。 那么,这有什么用呢?好吧,先看
第一个参数, 嗯,没什么奇怪的,就是我们从 socket_create() 那里得到的
socket 描述符。每个关于 socket 的呼叫都要用到这个描述符,以使得MudOS
明白我们想操作的是哪个 socket。 你还记得服务器端是经常同时运作超过一
个客户 socket 的吧?所以必须保证它们不被混淆。第二个参数很简单,就是
端口号,端口号必须为从 1024 到 65535 的整数,你看对不对? (其实 0 也
是合法的,我们会在稍后谈到这个。)
呼叫 socket_bind() 之后, 我们再检查一下返回值,看看是否发生了错误,
目前的情况下也就是和 EESUCCESS 比较一下,在绝大多数场合(socket_crea-
te() 除外) EESUCCESS 都代表这个 socket efun 成功执行了。和 socket_c-
reate() 类似,如果发生了错误,我们也使用 socket_error() 来将它以字符
串显示出来。那么输出错误以后我们就返回了,对不对?错了!上面我们提到
了泄漏这个词,如果我们就这么返回了,这个物件就会一直占用着这个不再被
用到的 socket 从而导致别人没法利用它。因此当我们确定以后不再用到这个
socket 时,千万记住关闭它。从 socket_create() 的呼叫开始,直到 sock-
et_close() 被呼叫,这个 socket 都是打开的。因而,要做个良好的 socket
使用者,记住在你的工作完成时关闭它。
socket_bind() 有个挺有点“名气”的返回值——EEADDRINUSE。 为什么这么
说呢?如果有人绑定了一个 socket 到 12345 端口, 然后另一个 socket 也
尝试向绑定到同一个端口上,第二个 socket 的绑定将会失败。这很好理解,
socket 绑定到一个端口之后,该端口就属于那个 socket 了, 其他试图掺和
进来的就会收到 EEADDRINUSE 这个错误信息。这是个很常见的错误。 正确的
解决方案是:1) 检查一下是否同一个服务被启动了两次。 若是这样,关掉一
个,因为一个就够了。 2) 是否有几个的开发者为不同的服务选择了同一个端
口。这可不行,要避免这种情况可以让一个“端口管理员”来分配端口,其他
人不得插手。遗憾的是,网络不是孤立的,端口的分配需要所有你希望与之通
信的 MUD 都同意才行。
jjgod 注:在提到 EEADDRINUSE 错误描述符时, 作者用了“notorious(声名
狼藉的)”这个词,但在下文中并没有贬义, 所以就按照作者的本
意将之译为“名气”便罢。
安全
----
在继续下一步之前,我们需要回答几个关于非法的 socket 描述符的问题。当
我们传入的值是一个错误的 socket 描述符时,会怎样呢?别担心,MudOS 会
马上捕获你的行为,并告诉你这是行不通的。举个例子,当你传入一个小于 0、
或大于等于系统可用的 socket 最大值的描述符,MudOS 会发现你犯了个错误
并返回 EEFDRANGE。若你打算做得更狡猾一点的话,可以尝试给出一个合法但
却不是目前在用的数值,MudOS 还是会发觉并返回 EBADF。好吧,让我们再狡
猾一点,传入一个别的物件正在用的 socket 描述符。会怎么样呢?这就到了
解释为 LPC socket 建立的两层安全系统的时候了。
第一层的安全使用主控(master)物件来验证哪些物件可以、哪些物件不能使用
socket。 这可能在一些 MUD 中比较有用,例如我们打算让一些开发者有权限
使用 socket,而另一些没有, 或者出现个别的开发者滥用网络使用权而被禁
止使用 socket 的情况(后面的例子中那样的开发者最好让他离开算了)。为了
实现上述的意图,MudOS 调用了 valid_socket() 这个函数,valid_socket()
返回 0 或者 1 以标示请求的 socket 操作是否被允许。 如果没有 valid_s-
ocket() 这么个函数存在,MudOS 就假定返回值是 0,这样所有的 LPC sock-
et 操作将被全部禁止。所以,大多数的 MUD 的 master.c 都会包括下面这个
valid_socket():
int valid_socket(object eff_user, string fun, mixed *info)
{
return 1;
}
第二层的安全更为严格些,它用于防止一个物件通过某些手段妨害其他的物件。
我们已经知道, 当一个 socket 创建时,呼叫 socket_create() 的那个物件
就等于拥有了这个 socket。每当一个 socket efun 被呼叫时,它都会比较一
下呼叫者是否那个拥有者。如果不同,这次呼叫 socket efun就被中断了。下
面有段很是可恶的代码:
int s;
for (s = 0; s < 100; s++)
socket_close(s);
它并不会成功的关闭 MUD 中所有的 socket,而只能关闭所有这个呼叫者自己
拥有的那些 socket, 因为所有其他的 socket 都由第二层安全措施保护着呢。
如果被第一和第二层安全措施中的任一个拒绝,将返回 EESECURITY 来告诉那
位呼叫者:socket efun 因为安全原因拒绝了他的请求。如果你在写代码的过
程中遇到了这样的错误,多数是因为你传入了一个错误的 socket 描述符,毕
竟这偶尔是会出现的。
监听连接
--------
当 socket 一创建出来,端口一被绑定,服务器就必须开始监听连接了。这是
由 socket_listen() 来完成的。和 socket_bind() 一样,一个参数是要监听
的 socket,第二个参数则是那个“监听回叫(listen callback)”函数。回忆
一下 socket_create() 中那个“关闭回叫”函数? socket_listen() 也指定
了一个当接到连接就会呼叫的函数,在这个函数中,服务器可以决定是接受这
个连接呢,还是关闭它。在大多数的服务器中,确实没有什么就这样关掉一个
连接的理由,因为这样通常过于武断,应该避免。若客户端和服务器端达成了
某些鉴定的协议(例如密码检查)时,服务器端好歹应该返回一段消息,告诉客
户端为何连接被关闭了。当然,这可能更像风格的问题,但你得知道,要对一
个连上了马上就被莫名其妙地断开了的客户端或服务器端进行调试,是非常困
难的。如果你非得这么做的话,就要确保你在日志文件中记录下了关闭连接的
理由,这样管理员或者开发者才方便诊断原因,解决问题。
下面的代码,让一个已经创建了并绑定到端口上的 socket 开始进行连接监听:
error = socket_listen(s, "listen_callback");
if (error != EESUCCESS)
{
write("socket_listen: " + socket_error(error) + "\n");
socket_close(s);
return;
}
这和上边的代码简直就是一样的么: 我们呼叫 socket_listen() 函数,检查
返回值看看是否成功了,若发生了错误则输出包括了指定错误描述的错误的信
息,然后,因为我们已经完成了工作,就关闭连接并返回。事实上,很多必需
的 socket 应用程序代码中都是按照这个模式写成的。
很明显,下一步就是讨论这个 listen_callback 函数,是吗? 按道理说没错,
但我们还是不这么做,而是换换空气,改为看点客户端的代码吧。理由很简单,
在客户端可以发起一个连接以前,讨论服务器端的问题太不现实了,因为这之
前服务器端并不会运行比上边更多的代码。因此,稍微离题一下,去讨论客户
端的问题是有必要的。
尊重作者 转载请注明出处52mud.com