Q语言——I/O操作

前言

Q的I/O操作是非常简洁和强大的,每个I/O操作的函数的信息熵都很高。通过句柄的操作方式,我们可以把硬盘中的数据导入到内存中,或者把内存中的数据存储到硬盘里。同样我们也要养成用时打开句柄,用完之后关闭的习惯。

一、二进制数据

在Q中,文件的存储和导入我们可以分为两类:文本和二进制数据。处理文本数据,我们会用到参数“0”,二进制数据我们会用到参数“1”。文本文件我们可以理解为是一个字符串的列表,而二进制文件我们可以理解是一个二进制字节的列表。虽然所有文本文件我们可以作为二进制数据来处理,但是不是所有二进制数据文件我们可以用文本数据的处理方式来处理。

1. 文件句柄

文件句柄:存储在硬盘中的数据的一个路径+名称。具体如下:

`:[ 文件路径 ] 文件名称

通常可能我们在使用路径和名称的时候我们使用字符串更容易,以便可以轻松处理空白和其他特殊字符。因此Q提供了内置函数hsym,我们可以通过hsym将字符串的句柄转换为标准的Q可以识别的标准读写句柄。

q)hsym `$"/data/file name.csv" /使用hsym转换将字符串的句柄转换为标准的句柄方式
`:/data/file name.csv

2. 文件大小的查看(hcount)与文件的删除(hdel)

我们可以通过hcount来查看要导入的一个文件的大小,同时我们要删除某个文件的时候我们可以通过hdel来删除。这里一定要谨慎使用hdel,因为它是直接就永久删除了(回收站也没有),删除之前一定要确认是否可以删除。

对于文件的读写,我们还有一个重要的事情,就是先确认一下我们Q语言运行的环境目录,我们可以通过\cd命令来查看我们当前的环境目录,一般情况下的环境目录是在你的安装目录,这里我修改了q.q的文件,初始化的环境目录就是我的E盘。同时我们也可以使用\cd命令进入到我们想要的目录中。

q)\cd /查看当前的环境目录
"E:\\"
q)\cd c: /进入我们想要的环境目录
q)\cd
"C:\\"
q)\cd c:/q /进入我们想要的环境目录
q)\cd
"c:\\q"

下面我们就开始尝试使用hcount与hdel命令。这里我创建了一个test.txt的文件,大小为29K。并且我在桌面和我的E盘的data文件夹下分别保存了一份。

q)hcount `E:/data/test.txt /如果我们要查看的文件不在当前环境目录下的查看方式
29575 /返回文件大小,单位为字节
q)hcount `C:/Users/caoxi/Desktop/test.txt /如果我们要查看的文件不在当前环境目录下的查看方式
29575
q)hcount `:test.txt /所要查看的文件就在当前环境目录下
29575
q)hdel `C:/Users/caoxi/Desktop/test.txt /但是我发现hdel删除命令就不能
删除不在当前环境目录下的文件,删除不在当前环境目录下的文件就会报错
'nyi
q)hdel `:test.txt /成功删除当前环境目录下的文件,返回删除的文件名称,但是
一定要确认有备份!!!这种删除是直接永久删除,在回收站中都不存在的
`:test.txt

3. 文件的序列化(写入到本地)和反序列化(读入到内存)

每个Q实体都可以序列化并持久存储。而且非常简单方便,不像传统文件那样,文件的读和写比较复杂,通常我们使用set命令将实体序列化到本地,使用get命令将本地文件导入到内存中。具体读写方式如下:

:[ 文件路径 ]/实体名称 set 文件名称 get:[ 文件路径 ]/实体名称

下面我们看一些具体案例

q)\cd /首先可以查看一下我们所在的环境目录
"E:\\"
q)`:/data/variable_a set 2019 /序列化一个变量到对应的路径下,
如果对应的路径下文件夹不存在,则会自动生成一个对应的文件夹
`:/data/variable_a /返回对应的路径+序列化实体对象表示成功
q)`:/data/list_L set 2019 2020 2021 /序列化一个列表到对应的路径下
`:/data/list_L
q)`:/data/table_T set ([] c1:`a`b`c; c2:10 20 30) /序列化一个表到对应的路径下
`:/data/table_T

这个时候我们可以打开我们序列化使用的路径的文件夹下,我们可以看到所序列化的对应的二进制文件。

序列化后的二进制文件

我们可以通过get命令将序列化的文件来反序列化(就是读入到内存)到我们的内存中。

q)get `:/data/variable_a /反序列化一个变量,返回变量的具体内容
2019 
q)get `:/data/list_L /反序列化一个列表,返回列表的具体内容
2019 2020 2021
q)get `:/data/table_T /反序列化一个表,返回表的具体内容
c1 c2
-----
a 10
b 20
c 30
q)value `:/data/table_T /当然我们也可以使用value命令来进行反序列化
c1 c2
-----
a 10
b 20
c 30

但我们使用get或者value命令将本地的文件反序列话到我们的内存中,那是否内存中就有了这一个变量呢?其实不是的,我们可以理解为只是将本地文件的内容反序列化后在我们内存中进行显示,但是并没有保存到我们的内存当中。如果我们要将本地的文件导入到内存中并保存,那我们要使用\l命令。前面我们也曾经提到过\l命令的使用。具体操作方式如下:

\l /[ 路径 ]/实体名称

q)table_T  /上面我们通过get命令和value命令将本地文件反序列化后,
我们直接查看对应的实体内容,发现内存中并没有,然后报错。 
q)\l /data/table_T /这时我们就要通过\l命令来将本地文件导入到内存中,
这时对应的实体才保存到了我们的内存中
`table_T /导入成功返回对应的实体名称
q)table_T /查看实体内容
c1 c2
-----
a 10
b 20
c 30

4. 二进制数据文件

我们知道,对于一个文件,我们可能会在后面继续追加文件内容的操作,用set序列化后我们如何在序列化后的文件中继续添加内容呢。这就与传统编程语言就非常类似了,我们需要先打开一个句柄(对应的文件路径),然后写入内容,最后关闭文件句柄。通常我们使用hopen命令和hclose命令来打开和关闭句柄。

q)`:/data/L set 10 20 30 /序列化一个列表
`:/data/L
q)h:hopen `:/data/L /使用hopen打开
q)h 
876i /返回的一个编号(个人觉得这个编号是一个对应的通道编号)
q)h[42] /添加42这条数据
876i
q)h[1] /添加1这条数据
876i
q)h 100 200 300 /同时添加一个列表
876i
q)hclose h /关闭通道
q)get `:/data/L /可以看到添加数据成功
10 20 30 42 1 100 200 300

5. 二进制数据文件的读与写

通常我们使用read1将序列化的文件以二进制的方式读入到内存中。

q)read1 `:/data/table_T
0xff016200630b00020000006331006332000000020000000b000300000061006200630007000..
q)read1 `:/data/list_L
0xfe200700000000000300000000000000e307000000000000e407000000000000e5070000000..
q)read1 `:/data/variable_a
0xff01f9e307000000000000

当然我们也可以直接将二进制数据序列化到我们的本地

q)`:/data/answer.bin 1: 0x06072a /将二进制文件序列化到本地
`:/data/answer.bin
q)read1 `:/data/answer.bin /从本地直接将二进制文件读入到内存中
0x06072a

6. :号的使用

我们有另外一种方式来序列化数据,非常类似于前面我们介绍的函数形式,具体如下:

.[ 路径;(); :;数据] /第一次序列化
.[ 路径;(); ,;数据] /对已经存在的文件中附加数据

q).[`:/data/raw; (); :; 10 20 30] /注意[ ]前面有个点号“.”
`:/data/raw
q)get `:/data/raw
10 20 30
q).[`:/data/raw; (); ,; 100 200 300]
`:/data/raw
q)get `:/data/raw
10 20 30 100 200 300

二、表的保存与载入

前面我们已经介绍过表的序列化(保存)与反序列化(载入),对于序列化好的文件,我们要使用的时候,一定要使用load或者\l命令将文件载入到内存中才可以使用,get命令只是将序列化好的文件内读取到内存中,两者是有区别的。其中load命令和\l命令在路径参数上有一些差别,具体如下:

q)load `C:/Users/caoxi/Desktop/t  /load的路径参数前面有一个`符号
`t
q)\l C:/Users/caoxi/Desktop/t1
`t1
q)t
c1 c2 c3
---------
a  10 1.1
b 20 2.2
c 30 3.3
q)t1
c1 c2 c3
---------
a 10 1.1
b 20 2.2
c 30 3.3

我们还可以将表以txt格式、csv格式、xml格式或者xls格式序列化到本地磁盘上。

q)save `:/data/t.txt
`:/data/t.txt
q)save `:/data/t.csv
`:/data/t.csv
q)save `:/data/t.xml
`:/data/t.xml
q)save `:/data/t.xls
`:/data/t.xls

但是这四种文件只能通过read0的形式读取到我们的内存中,使用get、load或者\l命令都无法读取这些文件。

q)read0 `:/data/t.txt
"c1\tc2\tc3"
"a\t10\t1.1"
"b\t20\t2.2"
"c\t30\t3.3"
q)read0 `:/data/t.csv
"c1,c2,c3"
"a,10,1.1"
"b,20,2.2"
"c,30,3.3"
q)read0 `:/data/t.xml
""
"a101.1"
"b202.2"
"c303.3"
""
q)read0 `:/data/t.xls
""
"

几种文件的具体内容如下:

xml文件内容

txt文件内容

csv文件内容

xls文件内容

磁盘中的文件

三、扩展表

有时候可能会这种情况,那就是我们一张表有很多个字段,但是我们有时候可能只会临时使用到其中的一个或者两个字段,然后我们这张表可能还有个好几百万行,那我们将这种表导入到内存中就会造成很大的内存浪费,毕竟内存是非常宝贵的,这种情况下Q就采取了一种非常高效的方式,那就是按照字段存储。

对一个大型表不适合直接载入到内存中的表,通过Q序列化每个字段到指定路径下,对表的这种操作我们称为扩展表(splayed table)。

比如我们有这样一个表t

t:([] c1:10 20 30; c2:1.1 2.2 3.3; c3: 2001 2002 2003)

我们就可以分为三个字段序列化到我们的本地磁盘上。具体操作如下:

`:[ 路径 ]/table_name/ set table_name

注意这里的/是必须的,这也是表普通序列化和扩展序列化的区别。

这里我们对表t进行扩展序列化:

`:/data/tsplay/ set t

这时我们看到我们的data文件夹下多了一个tsplay的文件夹,而不是一个文件,在tsplay文件夹下有4个文件,一个是“.d”的文件,该文件包含了字段信息,其他的文件就是序列化的字段数据。

扩展表的序列化文件

这样我们在导入的时候就可以直接导入某一个字段了,我们可以直接查看.d文件来看这个表的字段信息。

q)get `:/data/tsplay/.d /查看表tsplay的字段信息
`c1`c2`c3
q)get `:/data/tsplay/c2 /查看表tsplay的c2字段的内容
1.1 2.2 3.3
q)c2 /这时我们还没有导入c2字段的内容,因此会报错
'c2
q)load `:/data/tsplay/c2 /导入表tsplay的c2字段的内容
`c2
q)c2
1.1 2.2 3.3

Q语言对表的扩展也有一定的限制:

1) 所有的字段必须是简单列表或者符合列表
2) 如果一个字段全是symbol类型的话就必须使用枚举

q)`:/data/tsplay1/ set ([] c1:2000.01.01+til 3; c2:1 2 3) 
`:/data/tsplay1/
q)`:/data/tsplay2/ set ([] c1:1 2 3; c2:(1.1 2.2; enlist 3.3; 4.4 5.5))
`:/data/tsplay2/
q)`:/data/tsplay3/ set ([] c1:1 2 3; c2:(1;`1;"a"))
`:/data/tsplay3/
q)`:/data/tsplay4/ set ([] c1:`a`b`c; c2:10 20 30) /这时c1字段就是全symbol类型的数据,
进行扩展序列化时就失败了
'type
q)`:/db/tsplay4/ set ([] `sym?c1:`a`b`c; c2:10 20 30) /对于包含全symbol类型的字段,
我们就需要对这些特殊字段枚举处理后扩展序列化
`:/db/tsplay4/
Q官方也给了一个使用的工具,那就是 .Q.en[`路径; table_name]来进行扩展序列化,
这时对于字段数据类型就没有限制了。
q)`:/db/tsplay5/ set .Q.en[`:/db; ([] c1:`a`b`c; c2:10 20 30)]
`:/db/tsplay5/

四、文本数据处理

前面我们介绍的都是二进制数据文件的处理,现在我们介绍一下文本文件的数据处理。通常一个文本文件会被认为是char类型的列表,也就是字符串。因此读取文本文件会生成一个字符串列表,同时也可以将一个字符串列表写入到文本文件中。

1. 文本文件的读写

前面我们二进制文件通常使用的参数是1:,对于文本文件,通常我们使用的参数都是0:。

下面我们在data文件夹下创建一个文本文件test.txt。我们使用read0就可以将文本文件读入到我们的内存中。

q)read0 `:/data/test.txt
"So long"
"and thanks"
"for all the fish"

我们可以看到read0会将文本文件的每一行转换为一个字串符的列表。当然我们也可以使用read1读取文本文件,只不过返回的结果是一个二进制的数据。

q)read1 `:/data/test.txt
0x536f206c6f6e670d0a616e64207468616e6b730d0a666f7220616c6c207468652066697368
q)"x"$ read0 `:/data/test.txt /上面的可能看起来比较混乱,我们可以把read0读取的
文件通过类型转换的形式转换成二进制数据
0x536f206c6f6e67
0x616e64207468616e6b73
0x666f7220616c6c207468652066697368
q)"c"$ read1 `:/data/test.txt /我们也可以将read1读取的二进制文件转换成字符转的类型
"So long\r\nand thanks\r\nfor all the fish"

下面就是介绍如何将数据序列化到文本文件,与方式一样,只是将set换成了0:参数。

q)`:/data/test1.txt 0: ("Life"; "The Universe"; "And Everything") /序列化到文本文件
`:/data/test1.txt
q)read0 `:/data/test1.txt
"Life"
"The Universe"
"And Everything"
序列化文本文件的内容

2. hopen与hclose的使用

跟前面介绍的hopen和hclose操作方式相似。只是对于文本文件我们要用到neg函数来写入数据到文本文件中,而不是像前面一样可以直接写入。

q)h:hopen `:/data/new.txt
q)neg[h] enlist "this" /通过neg[h]来写入数据到文本文件
-960i
q)neg[h] ("and"; "that") /每个字符串就会占一行
-960i
q)hclose h /关闭通道的时候直接用h,不是neg[h]
q)read0 `:/data/new.txt /读取文本文件
"this"
"and"
"that"
q)get `:/data/new.txt /这时我们发现使用get已经不能得到文本文件内容了,
开始报错了,只能用read0的形式
'/data/new.txt
q)h:hopen `:/data/new.txt
q)neg[h] ("and"; "more")
-1012i
q)hclose h
q)read0 `:/data/new.txt /读取文本文件内容
"this"
"and"
"that"
"and"
"more"

五、数据解析

我们也可以将文本类文件或者二进制文件通过特定的方式解析为特定的字段,和前面介绍的一样,文本文件我们用参数0:,二进制文件我们用参数1:。具体在解析为什么数据类型的时候我们可以参考下面的图。

数据解析类型参考

这里0这列是在解析文本类文件的时候选择的字段类型简写(注意是大写);1这列是解析二进制文件的时候选择的字段类型简写(注意是小写);type这列表示对应的数据类型;width表示字节宽度;后面的format列表示可以接受的格式类型。

1. 固定宽度解析

对于固定宽度解析,就是我们按照预先给定的一个宽度来裁剪一样,然后将每段内容放到相应的字段。具体模式如下:

(“指定字段类型”;指定每个字段类型的宽度) 0:read0 :[文件路径] (“指定字段类型”;指定每个字段类型的宽度) 1:read1:[文件路径]

我们的所有指定宽度的总和要小于等于我们文本文件中的每行的字数。

现在假设我们有一个如下记录的文本文件(注意这里1001和98.000之间有两个空格):

1001 98.000ABCDEF1234Garbage2015.01.01
1002 42.001GHUJKL0123Garbage2015.01.02
1003 44.123nopqrs9876Garbage2015.01.03

然后我要解析成下面的形式(其中我们省略了一部分信息):

c1 c2 c3 c4
---------------------------------
1001 98 ABCDEF1234 2015.01.01
1002 42.001 GHUJKL0123 2015.01.02
1003 44.123 nopqrs9876 2015.01.03

那我们首先就可以通过固定宽度解析,再通过翻转得到(对于要省略的信息,在给定指定解析字段类型的时候我们使用空格,比如这里("JFS D";4 8 10 7 10),在S的10个宽度解析完之后就会跳过7个空格开始解析D的10个宽度):

q)("JFS D";4 8 10 7 10) 0: `:/data/text.txt 
1001 1002 1003
98 42.001 44.123
ABCDEF1234 GHUJKL0123 nopqrs9876
2015.01.01 2015.01.02 2015.01.03

这里的4+8+10+7+10=39,宽度要和text.txt中的每一行宽度是一样的,这就是定宽度解析。我们发现解析出来的结果是列向的,不是横向的,因此我们要反转一下就得到了下面的结果形式,同时我们可以给每个字段添加一个名称。

q)flip `c1`c2`c3`c4!("JFS D";4 8 10 7 10) 0: `:/data/text.txt
c1 c2 c3 c4
---------------------------------
1001 98 ABCDEF1234 2015.01.01
1002 42.001 GHUJKL0123 2015.01.02
1003 44.123 nopqrs9876 2015.01.03

2. 变长记录解析

有时候对于每一行的长度可能不清楚,同时每个字段数据也不一样,那我们就不能使用固定宽度来解析了,我们得使用边长记录来解析,或者说是分隔标识来解析,解析模式如下:

(“指定字段类型”;“分隔标识符”) 0: :[文件路径] (“指定字段类型”;“分隔标识符”) 1::[文件路径]

假设我们有一个下面的csv文件记录(我们知道csv文件基本上是以,号分隔的):

1001,DBT12345678,98.6
1002,EQT98765432,24.75
1004,CCR00000001,121.23

最终我们可以解析到如下形式:

c1 c2 c3
-----------------------
1001 DBT12345678 98.6
1002 EQT98765432 24.75
1004 CCR00000001 121.23

跟上面固定宽度解析流程一样,我们先通过标识符分隔后,然后再翻转。

q)("JSF"; ",") 0: `:/data/simple.csv
1001 1002 1004
DBT12345678 EQT98765432 CCR00000001
98.6 24.75 121.23
q)flip `c1`c2`c3!("JSF"; ",") 0: `:/data/simple.csv
c1 c2 c3
-----------------------
1001 DBT12345678 98.6
1002 EQT98765432 24.75
1004 CCR00000001 121.23
q)flip `c1`c2`c3!("J*F"; ",") 0: `:/data/simple.csv
c1 c2 c3
-------------------------
1001 "DBT12345678" 98.6
1002 "EQT98765432" 24.75
1004 "CCR00000001" 121.23

对于我们有些有字段名称的csv文件,我们在导入解析的时候就可以直接解析成表的形式。如我们有下面内容的文件(titles.csv):

id,ticker,price
1001,DBT12345678,98.6
1002,EQT98765432,24.7
1004,CCR00000001,121.23

第一行为字段名称,这时我们就可以解析成Q的表。

id ticker price
-----------------------
1001 DBT12345678 98.6
1002 EQT98765432 24.7
1004 CCR00000001 121.23

具体操作如下:

q)("JSF"; enlist ",") 0: `:/data/titles.csv
id ticker price
-----------------------
1001 DBT12345678 98.6
1002 EQT98765432 24.7
1004 CCR00000001 121.23

3. 类正则表达式

下面的操作就非常类似我们Python里面的正则表达式了。大家可以慢慢领会一下。

q)"S=;" 0: "one=1;two=2;three=3"
one two three
,"1" ,"2" ,"3"
q)"S:/" 0: "one:1/two:2/three:3"
one two three
,"1" ,"2" ,"3"
q)"I=;" 0: "1=one;2=two;3=three"
1 2 3
"one" "two" "three"
q)flip `k`v!"I=;" 0: "1=one;2=two;3=three"
k v
---------
1 "one"
2 "two"
3 "three"

六、进程通信

Q语言的进程间的通信还是比较简单的,启动通信的一端称为客户端,而接收和处理请求的一端称为服务器。只要可以访问,服务器进程可以位于同一台计算机,同一网络,不同网络或Internet上。通信可以是同步的(等待返回结果)或异步(不等待,也不返回结果)。

这里我们使用同一台机器来测试进程间的通信,我们首先需要打开一个会话,打开的同时我们需要打开端口,其命令如下:

Server端: q -p 5042(端口号) 或者如果已经打开q会话,则输入\p 5042
Client端: hopen `:[Server]:port(端口号)

1. 通信句柄

根据服务器端,大概有四种方式可以来访问我们的服务器。通信句柄为:

`:[服务器]:端口号

1) 本地访问

本地访问我们常见的两种方式为:

::5042或者:localhost:5042

这里的端口号可以根据自己的实际情况来设置。

2) 同一网络访问

同一网络下我们还可以通过PC名称来访问,比如你还有一台电脑的名称叫zhagnsan,则可以通过如下方式访问:

`:zhagnsan:5042

3) IP地址访问

我们也可以通过服务器端的IP地址来访问,其访问形式相似:

`:192.168.0.1:5042

4) URL访问

URL访问方式比较独特,自己可以测试一下:

`:http://www.myrrl.com:5042

下面我们进行一个简单的通信测试(为了方便,我就在同一台机器上进行通信测试)。首先我们需要打开一个Q会话作为服务器端,在cmd中,我们输入q -p 5042。同时再打开一个普通的Q会话我们作为客户端。在客户端,我们通过hopen命令来与我们的服务端进行通信。

简单的进程通信测试

图中两个Q会话窗口中,黑色背景的就是服务器端,白色背景的就是客户端。作为服务器端的会话,打开的时候我们需要加-p 5042的参数。打开后我们在服务器端输入一个a,发现没有这个变量,因此报错。这时我们在我们的客户端通过hopen命令来与我们服务器端取得通信联系。我们通过客户端我们执行一条语句a:6*7(这里需要用引号引起来,作为一个字符串的形式来发送处理,否则会报错),然后会自动的发送到我们的服务器端,我们这时再到服务器端输入一个a,你就会发现此时服务器端就有了a变量的信息。这就完成了一次简单的进程通信。

2. 远程执行

上面我介绍的简单的进程通信方式是非常不安全的,因为执行a:6*7是在我们的服务器端执行,而不是在我们的客户端执行,那说明我们服务器端没有对客户端做一个限制,那很容易被攻击的。

下面我们介绍一种远程的执行方式,我们用一种列表的方式从我们的客户端给我们的服务器端来发送请求。该列表形式为(function; arg1; arg2; ……),这里的function可以是客户端的,也可以是服务器端的。

远程执行

我们首先在我们的服务器端定义一个func的函数,然后在我们的客户端调用这个函数并传递相应的参数(这里函数调用的时候要加symbol符号)。同时我还在我们的服务器端定义一个表t和定义一个表的查询函数f,然后再到我们的客户端通过调用查询函数f来查询服务器端表t的内容。

3. 同步和异步消息

上面我们介绍的都是同步消息,同步消息就意味着在通信进程打开的时候,当客户端发送信息到我们的服务端,客户端进程就会等待服务端返回相应的结果。具体的流程如下:

同步执行流程

当客户端在同步消息传递中向服务器发送多条消息时,在收到上一条消息的结果之前,不会发送下一条消息。因此,消息总是按照发送顺序到达服务器。此外,服务器端的结果按原始消息的发送顺序返回客户端。

也可以在Q中执行异步IPC。在这种情况下,消息被发送到服务端后客户端可以立即继续执行下一条语句,不用等待客户端的返回结果。特别是,服务端没有返回值。当不关心结果时,这对于在服务端上启动任务很有用。

异步执行

图中案例:我们在服务端定义一个sq函数(这里的0N!xx表示显示xx的执行结果),然后我们在我们的客户端使用neg函数来调用服务端的sq函数,当我们执行

neg[h] (sq; 5)的时候我们发现服务端会立即执行sq函数,可能这看不出来异步通信,所以重新在客户端定义一个函数{neg[h] (sq; 8); 88},该函数中第一条语句为向服务端发送信息,第二条为在客户端执行的一条简单语句,我们会发现客户端执行{neg[h] (`sq; 8); 88}[ ]该函数的时候,立即显示了88的结果,没有等待我们服务端返回的64的结果。

由于Q会话默认是单线程的,因此服务器将按接收顺序处理消息。但是,在异步消息传递中,无法保证消息按照发送顺序到达服务器。

4. 消息处理

假设从服务器端向服务端调用客户端函数或服务器端的函数名称,则在服务端上执行相应的函数。在服务端执行期间,客户端进程的通信句柄在系统变量.z.w(“who”被调用)中也是可用。

当客户端和服务端之间的连接打开时,它们都具有连接句柄,这些句柄是独立分配的,它们的int值通常不同的。

下面我们通过一个简单的案例来展示如何使用.z.w将消息发送到客户端上。首先我们在服务端定义一个函数f:{show "Received ",string x; .z.w (mycallback; x+1)},该函数第一条语句为显示收到的参数,第二条语句为回调与服务端通信的客户端函数,并将从客户端传递过来的参数+1后作为新的回调客户端函数的参数。这里执行异步消息传递时,始终使用.z.w`以确保所有消息都是异步的。否则,当每个进程等待另一个进程时,将遇到死锁。

.z.w系统变量测试

我们可以通过将自己的处理程序分配给适当的系统变量来覆盖Q中消息处理的默认行为。如将自己的函数分配给变量.z.pg以捕获和处理同步消息,将自己的函数分配给变量.z.ps以捕获和处理异步消息。名称以'g'和's'结尾,因为同步处理具有“get”语义,异步处理具有“set”语义。

.z.ps系统变量测试

图中我们在服务端定义了两个函数,函数sq为一个普通函数,我们对系统变量.z.ps进行了重新定义(简单的打印ignore到会话窗口中)。当我们在客户端向服务端发送信息的时候,我们的服务端立即执行了系统变量.z.ps,将ignore打印出来。

现在我们将同步处理程序设置为只接受按函数名称“安全”远程调用。然后通过对传递的参数进行判断来保护客户端的执行,从而确保失败的应用程序不会挂起服务器。

.z.pg系统变量测试

图中我们在服务端重新定义了系统变量.z.pg。通过条件语句对传递进来的参数进行了判断。首先判断是否为symbol类型,如果是说明调用的是服务端的函数,这时开始执行.[value first x; 1_x; ::]这条语句,同时开始判断传递的第二个参数,不是函数需要的数据类型,则返回一个type错误;如果不是symbol类型,则返回一个不支持的类型错误。

Q还有很多可以重新定义的系统变量。大家可以研究一下官方网站提供的所有系统变量的用法。https://code.kx.com/v2/ref/dotz/#zpg-get

接下来在介绍一下系统变量.z.po和.z.pc。当进程通信打开的时候,.z.po就会自动处理打开的进程信息,当进程通信关闭的时候,.z.pc就会自动处理关闭的进程信息。我们通过重新定义这两个系统变量来进行测试一下。首先在服务端定义一个表叫Registry,来保存客户端发给服务端的进程通信信息,同时定义一个register函数,该函数将客户端发送给服务端的信息上传到Registry的表中。然后我们重新定义系统变量.z.po,当进程通信打开的时候会将通信通道的编号和`unregistered上传传到Registry表中;同时.z.pc在进程通信关闭的时候将Registry表中的信息进行删除。

.z.po和.z.pc系统变量测试

图中我们可以看到,当我们在客户端打开进程通信的时候,我们的服务端的系统变量.z.po执行了相应的语句;然后当我们在客户端执行相应的服务端函数调用的时候,这个时候我们可以看出表Registry中有信息更新;最后当我们在客户端关闭进程通信的时候,我们服务端的系统变量.z.pc执行了相应的语句,也就是删除了表Registry中的对应信息。

5. 远程查询案例

下面我介绍一个通过客户端来远程查询服务端中表的信息。

远程查询案例

首先我们在服务端导入一个trades的表(前期我通过.Q.en序列化到本地的扩展表)。打开进程通信之后,我们在服务端和客户端都可以查询,但是我们在客户端的查询是通过最原始的方式,这样的方式非常不安全,因此我们可以在我们的服务端定义好查询函数,然后在客户端进行调用和传递相应参数即可。

七、HTTP与WEB的Sockets

(这部分还没完全弄明白,等弄明白了补上)

你可能感兴趣的:(Q语言——I/O操作)