更多:Racket系统编程
作者: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连接,listener
由tcp-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))))
上面的代码并不完全正确,因为但线程结束后,in
和out
流依然是打开的。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))))
现在in
,out
以及调用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->url
,url-path
,path/param-path
和url-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-handle
的cust
定义之后,加上以下代码:
(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系统编程相关推荐
- 外网访问arm嵌入式linux_嵌入式Linux系统编程——文件读写访问、属性、描述符、API
Linux 的文件模型是从 Unix 的继承而来,所以 Linux 继承了 UNIX 本身的大部分特性,然后加以扩展,本章从 UNIX 系统接口来描述 Linux 系统结构的特性. 操作系统是通过一系 ...
- linux系统发送信号的系统调用是,linux系统编程之信号:信号发送函数sigqueue和信号安装函数sigaction...
信号发送函数sigqueue和信号安装函数sigaction sigaction函数用于改变进程接收到特定信号后的行为. sigqueue()是比较新的发送信号系统调用,主要是针对实时信号提出的(当然 ...
- IA-32系统编程指南 - 第三章 保护模式的内存管理【1】
第三章 保护模式的内存管理[1] [作者:lion3875 原创文章 参考文献<Intel 64 and IA-32 system programming guide>] IA-32保护模 ...
- UNIX系统编程(1)
注:本文来自"网易"博主,仅阅读,学习 第一章:什么是系统编程 UNIX系统编程,简单的说就是"C语言+系统调用(system call)",学会了C语言再知 ...
- C语言嵌入式系统编程修炼
C语言嵌入式系统编程修炼之内存操作篇 数据指针 在嵌入式系统的编程中,常常要求在特定的内存单元读写内容,汇编有对应的MOV指令,而除C/C++以外的其它编程语言基本没有直接访问绝对地址的能力.在嵌入式 ...
- linux线程并不真正并行,Linux系统编程学习札记(十二)线程1
Linux系统编程学习笔记(十二)线程1 线程1: 线程和进程类似,但是线程之间能够共享更多的信息.一个进程中的所有线程可以共享进程文件描述符和内存. 有了多线程控制,我们可以把我们的程序设计成为在一 ...
- 实验六 Linux进程编程,Linux系统编程实验六:进程间通信
<Linux系统编程实验六:进程间通信>由会员分享,可在线阅读,更多相关<Linux系统编程实验六:进程间通信(10页珍藏版)>请在人人文库网上搜索. 1.实验六:进程间通信l ...
- C语言嵌入式系统编程修炼之道——屏幕操作篇
C语言嵌入式系统编程修炼之道--屏幕操作篇 作者:宋宝华 e-mail:[email]21cnbao@21cn.com[/email] 1.汉字处理 现在要解决的问题是,嵌入式系统中经常要使用的并非 ...
- 【Linux系统编程】进程间通信之无名管道
00. 目录 文章目录 00. 目录 01. 管道概述 02. 管道创建函数 03. 管道的特性 04. 管道设置非阻塞 05. 附录 01. 管道概述 管道也叫无名管道,它是是 UNIX 系统 IP ...
- 【Linux系统编程】特殊进程之守护进程
00. 目录 文章目录 00. 目录 01. 守护进程概述 02. 守护进程查看方法 03. 编写守护进程的步骤 04. 守护进程代码 05. 附录 01. 守护进程概述 守护进程(Daemon Pr ...
最新文章
- 合作方变股东:Aurora无人车获现代汽车3千万美元投资,与大众分手
- python课程与c+课程有什么不同-南通渡课少儿编程:python和C的区别是什么?
- 渥太华大学计算机工程,渥太华大学电气与计算机工程硕士专业.pdf
- php redis 源码分析,从源码中分析关于phpredis中的连接池可持有数目
- Apollo自动驾驶入门课程第①讲—无人驾驶概览
- vijos 1057 盖房子 dp 最大子正方形
- 【今日CV 计算机视觉论文速览】Tue, 12 Mar 2019
- 模板模式与策略模式/template模式与strategy模式/行为型模式
- unity下载与安装
- 一箭N雕:多任务深度学习实战
- wps设置页码,从某一页重新开始编号
- 语音计算机在线算使用方法,计算器在线计算
- 网络攻防技术(摆烂一天)
- DFS入门级(模板)
- 苹果ANCS协议分析
- HRBUST-1022 JiaoZhu and SC(C语言)
- 转载为什么USART的RX和TX和SPI的MISO、MOSI都被配置成推挽输出,他们还能正常工作
- 尘锋信息scrm与企鲸客的功能差别
- 关于qq邮箱 该文件已达到200次的下载限制,您已不能下载该文件 的问题处理
- 万字长文--详解AJAX(快速入门)