WCF 中,最让我感到兴奋的方面是它可以把服务托管在任意程序中,而不一定是网站服务器。这开启了一种可能性,即创建的服务,其实现可以动态改变的能力,因为它们能够托管在 fsi.exe。为了使前面的示例能够运行在 fsi.exe,还需要对它做一些修改,但修改相当简单。
清单 11-13 展示了清单 11-9修改后的版本,它能够运行在 fsi.exe。
清单 11-13 托管在 F#交互中的服务
#I @"C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0";;
#r "System.ServiceModel.dll";;
open System
open System.ServiceModel
open System.Runtime.Serialization
// a range of greetings that could be used
let mutable f = (fun x -> "Hello: " + x)
f <- (fun x -> "Bonjour: " + x)
f <- (fun x -> "Goedendag: " + x)
// the service contract
[<ServiceContract
(Namespace =
"http://strangelights.com/FSharp/Foundations/WCFServices")>]
type IGreetingService =
[<OperationContract>]
abstract Greet : name:string -> string
// the service implementation
type GreetingService() =
interface IGreetingService with
member x.Greet( name ) = f name
// create a service host
let myServiceHost =
let baseAddress = new Uri("http://localhost:8080/service")
let temp = new ServiceHost(typeof<GreetingService>, [|baseAddress|])
let binding =
let temp =
new WSHttpBinding(Name = "binding1",
HostNameComparisonMode =
HostNameComparisonMode.StrongWildcard,
TransactionFlow = false)
temp.Security.Mode <- SecurityMode.Message
temp.ReliableSession.Enabled <- false
temp
temp.AddServiceEndpoint(typeof<IGreetingService>, binding, baseAddress)
|> ignore
temp
// open the service host
myServiceHost.Open()
注意,在清单 11-13
中的类型 IGreetingService 和 GreetingService与清单 11-9
相比,大部分没有改变,只是把 GreetingService类型改成了可以使用可变函数,这是为了可以控制运行时的动作;然后需要创建服务主机,完成前面示例中网站服务器和 web.config的工作。可以看到在清单 11-10
中的 web.config和在清单 11-9
中的服务本身。注意,myServiceHost包含 baseAddress,这是服务监听请求的地址,还有一个 binding,它控制使用的协议;最后,调用 myServiceHost的
Open 方法,设置服务监听。
然后,对客户端做一改动,使它能重复调用这个服务,如清单 11-14所示,这样,可以看到服务的结果随时间而改变。
清单 11-14 客户端访问托管在 F#交互中的服务
let client = new GreetingServiceClient()
while true do
printfn "%s" (client.Greet("Rob"))
Console.ReadLine() |> ignore
也需要修改客户端的 .config文件,以指向正确的地址:
<endpoint address="http://localhost:8080/service"
做完这些之后,就可以动态改变服务了,如图 11-5所示。
图 11-5 调用动态的 WCF服务
把服务托管在程序的另一个重要原因是能够创建桌面应用程序,监听中心服务器的更新。传统上,这种类应用程序必须选举中心服务器,然而,我们知道,如果这种选举过于频繁,会导致大量不必要的网络流量。
清单 11-15 演示了具体做法。这是一个空窗体,托管了监听来自客户端更新的服务;这里,更新是需要显示的背景图像。服务定义了一个函数 ReceiveImage,它接收组成图像的二进制数据。服务的实现是这样的,每接收到一个图像,就触发事件
newImgEvent;这样,每次接收到新图像以后,窗体就更新。把窗体和事件连接起来非常简单:
newImgEvent.Add(fun img -> form.BackgroundImage <- img)
只需要调用这个事件的 Add
方法,并把它传递给更新窗体的函数就可以了。你将注意到,托管服务所需要的代码(即,定义 myServiceHost的代码)与前面示例相比,没有改变。
清单 11-15 有内置服务的 Windows窗体
open System
open System.IO
open System.Drawing
open System.ServiceModel
open System.Windows.Forms
let myServiceHost =
let baseAddress = new Uri("http://localhost:8080/service")
let temp = new ServiceHost((type Service.ImageService), [|baseAddress|])
let binding =
let temp = new WSHttpBinding()
temp.Name <- "binding1"
temp.HostNameComparisonMode <-
HostNameComparisonMode.StrongWildcard
temp.Security.Mode <- SecurityMode.Message
temp.ReliableSession.Enabled <- false
temp.TransactionFlow <- false
temp
temp.AddServiceEndpoint((type Service.IImageService), binding, baseAddress)
|> ignore
temp
myServiceHost.Open()
let form = new Form()
Service.newImgEvent.Add(fun img -> form.BackgroundImage <- img)
[<STAThread>]
do Application.Run(form)
为了创建客户端,首先必须创建代理,方法同清单 11-11;运行工具 SvcUtil.exe,把服务的 URL作为参数,这将创建 C#
代理,可以把它编译成 .NET程序集,在 F#
中使用。这里,代理命名为 ImageServiceClient。清单 11-16中的客户端定义看上去有点复杂,但是,大量的代码只是绘制窗体上的控件,或打开图像文件;真正重要的代码在最后,把一个函数添加到按钮 Send的
click事件。这段代码从磁盘上读图像,并把它加载到字节数组;字节数组然后传递给代理的 ReceiveImage方法。
清单 11-16 给服务器发送图像的客户端
open System
open System.IO
open System.Windows.Forms
// create a form for sending images
let form =
// create the form itself
let temp = new Form(Width=272, Height=64)
// text box for path to the image
let imagePath = new TextBox(Top=8, Left=8, Width=128)
// browse button to allow the user to search for files
let browse = new Button(Top=8, Width=32, Left=8+imagePath.Right,
Text = "...")
browse.Click.Add(fun _ ->
let dialog = new OpenFileDialog()
if dialog.ShowDialog() = DialogResult.OK then
imagePath.Text <- dialog.FileName)
// send button to send the image to the server
let send = new Button(Top=8, Left=8+browse.Right, Text = "Send")
send.Click.Add(fun _ ->
// open and send the file
let buffer = File.ReadAllBytes(imagePath.Text)
let service = new ImageServiceClient()
service.ReceiveImage(buffer))
// add the controls and return the form
temp.Controls.Add(imagePath)
temp.Controls.Add(browse)
temp.Controls.Add(send)
temp
// show the form
[<STAThread>]
do Application.Run(form)
图 11-6 展示了正在执行中的示例程序;用户将要选择图像发送给客户端。
图 11-6 托管在 Windows窗体中的 WCF
服务
对于能够监听更新的桌面应用程序来说,这远不是故事的全部。发送更新的“客户端”需要知道它应该发送更新给哪一个服务和桌面应用程序。在这个服务中,我们直接通过硬编码服务的地址,使客户端知道的;在实际环境中,还需要用其他方法实现服务。这个服务应该告诉中心“客户端”服务正在监听更新,当服务停止时,向中心“客户端”发出警报;然后中心“客户端”应该需要循环遍历正在监听更新的所有服务,把数据推给每一个服务。
第十一章小结
这一章讨论了用 F#
创建分布式应用程序的几个主要选择;学习了通过 F# 和 .NET库的联合,我们能够更集中精力于创建分布式应用程序的关键技术挑战;还学习了使用 F#
功能控制应用程序的复杂度。在下一章,我们将讨论面向语言编程(language-oriented programming),这项技术经过函数程序员多年的考验,值得信赖,可以真正使程序员的生活更简单。
托管 WCF 服务