【发布时间】:2016-08-07 13:51:30
【问题描述】:
我目前正在用 F# 编写一个小型服务器类型,它基本上存储了与它的活动 TCP 连接。在其循环中,它会检查新连接,并使用以下代码检查当前连接是否仍然存在:
type TCPListenerServer(discoveryPort:int) =
let server = new TcpListener (IPAddress.Loopback, discoveryPort)
let activeConnections = new List<TcpClient>()
let cancellationToken = new CancellationTokenSource()
let connectionIsStillActive (client:TcpClient) =
let ipProperties = IPGlobalProperties.GetIPGlobalProperties ()
let allTcpConnections = ipProperties.GetActiveTcpConnections ()
let relevantTcpConnections = Array.filter (fun (connectionInfo:TcpConnectionInformation) ->
(connectionInfo.LocalEndPoint = (client.Client.LocalEndPoint :?> IPEndPoint)) && (connectionInfo.RemoteEndPoint = (client.Client.RemoteEndPoint :?> IPEndPoint))) allTcpConnections
try
let stateOfConnection = (Array.get relevantTcpConnections 0).State
match stateOfConnection with
| TcpState.Established ->
true
| _ ->
false
with
| :? System.IndexOutOfRangeException as ex ->
false
let rec loop (pendingConnection:Task<TcpClient>) = async {
let newPendingConnection, client =
match pendingConnection.Status with
| TaskStatus.Created | TaskStatus.WaitingForActivation | TaskStatus.WaitingToRun
| TaskStatus.WaitingForChildrenToComplete | TaskStatus.Running ->
(None, None)
| TaskStatus.Faulted ->
let result = pendingConnection.Exception
raise (new System.NotImplementedException())
| TaskStatus.Canceled ->
raise (new System.NotImplementedException())
| TaskStatus.RanToCompletion ->
let connectionTask = server.AcceptTcpClientAsync ()
(Some connectionTask, Some pendingConnection.Result)
| _ ->
raise (new System.NotImplementedException())
// Add the new client to the list
Option.iter (fun c -> activeConnections.Add c) client
// Switch the new pending connection if there is one
let connectionAttempt = defaultArg newPendingConnection pendingConnection
// Check that the connections are still alive
let connectionsToRemove = Seq.filter (fun (connection:TcpClient) -> not (connectionIsStillActive connection)) activeConnections
Seq.iter (fun connection -> activeConnections.Remove connection |> ignore) connectionsToRemove // <-- Exception happens here
Async.Sleep 1000 |> Async.RunSynchronously
return! loop connectionAttempt
}
member x.Start () =
if not cancellationToken.Token.CanBeCanceled
then
raise (new System.Exception("Cancellation token cannot be used to cancel server loop task."))
try
server.Start ()
let connectionTask = server.AcceptTcpClientAsync ()
Async.Start (loop connectionTask, cancellationToken.Token)
with
| :? SocketException as ex ->
server.Stop ()
raise ex
member x.Stop () =
cancellationToken.Cancel ()
Async.Sleep 2000 |> Async.RunSynchronously
server.Stop ()
activeConnections.Clear ()
member x.ActiveConnections =
activeConnections
为了看看这是否有效,我实现了这个简单的单元测试:
[<TestMethod>]
[<TestCategory(Networking)>]
member x.``Start Server, Client Connects, then Disconnects`` () =
let server = new TCPListenerServer(44000)
server.Start ()
let client = createClientAndConnect 44000
Async.Sleep 5000 |> Async.RunSynchronously
client.GetStream().Close()
client.Close()
Async.Sleep 5000 |> Async.RunSynchronously
Assert.IsTrue(server.ActiveConnections.Count = 0, "There are still connections in the server's active connections list.")
cleanupTest server (Some [client])
不幸的是,当我运行这个测试时,一旦我在客户端上调用了Close(),就会得到以下异常:
System.InvalidOperationException:集合已修改;枚举操作可能无法执行。 在 System.ThrowHelper.ThrowInvalidOperationException(ExceptionResource 资源) 在 System.Collections.Generic.List
1.Enumerator.MoveNextRare() at System.Collections.Generic.List1.Enumerator.MoveNext() 在 Microsoft.FSharp.Collections.IEnumerator.next@224[T](FSharpFunc2 f, IEnumerator1 e, FSharpRef1 started, Unit unitVar0) at Microsoft.FSharp.Collections.IEnumerator.filter@219.System-Collections-IEnumerator-MoveNext() at Microsoft.FSharp.Collections.SeqModule.Iterate[T](FSharpFunc2 action, IEnumerable1 source) at TCPListenerServer.clo@35.Invoke(Unit unitVar) in E:\Documents\Source Control Projects\WiDroid\Core.Networking\TCPListenerServer.fs:line 61 at Microsoft.FSharp.Control.AsyncBuilderImpl.callA@851.Invoke(AsyncParams1 args)
我了解当您在迭代集合时修改集合时会发生此异常。但从我所见,我并没有直接在我的代码中这样做。我想知道为什么我会收到此异常?
【问题讨论】:
-
我不知道 F#,但我猜是非活动套接字清理代码:
let connectionsToRemove = Seq.filter (fun (connection:TcpClient) -> not (connectionIsStillActive connection)) activeConnections Seq.iter (fun connection -> activeConnections.Remove connection |> ignore) connectionsToRemove。我认为迭代connectionsToRemove实际上是迭代activeConnections,当您在迭代期间从activeConnections中删除一个项目时会导致问题。 -
@wablab 异常发生在那个地方:
Seq.iter (fun connection -> activeConnections.Remove connection |> ignore) connectionsToRemove确实如此。我看不出这两个列表是如何连接的,为什么迭代一个会迭代另一个? -
您可能需要考虑使用二进制搜索来将您的问题缩减为Minimal, Complete, and Verifiable example。通常,根据我的经验,仅仅这样做就可以让您自己解决问题。
-
我相信
connectionsToRemove不是一个真实的对象列表;相反,它表示一个延迟执行的表达式,在您迭代它之前不会做任何事情,此时它开始迭代底层的activeConnections列表。请参阅msdn.microsoft.com/en-us/visualfsharpdocs/conceptual/… 了解更多信息。
标签: .net exception collections f#