服务器程序开发手册

 

服务器子平台以GO语言作为本地语言,开发此子平台程序与开发其它子平台程序基本没有区别,唯一需要注意和了解的事项如下:

 

一. 服务器子平台差异:

 

1. 数据类型的实现差异

服务器子平台以GO语言作为本地语言,所有火山基本数据类型与各子平台本地数据类型的对应表如下(所对应的本地数据类型初级用户无需了解):

火山数据类型 服务器子平台

GO本地数据类型

视窗子平台

C++本地数据类型

安卓子平台

Java本地数据类型

服务器子平台与其它子平台之间的差异
字节 byte, uint8 S_BYTE byte 在服务器子平台中,本数据类型无符号,有效值范围从0到255;

在其它子平台中,本数据类型为有符号基本数据类型,有效值范围从-128到127,占用1个字节空间.

字符 rune, int32 TCHAR char 在服务器子平台中,本数据类型等同于32位整数,占用4个字节空间,有效值范围从-2147483648到2147483647;

在其它子平台中,本数据类型有效值范围从0到65535,占用2个字节空间.

整数 int INT int 在编译64位服务器子平台程序时,有效值范围从-9223372036854775808到9223372036854775807,占用8个字节空间,编译32位服务器子平台程序时与其它子平台相同.

在服务器子平台中 纯粹的32位整数请使用"字符"数据类型等同替代;

在其它子平台中,本数据类型有效值范围从-2147483648到2147483647,占用4个字节空间.

短整数 int16 SHORT short 与其它子平台没有区别
变整数 int INT_P int
长整数 int64 INT64 long
单精度小数 float32 FLOAT float
小数 float64 DOUBLE double
逻辑型 bool BOOL boolean
文本型 string CVolString String 在服务器子平台中,文本数据的编码格式为 utf-8 ,其它子平台是 unicode .

 

2. 文本编码

服务器程序中的文本数据编码和其它子平台不一样,其它子平台是 unicode 编码,服务器子平台是 UTF-8 编码,而且文本数据类型实际上等于字节数组数据类型. 因此如果程序中通过索引位置来访问文本内容,一定要注意从其中每个字符的首部开始,否则容易取到半截字符值.

服务器程序中字符数据类型基于的编码与其它子平台一致,都是 unicode 编码.

<火山程序 类型 = "通常" 版本 = 1 />

变量 文本变量1 <类型 = 文本型 值 = "火山A">
变量 字符变量1 <类型 = 字符>
变量 字节集1 <类型 = 字节集类>
 

// 由于文本数据以字节集的方式保存,此处所输出的文本长度为"火山A"所对应的 UTF-8 编码字节数据的尺寸 7 而不是字符的数量 3 .
换行输出 (取文本长度 (文本变量1))

// 由于文本数据就是字节集数据,因此文本数据可以直接转换到字节集,可以看到本语句输出的是与上语句相同的值: 7
字节集1 = 文本到字节集 (文本变量1)
换行输出 (字节集1.取尺寸 ())

// 同样的道理,字节集数据也可以直接转换到文本:
换行输出 (字节集到文本 (字节集1))

// 字符编码和文本数据编码不一样,为 unicode 编码,可以通过下面的方式枚举文本数据中每一个字符的 unicode 编码,分别为: '火', '山', 'A'.
字符遍历循环 (文本变量1, 字符变量1)
{
    换行输出 (取十六进制文本 ((整数)字符变量1))
}

 

本例程执行后的输出内容:

7
7
火山A
706b
5c71
41

 

3. 对象和数组参考

在服务器程序中,对象型或数组型变量/参数与安卓子平台一致,均存放指向对象或数组实例的参考而不是实例自身. 如下程序:

A. 对象型数据:

<火山程序 类型 = "通常" 版本 = 1 />

类 启动类 <公开 基础类 = 程序类>
{
    方法 启动方法 <公开 类型 = 整数>
    {
        变量 变量1 <类型 = 测试类>
        变量 变量2 <类型 = 测试类 注释 = "此处为了说明问题,没有设置\"参考\"标志.">


        变量1.成员变量1 = 1 
// 修改"变量1"所参考到对象实例的成员变量
        变量2 = 变量1  // 将"变量1"所指向对象实例的参考赋值到"变量2",此时"变量1"和"变量2"均参考到同一个对象实例.
        变量2.成员变量1 = 2  // 修改"变量2"所参考到对象实例的成员变量


       
// 由于"变量1"和"变量2"此时参考到同一个对象实例,因此上一语句基于"变量2"的修改也同时影响到"变量1",此语句将输出相同的修改后的值: 2 2
        换行输出 (变量1.成员变量1, 变量2.成员变量1)
        返回 (1)
    }
}

类 测试类
{
    变量 成员变量1 <公开 类型 = 整数>
}
 

如欲将非参考值赋值过去,需要使用"复制对象"全局方法:

<火山程序 类型 = "通常" 版本 = 1 />

类 启动类 <公开 基础类 = 程序类>
{
    方法 启动方法 <公开 类型 = 整数>
    {
        变量 变量1 <类型 = 测试类>
        变量 变量2 <类型 = 测试类 注释 = "此处为了说明问题,没有设置\"参考\"标志.">


        变量1.成员变量1 = 1 
// 修改"变量1"所参考到对象实例的成员变量
        变量2 = 复制对象 (变量1) 
// 将"变量1"所指向对象实例数据复制后赋值到"变量2",此时"变量1"和"变量2"参考到不同的对象实例.
        变量2.成员变量1 = 2  // 修改"变量2"所参考到对象实例的成员变量


       
// 由于"变量1"和"变量2"此时参考到不同的对象实例,因此上一语句基于"变量2"的修改不会影响到"变量1",此语句将输出: 1 2
        换行输出 (变量1.成员变量1, 变量2.成员变量1)
        返回 (1)
    }
}

类 测试类
{
    变量 成员变量1 <公开 类型 = 整数>
}

 

B. 数组型数据:

数组型数据推荐使用所提供的各种数组类,如"整数数组类","文本数组类"等等. 如果使用基本数组数据,则需要和对象型数据一样操作:

<火山程序 类型 = "通常" 版本 = 1 />

变量 数组变量1 <类型 = "整数 []" 值 = { 1, 2 }>
变量 数组变量2 <类型 = "整数 []">


数组变量2 = 数组变量1 
// 此处将"数组变量1"所对应数组数据的参考赋值到"数组变量2"
数组变量2 [1] = 3  // 由于"数组变量1"和"数组变量2"此时所参考到的是同一份数组数据,因此修改"数组变量2"也会同时影响到"数组变量1".


换行输出 (数组变量1 [1], 数组变量2 [1]) 
// 由于两者参考到的是同一份数组数据,此时的输出结果为: 3 3
 

如欲将非参考值赋值过去,需要使用"复制数组"全局方法:

<火山程序 类型 = "通常" 版本 = 1 />

变量 数组变量1 <类型 = "整数 []" 值 = { 1, 2 }>
变量 数组变量2 <类型 = "整数 []">


数组变量2 = 复制数组 (数组变量1) 
// 此处将"数组变量1"所对应数组数据复制后赋值到"数组变量2",之后两者将参考到不同的数组数据.
数组变量2 [1] = 3  // 由于"数组变量1"和"数组变量2"所参考到的不是同一份数组数据,因此修改"数组变量2"不会影响到"数组变量1".


换行输出 (数组变量1 [1], 数组变量2 [1]) 
// 由于两者参考到的不是同一份数组数据,此时的输出结果为: 2 3
 


 

二. 服务器程序特性:

 

1. 协程

在服务器程序中,多线程采用go的协程实现.

协程为超轻量级线程,在服务器程序中处理来自客户端的请求时,完全可以为每一个客户启动一个专用处理协程,以简化程序处理逻辑.

协程通过"启动协程"全局方法启动,样例如下:

<火山程序 类型 = "通常" 版本 = 1 />

变量 等待管道 <参考 类型 = 整数管道类>
等待管道 = 整数管道类.创建 () 
// 创建一个管道,用作在协程之间同步使用. 关于管道见下节.
启动协程 ()  // 注意: "启动协程"语句的子语句体中所有代码将在另一个协程而不是当前主协程中执行:
{
    换行输出 ("本语句在另一个协程中被执行")
    // 提示: 在所启动协程的子语句体中可以随意访问其外部变量.
    等待管道.写 (1)  // 向"等待管道"中写出一个值,以放行下面主协程中的"等待管道.读"语句.

    // 注意当协程中的代码执行到此处时,并不会去顺序执行下面的代码,而是结束此协程的执行.
}

// 下面代码依旧在当前主协程中执行:
等待管道.读 ()  // 等待前面所启动的协程执行完毕后通过向"等待管道"中写出一个值发出的放行通知
换行输出 ("协程中的代码执行完毕")

 

本例程执行后的输出内容:

本语句在另一个协程中被执行
协程中的代码执行完毕
 

2. 管道通讯

管道是GO语言中实现协程间通信的重要方式,它是一种线程安全的数据结构,用于在程序协程之间中通过读和写数据的方式来传递信息.

可以形象地想象一下: 管道和我们日常生活时所使用的物理管道类似,只不过其中传递的是数据而已.

管道根据其读写能力分为只读/只写/读写三种,一般情况下创建管道时均创建可读写管道,然后对外提供此管道对象时根据需要转换为只读/只写管道,以限制该管道在外部操作的权限.

使用管道时需要注意以下几点:

A. 管道是阻塞式的:

管道的读和写操作都是阻塞式的,即如果没有对应的读和写操作匹配,那么协程会一直阻塞在这个操作上. 假设我们创建了一个整数类型的管道,分别进行了写出和读入操作.在数据发送操作协程中,我们向管道中写入了一个值,在另一个数据接收操作协程中,我们从管道中读出了该值并赋值给了变量a. 由于写出和读入操作都是阻塞式的,所以这个程序会一直等待直到这两个协程的读入和写出操作匹配,才能正常结束.

可以参考前面"协程"章节中的例子,主协程中在执行"等待管道.读 ()"语句时,会一直阻塞在那里,直到前方所启动协程中的"等待管道.写 (1)"语句被执行在"等待管道"中写入了数值1才会将该数值读入后返回.

B. 关闭后的管道无法再发送数据,读取数据时会不阻塞立即返回一个对应数据类型的零值;

C. 如果创建管道时所提供的"容量"大于0,表明该管道为一个缓冲管道,向该管道内写出数据时,如果尚未找到对应匹配的管道读入操作,可以最多缓存所指定数目的被写出数据项而不会导致写出阻塞.读入该管道中的数据时,如果缓冲区中存在先前所写入数据将直接读入该数据而不会阻塞.

D. 支持同时操作多个管道.

具体请参阅服务器样例解决方案中的"基本->多线程->协程及管道通讯"例程.

 

例程(使用管道向协程发送数据和通知):

<火山程序 类型 = "通常" 版本 = 1 />

类 启动类 <公开 基础类 = 程序类>
{
    方法 启动方法 <公开 类型 = 整数>
    {
        管道演示方法 ()
        返回 (1)
    }

    方法 管道演示方法
    {
        变量 退出通知管道 <类型 = 整数管道类 注释 = "
用作通知所启动协程退出">
        变量 数据管道 <类型 = 任意值管道类>
        // 创建相应管道对象实例
        退出通知管道 = 整数管道类.创建 ()
        数据管道 = 任意值管道类.创建 ()

        启动协程 ()
 // 启动演示协程
        {
            变量 变量1 <类型 = 任意值 值 = 空对象>
            判断循环 (真) 
// 无限循环等待来自主协程的退出通知和数据
            {
                管道分支操作 ()
                {
                    管道读通知分支 (退出通知管道) 
// 读入到了退出通知?
                    {
                        换行输出 ("协程准备退出")
                       
// 进行各种清理工作 ...
                        换行输出 ("协程退出完毕")
                        退出通知管道.写 (1) 
// 通知主协程,本协程已经退出完毕. 此语句将一直阻塞直到主协程将此处写入的值读出.
                        返回  // 退出本协程代码
                    }
                    管道读分支 (数据管道, 变量1) 
// 读入到了主协程中所传递过来的数据?
                    {
                        换行输出 ("接收到了数据:", 变量1.取 (测试类1).成员1)
                       
// 进行相关处理
                    }
                }
            }
        }

        变量 协程数据变量1 <类型 = 测试类1 成员1 = "传递到协程的数据1">
        变量 协程数据变量2 <类型 = 测试类1 成员1 = "传递到协程的数据2">


        数据管道.写 (取对象任意值 (协程数据变量1)) 
// 发送数据1到所启动协程
        数据管道.写 (取对象任意值 (协程数据变量2))  // 发送数据2到所启动协程
        换行输出 ("通知所启动协程退出")
        退出通知管道.写 (1) 
// 通知所启动协程退出,此语句将一直阻塞直到所启动协程将此处写入的值读出.
        退出通知管道.读 ()  // 等待所启动协程退出完毕(等待所启动协程向此管道写入值)
        换行输出 ("所启动协程已经退出")
    }
}

类 测试类1
{
    变量 成员1 <公开 类型 = 文本型 @属性变量 = 真>
}

 

本例程执行后的输出内容:

接收到了数据: 传递到协程的数据1
接收到了数据: 传递到协程的数据2
通知所启动协程退出
协程准备退出
协程退出完毕
所启动协程已经退出

 

3. 延迟执行

服务器子平台基于GO语言提供了代码的"延迟执行"机制,用来登记在调用语句所处方法返回时将被自动执行的代码,常用于释放资源和连接、关闭文件、释放锁、捕获异常等.

所登记的代码("延迟执行"方法调用语句的子语句体)不会被立即执行,将延迟到如下时刻被自动执行: 该延迟执行语句所处方法返回到其调用方时(无论是正常返回还是因为后续代码触发异常后中断返回).

如果多次调用"延迟执行"方法登记了多段被延迟执行的代码,将按照先进后出(最先调用的最后执行)的顺序自动执行.

详细机制演示请参阅服务器样例解决方案中的"基本->延迟执行及异常处理"例程.

 

例程片段(使用延迟执行自动关闭所打开的文件):

<火山程序 类型 = "通常" 版本 = 1 />

变量 文件1 <参考 类型 = 文件类>
变量 错误1 <类型 = 错误接口>


文件1 = 文件类.只读打开 ("./test", 错误1.取地址 ()) 
// 打开所指定的文件
如果 (错误1 == 空对象) 
// 文件打开成功?
{
    延迟执行 () 
// "延迟执行"语句的子语句体中的所有代码不会被立即执行,而会等到该语句所处方法返回时自动执行.
    {
        文件1.关闭接口.关闭 () 
// 在退出本方法时自动关闭所打开的文件.
    }

    // 对所打开文件进行操作 ......

    // 注意延迟执行代码块中执行到此处时,并不会去顺序执行下面的代码,而是结束此延迟执行代码块的执行.
}

 

例程片段(使用延迟执行自动关闭相关资源):

<火山程序 类型 = "通常" 版本 = 1 />

变量 滴答器1 <参考 类型 = 滴答器类>
滴答器1 = 滴答器类.创建 (时间段.秒) 
// 创建一个触发时间为1秒的滴答器


延迟执行 () 
// "延迟执行"语句的子语句体中的所有代码不会被立即执行,而会等到该语句所处方法返回时自动执行.
{
    滴答器1.停止 () 
// 在退出本方法时自动停止滴答器
}

// 对所创建滴答器进行操作 ......
 

例程片段(使用延迟执行自动关闭所打开的数据库):

<火山程序 类型 = "通常" 版本 = 1 />

变量 错误 <类型 = 错误接口 值 = 空对象>
变量 数据库 <参考 类型 = SQL数据库类 值 = 空对象>
 

数据库 = SQL数据库类.打开 ("mysql", "username:password123456@/testdb", 错误.取地址 ())  // 打开数据库
如果真 (错误 == 空对象)  // 数据库打开成功?
{
    延迟执行 () 
// "延迟执行"语句的子语句体中的所有代码不会被立即执行,而会等到该语句所处方法返回时自动执行.
    {
        数据库.关闭 () 
// 在退出本方法时自动关闭所打开数据库
    }

    // 对所打开数据库进行操作 ......

}
 

4. 异常处理

服务器程序的异常传递机制如下:

假设正在执行"方法1",当其代码中通过调用"抛出异常"全局方法抛出了一个异常,或者产生了一个运行时异常(如除零/数组索引超出有效范围等)时,"方法1"中在该异常产生前通过调用"延迟执行"全局方法登记的所有代码将被自动执行,然后跳转去执行"方法1"的上一层调用方法中已经登记的所有延迟执行代码,以此顺序一直处理到最顶层调用方法,然后终止整个程序的执行.

在异常的向上传递过程中,可以通过在任何一层被调用方法的已登记延迟执行代码中调用"俘获异常"全局方法来俘获该异常,并中止该异常的后续自动处理. "俘获异常"方法如果俘获到了异常,将必定返回非空对象的对应异常数据,否则将返回空对象. 注意: 该方法只能在延迟执行代码中被执行.

以上就是服务器程序的异常产生/传递/俘获机制. 详细机制演示请参阅服务器样例解决方案中的"基本->延迟执行及异常处理"例程.

 

例程片段(俘获后续代码中所产生的任何异常):

<火山程序 类型 = "通常" 版本 = 1 />

方法 异常演示方法 <静态>
{
    延迟执行 () 
// "延迟执行"语句的子语句体中的所有代码不会被立即执行,而会等到该语句所处方法返回时自动执行.
    {
        // 演示俘获后面代码中所抛出的异常:
        变量 所俘获异常 <类型 = 任意值>
        所俘获异常 = 俘获异常 () 
// 尝试俘获异常,如果本延迟执行代码为所处方法正常返回时被执行,此"俘获异常"方法调用将返回空对象.
        如果 (所俘获异常 == 空对象)  // 本延迟执行代码不为因为产生异常而被执行?
        {
            换行输出 ("未俘获到异常")
        }
        否则
        {
            换行输出 ("所俘获到的异常:", 所俘获异常)
        }
    }

    异常抛出方法 () 
// 演示在下层被调用方法中抛出异常后在本方法的延迟执行代码中俘获处理
    换行输出 ("此处的代码不会被执行1")  // 由于在前面抛出了异常,将导致程序执行流程转向本方法中在前面登记的延迟执行代码,所以本语句不会被执行.
}

方法 异常抛出方法 <静态>
{
    抛出异常 ("异常抛出方法所抛出的异常")
 // 本语句执行后,将自动转去执行先前程序执行流程中加入的所有延迟执行代码. 在本示例中,此处所抛出的异常将在上一层"异常演示方法"方法内被其中的延迟执行代码所俘获处理.
    换行输出 ("此处的代码不会被执行2")  // 在前面抛出异常将导致程序执行流程转向最近登记的延迟执行语句块,所以此处的语句不会被执行.
}
 

本例程执行后的输出内容:

所俘获到的异常: 异常抛出方法所抛出的异常

 

5. 接口

服务器程序的接口和安卓子平台中的一致,都用作代表某个类需要实现的一个方法集合.

接口在服务器程序中使用得很广泛,有很多本地类都实现了一个或多个接口. 最常见的接口是基本类库中提供的输入输出类接口,如: "读接口", "写接口", "关闭接口"等等.

用户程序的类也可以实现指定的本地接口,具体请参阅"基本->接口转发"例程. 另外一种方法是通过 "编辑->插入->插入特定内容->代码片段"菜单项功能(Ctrl+I) 在程序类中直接插入对应的接口实现代码片段来在你的类中实现指定接口,参见"数据处理->JSON"中的"动物"类.

 

例程片段(通过接口访问文件类对象):

<火山程序 类型 = "通常" 版本 = 1 />

类 启动类 <公开 基础类 = 程序类>
{
    方法 启动方法 <公开 类型 = 整数>
    {
        变量 文件1 <参考 类型 = 文件类>
        变量 错误1 <类型 = 错误接口 值 = 空对象>
        文件1 = 文件类.打开 ("_vol_test.txt", , , 错误1.取地址 ())
 // 打开一个所指定名称的文件
        如果 (错误1 == 空对象)  // 打开成功?
        {
            延迟执行 () 
// 登记延迟执行代码
            {
                文件1.关闭接口.关闭 () 
// 调用"文件类"所实现"关闭接口"的"关闭"方法,以在本方法退出时自动关闭所打开文件.
            }

            如果 (添加新内容 (文件1.读接口, 文件1.定位写接口))
 // 以"文件类"所实现的"读接口"和"定位写接口"对象作为参数调用"添加新内容"方法
            {
                换行输出 ("添加新内容成功")
            }
        }

        返回 (1)
    }

    方法 添加新内容 <类型 = 逻辑型 注释 = "
本方法从指定的读接口对象中读入所有内容,然后添加一段新的内容后写出到指定的定位写接口对象中,用作演示如何使用接口对象作为参数."
    返回值注释 = "
返回是否成功">
    参数 读接口对象 <类型 = 读接口 注释 = "
本接口对象可以来自任何实现了\"读接口\"的对象,不仅仅是\"文件类\"对象,譬如各种数据流类对象等等.">
    参数 写接口对象 <类型 = 定位写接口 注释 = "
本接口对象可以来自任何实现了\"定位写接口\"的对象">
    {
        变量 错误1 <类型 = 错误接口 值 = 空对象>
        变量 字节集1 <参考 类型 = 字节集类>


        字节集1 = 读接口对象.读全部数据 (错误1.取地址 ()) 
// 调用所提供的"读接口"参数对象的"读全部数据"方法读入其中的所有数据
        如果 (错误1 == 空对象)  // 读入成功?
        {
            字节集1.加入文本 ("\r\n加入的新文本 - my new text") 
// 在原有内容后加入一段新文本
            写接口对象.写字节集 (0, 字节集1, 错误1.取地址 ())  // 调用所提供的"定位写接口"参数对象的"写字节集"方法将更新后的内容写入到该对象首部
        }
        返回 (错误1 == 空对象)
    }
}


6. 本地类

GO程序类库中,有一些情况(譬如类数据的格式化流存储等)下需要提供完全本地化的类数据,可以在定义类时使用"@本地类"属性去除类中所有火山相关的内容,可以使用"@服务器.标签"属性在成员变量上提供相关的GO标签 文本.

具体请参见"服务器样例解决方案"中的"yaml数据访问","数据编解码","JSON"等例程.

 

7. WIN7支持

GO的最新版本已经不支持 WIN7 ,如果欲在 WIN7 下使用服务器子平台,请删除系统安装目录"plugins\vprj_server\sdk\GoLang"中的原有内容(注意一定要先删除),然后到此 GO官网地址下载旧版 GO 1.21.0 版覆盖到此目录即可.

 

--- 完 ---