Hello World
Spiga

数十行F#打造简易Comet聊天服务

2009-12-11 12:00 by 老赵, 9603 visits

普通的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<string, MailboxProcessor<ChatMsg>>()

let private agentFactory = new Func<string, MailboxProcessor<ChatMsg>>(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<ChatMsg>) 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<ChatMsg>结果,如果这个结果为None,则表示队列已为空。于是在receive这个主函数中,便先使用receive’函数获取已有消息,如果存在则立即返回,否则便接收3秒钟内获得的第一个消息,如果3秒结束还没有收到则返回None。

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

async {
    let req = WebRequest.Create("http://www.cnblogs.com/")
    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://www.cnblogs.com/"), (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#对异步编程的支持也有相应的介绍

本文代码

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

87 条回复

  1. 无常
    *.*.*.*
    链接

    无常 2009-12-11 12:12:00

    Java这种劣质语言……

  2. 余魁
    *.*.*.*
    链接

    余魁 2009-12-11 12:16:00

    学了有些时间了。comet还是没有学会。笨。

  3. 老赵
    admin
    链接

    老赵 2009-12-11 12:22:00

    无常:Java这种劣质语言……


    还是Scala好。

  4. jacky song
    *.*.*.*
    链接

    jacky song 2009-12-11 12:22:00

    合理利用语言特性确实带来不少惊喜
    ps:java就这么让你不爽啊

  5. 老赵
    admin
    链接

    老赵 2009-12-11 12:25:00

    @jacky song
    是的,相当不爽,尤其是出现Scala以后,就怕货比货……

  6. 遛扬狗
    *.*.*.*
    链接

    遛扬狗 2009-12-11 12:43:00

    受教了,F#看不懂,但是文章中链接的Comet异步机制给我很大启发,谢谢。

  7. toEverybody
    *.*.*.*
    链接

    toEverybody 2009-12-11 12:48:00

    F#,C#平台上的丑角,由所谓的程序员来演
    但用C++,Delphi,C/汇编可以拆台,让你演不成
    哈哈..

  8. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-12-11 12:57:00

    toEverybody:
    F#,C#平台上的丑角,由所谓的程序员来演
    但用C++,Delphi,C/汇编可以拆台,让你演不成
    哈哈..




    深奥。。。。看不懂。

  9. chol
    *.*.*.*
    链接

    chol 2009-12-11 12:58:00

    现在有好的C#开源项目吗?可以做WebIM的?

  10. 老赵
    admin
    链接

    老赵 2009-12-11 13:10:00

    @Ivony...
    同不懂……

  11. 老赵
    admin
    链接

    老赵 2009-12-11 13:10:00

    @chol
    似乎没有现成的

  12. 韦恩卑鄙 alias:v-zhewg
    *.*.*.*
    链接

    韦恩卑鄙 alias:v-zhewg 2009-12-11 13:10:00

    在gac里面完全没找到

    System.Collections.Concurrent
    的mailboxprocessor


    告诉我哪个dll?

  13. 謝霆鋒[未注册用户]
    *.*.*.*
    链接

    謝霆鋒[未注册用户] 2009-12-11 13:22:00

    感覺老趙的java還沒入門

  14. 老赵
    admin
    链接

    老赵 2009-12-11 13:23:00

    @韦恩卑鄙 alias:v-zhewg
    MailboxProcessor是F#核心库里的,在FSharp.Core.dll里。

  15. 老赵
    admin
    链接

    老赵 2009-12-11 13:23:00

    謝霆鋒:感覺老趙的java還沒入門


    为啥这样说呀?其实我对Java语言的了解不比C#少……

  16. egmkang
    *.*.*.*
    链接

    egmkang 2009-12-11 13:27:00

    看了一下,这玩意儿貌似是不支持.NET CF啊

  17. egmkang
    *.*.*.*
    链接

    egmkang 2009-12-11 13:30:00

    算法,现在WM上面的开发,缺少的是性能...
    我还是不搞这玩意儿..

  18. 韦恩卑鄙 alias:v-zhewg
    *.*.*.*
    链接

    韦恩卑鄙 alias:v-zhewg 2009-12-11 13:40:00

    Jeffrey Zhao:
    @韦恩卑鄙 alias:v-zhewg
    MailboxProcessor是F#核心库里的,应该FSharp.Core.dll里。


    f# 在.net 2.0 的这个库对我现在的工作很有帮助 我试验下 3q

  19. 木乃伊
    *.*.*.*
    链接

    木乃伊 2009-12-11 13:41:00

    学习了。

  20. 韦恩卑鄙 alias:v-zhewg
    *.*.*.*
    链接

    韦恩卑鄙 alias:v-zhewg 2009-12-11 13:44:00

    egmkang:看了一下,这玩意儿貌似是不支持.NET CF啊


    2.0 4.0 和sl2 sl3 sl4

  21. 老赵
    admin
    链接

    老赵 2009-12-11 13:45:00

    @韦恩卑鄙 alias:v-zhewg
    顺便一说,F#的Async Workflow不一定要和MailboxProcessor一起用的,说不定你也可以不用MailboxProcessor,除非是Message Passing场景。
    还有,如果要Message Passing场景但不能用F#的话,也可以试试看CCR。
    最后就是……不能运行在.NET 2.0吧,至少3.5……

  22. 老赵
    admin
    链接

    老赵 2009-12-11 13:45:00

    @egmkang
    是的,不支持CF……

  23. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-12-11 13:45:00

    终于把最难懂的一行代码搞明白了:
    let beginReceive, e, _ = Async.AsBeginEnd receive


    放到VS2010里面根据智能提示很容易就猜出来了。

    原来下划线就是丢弃这个值的意思。。。

  24. oec2003
    *.*.*.*
    链接

    oec2003 2009-12-11 13:46:00

    謝霆鋒:感覺老趙的java還沒入門


    老赵只是单指java语言来说的
    平台和语言是两回事 有些人可能有误解

  25. 老赵
    admin
    链接

    老赵 2009-12-11 13:50:00

    @oec2003
    没错,尤其是Java平台上已经有Scala语言了……

  26. 韦恩卑鄙 alias:v-zhewg
    *.*.*.*
    链接

    韦恩卑鄙 alias:v-zhewg 2009-12-11 14:56:00

    @Jeffrey Zhao
    已经找到2.0的实现了
    Fsharp.Core.dll 的2.0.0 版本

  27. 老赵
    admin
    链接

    老赵 2009-12-11 15:05:00

    @韦恩卑鄙 alias:v-zhewg
    是的,在.NET 3.5上的就是2.0版本的程序集。

  28. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-12-11 15:16:00

    为啥感觉F#的语法还不够严谨呢?

  29. 老赵
    admin
    链接

    老赵 2009-12-11 15:36:00

    @Ivony...
    我有时也有这样的想法,比如那段代码里的:

    let private agentCache = new ConcurrentDictionary<string, MailboxProcessor<ChatMsg>>()
    
    这会编译成静态的field/property,但如果写成:
    let private agentCache = new ConcurrentDictionary<_, _>()
    
    就变成一个泛型函数了……
    此类细节,有时候比较烦人。

  30. Gnie
    *.*.*.*
    链接

    Gnie 2009-12-11 15:59:00

    老赵的文章看不懂啊

  31. xland
    *.*.*.*
    链接

    xland 2009-12-11 16:30:00

    Gnie:老赵的文章看不懂啊


    老赵脱离大众好多年了

  32. BillGan
    *.*.*.*
    链接

    BillGan 2009-12-11 16:31:00

    异步就不占用线程资源吗?????????????
    有那么好用么??????

  33. 老赵
    admin
    链接

    老赵 2009-12-11 16:33:00

    @BillGan
    具体如何可以查阅相关资料并自己试验一下,关键字是IOCP,ASP.NET异步请求。

  34. 小白TWO[未注册用户]
    *.*.*.*
    链接

    小白TWO[未注册用户] 2009-12-11 17:09:00

    我的评 论呢。。。在在在在哪里去了。第一次留言就成这样。晕
    在线等回复。

  35. 小白TWO[未注册用户]
    *.*.*.*
    链接

    小白TWO[未注册用户] 2009-12-11 17:09:00


    核心方法的实现。
    是不是来一个AJAX请求一个页面。然后这个页面执行一个while(true)一直阻塞。在需要的时候Response.Write相应内容。

    但是AJAX请求这个页面。如果页面一直未加载完。根本就得不到内容啊。更别说得到输出的最新内容了

    不知道我理解是否有误。请老赵解释一下。GOOGLE了一番都不太清楚这个。

  36. 小白TWO[未注册用户]
    *.*.*.*
    链接

    小白TWO[未注册用户] 2009-12-11 17:10:00

    原来是没有发出去啊。。为了回复一下。还专门去下个FF浏览器。囧

  37. 老赵
    admin
    链接

    老赵 2009-12-11 17:11:00

    @小白TWO
    看我文章里的解释

  38. 申飞
    *.*.*.*
    链接

    申飞 2009-12-11 21:54:00

    @toEverybody

    toEverybody:
    F#,C#平台上的丑角,由所谓的程序员来演
    但用C++,Delphi,C/汇编可以拆台,让你演不成
    哈哈..



    哎。。。。。
    你。。。。。
    我。。。。。



    在我的印象中,C/汇编 好像不是很难吧, 感觉 还没有C#难。

  39. 老赵
    admin
    链接

    老赵 2009-12-11 22:11:00

    @申飞
    不过难易倒不是关键

  40. 蔡探长
    *.*.*.*
    链接

    蔡探长 2009-12-11 22:13:00

    我感觉Comet技术必将在未来一两年内大放异彩。正因为有java社区,net社区的功劳很少,因为net受至于服务器没有开源,微软也没有想搞,造成目前的comet的应用基本上是以java为主。

    正如2005年的ajax技术。

    正如orm在目前正逐渐成为主流(特别是linq sql,当然最早还是java先火了orm,才有net的跟进学习),毕竟大家都喜欢爽的感觉,不喜欢再写讨厌的sql语句。

    为什么这么说呢,可以说Comet正是因为有java社区目前越来越多的服务器的支持,java中各种人才对Comet的研究造成Comet今日逐渐为世人所知。

    大家可以在ibm开发社区里面用“comet”搜索一下就知道多少的comet技术文章。

    没错c#等语言的语言特性确实比java有更多的好些。但是我感觉一些总要技术总是java社区先挑起来的,后来net才跟进。微软应该好好思考些。

  41. 蔡探长
    *.*.*.*
    链接

    蔡探长 2009-12-11 22:22:00

    目前net搞comet基本上还是处在瞎搞阶段,因为没有服务器支持啊,不能说是遗憾。完成相信微软最终还是会让iis支持comet这种风格模型。

    有好的技术管它什么语言,能派上用场就好(这场合用不上,其它场合可能就是牛b),语言就是工具罢了,还是多了解一些其它语言,有好处的

  42. 蔡探长
    *.*.*.*
    链接

    蔡探长 2009-12-11 22:27:00

    以后的web服务器我感断定是逐渐都成为异步服务器。
    就象处理器一样是多核并行发展。
    就象现在的语言的趋势也是逐渐成为类似js的那种脚本语言,很灵活很强大,完全脚本化当然也不行,但是有很多东西可以让现在所谓的先进语言好好学习一下。

  43. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-12-11 23:21:00

    蔡探长:
    我感觉Comet技术必将在未来一两年内大放异彩。正因为有java社区,net社区的功劳很少,因为net受至于服务器没有开源,微软也没有想搞,造成目前的comet的应用基本上是以java为主。

    正如2005年的ajax技术。

    正如orm在目前正逐渐成为主流(特别是linq sql,当然最早还是java先火了orm,才有net的跟进学习),毕竟大家都喜欢爽的感觉,不喜欢再写讨厌的sql语句。

    为什么这么说呢,可以说Comet正是因为有java社区目前越来越多的服务器的支持,java中各种人才对Comet的研究造成Comet今日逐渐为世人所知。

    大家可以在ibm开发社区里面用“comet”搜索一下就知道多少的comet技术文章。

    没错c#等语言的语言特性确实比java有更多的好些。但是我感觉一些总要技术总是java社区先挑起来的,后来net才跟进。微软应该好好思考些。




    我倒不觉得,Comet技术么,最早应该是GTalk让世人所知的,反正我是通过GTalk才知道Comet的。

    无论在哪一个领域微软和.NET都贡献了大量的技术,只是大家都喜欢用有色眼镜看罢了。

    在Web领域有服务器控件模型,这显然是ASP.NET出来后,JSP才跟进的。在语言方面,也是微软最早提出跨语言共享平台。

    更进一步,考虑微软的专有平台,WPF、CAS、ClickOnce、WCF、WF这些东西至今在Java平台也没有相应的技术。所以.NET平台上充斥着各种各样的外来技术恰恰说明.NET的开放性,老实说,从Java平台上移植技术到.NET平台的周期远远小于.NET平台技术移植到Java的周期。这当然有微软的不公开源代码的壁垒存在,但有时候与某平台的傲慢的态度不无关系,显然他们现在刚刚醒悟过来。

  44. 老赵
    admin
    链接

    老赵 2009-12-12 01:02:00

    蔡探长:
    目前net搞comet基本上还是处在瞎搞阶段,因为没有服务器支持啊,不能说是遗憾。完成相信微软最终还是会让iis支持comet这种风格模型。

    有好的技术管它什么语言,能派上用场就好(这场合用不上,其它场合可能就是牛b),语言就是工具罢了,还是多了解一些其它语言,有好处的


    IIS真不行吗?我今天下午/晚上试了试,建立个2-3万长连接没有问题,异步之后性能很高,又不占用资源,而且都还是我的破机器而已,呵呵。
    而且,IIS真不行的话,咱可以自己搞一个Server咯。Java那边所谓服务器,说白了也就是哪Socket堆出来而已,如Jetty,Mina,该有的技术Windows/.NET都有。

  45. 老赵
    admin
    链接

    老赵 2009-12-12 01:08:00

    @蔡探长
    Java的就是.NET的,提出一大堆东西,.NET都可以采用,呵呵。

  46. shenzhen
    *.*.*.*
    链接

    shenzhen 2009-12-12 01:58:00

    哎。。楼主能告诉我F#的学习资料到哪里找吗?想了解下。。谢谢!

  47. ngaut
    *.*.*.*
    链接

    ngaut 2009-12-12 02:01:00

    老赵用Scala也实现一个,对比看看:)

  48. 蔡探长
    *.*.*.*
    链接

    蔡探长 2009-12-12 11:01:00

    @Jeffrey Zhao
    你没有骗我吧。改天我也试试,免得我在同事面前一说iis不支持,有人就不乐意,说我要搞出一个net的例子出来,别搞那些不懂java(我挺想再继续跟他们说其实没事啊,不要抵触对立社区的知识啊,自己能学到最要紧,管什么语言实现的),因为我从来没有在网上看过人家这么弄过,也没有听说iis支持过,也不知道它为什么支持,为什么不支持。
    我给你一些j2ee服务为什么支持的链接,你参考看看。
    http://www.infoq.com/cn/articles/request-asyn-risk-reduce
    http://www.ibm.com/developerworks/cn/web/wa-cometjava/

  49. 老赵
    admin
    链接

    老赵 2009-12-12 12:33:00

    @蔡探长
    骗你做啥,你自己试试看就知道咯。
    总有人说IIS连接什么几千限制的,改个配置就能支持N万。
    总有人说Windows性能不好,其实IOCP是各平台上最早出现的异步通信技术。
    .NET上有异步IO封装的时候,Java的NIO还不知道在哪里呢。
    Java实现Comet就是用NIO,.NET也一样,所以我这里不是用异步请求吗?
    你给的文章都是旧闻了啊,我还是拿那句话说,Windows既不缺功能也不缺性能。
    说白了,这一切就是靠异步说话,Windows上靠iocp,其他平台上靠epoll(linux)或是kqueue(freebsd)。

  50. 老赵
    admin
    链接

    老赵 2009-12-12 12:33:00

    shenzhen:哎。。楼主能告诉我F#的学习资料到哪里找吗?想了解下。。谢谢!


    互联网

  51. 蔡探长
    *.*.*.*
    链接

    蔡探长 2009-12-12 13:59:00

    @Jeffrey Zhao
    不对阿,我查了一下,java2002年jdk1.4.2出来的时候就有nio了,
    具体请看
    http://www.ibm.com/developerworks/cn/java/j-javaio/
    http://java.sun.com/j2se/1.4/docs/guide/nio/
    我决定要好好搞一把,这技术还是挺有前途的。

    我不排斥net不排斥java,哪个适合、方便就用哪个,不懂就学,无所谓啦。

  52. 老赵
    admin
    链接

    老赵 2009-12-12 14:20:00

    @蔡探长
    如果我没记错的话,“真正”的基于event的异步IO是后来才有的(Java 5?6?),一开始只是select + “非占用线程”的这种异步方式。
    要知道epoll是Linux Kernel 2.5(还是2.6)开始才集成进去的,当时已经02、03年了吧。2.4的Kernel版本还要通过后来的patch才能获得epoll,Java跟不了这么紧吧。
    而且我可以确定的是,包括现在Java 6 for Windows里的NIO,还没有使用IOCP,还是使用select,换句话说还是假的NIO。
    所以我一直不认同Java所谓的跨平台,性能完全不同么,呵呵。不过Java 7里说是已经用了IOCP了。

  53. 老赵
    admin
    链接

    老赵 2009-12-12 14:22:00

    @蔡探长
    忘了说了,IOCP是NT 3.5里加入的,也就是说至少Windows 2000开始就有IOCP支持的。
    只可惜,微软不知道什么技术推广策略,推广不力,开发人员也不太关注这些。
    这又要说到AJAX了,话说XMLHttpRequest也是IE 5.5就有的,可惜被微软用于企业产品(OWA)了,没有推广……
    直到Gmail……总之微软很多事情的确我看不懂,很可惜。

  54. 蔡探长
    *.*.*.*
    链接

    蔡探长 2009-12-12 16:42:00

    @Jeffrey Zhao
    嗯,静候我的佳音,我会用事实解密net和java的comet模式的优缺点。在这里没有事实是没有办法再说下去了

  55. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-12-12 16:42:00

    Jeffrey Zhao:
    @蔡探长
    忘了说了,IOCP是NT 3.5里加入的,也就是说至少Windows 2000开始就有IOCP支持的。
    只可惜,微软不知道什么技术推广策略,推广不力,开发人员也不太关注这些。
    这又要说到AJAX了,话说XMLHttpRequest也是IE 5.5就有的,可惜被微软用于企业产品(OWA)了,没有推广……
    直到Gmail……总之微软很多事情的确我看不懂,很可惜。




    微软是赚钱的公司,其目的不是成为技术领袖,技术只是为了商业而存在的。很多技术微软弄出来了,市场那边看了看,没啥好推的,就放那里了。

    微软不可能自己去做Gmail,这会与他的Office竞争,毕竟Office很赚钱。再说微软控制着操作系统和浏览器这两块地盘,也让他根本没有任何动力去做Gmai这样的在线邮箱。如果确实需要,微软更倾向于提供Outlook Express这样的客户端。

    微软没有在互联网上找到赚钱的地方,这也是微软将来最重要的事情。微软不太可能像Google一样去卖广告,因为现在Google已经几乎事实垄断了互联网广告。所以可以看到微软在云计算上的投入不逊色于Google(因为这个可以赚钱),而在线应用却不愿意花精力。

  56. 老赵
    admin
    链接

    老赵 2009-12-12 21:14:00

    蔡探长:
    @Jeffrey Zhao
    嗯,静候我的佳音,我会用事实解密net和java的comet模式的优缺点。在这里没有事实是没有办法再说下去了


    嗯嗯,有啥进展及时说说,呵呵

  57. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-12-12 22:58:00

    有想法把这个F#的东东翻译成C#的。。。。。

    刚刚想了一部分语法的变换,看起来是可行的。

  58. 老赵
    admin
    链接

    老赵 2009-12-12 23:14:00

    @Ivony...
    那个let!估计只能用yield了吧……

  59. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-12-13 01:01:00

    翻译成C#应该不是难事,移植到C#就很麻烦,需要建立大量的辅助方法和Provider,我大体上进行了一些简单的翻译工作。

    首先是做一些简单的变换:

    #light
    
    module internal Comet.Chating.Chat
    
    open System
    open System.Collections.Concurrent
    
    type ChatMsg = {
        From: string;
        Text: string;
    }
    
    let private agentCache = new ConcurrentDictionary<string, MailboxProcessor<ChatMsg>>()
    
    let private agentFactory = new Func<string, MailboxProcessor<ChatMsg>>(fun _ -> 
        MailboxProcessor.Start(fun o -> async { o |> ignore }))
    
    let private GetAgent name = agentCache.GetOrAdd(name, agentFactory)
    
    let send fromName toName msg = 
        let agent = GetAgent toName
        { From = fromName; Text = msg; } |> agent.Post
    
    let receive name = 
        let rec receive' (agent: MailboxProcessor<ChatMsg>) 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]
        }
    


    变换为:

    namespace  Comet.Chating
    {
      using System;
      using System.Collections.Concurrent;
    
    
      public static class Chat
      {
    
        private class ChatMsg
        {
          public string From { get; set;}
          public string Text { get; set;}
        }
    
        private static ConcurrentDictionary<string, FSharpMailboxProcessor<ChatMsg>> agentCache = new ConcurrentDictionary<string,FSharpMailboxProcessor<ChatMsg>>();
        
        private static Func<string, FSharpMailboxProcessor<ChatMsg>> agentFactory = any => 
          FSharpMailboxProcessor<ChatMsg>.Start( o => /*async { ignore( o ) }*/ , null );
    
        private static FSharpMailboxProcessor<ChatMsg> GetAgent( string name ) { return agentCache.GetOrAdd( name , agentFactory ); }
    
        public static void send( string fromName, string toName, string msg )
        {
          var agent = GetAgent( toName );
          agent.Post( new ChatMsg(){ From=fromName, Text = msg } );
        }
    
        public static void receive( string name )
        {
          /*
          let rec receive' (agent: MailboxProcessor<ChatMsg>) messages = 
              async {
                  let! msg = agent.TryReceive 0
                  match msg with
                  | None -> return messages
                  | Some s -> return! receive' agent (s :: messages)
              }
          */
    
          var 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]
          }
          */
        }
      }
    }
    
    


    接下来就是妥善的处理async块语法了,已经有了一些初步的想法,还需要实验论证。

    其实也可以看出来,C#除了在类型上麻烦些,大体上还是能保持和F#语法差不多的。

  60. 蛙蛙王子
    *.*.*.*
    链接

    蛙蛙王子 2009-12-13 09:05:00

    这个东西我也想过要做,后来没时间了,老赵做的很棒,这个东西在《ajax设计模式》里的通信一部分有讲。
    当时一直怀疑,不知道性能如何,真的要用,感觉得自己用socket写一个http服务

  61. 老赵
    admin
    链接

    老赵 2009-12-13 15:21:00

    @蛙蛙王子
    可以这么做的,呵呵,Java那边其实都是这么做的。
    还有就是,我不知道这个Host如果使用WCF会如何……

  62. 没剑
    *.*.*.*
    链接

    没剑 2009-12-14 14:12:00

    老赵,等着你在iis上测试性能的文章,期待中!

  63. 遛扬狗
    *.*.*.*
    链接

    遛扬狗 2009-12-18 23:40:00

    我翻译成c#后测试了,最初的每个对象会占用大概52K,然后会逐渐增多,当内存到80M左右的时候,内存增长开始变得缓慢而且没有规律,此时w3wp.exe会无先兆的重启服务,如果w3wp.exe不重启的话,那么就会定在某个内存值上,150多M的样子,紧接着IIS就会报告停止服务。我测试了很多次,都是这样的。最后说一下,测试结果差不多2000多个就是极限了。可能和我测试手段有关系,开的线程太多导致,我会再做测试的。

  64. 老赵
    admin
    链接

    老赵 2009-12-19 00:19:00

    @遛扬狗
    你是怎么翻译的啊?不该开太多线程的,如F#这种做法线程是很少的,因为是异步请求。
    我的测试中最多同时建立20k个长连接(没有断开),而且这不是极限,只是我没有测试更多的,呵呵。

  65. 遛扬狗
    *.*.*.*
    链接

    遛扬狗 2009-12-19 09:28:00

    正想向你咨询如何做到20k的,我的代码是这样的,请你看看是不是那里不对了?我用的是c#2.0+iis6 ,iis6配置是40000个核心排队,内存配置为1g。机器是双cpu xeon 3.16G,内存2g


    //异步
    namespace CometAsync.Comet
    {
        public class CometAsyncHandler : IHttpAsyncHandler
        {
            #region IHttpAsyncHandler Members
    
            public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)
            {
               
    
                    //保存状态
    
                CometAsyncResult result = new CometAsyncResult(context.Response, cb, extraData);
    
                    result.AddCometAsyncResult(context.Request.QueryString["name"]);
    
                    //  ok, return it
                    return result;
    
            }
    
            public void EndProcessRequest(IAsyncResult result)
            {
    
            }
    
            #endregion
    
            #region IHttpHandler Members
    
            public bool IsReusable
            {
                get { return true; }
            }
    
    
            #endregion
        }
    }
    
    


    namespace CometAsync.Comet
    {
        public class CometAsyncResult : IAsyncResult
        {
            private HttpResponse Response;
            private AsyncCallback callback;
            private object asyncState;
            private bool isCompleted = false;
            private object responseObject;
            public DateTime Time;
            /// <summary>
            /// 在线状态
            /// </summary>
            public string State;
            /// <summary>
            /// 单个用户所有的消息
            /// </summary>
            public List<string> Messages = new List<string>();
            private readonly char BREAK='\x0000';
    
            public CometAsyncResult(HttpResponse Response, AsyncCallback callback, object asyncState)
            {
                this.callback = callback;
                this.Response = Response;
                this.asyncState = asyncState;
                this.Time = DateTime.Now;
    
                Response.Write(BREAK);
                Response.Flush();
            }
    
            #region IAsyncResult Members
    
            public object AsyncState
            {
                get { return this.asyncState; }
            }
    
            public WaitHandle AsyncWaitHandle
            {
                get { throw new InvalidOperationException("ASP.NET Should never use this property"); }
            }
    
            public bool CompletedSynchronously
            {
                get { return false; }
            }
    
            public bool IsCompleted
            {
                get { return this.isCompleted; }
            }
    
            #endregion
    
    
            public void AddCometAsyncResult(string Username)
            {
                string name = Username ?? "user" + HttpContextRemoting.UsersOnline.Count.ToString();
                if (!HttpContextRemoting.UsersOnline.ContainsKey(name))//初次连接
                {
                    HttpContextRemoting.UsersOnline.Add(name, this);
    
                }
    
            }
            /// <summary>
            /// 写入客户端流,成功返回true,失败返回false
            /// </summary>
            /// <param name="Message"></param>
            /// <returns></returns>
            public bool Write(string Message)
            {
                if (this.Response.IsClientConnected)
                {
                    this.Response.Write(Message);
                    this.Response.Write(BREAK);                
                    this.Response.Flush();
                    return true;
                }
                return false;
              
            }
            internal void SetCompleted()
            {
                this.isCompleted = true;
    
                if (callback != null)
                    try
                    {
                        callback(this);
                    }
                    catch (Exception)
                    {
                    }
            }
    
        }
    }
    

  66. 遛扬狗
    *.*.*.*
    链接

    遛扬狗 2009-12-19 09:30:00

    //这里静态变量保存用户状态
    namespace CometAsync.Remoting
    {
        public partial class HttpContextRemoting : MarshalByRefObject
        {
    
            public static SynchronizedDictionary<string, CometAsyncResult> UsersOnline = new SynchronizedDictionary<string, CometAsyncResult>(10000);
            
    
         public HttpContextRemoting()
         {
    
         }
    
            /// <summary>
            /// 返回用户对象
            /// </summary>
            /// <param name="To"></param>
            /// <returns></returns>
         private CometAsyncResult GetCometAsyncResult(string To)
         {
             if (UsersOnline.ContainsKey(To))
             {
                 return UsersOnline[To];
    
             }
             return null;
    
         }
        }
        
    }
    
    

  67. 老赵
    admin
    链接

    老赵 2009-12-19 11:48:00

    @遛扬狗
    今天有些事情,明天看看,实在不行周一试验一下。:)

  68. 遛扬狗
    *.*.*.*
    链接

    遛扬狗 2009-12-19 22:07:00

    晚上我又测试了几个钟头,发现导致w3wp重启是因为iis6有个“启用快速失败保护”的选项,默认打开,设置为5分钟错5次就会重启线程,关闭这个选项后就不会重启了。现在测试最高是4000多个连接,内存188M左右,再多就报告server too busy了。下面是一些测试数据:

    连接数(个) 内存占用(M)
    1994 123
    2882 132
    3555 179
    4421 183

  69. 老赵
    admin
    链接

    老赵 2009-12-19 22:15:00

    @遛扬狗
    有没有修改过最大连接数?IIS 7默认最大是5000个,IIS 6忘了咋修改了。

  70. 遛扬狗
    *.*.*.*
    链接

    遛扬狗 2009-12-20 12:53:00

    我试了试你说的方法,没有效果,有文章说2003 sp1 x64能支持到50K+,没有提到sp2 x32,我还是装个2008来试一试了。如果你能在2003+iis6下测试一下,我将不胜感激。

  71. 遛扬狗
    *.*.*.*
    链接

    遛扬狗 2009-12-20 18:39:00

    The default maximum number of ephemeral TCP ports is 5000 in the products that are included in the "Applies to" section. A new parameter has been added in these products. To increase the maximum number of ephemeral ports, follow these steps:
    Start Registry Editor.
    Locate the following subkey in the registry, and then click Parameters:
    HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters
    On the Edit menu, click New, and then add the following registry entry:
    Value Name: MaxUserPort
    Value Type: DWORD
    Value data: 65534
    Valid Range: 5000-65534 (decimal)
    Default: 0x1388 (5000 decimal)
    Description: This parameter controls the maximum port number that is used when a program requests any available user port from the system. Typically, ephemeral (short-lived) ports are allocated between the values of 1024 and 5000 inclusive. After the release of security bulletin MS08-037, the behavior of Windows Server 2003 was changed to more closely match that of Windows Server 2008 and Windows Vista. For more information about Microsoft security bulletin MS08-037, click the following article numbers to view the articles in the Microsoft Knowledge Base:


    应该是这个问题吧,但我修改了没啥反应。

  72. 遛扬狗
    *.*.*.*
    链接

    遛扬狗 2009-12-21 17:17:00

    今天装了2008,然后按照http://www.cnblogs.com/dudu/archive/2009/11/10/1600062.html这篇文章设置了系统,在13k的时候IIS崩溃了。我开了5台机器,用WAS来测试的。另外我又用amd 64+2003的系统来测试了一次,也是在5000连接处停止服务了。

  73. 老赵
    admin
    链接

    老赵 2009-12-21 17:21:00

    @遛扬狗
    一会儿看看你的代码,呵呵。

  74. 老赵
    admin
    链接

    老赵 2009-12-21 22:32:00

    @遛扬狗
    你的代码没咋看懂唉……我再看看……

  75. 老赵
    admin
    链接

    老赵 2009-12-21 22:35:00

    @遛扬狗
    你的代码很奇怪,怎么跑得起来啊?SetComplete没见调用过,不是么……

  76. 遛扬狗
    *.*.*.*
    链接

    遛扬狗 2009-12-22 18:02:00

    恩,其他地方调用的,要不我把程序传给你吧,你看怎么给你。

  77. 链接

    Kira 2010-05-07 11:52:16

    当年我的毕业设计就是"基于Comet技术的投票系统",用了个DWR的框架搞的,不过是Java平台的。

    当时.net平台上貌似没成熟的框架

  78. 老赵
    admin
    链接

    老赵 2010-05-07 13:08:17

    @Kira

    DWR其实和Comet无关吧?其实.NET上有Script#,可以有部分类似的功能,我是指C#到JavaScript的转换。

  79. 链接

    Kira 2010-05-07 16:40:41

    我觉得应该算是有关系的吧。

    网上有资料的,dwr提供了3种 反转ajax

    1. 轮询
    2. comet
    3. piggybacking

    我记得只要配置一下config文件就可以了在3种方式切换了。还记得DWR官方网站上有个例子也是聊天系统。要不我回去整理一下搞个Demo发上来大家研究看看?

    Script#是不是.net上是不是Ajaxpro.dll这个东西?

  80. 老赵
    admin
    链接

    老赵 2010-05-07 19:23:44

    @Kira

    Comet的关键在于服务器端,DWR只是关于客户端的吧?

    Script#是微软ASP.NET团队的架构师Nikhil Kothari写了好几年的东西,不过微软好像最近才重视起来。

  81. 链接

    Kira 2010-05-08 20:52:41

    不好意思我回家晚了,回复太晚了。

    我说的是这个,reverse-ajax是DWR 2.0最大的一个新特性。我那个程序里最核心的就这段,这个应该是在服务器端的吧。 遍历所有连在result.jsp的用户,然后发送消息过去。

    private void sendToOtherPage(int vid) {
        //设定返回调用的Javascript
        ScriptBuffer script = new ScriptBuffer();
        script.appendScript("receiveMessages(").appendData(vid).appendScript(");");
        WebContext wctx = WebContextFactory.get();
        ScriptSession currentSession = wctx.getScriptSession();
    
        //搜集所有连接在结果上的页面的脚本Session
        Collection pages = wctx.getScriptSessionsByPage("/vote/result.jsp");
        for (Iterator it = pages.iterator(); it.hasNext();)
        {
            //遍历所有session,调用Javascript内容
            ScriptSession otherSession = (ScriptSession) it.next();
            otherSession.addScript(script);
        }
    }
    

    Script#这个东西我还真没听过。有空好好去学习下。

  82. 链接

    rocklau83 2010-11-01 09:40:34

    我在Vs2010测试你给出的代码,ReceiveHandler.fs,报2个错如下:

    let beginWork, e, _ = Async.AsBeginEnd receive
    

    错误 1 此表达式应具有类型 'a -> Async<'b> 而此处具有类型 Async[Chat.ChatMsg list]

    beginWork (cb, state)
    

    错误 2 类型不匹配。应为 'a * AsyncCallback * 'b 而给定的是 'a * AsyncCallback 元组具有不同长度的 3 和 2

    不知道如何修正这2处错误,请指教

  83. 老赵
    admin
    链接

    老赵 2010-11-01 10:00:19

    @rocklau83

    语法方面的问题还是自己解决吧……

  84. 链接

    rocklau83 2010-11-01 10:19:22

    @老赵

    好,谢谢.

  85. 链接

    rocklau83 2010-11-03 12:06:22

    还有个问题,关于Chat.aspx内的receive方法在IE下会内存无法释放,老赵有没有方法解决?

  86. 链接

    rocklau83 2010-11-03 14:00:28

    解决了,我用Concurrent.Thread.js

  87. lpkkk
    211.139.149.*
    链接

    lpkkk 2010-11-30 15:02:00

    而根据监视,测试用的客户端小程序CPU占用超过50%,而服务器进程对应的w3wp.exe的CPU占用却小于10%。因此,我们可以这样推断,其实服务器端的性能并没有用足,也有可能是MailboxProcessor的调度方式不甚理想。至于具体是什么原因,我还在调查之中。

    调查结果是什么呢?

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我