LPC 语言基础教程:8.3 socket 介绍及游戏开发实战

本文节选自《LPC语言基础教程:从零学习游戏开发》,版权归@mudren,欢迎转载,但必须注明来源(mud.ren)。

Socket 编程面向的是中高级 LPC 开发者,通过socket接口可以实现tcp/udp协议传输数据,利用这个我们可以在MUD中实现所有相关的应用层协议,包括http/ftp/telnet/ssh等,具体游戏中可以轻易的实现 MUD 互联、FTP 文件传输、读取网页等能力,让你在MUD中玩出各种花样。

Socket 介绍

LPC 中有五种 socket 模式:MUD, STREAM, DATAGRAM, STREAM_BINARY 和 DATAGRAM_BINARY。可以从驱动提供的 头文件查看定义:

#ifndef SOCKET_H
#define SOCKET_H

#define MUD                 0   // for sending LPC data types using TCP protocol.
#define STREAM              1   // for sending raw data using TCP protocol.
#define DATAGRAM            2   // for using UDP protocol.
#define STREAM_BINARY       3
#define DATAGRAM_BINARY     4

#endif          /* SOCKET_H */

MUD 模式

MUD 模式是一种面向连接(TCP)的通信模式,在 MUD 模式下,可以把除对象以外的所有 LPC 数据类型从一个 MUD 发送到另一个 MUD。

STEAM 数据流模式

STEAM 模式也是一种面向连接(TCP)的通信模式,和 MUD 模式不同的是所有数据以字符串的形式收发。STEAM 模式没有 MUD 模式强大,但是比 MUD 模式更快,也更节省资源占用。

DATAGRAM 数据报模式

DATAGRAM 模式是无连接的(UDP)通信模式,所有数据都是以数据报的消息发送给目标MUD。

STREAM_BINARY 和 DATAGRAM_BINARY 二种模式下数据以二进制(buffer数据类型)的形式收发,在MUD中如果是不同编码的MUD通信,使用这种模式做编码转换比较方便。

LPC中有大量和 socket 操作相关的外部函数,包括:socket_accept、socket_acquire、socket_address、socket_bind、socket_close、 socket_connect、socket_create、socket_error、socket_listen、socket_release、socket_status、 socket_write。另外还有一个 apply 方法:valid_socket。因为函数比较多,这里不一一列出使用说明,请从 mud.wiki 查看。

在 TCP 通信模式下,通信 MUD 双方是以服务器/客户端的模式工作,具体流程如下:

  1. 服务端使用 socket_create() 创建 socket 连接,成功返回 socket 连接描述符 fd,失败返回错误 err
  2. 服务端使用 socket_bind() 绑定 socket 连接 fd 到指定的端口,成功返回 EESUCCESS,失败返回错误 err
  3. 服务端使用 socket_listen() 监听已绑定端口的 socket 连接,成功返回 EESUCCESS,失败返回错误 err
  4. 客户端使用 socket_create() 创建 socket 连接,成功返回 socket 连接描述符 fd,失败返回错误 err
  5. 客户端使用 socket_connect() 连接服务器,成功返回 EESUCCESS,失败返回错误 err
  6. 服务端使用 socket_accept() 接受客户端的连接请求,成功返回和客户端通信的 socket 连接描述符 fd,失败返回错误 err
  7. 连接建立后服务端和客户端都可以在回调函数中使用 socket_write() 传送数据。

在 UDP 通信模式下,没有以上流程的第3、5、6步,客户端直接使用 socket_write() 发送数据到服务端,具体流程如下:

  1. 服务端使用 socket_create() 创建 socket 连接,成功返回 socket 连接描述符 fd,失败返回错误 err
  2. 服务端使用 socket_bind() 绑定 socket 连接 fd 到指定的端口,成功返回 EESUCCESS,失败返回错误 err
  3. 客户端使用 socket_create() 创建 socket 连接,成功返回 socket 连接描述符 fd,失败返回错误 err
  4. 客户端使用 socket_write() 传送数据到服务端。

需要说明的是:

  1. 因为 fd 数量有限,如果服务器绑定端口或监听连接失败,最好使用 socket_close() 释放这个 socket 连接;
  2. 因为TCP模式客户端和服务端建立了连接,可以互相通信,客户端不需要使用 socket_bind() 绑定端口;而UDP模式客户端不需要其他客户端主动连接它,所以也可以不使用 socket_bind() 绑定端口,但是如果需要能被动接受其它服务端或客户端发送信息,需要使用 socket_bind() 绑定端口,让自己也成为服务端;
  3. TCP模式建立连接后外网服务端可以和内网的客户端相互通信,而UDP模式外网服务端可以在内网客户端主动发送消息后根据接收消息的地址向内网客户端发送消息;
  4. 可以使用 socket_address() 获取服务端 fd 的远程连接地址;
  5. 可以使用 socket_status() 获取 socket 连接描述符的状态;
  6. 可以使用 socket_error() 获取错误 err 的详细描述;
  7. socket 通信安全校验由 master 对象中的 valid_socket 负责。

Socket 函数

先看看以上流程使用到的函数使用语法:

名称

socket_create() - 创建一个 socket

语法

int socket_create( int mode, string read_callback );
int socket_create( int mode, string read_callback, string close_callback );

描述

socket_create() 创建一个 socket。参数 `mode` 决定要创建哪种模式的 socket。目前支持的 socket 模式为:

MUD         使用 TCP 协议传输 LPC 类型数据
STREAM      使用 TCP 协议传输原始数据
DATAGRAM    使用 UDP 协议
------------------------------------------------
#define MUD                 0 // for sending LPC data types using TCP protocol.
#define STREAM              1 // for sending raw data using TCP protocol.
#define DATAGRAM            2 // for using UDP protocol.
#define STREAM_BINARY       3
#define DATAGRAM_BINARY     4
------------------------------------------------

参数 `read_callback` 是 socket 接收数据后驱动程序调用的函数名称,此函数原型应该是以下格式:

void read_callback(int fd, mixed message, void|string addr);

其中参数 `fd` 是接收到数据的 socket 连接;参数 `message` 是接收到的数据,在非二进制模式下返回值是 utf-8 格式的字符串,在二进制模式下返回值是 buffer;在UDP模式下,第三个参数 `addr` 为客户端IP地址。

参数 `close_callback` 是 socket 意外关闭时驱动程序调用的函数名称(如不是通过 socket_close() 关闭)。此函数原型应该是以下格式:

void close_callback(int fd);

其中参数 `fd` 是被关闭的 socket 连接。注意:close_callback() 不能在 DATAGRAM 模式下使用。

返回值

成功时返回一个非负连接描述符,失败时返回以下意义的负值:

错误

EEMODENOTSUPP  不支持的 Socket 模式
EESOCKET       创建 socket 时的问题
EESETSOCKOPT   设置 socket 选项(setsockopt)时的问题
EENONBLOCK     设置非阻塞模式(non-blocking mode)时的问题
EENOSOCKS      没有空余的 efun sockets
EESECURITY     试图违反安全性

以上错误定义在驱动程序源文件的 `socket_err.h` 头文件中,具体文件内容如下:
-----------------------------------------
#ifndef _SOCKET_ERRORS_H
#define _SOCKET_ERRORS_H

#define EESUCCESS 1       /* Call was successful */
#define EESOCKET -1       /* Problem creating socket */
#define EESETSOCKOPT -2   /* Problem with setsockopt */
#define EENONBLOCK -3     /* Problem setting non-blocking mode */
#define EENOSOCKS -4      /* UNUSED */
#define EEFDRANGE -5      /* Descriptor out of range */
#define EEBADF -6         /* Descriptor is invalid */
#define EESECURITY -7     /* Security violation attempted */
#define EEISBOUND -8      /* Socket is already bound */
#define EEADDRINUSE -9    /* Address already in use */
#define EEBIND -10        /* Problem with bind */
#define EEGETSOCKNAME -11 /* Problem with getsockname */
#define EEMODENOTSUPP -12 /* Socket mode not supported */
#define EENOADDR -13      /* Socket not bound to an address */
#define EEISCONN -14      /* Socket is already connected */
#define EELISTEN -15      /* Problem with listen */
#define EENOTLISTN -16    /* Socket not listening */
#define EEWOULDBLOCK -17  /* Operation would block */
#define EEINTR -18        /* Interrupted system call */
#define EEACCEPT -19      /* Problem with accept */
#define EEISLISTEN -20    /* Socket is listening */
#define EEBADADDR -21     /* Problem with address format */
#define EEALREADY -22     /* Operation already in progress */
#define EECONNREFUSED -23 /* Connection refused */
#define EECONNECT -24     /* Problem with connect */
#define EENOTCONN -25     /* Socket not connected */
#define EETYPENOTSUPP -26 /* Object type not supported */
#define EESENDTO -27      /* Problem with sendto */
#define EESEND -28        /* Problem with send */
#define EECALLBACK -29    /* Wait for callback */
#define EESOCKRLSD -30    /* Socket already released */
#define EESOCKNOTRLSD -31 /* Socket not released */
#define EEBADDATA -32     /* sending data with too many nested levels */
#define ERROR_STRINGS 33  /* sizeof (error_strings) */

#endif /* SOCKET_ERRORS_H */
-----------------------------------------

名称

socket_bind() - 绑定 IP 和端口到 socket 连接

语法

int socket_bind( int s, int port );

描述

socket_bind() 为未名称的 socket 指定名称, 当通过 socket_create() 创建一个 socket 连接后,它存在于命名空间中但没有指定名称(译者注:简单的说就是有 IP 没端口),socket_bind() 会请求把端口 `port` 分配给 socket `s`。

返回值

成功时返回 EESUCCESS,失败(错误)时返回代表以下意义的负值:

错误

EEFDRANGE      连接描述符(Descriptor)超出范围
EEBADF         无效的连接描述符
EESECURITY     试图违反安全性
EEISBOUND      Socket 已经绑定(被命名)
EEADDRINUSE    地址已经被占用
EEBIND         绑定(命名)出问题
EEGETSOCKNAME  获取 socket 名称(getsockname)出问题

注:所有错误名称定义在驱动程序源文件的 `socket_err.h` 头文件中。

名称

socket_listen() - 监听一个 socket 连接

语法

int socket_listen( int s, string listen_callback );

描述

为了接收连接,需要先使用 socket_create() 创建 socket,并使用 socket_listen() 进入监听模式,最后使用 socket_accept() 接收连接。socket_listen() 仅仅在 STEAM 或 MUD 模式有效。

参数 `listen_callback` 是监听中的 socket 收到连接请求时驱动程序调用的函数名称,函数原型格式如下:

void listen_callback(int fd)

其中参数 `fd` 是正在监听中的 socket 连接。

返回值

成功时返回 EESUCCESS,失败(错误)时返回代表以下意义的负值:

错误

EEFDRANGE      连接描述符(Descriptor)超出范围
EEBADF         无效的连接描述符
EESECURITY     试图违反安全性
EEMODENOTSUPP  不支持的 Socket 模式
EENOADDR       Socket 没有绑定地址
EEISCONN       Socket 已连接
EELISTEN       监听有问题

名称

socket_connect() - 启动一个 socket 连接

语法

int socket_connect( int s, string address, string read_callback, string write_callback );

描述

参数 `s` 是一个 socket 连接,`s` 必须是 STREAM 模式 或 MUD 模式. 参数 `address` 是将要连接的 socket 地址,地址格式类似: "127.0.0.1 23"。

参数 `read_callback` 是 socket 获取数据时驱动程序调用的函数名称,函数原型应该是以下格式:

void read_callback(int fd, mixed message)

其中参数 `fd` 是接受数据的 socket 连接,参数 `message` 是接收到的数据。

参数 `write_callback` 是 socket 准备写入数据时驱动程序调用的函数名称,函数原型应该是以下格式:

void write_callback(int fd)

其中参数 `fd` 是将被写入数据的 socket 连接。

返回值

成功时返回 EESUCCESS ,失败(出错)时返回代表以下意义的负值:

错误

EEFDRANGE      连接描述符(Descriptor)超出范围
EEBADF         无效的连接描述符
EESECURITY     试图违反安全性
EEMODENOTSUPP  不支持的 Socket 模式
EEISLISTEN     Socket 正在监听中
EEISCONN       Socket 已连接
EEBADADDR      地址格式有问题
EEINTR         中断的系统调用
EEADDRINUSE    地址已经被占用
EEALREADY      操作已在进行中
EECONNREFUSED  连接被拒绝
EECONNECT      连接有问题

名称

socket_accept() - 在一个 socket 上接受连接

语法

int socket_accept( int s, string read_callback, string write_callback );

描述

参数 `s` 是使用 socket_create() 创建的已调用 socket_bind() 绑定地址并通过 socket_listen() 进入监听状态的 socket。 socket_accept() 从待处理的连接队列中提取第一个连接创建一个和 `s` 有相同属性的新的 socket 并分配一个新的文件描述符,如果队列中不存在待处理的连接,socket_accept() 返回一个下面描述的错误。已接受的 socket 用来从已连接到它的 socket 中读数据或向其中写数据,它不能用来接受别的连接,原始的socket `s` 会保持打开状态以接受新的连接。

参数 `read_callback` 是当新的 socket (不是正在接收中的 socket)收到数据时驱动程序调用的函数名称,这个函数的原型应该是以下格式:

void read_callback(int fd);

其中参数 `fd` 是已准备好接收数据的 socket。

参数 `write_callback` 是当新的 socket (不是正在接收中的 socket) 准备好写入数据时驱动程序调用的函数名称,这个函数的原型应该是以下格式:

void write_callback(int fd);

其中参数 `fd` 是已准备好被写入数据的 socket。

注意:当新的socket 异常关闭时,正在接收中的 socket (不是新的 socket)中的 close_callback 方法会被调用,和调用 socket_close() 的结果不同,close_callback 函数原型应该是以下格式:

void close_callback(int fd)

其中参数 `fd` 是被关闭的 socket 连接。

返回值

socket_accept() 在成功时为接受的 socket 返回一个非负描述符,失败时返回一个错误值,可以使用 socket_error() 外部函数获取错误值的文字描述。

错误

EEFDRANGE      连接描述符(Descriptor)超出范围
EEBADF         无效的连接描述符
EESECURITY     试图违反安全性
EEMODENOTSUPP  不支持的 Socket 模式
EENOTLISTN     Socket 没有开启监听
EEWOULDBLOCK   操作会阻塞
EEINTR         中断的系统调用
EEACCEPT       接收出问题
EENOSOCKS      没有空余的 efun sockets

名称

socket_write() - 从 socket 发送信息

语法

int socket_write( int s, mixed message );
int socket_write( int s, mixed message, string address );

描述

socket_write() 在 socket `s` 上发送消息 `message`,如果 socket `s` 是 STREAM 或 MUD 模式,socket 必须已经连接且不能指定参数 `address`,如果 socket `s` 是 DATAGRAM 模式, 地址参数 `address` 必须指定,地址格式类似: "127.0.0.1 23"。

返回值

成功时返回 EESUCCESS,失败(错误)时返回代表以下意义的负值:

错误

EEFDRANGE      连接描述符(Descriptor)超出范围
EEBADF         无效的连接描述符
EESECURITY     试图违反安全性
EENOADDR       Socket 没有绑定地址
EEBADADDR      地址格式有问题
EENOTCONN      Socket 没有连接
EEALREADY      操作已在进行中
EETYPENOTSUPP  不支持的对象类型
EEBADDATA      发送嵌套级别过多的数据
EESENDTO       sendto 有问题
EEMODENOTSUPP  不支持的 Socket 模式
EEWOULDBLOCK   操作会阻塞
EESEND         send 有问题
EECALLBACK     等待回调中

名称

socket_error() - 返回 socket 错误的文字描述

语法

string socket_error( int error );

描述

socket_error() 返回 socket 错误 `error` 的文字描述

返回值

报错时返回错误描述,否则返回 "socket_error: invalid error number"。

名称

socket_close() - 关闭一个 socket

语法

int socket_close( int s );

描述

socket_close() 关闭 socket `s`,这将释放一个 socket 位置以供使用。

返回值

成功返回 EESUCCESS ,失败返回代表以下错误的负值:

错误

EEFDRANGE      连接描述符(Descriptor)超出范围
EEBADF         无效的连接描述符
EESECURITY     试图违反安全性

Socket 基础示例

我们先上示例,在 /cmds/test/ 目录新建 8.3.1.c,代码如下:

/**
 * sockets 相关测试,这里是服务端
 */
void close_callback(int fd);
void write_callback(int fd);
void listen_callback(int fd);

nosave int S;

void socket_init(int mode)
{
    int s, err, port = 6000;

    // 创建一个 efun socket 连接
    s = socket_create(mode, "read_callback", "close_callback");
    if (s < 0)
    {
        debug("【8.3.1】socket_create error : " + socket_error(s));
    }
    else
    {
        debug("【8.3.1】socket_create fd = " + s);
        // 绑定端口到 socket 连接
        err = socket_bind(s, port);
        if (err < 0)
        {
            debug("【8.3.1】socket_bind error : " + socket_error(err));
            socket_close(s);
        }
        else
        {
            debug("【8.3.1】socket_bind SUCCESS!");
            if (mode != 2 && mode != 4)
            {
                // 监听一个 socket 连接
                err = socket_listen(s, "listen_callback");
                if (err < 0)
                {
                    debug("【8.3.1】socket_listen error : " + socket_error(err));
                    socket_close(s);
                }
                else
                {
#ifdef FLUFFOS
                    debug("【8.3.1】socket_listen ON " + socket_address(s, 1));
#else
                    debug("【8.3.1】socket_listen ON " + socket_address(s));
#endif
                }
            }
            else
            {
#ifdef FLUFFOS
                debug("【8.3.1】UDP Socket 服务已启动 : " + socket_address(s, 1));
#else
                debug("【8.3.1】UDP Socket 服务已启动 : " + socket_address(s));
#endif
            }
        }
    }
}

void create()
{
    socket_init(0);
}

int main(object me, string arg)
{
    int err;
    if (arg == "udp")
    {
        socket_init(2);
    }
    else if(arg == "release")
    {
        err = socket_release(S, load_object(__DIR__"8.3.5"), "release_callback");
        if (err < 0)
        {
            debug("【8.3.1】socket_release error : " + socket_error(err));
        }
        else
        {
            debug("【8.3.1】socket_release SUCCESS!");
        }
    }
    else if (arg)
    {

        err = socket_write(S, arg);
        if (err < 0)
        {
            debug("【8.3.1】socket_write error : " + socket_error(err));
        }
        else
        {
            debug("【系统】消息已发送!");
        }
    }

    return 1;
}

void listen_callback(int fd)
{
    int err;
    debug("【8.3.1】listen_callback fd : " + fd);
    // 在一个 socket 上接受连接
    S = socket_accept(fd, "read_callback2", "write_callback");
    if (S < 0)
    {
        debug("【8.3.1】socket_accept error : " + socket_error(S));
    }
    else
    {
        debug("【8.3.1】socket_accept fd = " + S);
        err = socket_write(S, "欢迎连接到服务器 mud.ren ^_^");
        if (err < 0)
        {
            debug("【8.3.1】socket_write error : " + socket_error(err));
        }
    }
}

// udp模式使用
void read_callback(int fd, mixed message, string addr)
{
    debug("【8.3.1】read_callback fd : " + fd);
    debug("【8.3.1】read_callback from : " + addr);
    shout("【8.3.1】read_callback : " + message + "\n");
    // 发送消息给客户端,需要udp客户端绑定端口
    if (message == "hi")
    {
        socket_write(fd, "你好,客户端!", addr);
    }
}

// tcp模式使用
varargs void read_callback2(int fd, mixed message)
{
    debug("【8.3.1】read_callback2 fd : " + fd);
    shout("【8.3.1】read_callback2 : " + message + "\n");
    // 发送消息给客户端
    if (message == "hi")
    {
        socket_write(fd, "你好呀。");
    }
}

void write_callback(int fd)
{
    debug("【8.3.1】write_callback fd : " + fd);
}

void close_callback(int fd)
{
    debug("【8.3.1】close_callback fd: " + fd);
    socket_close(fd);
}

以上代码为服务端,我们执行指令8.3.1默认创建一个TCP模式的 socket,绑定端口为 6000。如果运行 8.3.1 udp 会创建一个UDP模式的 socket。

再新建文件 8.3.2.c 代码如下:

/**
 * sockets MUD 模式客户端测试
 */
nosave int s;

void socket_init(int mode)
{
    int err;
    string addr = "127.0.0.1 6000";
    // 创建一个 efun socket 连接
    s = socket_create(mode, "read_callback", "close_callback");
    if (s < 0)
    {
        debug("【8.3.2】socket_create error : " + socket_error(s));
    }
    else
    {
        debug("【8.3.2】socket_create fd = " + s);
        if (mode != 2 && mode != 4)
        {
            // 启动一个 socket 连接
            err = socket_connect(s, addr, "read_callback2", "write_callback");
            if (err < 0)
            {
                debug("【8.3.2】socket_connect error : " + socket_error(err));
                socket_close(s);
            }
            else
            {
                debug("【8.3.2】socket_connect SUCCESS!");
            }
        }
    }
}

void create()
{
    socket_init(0);
}

int main(object me, string arg)
{
    int err;
    if(arg == "release")
    {
        err = socket_release(s, load_object(__DIR__"8.3.5"), "release_callback");
        if (err < 0)
        {
            debug("【8.3.2】socket_release error : " + socket_error(err));
        }
        else
        {
            debug("【8.3.2】socket_release SUCCESS!");
        }
        return 1;
    }
    err = socket_write(s, arg);
    if (err < 0)
    {
        debug("【8.3.2】socket_write error : " + socket_error(err));
    }
    else
    {
        debug("【系统】消息已发送!");
    }
    return 1;
}

void write_callback(int fd)
{
    int err;
    debug("【8.3.2】write_callback fd : " + fd);
    err = socket_write(fd, "你好,这是来自客户端的问候^_^");
    if (err < 0)
    {
        debug("【8.3.2】socket_write error : " + socket_error(err));
    }
}

void read_callback(int fd, mixed message)
{
    debug("【8.3.2】read_callback fd : " + fd);
    shout("【8.3.2】read_callback : " + message + "\n");
}

void read_callback2(int fd, mixed message)
{
    debug("【8.3.2】read_callback2 fd : " + fd);
    shout("【8.3.2】read_callback2 : " + message + "\n");
}

void close_callback(int fd)
{
    debug("【8.3.2】close_callback fd : " + fd);
    socket_close(fd);
}

直接运行 8.3.2 会建立一个 socket 链接并连接到8.3.1服务端,如果输入8.3.2 内容,服务端会正常收到。

再建一个文件 8.3.3.c,代码如下:

/**
 * sockets DATAGRAM 模式客户端测试
 */
nosave int s;

void create()
{
    int mode = 2;
    // 创建一个 efun socket 连接
    s = socket_create(mode, "read_callback");
    // 如果不绑定端口,将使用随机端口连接服务器,但无法收到服务端返回的消息
    socket_bind(s, 6001);
    if (s < 0)
    {
        debug("【8.3.3】socket_create error : " + socket_error(s));
    }
    else
    {
        debug("【8.3.3】socket_create fd = " + s);
    }
}

int main(object me, string arg)
{
    int err;
    // UDP 发送消息到服务器
    if (!arg)
    {
        debug("请输入要发送的内容~~~");
        return 1;
    }

    err = socket_write(s, arg, "127.0.0.1 6000");
    if (err < 0)
    {
        debug("【8.3.3】socket_write error : " + socket_error(err));
        socket_close(s);
    }
    else
    {
        debug("【系统】消息已发送!");
    }
    return 1;
}

void read_callback(int fd, mixed message, string addr)
{
    debug("【8.3.3】read_callback fd : " + fd);
    debug("【8.3.3】read_callback from : " + addr);
    shout("【8.3.3】read_callback : " + message + "\n");
}

这里我们实现了一个UDP模式的客户端,在服务端运行 8.3.1 udp启动 udp 服务端,然后运行 8.3.3 hi试试。

下面,我们再使用 socket_status 查看所有 socket 连接信息,新建 8.3.4.c,代码如下:

// 显示 socket_status
int main(object me, string arg)
{
#ifdef FLUFFOS
    if (arg)
    {
        print_r(socket_status(atoi(arg)));
    }
    else
    {
        print_r(socket_status());
    }
#endif
    return 1;
}

直接输入 8.3.4 看看吧。

我们再补充 2 个 socket 函数的语法:

名称

socket_release() - 释放 socket 所有权给其它对象

语法

int socket_release( int socket, object ob, string release_callback );

描述

socket_release() 用来改变一个 socket 的所有权和控制权到其它对象。这在接受连接设置然后转换已接受的 socket 连接到其它对象以做后续处理的守护进程对象中很有用。

socket 所有权的转移涉及当前所属对象和转移目标对象之间的一次握手,当 socket_release() 调用时启动握手。socket_release() 执行必要的安全性和完整性检查后调用对象 `ob` 中的 `release_callback` 方法,这个方法用来通知准备接收 `socket` 所有权的对象 `ob`,对象 `ob` 有责任在 `release_callback` 方法中调用 socket_acquire() 外部函数。如果 socket_acquire() 被调用,握手完成,`socket` 的所有权成功的移交给对象 `ob`。对象 `ob` 也可以不调用 socket_acquire() 以拒绝接收 `socket` 的所有权,在这种情况下 `socket` 的所有权不会改变,而且 `socket` 的当前所有者必须决定如何响应拒绝。

如果 `socket` 的所有者转移成功,socket_release() 返回 EESUCCESS;如果对象 `ob` 拒绝授受,socket_release() 返回 EESOCKNOTRLSD。其它基于安全检查、错误的 socket 描述符等等错误码也可能返回。

名称

socket_acquire() - 获得 socket 的所有权

语法

int socket_acquire( int socket, string read_callback, string write_callback, string close_callback );

描述

socket_acquire() 在 socket_release() 把 `socket` 的所有权和控制权转移到新对象时被呼叫用来完成握手。 socket_release() 呼叫新拥有者对象中的 `release_callback` 方法用来通知对象它想移交 `socket` 的所有权。新对象有责任决定是否愿意接收 `socket`,如果愿意,就调用 socket_acquire() 外部函数完成转移,如果不愿意回调只是简单的回应而不完成握手。

在前一种情况下,握手完成,新对象成为 `socket` 的拥有者,读、写或关闭回调函数的参数都参照新对象中的函数,在新对象中的这些函数都需要定义以保证驱动程序知道调用哪些函数。后一种情况下 socket_release() 返回 EESOCKNOTRLSD 以保证原所有者可以做适合的处理。

socket_acquire() 应该仅在 `release_callback` 方法中调用并且只使用传递过来的这个 `socket`。

最后,再新建一个示例文件 8.3.5.c,代码如下:

/**
 * sockets release 相关测试
 */

nosave int S;

int main(object me, string arg)
{
    int err;
    err = socket_write(S, arg);
    if (err < 0)
    {
        debug("【8.3.5】socket_write error : " + socket_error(err));
    }
    else
    {
        debug("【系统】消息已发送!");
    }
    return 1;
}

void release_callback(int fd)
{
    S = fd;
    socket_acquire(fd, "read_callback", "write_callback", "close_callback");
}

void read_callback(int fd, mixed message)
{
    debug("【8.3.5】read_callback fd : " + fd);
    shout("【8.3.5】read_callback : " + message + "\n");
    // 发送消息给客户端
    if (message == "hi")
    {
        socket_write(fd, "hello ^_^");
    }
}

void write_callback(int fd)
{
    debug("【8.3.5】write_callback fd : " + fd);
}

void close_callback(int fd)
{
    debug("【8.3.5】close_callback fd: " + fd);
    socket_close(fd);
}

我们输入 8.3.1 release 会把 8.3.1 的 socket 连接转移到 8.3.5 上,输入 8.3.2 hi 试试会发现 8.3.5 正常接收信息,输入 8.3.5 hi 你会看到 8.3.2 也能收到回复。

在以上示例因为服务端和客户端是在一个游戏中,为方便了解相关流程,都有 debug 输出调试,可以看看自己驱动的console,能更有助于理解相关流程。另外,也可以使用不同的端口在你的电脑上运行2个驱动,然后一个运行服务端 8.3.18.3.5,另一个运行客户端8.3.28.3.3 测试,再分别在服务端和客户端运行 8.3.4 看看显示的 socket 状态。

Socket 项目案例

在以上演示中我们了解了 Socket TCP/UDP 模式下的工作流程,在游戏开发中,UDP典型的案例是实现各MUD基本信息互联(名称、地址、在线人数),而TCP的基本用法是在游戏中使用FTP服务。这里我们演示使用 Socket 读取网页API实现一个成语词典的功能。

用 socket 实现成语词典

因为网站是服务端,我们要访问网站读数据,只需实现客户端模块即可。这里我们以阿凡达数据API为例,先看看API文档:成语词典API

这个API提供了3种功能:1.根据关键词查询相关成语,2. 根据ID查询成语的解释,3. 随机返回一条成语解释。如要要在MUD中读取这些API实现相应功能,该如何实现呢?

我们先上代码,新建 8.3.6.c,代码如下:

#include <ansi.h>

#define STREAM 1
#define EESUCCESS 1

nosave mapping status = ([]);
nosave object receiver;

void write_data(int fd)
{
    socket_write(fd, "GET " + status[fd]["path"] + " HTTP/1.1\nHost: " + status[fd]["host"] + "\n\r\n\r");
}

void receive_data(int fd, mixed result)
{
    string idiom;
    result = result[strsrch(result, "{")..];
    // debug_message(sprintf("%d || %O", strlen(result), result));
    if (strsrch(result, "total") > -1)
    {
        int total;
        result = json_decode(result);
        total = result["total"];
        idiom = HIY "和 " + status[fd]["keyword"] + " 相关的成语共有 " + total + " 条,索引如下:" NOR + "\n";
        result = result["result"];
        idiom += "----------------------------------------\n";
        foreach (mapping list in result)
        {
            idiom += HIC "成语:" + list["name"] + "\n索引:" + list["id"] + NOR + "\n";
        }
        idiom += "----------------------------------------\n";

        if (total > 20)
        {
            idiom += HIR "相关成语超过二十条,推荐使用更精确的关键词查询。" NOR "\n";
        }

        idiom += HIG "可以使用 `8.3.6 索引` 阅读指定成语。" NOR "\n";

        tell_object(receiver, idiom);
    }
    else
    {
        result = json_decode(result);
        if (result["error_code"])
        {
            tell_object(receiver, "没有找到相关索引成语。\n");
        }
        else
        {
            result = result["result"];
            idiom = "----------------------------------------\n";
            idiom += HIY "成语:" + result["name"] + NOR "\n";
            idiom += HIW "读音:" + result["spell"] + NOR + "\n";
            idiom += "----------------------------------------\n";
            idiom += HIC "解释:" + result["content"] + NOR + "\n";
            if (result["derivation"])
            {
                idiom += HIW "出处:" + result["derivation"] + NOR + "\n";
            }
            if (result["samples"])
            {
                idiom += HIW "示例:" + result["samples"] + NOR + "\n";
            }
            idiom += "----------------------------------------\n";
            tell_object(receiver, idiom);
        }
    }
}

void receive_callback(int fd, mixed result, string addr)
{
    // 此方法无效
}

void socket_shutdown(int fd)
{
    socket_close(fd);
}

int main(object me, string arg)
{
    int fd;
    int ret;
    string host = "api.avatardata.cn";
    string addr = "121.42.196.237 80";
    string key = "f47de96f0bd04223817aca17ca150f0e";
    string path;

    receiver = me;
    if (is_chinese(arg))
    {
        path = "/ChengYu/Search?key=" + key + "&keyWord=" + arg;
        msg("info", "$ME开始查找和 " + arg + " 相关的成语。", receiver);
    }
    else if (strlen(arg) == 36)
    {
        path = "/ChengYu/LookUp?key=" + key + "&id=" + arg;
        msg("info", "$ME开始查阅成语的详细解释。", receiver);
    }
    else
    {
        path = "/ChengYu/Random?key=" + key;
        msg("info", "$ME开始学习成语。", receiver);
    }

    fd = socket_create(STREAM, "receive_callback", "socket_shutdown");
    status[fd] = ([]);
    status[fd]["host"] = host;
    status[fd]["path"] = path;
    status[fd]["keyword"] = arg;

    ret = socket_connect(fd, addr, "receive_data", "write_data");
    if (ret != EESUCCESS)
    {
        tell_object(receiver, "服务器连接失败。\n");
        socket_close(fd);
    }

    return 1;
}

注意这里使用了simul_efun json_decode,这个函数在 json.c 中实现。分析以上代码,会发现,最核心的一点是模拟 HTTP request header,关于 HTTP request header 相关知识可以自己百度,这里只用记住怎么使用即可:

socket_write(fd, "GET " + status[fd]["path"] + " HTTP/1.1\nHost: " + status[fd]["host"] + "\n\r\n\r");

运行代码看看效果: file file

用 socket 实现二维码生成

最后,布置一个小作业,使用 socket 在游戏中实现生成二维码的功能,二维码生成可以使用 http://qrenco.de/

具体效果如图: file


附socket游戏功能实例:

京ICP备13031296号-4