千家信息网

f#简易Comet聊天服务实例分析

发表于:2025-11-17 作者:千家信息网编辑
千家信息网最后更新 2025年11月17日,f#简易Comet聊天服务实例分析,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。Visual Studio 2010中关于F#的部分
千家信息网最后更新 2025年11月17日f#简易Comet聊天服务实例分析

f#简易Comet聊天服务实例分析,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。

Visual Studio 2010中关于F#的部分已经众人皆知,那么具体该怎么开发呢?这里作者将本来可以用C#开发的实例,改用F#来进行,也是为大家开阔眼界。

普通的Web应用程序,都是靠大量HTTP短连接维持的。如实现一个聊天服务时,客户端会不断轮询服务器端索要新消息。这种做法的优势在于简单有效,因此广为目前的聊天服务所采用。不过Comet技术与之不同,简单地说,Comet便是指服务器推(Server-Push)技术。它的实现方式是(这里只讨论基于浏览器的Web平台)在浏览器与服务器之间建立一个长连接,待获得消息之后立即返回。否则持续等待,直至超时。客户端得到消息或超时之后,又会立即建立另一个长连接。Comet技术的***优势,自然就是很高的即使性。

如果要在ASP.NET平台上实现Comet技术,那么自然需要在服务器端使用异步请求处理。如果是普通处理方式的话,每个请求都会占用一个工作线程,要知道Comet是"长连接",因此不需要多少客户端便会占用大量的线程,这对资源消耗是巨大的。如果是异步请求的话,虽然客户端和服务器端之间一直保持着连接,但是客户端在等待消息的时候是不占用线程的,直到"超时"或"消息到达"时才继续执行。

以前也有人实现过基于ASP.NET的Comet服务原型,不过是使用C#的。而现在我们用F#来实现这个功能。您会发现F#对于此类异步场景有其独特的优势。

F#常用的工作单元是"模块",其中定义了大量函数或字段。例如我们要打造一个聊天服务的话,我便定义了一个Chat模块:

#light  module internal Comet.Chating.Chat  open System  open System.Collections.Concurrent   type ChatMsg = {      From: string;      Text: string;  }   let private agentCache = new ConcurrentDictionary>()   let private agentFactory = new Func>(fun _ ->       MailboxProcessor.Start(fun o -> async { o |> ignore }))   let private GetAgent name = agentCache.GetOrAdd(name, agentFactory)

在这里我构建了一个名为ChatMsg的Record类型,一个ChatMsg对象便是一条消息。然后,我使用一个名为agentCache的ConcurrentDictionary对象来保存每个用户所对应的聊天队列--MailboxProcessor。它是F#核心库中内置的,用于实现消息传递式并发的组件,非常轻量级,因此我为每个用户分配一个也只使用很少的资源。GetAgent函数的作用是根据用户的名称获取对应的MailboxProcessor对象,自不必多说。

Chat模块中还定义了send和receive两个公开方法,如下:

let send fromName toName msg =       let agent = GetAgent toName      { From = fromName; Text = msg; } |> agent.Post   let receive name =       let rec receive' (agent: MailboxProcessor) messages =           async {              let! msg = agent.TryReceive 0              match msg with              | None -> return messages              | Some s -> return! receive' agent (s :: messages)          }       let agent = GetAgent name       async {          let! messages = receive' agent List.empty          if (not messages.IsEmpty) then return messages          else             let! msg = agent.TryReceive 3000              match msg with              | None -> return []              | Some s -> return [s]      }

send方法接受3个参数,没有返回值,它的实现只是简单地构造一个ChatMsg对象,并塞入对应的MailboxProcessor。不过receive方法是这里最关键的部分(没有之一)。receive函数的作用是接受并返回MailboxProcessor中已有的对象,或者等待3秒钟后超时--这么说其实不太妥当,因为receive方法其实只是构造了一个"做这件事情"的Async Workflow,而还没有真正执行它。至于它是如何执行的,我们稍候再谈。

receive函数的逻辑是这样的:首先我们构造一个辅助函数receive'来"尝试获取"队列中已有的所有消息。receive'是一个递归函数,每次获取一个,并递归获取剩余的消息。agent.TryReceive函数接受0,表示查询队列,并立即返回一个Option结果,如果这个结果为None,则表示队列已为空。于是在receive这个主函数中,便先使用receive'函数获取已有消息,如果存在则立即返回,否则便接收3秒钟内获得的***个消息,如果3秒结束还没有收到则返回None。

在receive和receive'函数中都使用了let!获取agent.TryReceive函数的结果。let!是F#中构造Workflow的关键字,它起到了"语法糖"的作用。例如,以下的Async Workflow:

async {      let req = WebRequest.Create("http://moma.org/")      let! resp = req.GetResponseAsync()      let stream = resp.GetResponseStream()      let reader = new StreamReader(stream)      let! html = reader.ReadToEndAsync()      html  }

事实上在"解糖"后就变成了:

async.Delay(fun () ->      async.Let(WebRequest.Create("http://moma.org/"), (fun req ->          async.Bind(req.GetResponseAsync(), (fun resp ->              async.Let(resp.GetResponseStream(), (fun stream ->                  async.Let(new StreamReader(stream), (fun reader ->                      async.Bind(reader.ReadToEndAsync(), (fun html ->                          async.Return(html))))))))))

let!关键字则会转化为Bind函数调用,Bind调用有两个参数,***个参数为Async<'a>类型,它便负责一个"回调",待回调后才执行一个匿名函数--也就是Bind函数的第二个参数。可见,let!关键字的一个重要作用,便是将流程的"控制权"转交给"系统",待合适的时候再继续执行下去。这便是关键,因为这样的话,在接受一个消息的时候,这等待的3秒钟是不占用任何线程的,也就是真正的纯异步。但是如果观察代码--难道不是纯粹的顺序型写法吗?

这就是F#的神奇之处。

在ASP.NET处理时需要Handler,于是在Send阶段便是简单的IHttpHandler:

#light   namespace Comet.Chating   open Comet  open System  open System.Web   type SendHandler() =       interface IHttpHandler with          member h.IsReusable = false         member h.ProcessRequest(context) =               let fromName = context.Request.Form.Item("from");              let toName = context.Request.Form.Item("to")              let msg = context.Request.Form.Item("msg")              Chat.send fromName toName msg              context.Response.Write "sent"

而Receive阶段则是个异步的IHttpAsyncHandler:

#light   namespace Comet.Chating   open Comet  open System  open System.Collections.Generic  open System.Web  open System.Web.Script.Serialization   type ReceiveHandler() =       let mutable m_context = null     let mutable m_endReceive = null      interface IHttpAsyncHandler with          member h.IsReusable = false         member h.ProcessRequest(context) = failwith "not supported"          member h.BeginProcessRequest(c, cb, state) =              m_context <- c               let name = c.Request.QueryString.Item("name")              let receive = Chat.receive name              let beginReceive, e, _ = Async.AsBeginEnd receive              m_endReceive <- new Func<_, _>(e)               beginWork (cb, state)           member h.EndProcessRequest(ar) =              let convert (m: Chat.ChatMsg) =                  let o = new Dictionary<_, _>();                  o.Add("from", m.From)                  o.Add("text", m.Text)                  o               let result = m_endReceive.Invoke ar              let serializer = new JavaScriptSerializer()              result              |> List.map convert              |> serializer.Serialize              |> m_context.Response.Write

这里的关键是Async.AsBeginEnd函数,它将Chat.receive函数生成的Async Workflow转化成一组标准APM形式的begin/end对,然后我们只要把BeginProcessRequest和EndProcessReqeust的职责直接交给即可。剩下的,便是一些序列化成JSON的工作了。

于是我们可以新建一个Web项目,引用F#工程,在Web.config里配置两个Handler,再准备一个Chat.aspx页面即可。您可以在文末的链接中查看该页面的代码,也可以在这里试用其效果。作为演示页面,您其实只能"自己给自己"发送消息,其主要目的是查看其响应时间而已。例如,以下便是使用效果一例:

2 - receiving...  3026 - received nothing (3024ms)  3026 - receiving...  6055 - received nothing (3028ms)  6055 - receiving...  7256 - sending 123654...  7268 - received: 123654 (1213ms)  7268 - receiving...  10281 - received nothing (3013ms)  10281 - receiving...  13298 - received nothing (3017ms)  13298 - receiving...  13679 - sending 123456...  13698 - received: 123456 (400ms)  13698 - receiving...  16716 - received nothing (3018ms)  16716 - receiving...  18256 - sending hello world...  18265 - received: hello world (1549ms)  18266 - receiving...  21281 - received nothing (3015ms)  21281 - receiving...

可见,如果没有收到消息,那么receive操作会在3秒钟后返回。当send一条消息后,先前的receive操作便会立即获得消息了,即无需等待3秒便可提前返回。这便是Comet的效果。

至于性能,我写了一个客户端小程序,用于模拟大量用户同时聊天,每个用户每隔1秒便给另外5个用户发送一条消息,然后查看这条消息收到时产生多少的延迟。经过本机测试(2.4GHz双核,2G内存),当超过2K个在线用户时(即2000个长连接)延迟便超过了1秒--到20K还差不多。这个性能其实并不理想。不过,我这个测试也很一般。因为测试环境相当马虎,大量程序(如N个VS)基本上已经完全用满了所有的物理内存,测试客户端和服务器也是同一台机器,甚至代码也是Debug编译的……而根据监视,测试用的客户端小程序CPU占用超过50%,而服务器进程对应的w3wp.exe的CPU占用却小于10%。因此,我们可以这样推断,其实服务器端的性能并没有用足,也有可能是MailboxProcessor的调度方式不甚理想。至于具体是什么原因,我还在调查之中。

***我想说的是,这个Comet实现只是一个原型,我最想说明的问题其实是F#在异步编程中的优势。目前我写的一些程序,例如一些网络爬虫,都已经使用F#进行开发了,因为它的Async Workflow实在是过于好用,为我省了太多力气。同时我还想证明,"语言特性"并非不重要,它对于编程的简化也是至关重要的。在我看来,"类库"也好,"框架"也罢都是可以补充的,但是语言特性是个无法突破的"限制"。例如,异步编程对于F#来说简化了不少,这是因为我们可以使用顺序的方式编写异步程序。在C#中略有不足,但还有yield可以起到相当作用,因此我们可以使用CCR和AsyncEnumerator简化异步操作。但如果您使用的是Java这种劣质语言……因此,放弃Java,使用Scala吧。

值得一提的是,Async Workflow并不是F#的语言特性,F#的语言特性是Workflow,而Async Workflow其实只是实现了一个Workflow Builder,也就是那个async { ... },以此来简化异步编程而已。PDC 09上关于F#对异步编程的支持也有相应的介绍。

关于f#简易Comet聊天服务实例分析问题的解答就分享到这里了,希望以上内容可以对大家有一定的帮助,如果你还有很多疑惑没有解开,可以关注行业资讯频道了解更多相关知识。

消息 函数 服务 客户 客户端 服务器 用户 关键 程序 作用 对象 方法 语言 测试 编程 优势 参数 只是 技术 方式 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 蓝山天气预报软件开发 数据库重复查询sql语句 南宁 软件开发 工资 京盛天上游网络技术有限公司 佛山电商软件开发定制 网络安全笔记图片 客户端同步服务器时间软件 网络安全守护你我的手抄报 做小程序为什么要服务器 网络安全检测装置是什么 由于网络安全隐患 新华在中国行业领军人物数据库 经营项目软件开发公司资质 手机定制软件开发 怡硕网络技术 网络安全整改建设议题 临沂戴尔服务器代理多少钱 爱快搭建文件服务器 蛋白免疫组化数据库 学校青少年网络安全教育活动 软件开发过程各阶段 比亚迪软件开发工程招聘 数据库服务器防护术 如何用法律来维护网络安全 宽带服务器管理软件 数据库密码忘记了怎么找回来 电脑的时间无法和服务器时间同步 软件开发创业公司压力好大 中国电子科技集团网络安全培训 皮卡解说我的世界小游戏服务器
0