Tcl/Tk:Web 应用程序的瑞士军刀

作者:Bill Schongar

虽然许多人认为 Tcl/Tk(工具命令语言,发音为“tickle”;Tk 代表 TCL 工具包)是跨平台 GUI(图形用户界面)应用程序的强大工具,但没有多少人考虑将其用于 Web 编程任务。 事实是,它的易用性和灵活性使其成为 CGI(通用网关接口)、服务器解析嵌入式脚本、通过插件交付的应用程序以及甚至从头开始创建自己的 Web 服务器的工具的自然选择。

我将介绍在这些情况下 Tcl 工作的示例,以及 Tcl 可以用于任何 Web 编程任务的一些想法和附加方法。

Tcl 中的 CGI 基础知识

读取环境变量和打印到标准输出是 CGI 的构建块。 当然,您希望处理传入的数据、运行有趣的进程并格式化输出,但首先要处理基本的事情。 通过了解任何给定语言如何执行基本任务,您可以开始构建实际执行某些操作的东西,而无需花费大量时间陷入细节。

列表 1 是一个简单的 Tcl 脚本,它读取 Web 服务器设置的环境变量,并将它们作为 HTML 输出发送回用户。 首先要注意的是用于解释脚本的 shell 的选择。 Tcl/Tk 发行版包括两个 shell:wishtclshwish 是“窗口”shell,它带有初始化 GUI 功能的开销,消耗的系统资源是其仅限命令行的对应程序的两倍以上。 对于本文,我假设您正在运行 8.0 或更高版本的 Tcl。(通过运行 tclsh,然后键入 info tclversion 来检查。)

接下来,发送一个标准标头以让浏览器知道正在传入的信息类型。 在列表 1 脚本中,浏览器了解到数据是 HTML 文本而不是纯文本、图像或其他完全不同的内容。 puts 是 Tcl 等效于 Perl print 或 C printf,带有 \n 用于换行符,并且输出可以重定向到文件而不是默认标准输出 (stdout)。 因此,一旦脚本告诉浏览器输入是 HTML,它就会提供 HTML,在本例中是标题标记和文本。

要打印出每个环境变量,我们需要生成一个现有变量的列表,然后循环遍历并写出名称及其值。 这样做需要更好地理解 Tcl 如何处理变量和执行命令。 首先,Tcl 不太关心变量中存储的数据类型——文本或任何长度的数字都可以。 其次,Tcl 中的变量有三种形式:单个变量、数组和格式化列表。 要了解这些是如何工作的,请查看以下代码行

set foo "123abc"
set junk(1) "a"
set junk(2) "b"
set bar "a b c d e f g h i j"

set 函数用于创建或修改变量的值。 在这种情况下,foo 是单个变量,而 junk 是数组,bar 是列表。 使 bar 成为列表的原因是每个字符都与其他字符用空格隔开,允许某些 TCL 函数自动解析数据。

当您在 Tcl 程序中看到方括号中的内容时,通常用于执行方括号中包含的内容,并将返回值替换为整个表达式。 例如,如果我有一个名为 countdown 的函数,它返回到 2000 年剩余的秒数,我可以轻松地通过在我的 Tcl 代码中编写 puts [countdown] 来显示该结果。 如果它返回 300,则会打印数字 300。 当然,这个一般规则也有例外,当我们解析用户输入时,我们会遇到其中一个例外。 所以,这一行

set mylist [array names env]

意思是“创建一个名为 mylist 的变量,并将其设置为命令 array names env 的结果”。

数组对于相关信息组很有用,例如环境变量。 “array”函数组允许您搜索数组元素的名称,以及许多其他有用的操作。 执行命令 array names junk 将创建一个数组中所有元素名称的列表,现在将是“1 2”。 通过告诉 Tcl 您想要查看 env 数组,您可以获得服务器设置的所有环境变量的名称列表,并将该列表存储在变量 mylist 中。

通过 foreach 命令可以轻松地循环遍历 mylist 中的每个项目。 它会自动解析 Tcl 列表,并将当前元素的值分配给您指定的变量名。 在列表 1 中,每个环境变量的名称将在其时间到来时存储在 foo 中,并且 puts 行将显示变量的名称,因为它最初出现,后跟其解释值。 “$”符号向 Tcl 指示这是一个变量,应替换其值。

处理用户数据

由于您希望做的不仅仅是打印出一些环境变量或静态数据,因此这里介绍如何在 Tcl 中处理传入数据。 第一步涉及访问环境变量,我们已经介绍过。 这将告诉我们数据是通过 GET 方法传入,存储在 QUERY_STRING 环境变量中,还是作为 POST 存储在标准输入中。 让我们使用 列表 2 中显示的先前程序的稍微修改版本来找出答案。 是的,这个程序要长得多。 然而,额外的长度来自于处理获取用户输入、格式化并将其存储在数组中。 一旦我解释了这些部分是如何工作的,我将向您展示一种更短的编写此程序的方法。

解析 (cgiParse) 和解码 (urlDecode) 过程取自 Brent Welch 的 Practical Programming in Tcl/Tk,并进行了少量修改。 解析例程相当简单。 它确定数据是存储在 QUERY_STRING 还是标准输入中,然后将其存储到名为 text 的变量中。

特殊字符可能会给 CGI 程序带来问题,因此服务器将百分号和斜杠编码为其十六进制等效项,并将空格编码为加号。 您的程序必须将数据转换为其原始形式。 在 C 和其他语言中执行此操作可能很困难,但 Tcl 使其相当容易。 正如您所看到的,每次处理数据时,无论是变量的名称还是值,该数据都会发送到 urlDecode。 在那里,regsub 命令发挥了它的魔力。 要使用它,请指定(按此顺序)搜索模式、原始数据、其替换以及存储它的变量。

请注意,cgiParse 过程末尾的 foreach 循环正在做我们之前的 foreach 循环没有做的两件事。 首先,它为每个循环指定多个临时值以供使用。 也就是说,第一次通过循环时,列表的元素 1 将存储在 name 中,元素 2 将存储在 value 中。 下一个循环将使用元素 3 和 4,依此类推。 可以用这种方式指定任意数量的变量,这使得处理长列表变得轻而易举。 它所做的第二件不同的事情是直接使用命令的结果作为列表,而不是先创建一个变量来保存列表。 您可以采用任一方式,但在本例中,您节省了一点处理时间和内存。

一旦解析和解码库被布置好,程序就开始运行。 内容标头被发送到浏览器,并且运行 cgiParse 以将所有用户输入的值(来自表单或其他方式)存储到变量数组 cgi 中。 然后它循环遍历 cgi 数组中的每个元素,并打印出所有元素的名称和值。

解析函数设置方式的一个好处是您可以在命令行上测试用户输入值。 由于它不依赖于查找 GET 或 POST 方法,它将尽可能从任何地方获取数据,默认为命令行。 因此,您可以在将 cgi 脚本上传到服务器之前轻松地对其进行测试,而无需创建复杂的包装器来设置环境变量。

函数库——您自己的和所有其他人的

Tcl 过程或 procs 是您的子例程。 如果您创建了一些 procs,您可以轻松地将它们放在自己的 Tcl 脚本中,然后使用 source 命令加载这些脚本,以便它们可以随时使用。 为了最大限度地减少您的代码,您可能希望使用列表 2 中显示的 cgiParse 和 urlDecode 例程。 如果您将它们保存为“cgistuff.tcl”,您可以将列表 2 中的脚本重写为

#!/usr/bin/tclsh
source cgistuff.tcl
puts "Content-type: text/html \n\n"
cgiParse
foreach foo [array names cgi] {
   puts "Variable: $foo Value: $cgi($foo)"
}

source 命令加载并执行 Tcl 脚本,因此请注意,不要在该脚本中隐藏任何过程之外的不需要的命令。

但是,在您开始编写过多的自己的过程之前,您需要查看已有的内容。 许多有才华的人花费时间和精力编写了文档完善、功能非常强大的过程库,例如 Don Libes 的 cgi.tcl 库,它涵盖了从基本解析到 cookie 和文件上传的所有内容。 (见资源。)

数据处理

有时您需要的数据不是来自用户。 产品目录、地图、时间表等等都来自某种外部数据文件,无论是像 Oracle 或 Informix 这样的真实数据库,还是像分隔的 ASCII 文件这样简单的东西。 无论您的数据需求如何,Tcl 都可以帮助您。

平面文件是最容易处理的。 打开一个文件,逐行读取直到到达末尾或找到您正在寻找的内容,然后关闭文件。 在 Tcl 中,逐行读取文件看起来像这样

set f [open foo.txt r]
while {[gets $f stuff] != -1} {
  # Do something with the line
  # of data
  (`stuff')
}
close $f

就像在 Perl 或 C 中一样,您创建一个文件句柄,所有后续操作都从中工作。 gets 命令一次从文件中抓取一行并将其存储到变量中。 如果 gets 的返回值是 1,则表示您已到达文件末尾。 那么一旦您拥有了数据,您会怎么做呢? 在大多数情况下,您将很快熟悉 splitlindex 命令。

split 打断一个字符串,无论是逐字符还是在每次出现指定字符时,并返回元素的新列表。 如果您想访问列表的特定元素,lindex 允许您指定列表和元素的位置,并返回该元素的值。 请注意,元素的编号从 0 开始,因此索引值 1 指向列表中的第二个元素。

在工作量方面更高一点的是处理特殊的数据库格式,例如 dBASE 文件或某些其他定义的数据库格式。 您可能很幸运能够找到这种文件的现有过滤器(dBASE 文件存在两种不同的过滤器),但是如果您需要编写自己的过滤器,Tcl 8.0 可以很好地处理二进制数据。 使用 read 命令抓取您想要的任何大小的字节块,然后使用 binary scan 快速分解和格式化它。

如果您担心速度或已经有 C 例程来解析您的外部数据,Tcl 可以轻松地创建封装在可加载库中的新 Tcl 命令。 对于大多数函数,它就像剪切并粘贴到 Tcl 提供的库框架中,并添加一些特定于 Tcl 的命令来创建或设置变量一样容易。

当您进入数据库世界的顶端并处理 Oracle 或 Informix 时,您已经得到了保障。 Tcl 扩展已为 Oracle、Informix 以及可能在您阅读本文时已为其他数据库完成。 它们中的大多数提供对数据库 SQL 层的访问,但您也可以访问系统的较低级别的功能。 它们都可以在线获得,尽管编译它们有时需要访问 RDBMS 附带的商业库。

客户端-服务器 CGI

基本 CGI 的一个问题是它不提供真正的持久性。 当然,您可以使用 cookie、服务器端基于文件的数据或将非常长的字符串附加到 URL,但这些都不是理想的解决方案。 此外,如果您正在从数据库加载库存数据等内容,则每次运行脚本时都必须考虑初始化时间和开销。

在某些情况下,最好的解决方案是运行辅助服务器进程,该进程以真正的客户端-服务器方式通过套接字与 Tcl 共享数据。 这样,您的服务器可以加载所需的信息并成为持久数据存储,用于任何目的。 在 Tcl 中,这是一项比您想象的更容易的任务。 虽然实际代码太长而无法包含在本文中,但我将在此处包含一个概述,并且很乐意通过电子邮件 (bills@multimedia.com) 提供更多详细信息。

Tcl 中的套接字设计为易于使用。 socket 命令供希望在端口上建立监听站点的应用程序以及希望连接到任何服务器(Tcl 或其他服务器)的客户端使用。 您如何在端口上监听? 只需使用

socket -server sayHello 9999

现在您有一个服务器在端口 9999 上监听,每当有新客户端连接时,它都会执行 Tcl 过程 sayHello。 如果您想要异步套接字怎么办? 使用

socket -server -async sayHello 9999
当客户端想要连接到您时,他们只需使用套接字命令指向您的 IP 地址和端口
socket 10.0.0.1 9999
sayHello 执行时,它接收三个参数:您的 Tcl 服务器为客户端打开的套接字通道、客户端的 IP 地址和客户端的端口。 您可以为套接字通道配置您想要的缓冲和阻塞类型,并且您通常会为通道设置一个 fileeventfileevent 用于在通道变得可读或可写时(您的选择或同时使用两者)生成通知,以便您不必一直轮询套接字以获取新数据。 现在您和客户端已准备好交换信息。

因此,一旦您决定了服务器将执行的操作,您的 CGI 程序可以像往常一样解析数据,快速建立套接字连接,然后让服务器处理信息。

扩展客户端——Tcl/Tk 插件

对于某些项目,您可能希望做超出浏览器支持范围的事情。 通过为最终用户提供插件,您可以获得在现有浏览器中运行真实应用程序的好处,而不会遇到太多麻烦。 大多数插件的一个缺点是它们仅在 Microsoft Windows 下运行,这使得它们不适合真正的跨平台工作。 Tcl 的插件没有这个问题——您可以下载适用于 Linux、Solaris、SunOS 甚至 MS Windows 的预编译二进制文件。 您可能还会发现,在您阅读本文时,它也已移植到其他平台,例如 Macintosh OS。

使用插件,您可以运行 Tclets,它是小型 Tcl/Tk 脚本,在受限制的(出于安全原因)Tcl 环境中运行。 您和您的用户可以定义您希望插件提供多少访问权限,消除或重新路由可能危害机器健康状况的命令和情况。

一旦您创建了 Tclet 并且您的用户拥有了 Tcl 插件,请使用

<EMBED SRC>

标签在 HTML 页面中引用它。 因此,如果您的 Tclet 名为 foo.tcl,则标签将如下所示

<embed src="foo.tcl" width=400 height=300>
如果您想知道已经制作了哪些类型的插件来利用插件,请访问 http://www.tcltk.com/tclets/,其中包含从俄罗斯方块克隆到自适应光学演示和 VRML 编辑器的一切内容。
使用 Tcl 扩展服务器——服务器解析的 Tcl 及更多

服务器解析的 HTML 已经存在一段时间了,范围从基本服务器端包含 (SSI) 到完整的集成环境,包括数据库访问。 它提供了动态生成的 HTML 页面,而无需调用外部 CGI 程序的开销,并且即使对于非程序员来说,也可以轻松访问它提供的所有功能。

通常,当引用具有特殊扩展名(例如 .foo)的文件时,服务器会扫描 HTML 并查找特殊标记。 当找到这些标记时,它会执行它们包含的任何指令,然后用来自命令的输出替换文档中的这些部分。 这些标记可以是任何内容,从当前日期到带有产品价格列表的动态生成的 HTML 表格。

存在几种使用 Tcl 作为服务器解析脚本语言的解决方案。 两个最强大的商业产品是 NeoSoft 的 NeoWebScript 和 Binary Evolution 的 Velocigen for Tcl。 这两种产品都使用进程内模块扩展了 Apache Web 服务器,因此它们始终以等待模式运行,随时准备工作。 两者之间的一个主要区别是,虽然 Velocigen 遵循使用特殊文件扩展名来标识需要解析的文件的常见趋势,但 NeoWebScript 遵循更传统的 SSI 结构,将命令嵌入到注释中。 示例显示在列表 3列表 4 中。

使用这些更高级的服务器端解析器,您还可以通过内部变量获得一定程度的数据持久性。 例如,您可以在您的网站上进行网络寻宝活动,以保留已访问页面的列表,当特定用户看到所有必需的页面时,该用户获胜。 赢得什么? 我不知道——让营销部门担心吧。

Tcl 中的 Web 服务器

您将无法外出与 Apache 竞争市场份额,但是用 Tcl 创建的 Web 服务器易于编写、可扩展且可跨所有平台移植。 正如我们之前看到的,套接字在 Tcl 中很容易实现,这让您有更多时间专注于自定义服务器以满足您的需求,而不是花费时间让基本知识工作起来。

如果您想了解这个概念的一个很好的实现,请查看 Scriptics 免费提供的 Tcl-HTTPD。 它具有 CGI 支持、服务器解析脚本和许多动态配置选项,仅举几个方面。 更多基本示例也可以从 Web 上的各种 Tcl 来源获得,以及 Steve Ball 的一篇优秀文章和 Brent Welch 的一份白皮书。 (见资源。)

结论

Tcl 提供了一种解决几乎所有 Web 编程问题的简便方法。 凭借庞大的开发社区、广泛的扩展选择和免费的功能库,它是一个等待被发现的 Web 强大工具。 无论是客户端还是服务器端,您都可以获得很多选择,而无需太多麻烦。

资源

Bill Schongar 通常会盯着屏幕——写作、玩游戏或实际做他的 LCD Multimedia 高级开发人员的工作。 如果没有,他就会和马匹和中世纪服装一起离开。 您可以通过 bills@lcdmultimedia.com 联系他,提出任何问题、意见或随意的想法。

加载 Disqus 评论