作者:Matthew Flatt

原文:https://docs.racket-lang.org/more/index.html


与快速:通过画图了解Racket给人的印象相不同,Racket并不是花瓶。在DrRacket图形化的外观之下,隐藏这一个复杂的线程和进程管理工具箱,这也是本章的主题。

具体的,我们展示如何构建一个安全的,多线程,可扩展的网络服务。我们会用到比之前更多的语法和函数,你可以点击不认识的语法或函数来查看文档。注意,最后几节的内容被认为是比较难的。如果你是个Racket新手或者没什么编程经验,可以先看看The Racket Guide。

为了领会本教程的精神,我们建议你先将DrRacket放在一边并在终端中使用Racket。你还需要一个文档编辑器和一个浏览器。

1 开始

下载安装DrRacket,打开终端输入racket

$ racket
Welcome to Racket v8.4 [cs].
>

进入Racket命令行,可以输入Racket代码执行,也可以执行命令,更多命令参考官方文档。

2 准备

在启动Racket命令的目录下创建文件"serve.rkt"并输入以下内容:

#lang racket(define (go)'yep-it-works)

3 运行

回到Racket命令行,加载并运行上面的代码:

> (enter! "server.rkt")
> (go)
'yep-it-works

修改"serve.rkt",然后运行(enter! "serve.rkt")重新加载模块,观察输入的变化。

4 Hello World

我们将会使用serve函数实现网络服务,参数中的端口号用来接收客户端连接。

(define (serve port-no)...)

服务器通过listener接收TCP连接,listenertcp-listen函数创建。为了方便交互式开发,我们使用#t作为tcp-listen的第三个参数,它让我们可以马上复用端口号而不用等待TCP超时。

(define (serve port-no)(define listener (tcp-listen port-no 5 #t))...)

服务还需要循环接收客户端连接。

(define (serve port-no)(define listener (tcp-listen port-no 5 #t))(define (loop)(accept-and-handle listener)(loop))(loop))

accept-and-handle函数通过tcp-accept函数接收连接,它会返回两个值,一个输入流,一个输出流。

(define (accept-and-handle listener)(define-values (in out) (tcp-accept listener))(handle in out)(close-input-port in)(close-output-port out))

接下来我们读取输入流并丢弃请求头,并向输出流写入"Hello world"。

(define (handle in out); 丢弃请求头(regexp-match #rx"(\r\n|^)\r\n" in); 发送响应(display "HTTP/1.0 200 OKay\r\n" out)(display "Server: k\r\nContent-Type: text/html\r\n\r\n" out)(display "<html><body>Hello, world!</body></html>" out)

注意,regexp-match函数直接操作输入流,比操作独立的行容易。

将上面三个函数复制到"serve.rkt",并在命令行中重新载入。

> (enter! "serve.rkt")[reloading serve.rkt]
> (serve 8080)

现在,在浏览器中输入http://localhost:8080,你将会看到来自服务端的问候。

5 服务线程

按下ctrl+c结束服务循环,然后重新启动服务。

> (serve 8080)
tcp-listen: listen on 8080 failed (address already in use)

发生错误的原因是listener依然在监听着原来的端口。

为了避免这个问题,我们将监听循环放到一个独立的线程,并让serve函数立即返回一个可以关闭服务线程和TCP监听器的函数。

(define (serve port-no)(define listener (tcp-listen port-no 5 #t))(define (loop)(accept-and-handle listener)(loop))(define t (thread loop))(lambda ()(kill-thread t)(tcp-close listener)))

重新加载并启动服务:

> (enter! "serve.rkt")[re-loading serve.rkt]
> (define stop (serve 8081))

现在服务地址为http://localhost:8081,此时你可以关闭服务并重启它。

> (stop)
> (define stop (serve 8081))
> (stop)
> (define stop (serve 8081))
> (stop)

6 连接线程

同样,我们可以为每个连接创建一个线程。

(define (accept-and-handle listener)(define-values (in out) (tcp-accept listener))(thread(lambda ()(handle in out)(close-input-port in)(close-output-port out))))

现在我们的服务可以同时处理多个线程了。为了验证这一点,可以在调用handle之前插入一行代码(sleep (random 10))。然后在浏览器同时发起多个请求,有些会很快返回,有些会延时,但不同请求的延时只取决于发起请求的时间。

7 结束连接

一个恶意的客户端连上服务器后可能不会发送HTTP请求头,导致连接线程一直空转。为了避免这种情况,我们希望给每个连接线程实现超时。

一种方式是创建另一个线程,等待10秒后结束调用handle的线程。Racket中的线程十分轻量,所以这种方式也可以工作良好。

(define (accept-and-handle listener)(define-values (in out) (tcp-accept listener))(define t (thread(lambda ()(handle in out)(close-input-port in)(close-output-port out)))); 监控线程(thread (lambda ()(sleep 10)(kill-thread t))))

上面的代码并不完全正确,因为但线程结束后,inout流依然是打开的。Racket提供了一种通用的资源关闭机制:custodian。custodian(监管器)是一种除内存外的资源的容器,通过custodian-shutdown-all函数来关闭容器内的所有资源,包括线程,流或其他有限的资源。

当创建一个线程或流时,它会被放入由current-custodian参数所指定的监管器中。为了将连接相关的资源都放入监管器,我们将资源创建进行参数化。

(define (accept-and-handle listener)(define cust (make-custodian))(parameterize ([current-custodian cust])(define-values (in out) (tcp-accept listener))(thread (lambda ()(handle in out)(close-input-port in)(close-output-port out)))); 监控线程(thread (lambda ()(sleep 10)(custodian-shutdown-all cust))))

现在inout以及调用handle的线程都属于cust。而且handle函数中创建的资源,比如打开文件,也属于cust,因此也会在cust关闭是被一起释放。

serve函数也可以使用custodian关闭资源。

(define (serve port-no)(define main-cust (make-custodian))(parameterize([current-custodian main-cust])(define listener (tcp-listen port-no 5 #t))(define (loop)(accept-and-handle listener)(loop))(thread loop))(lambda ()(custodian-shutdown-all main-cust)))

main-cust不仅拥有TCP监听器和主服务线程,还拥有每个连接创建的custodian。因此现在的服务关闭程序会立刻关闭所有活跃的连接,以及主服务循环。

更新你的代码,在Racket命令行输入以下内容:

> (enter! "serve.rkt")[re-loading serve.rkt]
> (define stop (serve 8081))
> (define-values (cin cout) (tcp-connect "localhost" 8081))

10秒钟之后尝试从cin读取数据,你就会看到服务端已经关闭了这个连接。

> (read-line cin)#<eof>

如果你立刻停止服务程序,不必等10秒也能看到连接被关闭。

> (define-values (cin2 cout2) (tcp-connect "localhost" 8081))
> (stop)
> (read-line cin2)#<eof>

8 路由转发

本章让我们实现一个简单的路由分发函数来处理来自不同URL的请求。

为了解析URL和格式化HTML输出,我们需要依赖下面两个库。

(require xml net/url)

xml库提供了xexpr->string函数将xexpr表达式转化为HTML。

> (xexpr->string '(html (head (title "hello")) (body "Hi!")))
"<html><head><title>Hello</title></head><body>Hi!</body></html>"

假设dispatch函数接收一个URL作为参数并返回一个xexpr表达式。

(define (handle in out)(define req; 提取请求URL(regexp-match #rx"^GET (.+) HTTP/[0-9]+\\.[0-9]+"(read-line in)))(when req; 丢弃剩下的请求头(regexp-match #rx"(\r\n|^)\r\n" in); 转发(let ([xexpr (dispatch (list-ref req 1))]); 发送响应(display "HTTP/1.0 200 Okay\r\n" out)(display "Server: k\r\nContent-Type: text/html\r\n\r\n" out)(display (xexpr->string xexpr) out))))

net/url库提供了string->urlurl-pathpath/param-pathurl-query函数用来提取URL中的各个部分。

> (define u (string->url "http://localhost:8080/foo/bar?x=bye"))
> (url-path u)
(list (path/param "foo" '()) (path/param "bar" '()))
> (map path/param-path (url-path u))
'("foo" "bar")
> (url-query u)
'((x . "bye"))

路径和处理器函数通过哈希表存储。

(define (dispatch str-path); 将请求转化为URL(define url (string->url str-path)); 提取路径(define path (map path/param-path (url-path url))); 查找处理器(define h (hash-ref dispatch-table (car path) #f))(if h; 调用处理器(h (url-query url)); 没有处理器`(html (head (title "Error"))(body(font ((color "red"))"Unknown page: ",str-path)))))(define dispatch-table (make-hash))

现在启动并访问服务器,你将看到一个错误页面。

我们可以通过下面的代码为"hello"绑定一个处理器。

(hash-set! dispatch-table "hello"(lambda (query)`(html (body "Hello, World!"))))

使用(enter! "serve.rkt")重启服务器,打开http://localhost:8081/hello,就可以看到最初的页面了。

9 Servlet和Session

使用查询参数,处理器可以响应用户通过表单提供的值。

下面的函数可以创建一个HTML表单。label参数是一个用于显示的字符串,next-url参数表示表单提交的地址,hidden参数告诉表单是否隐藏表单项。

(define (build-request-page label next-url hidden)`(html(head (title "Enter a Number to Add"))(body ([bgcolor "white"])(form ([action ,next-url] [method "get"]),label(input ([type "text"] [name "number"][value ""]))(input ([type "hidden"] [name "hidden"][value ,hidden]))(input ([type "submit"] [name "enter"][value "Enter"]))))))

使用上面的帮助函数,我们可以创建用户指定数量的"Hello"字符串。

(define (many query)(build-request-page "Number of greetings:" "/reply" ""))(define (reply query)(define n (string->number (cdr (assq 'number query))))`(html (body ,@(for/list ([i (in-range n)])" hello"))))(hash-set! dispatch-table "many" many)
(hash-set! dispatch-table "reply" reply)

重启服务器,在浏览器输入http://localhost:8081/many,输入一个数字,你将看到一个新页面。

10 限制内存使用

在上一个例子中,用户可以输入一个非常大的数字导致服务器内存耗尽。事实上,客户端发送一个巨大的HTTP请求也会导致类似的问题。

这类问题的解决方案是限制连接使用的内存。在accept-and-handlecust定义之后,加上以下代码:

(custodian-limit-memory cust (* 50 1024 1024))

我们假设50M对于所有Servlet都已足够。垃圾收集器开销意味着系统实际使用的内存可能是50M的好几倍。但是重要的是不同连接之间不会相互影响。

加上上面的代码,上个例子的问题就解决了。

"many"的例子只是冰山一角。除了限制处理时间和内存消耗之外,还有许多安全问题。Racket的racket/sandbox库为所有安全问题提供了支持。

11 Continuations

作为系统编程的例子,网络服务展现出了许多系统和安全的问题。示例也引出了Racket另一个经典话题:Continuations。事实上,在这方面,网络服务也需要受限的Continuations。

Continuations实在不好翻译,大概意思是说可以中断某个函数,然后再后面又回到中断的地方继续执行。但它又不同于函数调用,因为这个函数从中断的地方就已经结束了。

Continuations所解决的问题是session和计算跨连接的用户输入[Queinnec00]。这个问题的正解是在客户端计算,但许多问题也能通过其他技术很好的解决(比如利用浏览器的回退按钮)。

随着跨连接计算越来越复杂,通过URL传递参数也变得繁琐。例如,我们实现一个两数相加的服务,通过隐藏表单来记录第一个数。

(define (sum query)(build-request-page "First number:" "/one" ""))(define (one query)(build-request-page "Second number:""/two"(cdr (assq 'number query))))(define (two query)(let ([n (string->number (cdr (assq 'hidden query)))][m (string->number (cdr (assq 'number query)))])`(html (body "The sum is " ,(number->string (+ m n))))))(hash-set! dispatch-table "sum" sum)
(hash-set! dispatch-table "one" one)
(hash-set! dispatch-table "two" two)

虽然上面的代码可以运行,但是我们希望以一种更直观的方式书写代码:

(define (sum2 query)(define m (get-number "First number:"))(define n (get-number "Second number:"))`(html (body "The sum is " ,(number->string (+ m n)))))(hash-set! dispatch-table "sum2" sum2)

问题是get-number必须为当前连接返回一个HTML响应(输入数字的表单页面),并通过一个新的连接获得响应(用户输入的数字)。也就是说,它需要将build-request-page生成的页面转化为查询结果。

(define (get-number label)(define query... (build-request-page label ...) ...)(number->string (cdr (assq 'number query))))

Continuations让我们可以实现这一操作。send/suspend函数首先生成一个URL表示当前计算,同时作为continuation。然后它将生成的URL传递给一个用来生成页面的函数,该页面会作为当前连接的响应,而continuation则被终止。最后send/suspend从生成的URL的请求(在一个新连接中)中恢复中断的计算。

此时,get-number实现如下:

(define (get-number label)(define query; 为当前计算生成一个URL(send/suspend; k-url就是生成的URL(lambda (k-url); 为当前连接生成页面(build-request-page label k-url "")))); 在新的连接中请求生成的URL时,回到这里(string->number (cdr (assq 'number query))))

实现send/suspend之前,需要先引入一个依赖。

(require racket/control)

我们使用prompt来标记servlet启动的位置,然后就可以在这里终止它了。修改handle函数如下:

(define (handle in out)....(let ([xexpr (prompt (dispatch (list-ref req 1)))])....))

现在让我们来实现send/suspend函数。我们使用let/cc捕获当前计算到一个prompt,并将它绑定到k

(define (send/suspend mk-page)(let/cc k...))

第二步生成一个URL,并注册到分发器。

(define (send/suspend mk-page)(let/cc k(define tag (format "k~a" (current-inexact-milliseconds)))(hash-set! dispatch-table tag k)...))

最后我们终止当前计算,将mk-page生成的页面作为响应返回。

(define (send/suspend mk-page)(let/cc k(define tag (format "k~a" (current-inexact-milliseconds)))(hash-set! dispatch-table tag k)(abort (mk-page (string-append "/" tag)))))

当用户提交表单时,与表单URL关联的处理器是作为continuation的旧计算。调用continuation会恢复旧计算,并将参数传递会该计算。

更新代码并运行,在浏览器输入http://localhost:8081/sum2。

不过我在想,dispatch-table真的不会爆炸吗?

12 下一步

Racket发行版包含一个可用于生产环境的网络服务,它实现了本文提到了所有问题。更多参考继续:Racket网络编程,文档Web Applications in Racket,或者论文Krishnamurthi07。

如果你因为入门走到这里,那么接下来可以看The Racket Guide。

如果你对文中的话题感兴趣,可以看The Racket Reference的Concurrency and Parallelism和Reflection and Security两章。

参考论文:

GRacket[Flatt99]

内存计数[Wick04]

安全删除[Flatt04]

delimited continuations [Flatt07]

更多:Racket系统编程相关推荐

  1. 外网访问arm嵌入式linux_嵌入式Linux系统编程——文件读写访问、属性、描述符、API

    Linux 的文件模型是从 Unix 的继承而来,所以 Linux 继承了 UNIX 本身的大部分特性,然后加以扩展,本章从 UNIX 系统接口来描述 Linux 系统结构的特性. 操作系统是通过一系 ...

  2. linux系统发送信号的系统调用是,linux系统编程之信号:信号发送函数sigqueue和信号安装函数sigaction...

    信号发送函数sigqueue和信号安装函数sigaction sigaction函数用于改变进程接收到特定信号后的行为. sigqueue()是比较新的发送信号系统调用,主要是针对实时信号提出的(当然 ...

  3. IA-32系统编程指南 - 第三章 保护模式的内存管理【1】

    第三章 保护模式的内存管理[1] [作者:lion3875 原创文章 参考文献<Intel 64 and IA-32 system programming guide>] IA-32保护模 ...

  4. UNIX系统编程(1)

    注:本文来自"网易"博主,仅阅读,学习 第一章:什么是系统编程  UNIX系统编程,简单的说就是"C语言+系统调用(system call)",学会了C语言再知 ...

  5. C语言嵌入式系统编程修炼

    C语言嵌入式系统编程修炼之内存操作篇 数据指针 在嵌入式系统的编程中,常常要求在特定的内存单元读写内容,汇编有对应的MOV指令,而除C/C++以外的其它编程语言基本没有直接访问绝对地址的能力.在嵌入式 ...

  6. linux线程并不真正并行,Linux系统编程学习札记(十二)线程1

    Linux系统编程学习笔记(十二)线程1 线程1: 线程和进程类似,但是线程之间能够共享更多的信息.一个进程中的所有线程可以共享进程文件描述符和内存. 有了多线程控制,我们可以把我们的程序设计成为在一 ...

  7. 实验六 Linux进程编程,Linux系统编程实验六:进程间通信

    <Linux系统编程实验六:进程间通信>由会员分享,可在线阅读,更多相关<Linux系统编程实验六:进程间通信(10页珍藏版)>请在人人文库网上搜索. 1.实验六:进程间通信l ...

  8. C语言嵌入式系统编程修炼之道——屏幕操作篇

    C语言嵌入式系统编程修炼之道--屏幕操作篇 作者:宋宝华  e-mail:[email]21cnbao@21cn.com[/email] 1.汉字处理 现在要解决的问题是,嵌入式系统中经常要使用的并非 ...

  9. 【Linux系统编程】进程间通信之无名管道

    00. 目录 文章目录 00. 目录 01. 管道概述 02. 管道创建函数 03. 管道的特性 04. 管道设置非阻塞 05. 附录 01. 管道概述 管道也叫无名管道,它是是 UNIX 系统 IP ...

  10. 【Linux系统编程】特殊进程之守护进程

    00. 目录 文章目录 00. 目录 01. 守护进程概述 02. 守护进程查看方法 03. 编写守护进程的步骤 04. 守护进程代码 05. 附录 01. 守护进程概述 守护进程(Daemon Pr ...

最新文章

  1. 合作方变股东:Aurora无人车获现代汽车3千万美元投资,与大众分手
  2. python课程与c+课程有什么不同-南通渡课少儿编程:python和C的区别是什么?
  3. 渥太华大学计算机工程,渥太华大学电气与计算机工程硕士专业.pdf
  4. php redis 源码分析,从源码中分析关于phpredis中的连接池可持有数目
  5. Apollo自动驾驶入门课程第①讲—无人驾驶概览
  6. vijos 1057 盖房子 dp 最大子正方形
  7. 【今日CV 计算机视觉论文速览】Tue, 12 Mar 2019
  8. 模板模式与策略模式/template模式与strategy模式/行为型模式
  9. unity下载与安装
  10. 一箭N雕:多任务深度学习实战
  11. wps设置页码,从某一页重新开始编号
  12. 语音计算机在线算使用方法,计算器在线计算
  13. 网络攻防技术(摆烂一天)
  14. DFS入门级(模板)
  15. 苹果ANCS协议分析
  16. HRBUST-1022 JiaoZhu and SC(C语言)
  17. 转载为什么USART的RX和TX和SPI的MISO、MOSI都被配置成推挽输出,他们还能正常工作
  18. 尘锋信息scrm与企鲸客的功能差别
  19. 关于qq邮箱 该文件已达到200次的下载限制,您已不能下载该文件 的问题处理
  20. 万字长文--详解AJAX(快速入门)

热门文章

  1. matlab 图形对称,Matlab关于直线为轴对称与点为中心对称的图形代码
  2. 成功注册一个谷歌账号
  3. C# 字节(数组)与位之间的计算
  4. DOM操作简易年历案例
  5. 外文文献查找技巧方法有哪些
  6. 计算机打印机删除文件,怎么取消打印机文档|打印机任务无法删除解决方法
  7. 五一博客连载——毕业游记录
  8. bandizip修改压缩文件内容_BandiZip如何进行解压缩文件?BandiZip解压缩流程
  9. 二极管的分类、电路符号及万用表测发光二极管正负极
  10. html 给word插入页眉和页脚,如何在Word插入页眉和页脚