2010-04-03 69 views
4

使用F#中的原始套接字編寫異步Ping以使用盡可能少的線程啓用並行請求。不使用「System.Net.NetworkInformation.Ping」,因爲它似乎爲每個請求分配一個線程。我也對使用F#異步工作流感興趣。如何檢測使用異步Socket.BeginReceive時的超時?

下面的同步版本正確超時目標主機不存在/響應,但異步版本掛起。兩者都在主機確實響應時工作。不知道這是一個.NET的問題,或一個F#之一...

任何想法?

(注:該過程必須以管理員身份運行,讓原始套接字的訪問)

這將引發超時:

let result = Ping.Ping (IPAddress.Parse("192.168.33.22"), 1000) 

然而,這種掛起:

let result = Ping.AsyncPing (IPAddress.Parse("192.168.33.22"), 1000) 
      |> Async.RunSynchronously 

下面的代碼...

module Ping 

open System 
open System.Net 
open System.Net.Sockets 
open System.Threading 

//---- ICMP Packet Classes 

type IcmpMessage (t : byte) = 
    let mutable m_type = t 
    let mutable m_code = 0uy 
    let mutable m_checksum = 0us 

    member this.Type 
     with get() = m_type 

    member this.Code 
     with get() = m_code 

    member this.Checksum = m_checksum 

    abstract Bytes : byte array 

    default this.Bytes 
     with get() = 
      [| 
       m_type 
       m_code 
       byte(m_checksum) 
       byte(m_checksum >>> 8) 
      |] 

    member this.GetChecksum() = 
     let mutable sum = 0ul 
     let bytes = this.Bytes 
     let mutable i = 0 

     // Sum up uint16s 
     while i < bytes.Length - 1 do 
      sum <- sum + uint32(BitConverter.ToUInt16(bytes, i)) 
      i <- i + 2 

     // Add in last byte, if an odd size buffer 
     if i <> bytes.Length then 
      sum <- sum + uint32(bytes.[i]) 

     // Shuffle the bits 
     sum <- (sum >>> 16) + (sum &&& 0xFFFFul) 
     sum <- sum + (sum >>> 16) 
     sum <- ~~~sum 
     uint16(sum) 

    member this.UpdateChecksum() = 
     m_checksum <- this.GetChecksum() 


type InformationMessage (t : byte) = 
    inherit IcmpMessage(t) 

    let mutable m_identifier = 0us 
    let mutable m_sequenceNumber = 0us 

    member this.Identifier = m_identifier 
    member this.SequenceNumber = m_sequenceNumber 

    override this.Bytes 
     with get() = 
      Array.append (base.Bytes) 
         [| 
          byte(m_identifier) 
          byte(m_identifier >>> 8) 
          byte(m_sequenceNumber) 
          byte(m_sequenceNumber >>> 8) 
         |] 

type EchoMessage() = 
    inherit InformationMessage(8uy) 
    let mutable m_data = Array.create 32 32uy 
    do base.UpdateChecksum() 

    member this.Data 
     with get() = m_data 
     and set(d) = m_data <- d 
         this.UpdateChecksum() 

    override this.Bytes 
     with get() = 
      Array.append (base.Bytes) 
         (this.Data) 

//---- Synchronous Ping 

let Ping (host : IPAddress, timeout : int) = 
    let mutable ep = new IPEndPoint(host, 0) 
    let socket = new Socket(AddressFamily.InterNetwork, SocketType.Raw, ProtocolType.Icmp) 
    socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.SendTimeout, timeout) 
    socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, timeout) 
    let packet = EchoMessage() 
    let mutable buffer = packet.Bytes 

    try 
     if socket.SendTo(buffer, ep) <= 0 then 
      raise (SocketException()) 
     buffer <- Array.create (buffer.Length + 20) 0uy 

     let mutable epr = ep :> EndPoint 
     if socket.ReceiveFrom(buffer, &epr) <= 0 then 
      raise (SocketException()) 
    finally 
     socket.Close() 

    buffer 

//---- Entensions to the F# Async class to allow up to 5 paramters (not just 3) 

type Async with 
    static member FromBeginEnd(arg1,arg2,arg3,arg4,beginAction,endAction,?cancelAction): Async<'T> = 
     Async.FromBeginEnd((fun (iar,state) -> beginAction(arg1,arg2,arg3,arg4,iar,state)), endAction, ?cancelAction=cancelAction) 
    static member FromBeginEnd(arg1,arg2,arg3,arg4,arg5,beginAction,endAction,?cancelAction): Async<'T> = 
     Async.FromBeginEnd((fun (iar,state) -> beginAction(arg1,arg2,arg3,arg4,arg5,iar,state)), endAction, ?cancelAction=cancelAction) 

//---- Extensions to the Socket class to provide async SendTo and ReceiveFrom 

type System.Net.Sockets.Socket with 

    member this.AsyncSendTo(buffer, offset, size, socketFlags, remoteEP) = 
     Async.FromBeginEnd(buffer, offset, size, socketFlags, remoteEP, 
          this.BeginSendTo, 
          this.EndSendTo) 
    member this.AsyncReceiveFrom(buffer, offset, size, socketFlags, remoteEP) = 
     Async.FromBeginEnd(buffer, offset, size, socketFlags, remoteEP, 
          this.BeginReceiveFrom, 
          (fun asyncResult -> this.EndReceiveFrom(asyncResult, remoteEP))) 

//---- Asynchronous Ping 

let AsyncPing (host : IPAddress, timeout : int) = 
    async { 
     let ep = IPEndPoint(host, 0) 
     use socket = new Socket(AddressFamily.InterNetwork, SocketType.Raw, ProtocolType.Icmp) 
     socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.SendTimeout, timeout) 
     socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, timeout) 

     let packet = EchoMessage() 
     let outbuffer = packet.Bytes 

     try 
      let! result = socket.AsyncSendTo(outbuffer, 0, outbuffer.Length, SocketFlags.None, ep) 
      if result <= 0 then 
       raise (SocketException()) 

      let epr = ref (ep :> EndPoint) 
      let inbuffer = Array.create (outbuffer.Length + 256) 0uy 
      let! result = socket.AsyncReceiveFrom(inbuffer, 0, inbuffer.Length, SocketFlags.None, epr) 
      if result <= 0 then 
       raise (SocketException()) 
      return inbuffer 
     finally 
      socket.Close() 
    } 
+0

有要重新發明System.Net.NetworkInformation.Ping.SendAsync()什麼特別的原因?它已經支持超時。 – 2010-04-03 17:32:20

+0

SendAsync/SendToAsync與上面的AsyncSendTo不一樣...前者不與F#異步工作流集成(極大地簡化了編寫異步代碼)。 – 2010-04-03 18:12:02

+0

呃,重點是你不必寫它。 – 2010-04-03 18:44:15

回答

2

經過一番思考,提出了以下幾點。此代碼將一個AsyncReceiveEx成員添加到Socket,其中包含一個超時值。它在接收方法中隱藏了看門狗定時器的細節......非常整齊和獨立。現在,這就是我正在尋找的!

請參閱下面的完整異步ping示例。

不知道,如果鎖是必要的,但有備無患......

type System.Net.Sockets.Socket with 
    member this.AsyncSend(buffer, offset, size, socketFlags, err) = 
     Async.FromBeginEnd(buffer, offset, size, socketFlags, err, 
          this.BeginSend, 
          this.EndSend, 
          this.Close) 

    member this.AsyncReceive(buffer, offset, size, socketFlags, err) = 
     Async.FromBeginEnd(buffer, offset, size, socketFlags, err, 
          this.BeginReceive, 
          this.EndReceive, 
          this.Close) 

    member this.AsyncReceiveEx(buffer, offset, size, socketFlags, err, (timeoutMS:int)) = 
     async { 
      let timedOut = ref false 
      let completed = ref false 
      let timer = new System.Timers.Timer(double(timeoutMS), AutoReset=false) 
      timer.Elapsed.Add(fun _ -> 
       lock timedOut (fun() -> 
        timedOut := true 
        if not !completed 
        then this.Close() 
        ) 
       ) 
      let complete() = 
       lock timedOut (fun() -> 
        timer.Stop() 
        timer.Dispose() 
        completed := true 
        ) 
      return! Async.FromBeginEnd(buffer, offset, size, socketFlags, err, 
           (fun (b,o,s,sf,e,st,uo) -> 
            let result = this.BeginReceive(b,o,s,sf,e,st,uo) 
            timer.Start() 
            result 
           ), 
           (fun result -> 
            complete() 
            if !timedOut 
            then err := SocketError.TimedOut; 0 
            else this.EndReceive(result, err) 
           ), 
           (fun() -> 
            complete() 
            this.Close() 
            ) 
           ) 
      } 

下面是一個完整平安的例子。爲了避免耗盡源端口並防止一次收到太多的回覆,它一次掃描一個class-c子網。

module Ping 

open System 
open System.Net 
open System.Net.Sockets 
open System.Threading 

//---- ICMP Packet Classes 

type IcmpMessage (t : byte) = 
    let mutable m_type = t 
    let mutable m_code = 0uy 
    let mutable m_checksum = 0us 

    member this.Type 
     with get() = m_type 

    member this.Code 
     with get() = m_code 

    member this.Checksum = m_checksum 

    abstract Bytes : byte array 

    default this.Bytes 
     with get() = 
      [| 
       m_type 
       m_code 
       byte(m_checksum) 
       byte(m_checksum >>> 8) 
      |] 

    member this.GetChecksum() = 
     let mutable sum = 0ul 
     let bytes = this.Bytes 
     let mutable i = 0 

     // Sum up uint16s 
     while i < bytes.Length - 1 do 
      sum <- sum + uint32(BitConverter.ToUInt16(bytes, i)) 
      i <- i + 2 

     // Add in last byte, if an odd size buffer 
     if i <> bytes.Length then 
      sum <- sum + uint32(bytes.[i]) 

     // Shuffle the bits 
     sum <- (sum >>> 16) + (sum &&& 0xFFFFul) 
     sum <- sum + (sum >>> 16) 
     sum <- ~~~sum 
     uint16(sum) 

    member this.UpdateChecksum() = 
     m_checksum <- this.GetChecksum() 


type InformationMessage (t : byte) = 
    inherit IcmpMessage(t) 

    let mutable m_identifier = 0us 
    let mutable m_sequenceNumber = 0us 

    member this.Identifier = m_identifier 
    member this.SequenceNumber = m_sequenceNumber 

    override this.Bytes 
     with get() = 
      Array.append (base.Bytes) 
         [| 
          byte(m_identifier) 
          byte(m_identifier >>> 8) 
          byte(m_sequenceNumber) 
          byte(m_sequenceNumber >>> 8) 
         |] 

type EchoMessage() = 
    inherit InformationMessage(8uy) 
    let mutable m_data = Array.create 32 32uy 
    do base.UpdateChecksum() 

    member this.Data 
     with get() = m_data 
     and set(d) = m_data <- d 
         this.UpdateChecksum() 

    override this.Bytes 
     with get() = 
      Array.append (base.Bytes) 
         (this.Data) 

//---- Entensions to the F# Async class to allow up to 5 paramters (not just 3) 

type Async with 
    static member FromBeginEnd(arg1,arg2,arg3,arg4,beginAction,endAction,?cancelAction): Async<'T> = 
     Async.FromBeginEnd((fun (iar,state) -> beginAction(arg1,arg2,arg3,arg4,iar,state)), endAction, ?cancelAction=cancelAction) 
    static member FromBeginEnd(arg1,arg2,arg3,arg4,arg5,beginAction,endAction,?cancelAction): Async<'T> = 
     Async.FromBeginEnd((fun (iar,state) -> beginAction(arg1,arg2,arg3,arg4,arg5,iar,state)), endAction, ?cancelAction=cancelAction) 

//---- Extensions to the Socket class to provide async SendTo and ReceiveFrom 

type System.Net.Sockets.Socket with 

    member this.AsyncSend(buffer, offset, size, socketFlags, err) = 
     Async.FromBeginEnd(buffer, offset, size, socketFlags, err, 
          this.BeginSend, 
          this.EndSend, 
          this.Close) 

    member this.AsyncReceive(buffer, offset, size, socketFlags, err) = 
     Async.FromBeginEnd(buffer, offset, size, socketFlags, err, 
          this.BeginReceive, 
          this.EndReceive, 
          this.Close) 

    member this.AsyncReceiveEx(buffer, offset, size, socketFlags, err, (timeoutMS:int)) = 
     async { 
      let timedOut = ref false 
      let completed = ref false 
      let timer = new System.Timers.Timer(double(timeoutMS), AutoReset=false) 
      timer.Elapsed.Add(fun _ -> 
       lock timedOut (fun() -> 
        timedOut := true 
        if not !completed 
        then this.Close() 
        ) 
       ) 
      let complete() = 
       lock timedOut (fun() -> 
        timer.Stop() 
        timer.Dispose() 
        completed := true 
        ) 
      return! Async.FromBeginEnd(buffer, offset, size, socketFlags, err, 
           (fun (b,o,s,sf,e,st,uo) -> 
            let result = this.BeginReceive(b,o,s,sf,e,st,uo) 
            timer.Start() 
            result 
           ), 
           (fun result -> 
            complete() 
            if !timedOut 
            then err := SocketError.TimedOut; 0 
            else this.EndReceive(result, err) 
           ), 
           (fun() -> 
            complete() 
            this.Close() 
            ) 
           ) 
      } 

//---- Asynchronous Ping 

let AsyncPing (ip : IPAddress, timeout : int) = 
    async { 
     use socket = new Socket(AddressFamily.InterNetwork, SocketType.Raw, ProtocolType.Icmp) 
     socket.Connect(IPEndPoint(ip, 0)) 

     let pingTime = System.Diagnostics.Stopwatch() 
     let packet = EchoMessage() 
     let outbuffer = packet.Bytes 
     let err = ref (SocketError()) 

     let isAlive = ref false 
     try 
      pingTime.Start() 
      let! result = socket.AsyncSend(outbuffer, 0, outbuffer.Length, SocketFlags.None, err) 
      pingTime.Stop() 

      if result <= 0 then 
       raise (SocketException(int(!err))) 

      let inbuffer = Array.create (outbuffer.Length + 256) 0uy 

      pingTime.Start() 
      let! reply = socket.AsyncReceiveEx(inbuffer, 0, inbuffer.Length, SocketFlags.None, err, timeout) 
      pingTime.Stop() 

      if result <= 0 && not (!err = SocketError.TimedOut) then 
       raise (SocketException(int(!err))) 

      isAlive := not (!err = SocketError.TimedOut) 
          && inbuffer.[25] = 0uy // Type 0 = echo reply (redundent? necessary?) 
          && inbuffer.[26] = 0uy // Code 0 = echo reply (redundent? necessary?) 
     finally 
      socket.Close() 

     return (ip, pingTime.Elapsed, !isAlive) 
    } 

let main() = 
    let pings net = 
     seq { 
      for node in 0..255 do 
       let ip = IPAddress.Parse(sprintf "192.168.%d.%d" net node) 
       yield Ping.AsyncPing(ip, 1000) 
      } 

    for net in 0..255 do 
     pings net 
     |> Async.Parallel 
     |> Async.RunSynchronously 
     |> Seq.filter (fun (_,_,alive) -> alive) 
     |> Seq.iter (fun (ip, time, alive) -> 
          printfn "%A %dms" ip time.Milliseconds) 

main() 
System.Console.ReadKey() |> ignore 
+0

這現在非常接近你想要的。但是這仍然有一個問題,請參閱我的最新答案。 – Brian 2010-04-07 16:43:48

+0

更新了代碼以反映您的修復。謝謝! – 2010-04-07 18:07:32

1

一對夫婦......

首先,可以將.NET模式調整爲F#異步。 FSharp.Core庫爲WebClient執行此操作;我想你可以在這裏使用相同的模式。這裏的Web客戶端代碼

type System.Net.WebClient with 
    member this.AsyncDownloadString (address:Uri) : Async<string> = 
     let downloadAsync = 
      Async.FromContinuations (fun (cont, econt, ccont) -> 
        let userToken = new obj() 
        let rec handler = 
          System.Net.DownloadStringCompletedEventHandler (fun _ args -> 
           if userToken = args.UserState then 
            this.DownloadStringCompleted.RemoveHandler(handler) 
            if args.Cancelled then 
             ccont (new OperationCanceledException()) 
            elif args.Error <> null then 
             econt args.Error 
            else 
             cont args.Result) 
        this.DownloadStringCompleted.AddHandler(handler) 
        this.DownloadStringAsync(address, userToken) 
       ) 
      async { 
       use! _holder = Async.OnCancel(fun _ -> this.CancelAsync()) 
       return! downloadAsync 
      } 

,我想你可以SendAsync/SendAsyncCancel/PingCompleted做同樣的(我還沒有仔細通認爲它)。

二,將您的方法命名爲AsyncPing,而不是PingAsync。 F#異步方法名爲AsyncFoo,而具有事件模式的方法名爲FooAsync

我沒仔細看過你的代碼,試圖找出錯誤可能在哪裏。

+0

將PingAsync重命名爲AsyncPing。如果時間允許,我會研究其他想法,看看它是否克服了我的超時問題。 – 2010-04-03 18:37:36

+0

試圖將其封裝在Async.FromContinuations中,但它仍然存在創建數百個線程的問題,否則將無法擴展(在並行ping B類時耗盡內存)。代碼發佈在一個單獨的答案,以防有人發現它的使用... – 2010-04-03 23:11:07

0

這是一個使用Async.FromContinuations的版本。

但是,這不是我的問題的答案,因爲它不能縮放。該代碼可能對某人有用,所以在此張貼。

這不是答案的原因是因爲System.Net.NetworkInformation.Ping似乎使用每個Ping一個線程和相當多的內存(可能由於線程堆棧空間)。嘗試ping整個B類網絡將耗盡內存並使用100個線程,而使用原始套接字的代碼只使用少量線程並且不到10Mb。

type System.Net.NetworkInformation.Ping with 
    member this.AsyncPing (address:IPAddress) : Async<PingReply> = 
     let pingAsync = 
      Async.FromContinuations (fun (cont, econt, ccont) -> 
        let userToken = new obj() 
        let rec handler = 
          PingCompletedEventHandler (fun _ args -> 
           if userToken = args.UserState then 
            this.PingCompleted.RemoveHandler(handler) 
            if args.Cancelled then 
             ccont (new OperationCanceledException()) 
            elif args.Error <> null then 
             econt args.Error 
            else 
             cont args.Reply) 
        this.PingCompleted.AddHandler(handler) 
        this.SendAsync(address, 1000, userToken) 
       ) 
     async { 
      use! _holder = Async.OnCancel(fun _ -> this.SendAsyncCancel()) 
      return! pingAsync 
     } 

let AsyncPingTest() = 
    let pings = 
     seq { 
      for net in 0..255 do 
       for node in 0..255 do 
        let ip = IPAddress.Parse(sprintf "192.168.%d.%d" net node) 
        let ping = new Ping() 
        yield ping.AsyncPing(ip) 
      } 
    pings 
    |> Async.Parallel 
    |> Async.RunSynchronously 
    |> Seq.iter (fun result -> 
         printfn "%A" result) 
0

編輯:代碼更改爲工作版本。

James,我修改了你的代碼,它看起來可以和你的版本一樣工作,但是使用MailboxProcessor作爲超時處理引擎。代碼速度比您的版本低4倍,但使用的內存減少了1.5倍。

let AsyncPing (host: IPAddress) timeout = 
    let guard = 
     MailboxProcessor<AsyncReplyChannel<Socket*byte array>>.Start(
      fun inbox -> 
      async { 
       try 
        let socket = new Socket(AddressFamily.InterNetwork, SocketType.Raw, ProtocolType.Icmp) 
        try 
         let ep = IPEndPoint(host, 0) 
         let packet = EchoMessage() 
         let outbuffer = packet.Bytes 
         let! reply = inbox.Receive() 
         let! result = socket.AsyncSendTo(outbuffer, 0, outbuffer.Length, SocketFlags.None, ep) 
         if result <= 0 then 
          raise (SocketException()) 
         let epr = ref (ep :> EndPoint) 
         let inbuffer = Array.create (outbuffer.Length + 256) 0uy 
         let! result = socket.AsyncReceiveFrom(inbuffer, 0, inbuffer.Length, SocketFlags.None, epr) 
         if result <= 0 then 
          raise (SocketException()) 
         reply.Reply(socket,inbuffer) 
         return() 
        finally 
         socket.Close() 
       finally 
        () 
      }) 
    async { 
     try 
      //#1: blocks thread and as result have large memory footprint and too many threads to use 
      //let socket,r = guard.PostAndReply(id,timeout=timeout) 

      //#2: suggested by Dmitry Lomov 
      let! socket,r = guard.PostAndAsyncReply(id,timeout=timeout) 
      printfn "%A: ok" host 
      socket.Close() 
     with 
      _ -> 
       printfn "%A: failed" host 
       () 
     } 

//test it 
//timeout is ms interval 
//i.e. 10000 is equal to 10s 
let AsyncPingTest timeout = 
    seq { 
     for net in 1..254 do 
      for node in 1..254 do 
       let ip = IPAddress.Parse(sprintf "192.168.%d.%d" net node) 
       yield AsyncPing ip timeout 
    } 
    |> Async.Parallel 
    |> Async.RunSynchronously 
+0

我很確定這個將泄漏一個套接字和一個待處理的異步接收。使用郵箱是一個好主意,但它需要一個額外的命令來發送一個Close到套接字來解除懸而未決的I/O。看到我接受的答案是另一種方法。 – 2010-04-07 21:44:11

+0

@James:我發現泄漏,現在它也可以工作:)您的解決方案速度可達4倍。 – ssp 2010-04-22 09:04:56

7

詹姆斯,你自己接受的答案有一個問題,我想指出。您只分配一個計時器,使AsyncReceiveEx返回的異步對象成爲有狀態的一次性對象。下面是我下調一個類似的例子:

let b,e,c = Async.AsBeginEnd(Async.Sleep) 

type Example() = 
    member this.Close() =() 
    member this.AsyncReceiveEx(sleepTime, (timeoutMS:int)) = 
     let timedOut = ref false 
     let completed = ref false 
     let timer = new System.Timers.Timer(double(timeoutMS), AutoReset=false) 
     timer.Elapsed.Add(fun _ -> 
      lock timedOut (fun() -> 
       timedOut := true 
       if not !completed 
       then this.Close() 
       ) 
      ) 
     let complete() = 
      lock timedOut (fun() -> 
       timer.Stop() 
       timer.Dispose() 
       completed := true 
       ) 
     Async.FromBeginEnd(sleepTime, 
          (fun st -> 
           let result = b(st) 
           timer.Start() 
           result 
          ), 
          (fun result -> 
           complete() 
           if !timedOut 
           then printfn "err";() 
           else e(result) 
          ), 
          (fun() -> 
           complete() 
           this.Close() 
           ) 
          ) 

let ex = new Example() 
let a = ex.AsyncReceiveEx(3000, 1000) 
Async.RunSynchronously a 
printfn "ok..." 
// below throws ODE, because only allocated one Timer 
Async.RunSynchronously a 

理想情況下,你希望每一個通過AsyncReceiveEx返回有同樣的表現異步的,這意味着每次運行都需要自己的計時器的「運行」,並設置參考標誌。這是很容易修復正是如此:

let b,e,c = Async.AsBeginEnd(Async.Sleep) 

type Example() = 
    member this.Close() =() 
    member this.AsyncReceiveEx(sleepTime, (timeoutMS:int)) = 
     async { 
     let timedOut = ref false 
     let completed = ref false 
     let timer = new System.Timers.Timer(double(timeoutMS), AutoReset=false) 
     timer.Elapsed.Add(fun _ -> 
      lock timedOut (fun() -> 
       timedOut := true 
       if not !completed 
       then this.Close() 
       ) 
      ) 
     let complete() = 
      lock timedOut (fun() -> 
       timer.Stop() 
       timer.Dispose() 
       completed := true 
       ) 
     return! Async.FromBeginEnd(sleepTime, 
          (fun st -> 
           let result = b(st) 
           timer.Start() 
           result 
          ), 
          (fun result -> 
           complete() 
           if !timedOut 
           then printfn "err";() 
           else e(result) 
          ), 
          (fun() -> 
           complete() 
           this.Close() 
           ) 
          ) 
     } 
let ex = new Example() 
let a = ex.AsyncReceiveEx(3000, 1000) 
Async.RunSynchronously a 
printfn "ok..." 
Async.RunSynchronously a 

唯一的變化是把AsyncReceiveEx的體內async{...}並有最後一行return!

+0

很好抓,謝謝!答案已更新以反映此修復程序。 – 2010-04-07 17:47:04