【从零构建分布式在线评测系统】三、服务器端:评测机管理器和评测机类

        在服务器端,有很多事情要做:

1、管理Judge0服务器

2、轮询数据库:获取待评测的用户提交(1次用户提交对应多个评测点,每个评测点都需要下发给Judge0进行评测)

3、接收Judge0服务器的回调,根据Token来查询当前测试点是哪个题目的,将结果加入到该题目中。当一个题目的测试点全部返回,计算分数写入数据库。

        这中间有一些生产者消费者模型,也涉及到很多OJ和Judge0常量的转换、数据库格式等,基本上所有代码都要考虑异步和并发。本文主要展示上一篇中服务器管理器的具体实现以及Judge0Server是如何完成各项功能的。

一、服务器管理的完整代码

Imports System.Collections.Concurrent
Imports System.Net
Imports System.Text
Imports System.Text.Json
Imports System.Threading


Public Class Judge0ServerManager
    Structure CallBackPort
        Public port As Integer
        Public used As Boolean

        Sub New(p As Integer)
            port = p
            used = False
        End Sub
    End Structure
    Private Shared HttpListener As HttpListener
    Private Shared CallbackPortList As ConcurrentDictionary(Of Integer, CallBackPort)
    Private Shared Judge0ServerGroup0List As ConcurrentDictionary(Of String, Judge0Server)
    Private Shared Judge0ServerGroup1List As ConcurrentDictionary(Of String, Judge0Server)

    Friend Shared Async Sub Work()
        '开始监听服务器心跳
        BeginHttpListerner()
        Dim lastCleanupTime As DateTime = DateTime.UtcNow
        While True
            ' 每10秒清理一次超时token
            If (DateTime.UtcNow - lastCleanupTime).TotalSeconds >= 10 Then
                CleanupAllServersExpiredTokens()
                lastCleanupTime = DateTime.UtcNow
            End If

            '检查待评测队列内个数
            Dim qCountSmall As Integer = Judge0TestQueue.Count(0)
            If qCountSmall > 0 AndAlso Judge0ServerGroup0List.Count > 0 Then
                '更新每个服务器工作队列大小,为每个服务器分发任务。若某个服务器无法工作,移除它。
                Dim sum As Integer = qCountSmall
                For Each kv In Judge0ServerGroup0List
                    sum += kv.Value.Judge0QueueCount    '加上每个服务器现有负载
                Next
                '使服务器达到平均个数(不是强制的,服务器会自行决定是否摘取,如果队列为空服务器不会摘取)
                Dim a As Integer = (sum + Judge0ServerGroup0List.Count - 1) / Judge0ServerGroup0List.Count
                For Each v In Judge0ServerGroup0List.Values
                    StartServerProcessing(v, a)
                Next
            End If
            Dim qCountBig As Integer = Judge0TestQueue.Count(1)
            If qCountBig > 0 AndAlso Judge0ServerGroup1List.Count > 0 Then
                Dim sum As Integer = qCountBig
                For Each kv In Judge0ServerGroup1List
                    sum += kv.Value.Judge0QueueCount    '加上每个服务器现有负载
                Next
                '使服务器达到平均个数(不是强制的,服务器会自行决定是否摘取,如果队列为空服务器不会摘取)
                Dim a As Integer = (sum + Judge0ServerGroup1List.Count - 1) / Judge0ServerGroup1List.Count
                For Each v In Judge0ServerGroup1List.Values
                    StartServerProcessing(v, a)
                Next
            End If
            Await Task.Delay(200)   '每200毫秒检测一次
        End While
    End Sub
    Private Shared Sub CleanupAllServersExpiredTokens()
        Try
            For Each server In Judge0ServerGroup0List.Values
                server.RemoveExpiredTokens()
                server.ValidateAndFixQueueCount() ' 清理后校验计数
            Next

            For Each server In Judge0ServerGroup1List.Values
                server.RemoveExpiredTokens()
                server.ValidateAndFixQueueCount() ' 清理后校验计数
            Next
        Catch ex As Exception
            Console.WriteLine($"[{DateTime.Now}]" & $"CleanupAllServersExpiredTokens异常: {ex.Message}")
        End Try
    End Sub

    Private Shared Sub StartServerProcessing(server As Judge0Server, targetQueueSize As Integer)
        Task.Run(Async Function()
                     Dim workCount = targetQueueSize - server.Judge0QueueCount
                     Try
                         Dim success As Boolean = Await server.Work(workCount)
                         If Not success Then
                             Console.WriteLine("server.Work(workCount) 出现错误!")
                         End If
                     Catch ex As Exception
                         Console.WriteLine("RemoveServer(server) 出现错误!")
                     End Try
                 End Function)
    End Sub

    Private Shared Sub RemoveServer(ip As String, group As Integer)
        Try
            Dim removedServer As Judge0Server = Nothing
            Dim success As Boolean = False

            If group = 0 Then
                success = Judge0ServerGroup0List.TryRemove(ip, removedServer)
            Else
                success = Judge0ServerGroup1List.TryRemove(ip, removedServer)
            End If

            If success AndAlso removedServer IsNot Nothing Then
                Console.WriteLine($"[{DateTime.Now}]" & $"移除服务器(Group{group}): {ip}")
                ' 回滚该服务器上所有待处理的提交
                RollbackPendingSubmissions(removedServer)
            End If

            Console.WriteLine($"[{DateTime.Now}]" & $"当前服务器数量 - Group0: {Judge0ServerGroup0List.Count}, Group1: {Judge0ServerGroup1List.Count}")

        Catch ex As Exception
            Console.WriteLine($"[{DateTime.Now}]" & $"RemoveServer异常: {ex.Message}")
        End Try
    End Sub

    Private Shared Sub RollbackPendingSubmissions(server As Judge0Server)
        Try
            server.RollbackAllPendingSubmissions()
        Catch ex As Exception
            Console.WriteLine($"[{DateTime.Now}]" & $"RollbackPendingSubmissions异常: {ex.Message}")
        End Try
    End Sub

    Shared Function BeginHttpListerner() As Boolean
        CallbackPortList = New ConcurrentDictionary(Of Integer, CallBackPort)
        For i As Integer = OJConfig.ListerPortBegin To OJConfig.ListerPortEnd
            CallbackPortList.TryAdd(i, New CallBackPort(i))
        Next
        Judge0ServerGroup0List = New ConcurrentDictionary(Of String, Judge0Server)
        Judge0ServerGroup1List = New ConcurrentDictionary(Of String, Judge0Server)
        Try
            HttpListener = New HttpListener
            HttpListener.Prefixes.Add($"http://*:{OJConfig.ListerHearbeatPort}/")
            HttpListener.Start()
            ' 改为异步处理
            BeginGetContext()
            Task.Run(AddressOf HealthCheck)
            Return True
        Catch ex As Exception
            Console.WriteLine($"[{DateTime.Now.ToString}]" & "HttpCallBack Err:" & ex.ToString)
            Return False
        End Try
    End Function

    Private Shared Sub BeginGetContext()
        ' 异步开始接收请求
        HttpListener.BeginGetContext(AddressOf HandleRequest, HttpListener)
    End Sub

    Private Shared Sub HandleRequest(ar As IAsyncResult)
        Dim context As HttpListenerContext = Nothing
        Try
            Dim listener As HttpListener = CType(ar.AsyncState, HttpListener)

            context = listener.EndGetContext(ar)

            ' 立即开始监听下一个请求
            BeginGetContext()

            ' 获取客户端 IP 地址
            Dim clientIP As String = context.Request.RemoteEndPoint.Address.ToString
            ' 读取请求数据并复制到内存中,这样可以在异步处理中使用
            Dim requestBody As String = Nothing
            Using reader As New IO.StreamReader(context.Request.InputStream, context.Request.ContentEncoding)
                requestBody = reader.ReadToEnd()
            End Using


            ' Console.WriteLine($"[{datetime.now.tostring}]" &$"接收到一个回调。")

            ' 使用线程池处理请求,但不再传递context对象
            ThreadPool.QueueUserWorkItem(Sub(state)
                                             Try
                                                 ' 处理请求数据
                                                 Dim resultResponseApi = JsonSerializer.Deserialize(Of HeartbeatMessage)(requestBody)

                                                 ' 处理结果并加入队列
                                                 ProcessResult(resultResponseApi, clientIP)
                                             Catch ex As Exception
                                                 If String.IsNullOrEmpty(requestBody) Then
                                                     Console.WriteLine($"[{DateTime.Now.ToString}] {clientIP} 扫描了本地端口 {OJConfig.ListerHearbeatPort}")
                                                 Else
                                                     Console.WriteLine($"[{DateTime.Now.ToString}]" & clientIP & " Judge0ServerManager Request processing error: " & ex.ToString & vbCrLf & "原始内容:" & requestBody)
                                                 End If
                                             End Try
                                         End Sub)

            ' 立即响应客户端
            Dim responseBuffer As Byte() = Encoding.UTF8.GetBytes("Callback received successfully.")
            context.Response.ContentLength64 = responseBuffer.Length
            context.Response.StatusCode = 200
            context.Response.OutputStream.Write(responseBuffer, 0, responseBuffer.Length)
            context.Response.Close()

        Catch ex As Exception
            Console.WriteLine($"[{DateTime.Now.ToString}]" & "Judge0serverManager HandleRequest发生错误:" & ex.ToString)
            If context IsNot Nothing AndAlso context.Response IsNot Nothing Then
                context.Response.StatusCode = 500
                context.Response.Close()
            End If
        End Try
    End Sub

    Private Shared Sub ProcessResult(resultResponseApi As HeartbeatMessage, clientIP As String)

        If Judge0ServerGroup0List.ContainsKey(clientIP) OrElse Judge0ServerGroup1List.ContainsKey(clientIP) Then
        Else
            If resultResponseApi.Group = 0 Then
                'Console.WriteLine($"{Now.ToString} 收到{clientIP}的心跳消息,可用内存(MB):{resultResponseApi.AvailableMemoryOfMB},内存保护阈值(MB):{resultResponseApi.AvailableMemoryThresholdOfMB}。共有服务器:{Judge0ServerGroup0List.Count + 1}/{ Judge0ServerGroup1List.Count}")
            Else
                'Console.WriteLine($"{Now.ToString} 收到{clientIP}的心跳消息,可用内存(MB):{resultResponseApi.AvailableMemoryOfMB}, 内存保护阈值(MB):{resultResponseApi.AvailableMemoryThresholdOfMB}。共有服务器:{Judge0ServerGroup0List.Count }/{ Judge0ServerGroup1List.Count + 1}")
            End If
        End If

        ' 检查是否是服务器重启(之前存在,现在重新连接)
        Dim isRestart As Boolean = False
        If Judge0ServerGroup0List.ContainsKey(clientIP) Then
            Dim existingServer = Judge0ServerGroup0List(clientIP)
            If (DateTime.UtcNow - existingServer.Judge0ServerlastHeartbeat).TotalSeconds > 30 Then
                isRestart = True
                Console.WriteLine($"[{DateTime.Now}]" & $"检测到服务器重启(Group0): {clientIP}")
                RemoveServer(clientIP, 0)
            End If
        ElseIf Judge0ServerGroup1List.ContainsKey(clientIP) Then
            Dim existingServer = Judge0ServerGroup1List(clientIP)
            If (DateTime.UtcNow - existingServer.Judge0ServerlastHeartbeat).TotalSeconds > 30 Then
                isRestart = True
                Console.WriteLine($"[{DateTime.Now}]" & $"检测到服务器重启(Group1): {clientIP}")
                RemoveServer(clientIP, 1)
            End If
        End If

        Dim CallBackURL As String
        If isPrivateNetwork(clientIP) Then
            CallBackURL = OJConfig.OJPrivateNetWorkURL
        Else
            CallBackURL = OJConfig.OJPublicNetWorkURL
        End If
        If resultResponseApi.Group = 0 Then
            If Judge0ServerGroup1List.ContainsKey(clientIP) Then RemoveServer(clientIP, 1)
            Dim Server As Judge0Server = Judge0ServerGroup0List.GetOrAdd(
            clientIP,
            Function(key)
                Dim NextCallBackPort As Integer = FindMinUnusedPort()
                SetPortUsedStatus(NextCallBackPort, True)
                Return New Judge0Server(clientIP, resultResponseApi.Port, NextCallBackPort, CallBackURL, resultResponseApi.QueueMaxSize, resultResponseApi.RequestCountSize, resultResponseApi.AvailableMemoryThresholdOfMB, resultResponseApi.Group)
            End Function)
            ' 更新心跳时间
            Server.Judge0ServerlastHeartbeat = DateTime.UtcNow
            '更新可用内存大小
            Server.Judge0ServerAvailableMemoryOfMB = resultResponseApi.AvailableMemoryOfMB
        Else
            If Judge0ServerGroup0List.ContainsKey(clientIP) Then RemoveServer(clientIP, 0)
            Dim Server As Judge0Server = Judge0ServerGroup1List.GetOrAdd(
            clientIP,
            Function(key)
                Dim NextCallBackPort As Integer = FindMinUnusedPort()
                SetPortUsedStatus(NextCallBackPort, True)
                Return New Judge0Server(clientIP, resultResponseApi.Port, NextCallBackPort, CallBackURL, resultResponseApi.QueueMaxSize, resultResponseApi.RequestCountSize, resultResponseApi.AvailableMemoryThresholdOfMB, resultResponseApi.Group)
            End Function)
            ' 更新心跳时间
            Server.Judge0ServerlastHeartbeat = DateTime.UtcNow
            '更新可用内存大小
            Server.Judge0ServerAvailableMemoryOfMB = resultResponseApi.AvailableMemoryOfMB
        End If
        If isRestart Then
            Console.WriteLine($"[{DateTime.Now}]" & $"服务器{clientIP}重启完成,重新注册")
        End If
    End Sub

    Private Shared Sub LogServerStatus()
        Console.WriteLine($"[{DateTime.Now}]" & $"服务器状态 - Group0: {Judge0ServerGroup0List.Count}台, Group1: {Judge0ServerGroup1List.Count}台")
        For Each server In Judge0ServerGroup0List.Values
            Console.WriteLine($"[{DateTime.Now}]" & $"  Group0 - {server.Judge0ServerIP}: 队列={server.Judge0QueueCount}, 最后心跳={(DateTime.UtcNow - server.Judge0ServerlastHeartbeat).TotalSeconds}秒前")
        Next
        For Each server In Judge0ServerGroup1List.Values
            Console.WriteLine($"[{DateTime.Now}]" & $"  Group1 - {server.Judge0ServerIP}: 队列={server.Judge0QueueCount}, 最后心跳={(DateTime.UtcNow - server.Judge0ServerlastHeartbeat).TotalSeconds}秒前")
        Next
    End Sub


    Private Shared Function isPrivateNetwork(ip As String) As Boolean
        Try
            Dim address As IPAddress = IPAddress.Parse(ip)

            ' 如果是IPv6地址,可以根据需要处理或直接返回False
            If address.AddressFamily = Sockets.AddressFamily.InterNetworkV6 Then
                ' 对于IPv6,您可以在这里添加私有地址判断逻辑
                ' 这里先简单返回False
                Return False
            End If

            ' 获取IPv4地址的字节数组
            Dim bytes As Byte() = address.GetAddressBytes()

            ' 检查私有地址范围:

            ' 10.0.0.0/8: 10.0.0.0 - 10.255.255.255
            If bytes(0) = 10 Then Return True

            ' 172.16.0.0/12: 172.16.0.0 - 172.31.255.255
            If bytes(0) = 172 AndAlso bytes(1) >= 16 AndAlso bytes(1) <= 31 Then Return True

            ' 192.168.0.0/16: 192.168.0.0 - 192.168.255.255
            If bytes(0) = 192 AndAlso bytes(1) = 168 Then Return True

            ' 127.0.0.0/8: 环回地址
            If bytes(0) = 127 Then Return True

            ' 169.254.0.0/16: 链路本地地址
            If bytes(0) = 169 AndAlso bytes(1) = 254 Then Return True

            Return False

        Catch ex As Exception
            Console.WriteLine($"[{DateTime.Now}] IP地址解析错误: {ip} - {ex.Message}")
            Return False
        End Try
    End Function

    Private Shared Function FindMinUnusedPort() As Integer
        If CallbackPortList Is Nothing OrElse CallbackPortList.IsEmpty Then
            Return -1
        End If

        Dim minUnusedPort = CallbackPortList.Values.
                        Where(Function(portInfo) Not portInfo.used AndAlso CallbackPortList.ContainsKey(portInfo.port)).
                        OrderBy(Function(portInfo) portInfo.port).
                        FirstOrDefault()

        If minUnusedPort.port <> 0 Then
            Return minUnusedPort.port
        Else
            Return -1
        End If
    End Function

    ''' <summary>
    ''' 设置指定端口的used标志
    ''' </summary>
    ''' <param name="port">端口号</param>
    ''' <param name="isUsed">使用状态</param>
    ''' <returns>True表示设置成功,False表示端口不存在</returns>
    Private Shared Function SetPortUsedStatus(port As Integer, isUsed As Boolean) As Boolean
        If CallbackPortList Is Nothing OrElse Not CallbackPortList.ContainsKey(port) Then
            Return False
        End If

        Dim existingPort As CallBackPort
        Dim success = CallbackPortList.TryGetValue(port, existingPort)
        If success Then
            ' 创建更新后的端口信息
            Dim updatedPort = existingPort
            updatedPort.used = isUsed

            ' 更新字典中的值
            CallbackPortList.AddOrUpdate(port, updatedPort,
                                   Function(key, oldValue) updatedPort)
            Return True
        End If

        Return False
    End Function
    Private Shared Async Sub HealthCheck()
        While True
            Try
                Await Task.Delay(45000) ' 45秒检查一次

                Dim deadServers As New List(Of (String, Integer))

                ' 检查所有服务器
                For Each server In Judge0ServerGroup0List
                    If (DateTime.UtcNow - server.Value.Judge0ServerlastHeartbeat).TotalSeconds > 120 Then ' 2分钟无心跳
                        deadServers.Add((server.Key, 0))
                    End If
                Next

                For Each server In Judge0ServerGroup1List
                    If (DateTime.UtcNow - server.Value.Judge0ServerlastHeartbeat).TotalSeconds > 120 Then
                        deadServers.Add((server.Key, 1))
                    End If
                Next

                ' 处理死亡的服务器
                For Each deadServer In deadServers
                    Console.WriteLine($"[{DateTime.Now}]" & $"检测到死亡服务器(Group{deadServer.Item2}): {deadServer.Item1},最后心跳: {(DateTime.UtcNow - Judge0ServerGroup0List(deadServer.Item1).Judge0ServerlastHeartbeat).TotalSeconds:F1}秒前")
                    RemoveServer(deadServer.Item1, deadServer.Item2)
                Next

            Catch ex As Exception
                Console.WriteLine($"[{DateTime.Now}]" & $"HealthCheck异常: {ex.Message}")
            End Try
        End While
    End Sub

End Class


<Serializable>
Public Class HeartbeatMessage
    Public Property Port As Integer     'Judge0服务器端口
    Public Property QueueMaxSize As Integer     '队列最大大小
    Public Property RequestCountSize As Integer '同时最大投递数
    Public Property Group As Integer    '0为低性能组(仅分配小任务),1为高性能组(可分配任何任务)
    Public Property AvailableMemoryOfMB As Double  '可用内存大小MB
    Public Property AvailableMemoryThresholdOfMB As Double '可用内存保护阈值,可用内存低于该值后应不再向服务器投递评测
End Class

二、JudgeServer类

        Judge0Server 类是分布式评测系统中的工作节点代理,它封装了与单个 Judge0 服务器实例的所有交互逻辑。可以把它理解为每个 Judge0 节点的"管家",负责管理任务提交、状态监控、结果回调等全生命周期操作。

1. 任务提交与管理

向 Judge0 服务器提交评测任务

管理任务的 Token(唯一标识)

跟踪任务超时和状态

2. 负载控制

查询服务器实时状态(队列长度、工作线程等)

计算可用任务槽位

控制并发请求数

3. 结果回调处理

监听 Judge0 的回调请求

解析评测结果(支持超长响应简化)

将结果放入全局结果队列

4. 故障恢复

清理超时的 Token

服务器故障时回滚所有待处理任务

队列计数校验和修复

        其核心工作流程:

我们就核心的任务提交和Token管理进行说明:

Private Class TokenInfo
    Public Property SubmitInfo As SubmitInfo      ' 原始提交信息
    Public Property CreatedTime As DateTime       ' 创建时间
    Public Property LastCallbackTime As DateTime  ' 最后回调时间
    Public Property TimeoutMinutes As Integer     ' 超时时间(动态计算)
    
    Public ReadOnly Property IsExpired As Boolean ' 是否已超时
End Class
Private Async Function SendRequest(SubmitInfo As SubmitInfo) As Task(Of Integer)
    ' 1. 构建请求体(包含代码、参数、回调地址等)
    ' 2. 发送到 Judge0 /submissions API
    ' 3. 解析返回的 Token
    ' 4. 记录 TokenInfo(包含超时时间)
    ' 5. 增加队列计数
End Function

其完整代码:

   Private Async Function SendRequest(SubmitInfo As Judge0TestQueue.SubmitInfo) As Task(Of Integer)
       ' 定义请求的内容  
       Dim jsonContent As New With {
       .source_code = SubmitInfo.UserSource,
       .language_id = SubmitInfo.LanguageID,
       .stdin = SubmitInfo.Input,
       .expected_output = SubmitInfo.Output,
       .compiler_options = SubmitInfo.CompileOptions,
       .cpu_time_limit = SubmitInfo.TimeLimit,     '限制程序运行时间
       .cpu_extra_time = 3.0F,                     '额外的运行时间
       .wall_time_limit = .cpu_extra_time + 3.0F,  '包括编译等的额外时间
       .memory_limit = SubmitInfo.MemoryLimit,
       .callback_url = $"{Judge0CallBackURL}:{Judge0ServerCallBackPort}",
       .enable_network = False
       }

       Dim content As New StringContent(JsonSerializer.Serialize(jsonContent), Encoding.UTF8, "application/json")

       Try
           If (DateTime.UtcNow - Judge0ServerlastHeartbeat).TotalSeconds > 30 Then
               Console.WriteLine($"[{DateTime.Now}]" & $"{Judge0ServerIP}服务器心跳超时,跳过提交")
               Return -5001
           End If
           ' 发送 POST 请求  
           Dim response As HttpResponseMessage = Await httpClient.PostAsync(Judge0ServerSubmissionURL, content)
           If response.IsSuccessStatusCode Then
               Dim responseBody As String = Await response.Content.ReadAsStringAsync()

               ' 提取 token  
               Dim jsonResponse As JsonDocument
               Try
                   jsonResponse = JsonDocument.Parse(responseBody)
               Catch ex As Exception
                   Console.WriteLine($"[{DateTime.Now}]" & $"{Judge0ServerIP}解析responseBody{responseBody}出错:{ex}")
                   Return -3001
               End Try

               Dim token As String = String.Empty
               Try
                   token = jsonResponse.RootElement.GetProperty("token").GetString()
               Catch ex As Exception
                   Console.WriteLine($"[{DateTime.Now}]" & $"{Judge0ServerIP}获取token失败:{responseBody}")
                   Return -3002
               End Try

               If String.IsNullOrEmpty(token) OrElse token = "null" Then
                   Console.WriteLine($"[{DateTime.Now}]" & $"{Judge0ServerIP}获取token失败:{responseBody}")
                   Return -3003
               End If

               Dim tokenInfo As New TokenInfo With {
                   .SubmitInfo = SubmitInfo,
                   .CreatedTime = DateTime.UtcNow,
                   .LastCallbackTime = DateTime.MinValue,
                   .TimeoutMinutes = CalculateTimeoutMinutes(SubmitInfo)
               }

               ' 记录Token信息
               If TokenInfoDict.TryAdd(token, tokenInfo) Then
                   ' 只有Token记录成功后才增加队列计数
                   Interlocked.Increment(Judge0QueueCount)
                   'Console.WriteLine($"[{DateTime.Now}]" & $"{Judge0ServerIP}成功提交评测,Token: {token},SolutionID: {SubmitInfo.SolutionID},超时: {tokenInfo.TimeoutMinutes}分钟,当前队列数: {Judge0QueueCount}")
                   Return SubmitInfo.SolutionID
               Else
                   Judge0TestQueue.EnQueue(SubmitInfo)
                   Console.WriteLine($"[{DateTime.Now}]" & $"{Judge0ServerIP}Token记录失败: {token},已重新入队")
                   Return -4001
               End If
           Else
               Console.WriteLine($"[{DateTime.Now}]" & $"向{Judge0ServerURL}请求失败,状态码: {response.StatusCode}")
               '这个错误是因为参数超过了Judge0设置的容许值,包括但不限于:
               '    ' cpu_time_limit 的最大允许值
               '    ' memory_limit 的最大允许值
               '    ' language_id 是否在支持的范围内
               '    ' callback_url 格式正确
               '最可能的就是Judge0服务器剩余内存不足以运行当前提交的测试,所以被驳回。
               Return -2001
           End If
       Catch ex As Exception
           Console.WriteLine($"[{DateTime.Now}]" & $"给地址{Judge0ServerSubmissionURL}发送请求时发生错误:{ex.Message}")
           Return -1001
       End Try
   End Function

在得到Judge0服务器的评价结果之后(通过配置Judge0服务器使用回调),我们需要马上按格式解析其返回的内容,上一篇我们提到,有些代码在死循环中进行输出,这会对Judge0造成压力的同时给我们解析返回值、数据库造成较大影响。一般来说,一个OJ代码的输出不会太大的,我们需要进行有效的限制。


    Private HttpListener As HttpListener
    Private Function Init_HttpListerner() As Boolean
        Try
            HttpListener = New HttpListener
            HttpListener.Prefixes.Add($"http://*:{Judge0ServerCallBackPort}/")
            Console.WriteLine($"{Judge0ServerIP}开始监听:{$"http://*:{Judge0ServerCallBackPort}/"}的回调。")
            HttpListener.Start()
            ' 改为异步处理
            BeginGetContext()
        Catch ex As Exception
            Console.WriteLine($"[{DateTime.Now.ToString}]" & $"{Judge0ServerIP}HttpCallBack Err:" & ex.ToString)
            Return False
        End Try
        Return True
    End Function
    Private Sub BeginGetContext()
        ' 异步开始接收请求
        HttpListener.BeginGetContext(AddressOf HandleRequest, HttpListener)
    End Sub
    Private Sub HandleRequest(ar As IAsyncResult)
        Dim context As HttpListenerContext = Nothing
        Try
            Dim listener As HttpListener = CType(ar.AsyncState, HttpListener)
            context = listener.EndGetContext(ar)
            BeginGetContext()

            ' 定义常量
            Const MAX_REQUEST_SIZE As Long = 10 * 1024 * 1024 ' 10MB
            Const MAX_FIELD_SIZE As Integer = 4096 ' 字段最大长度 4KB
            Const EXCEED_MSG As String = "超出长度限制"

            Dim resultResponseApi As Judge0ResultQueue.ApiResponse

            ' 检查请求大小
            If context.Request.ContentLength64 > MAX_REQUEST_SIZE Then
                Console.WriteLine($"[{DateTime.Now.ToString}] {Judge0ServerIP} 警告: 请求超过10MB ({context.Request.ContentLength64} 字节),将解析并简化响应")

                ' 解析超长请求并简化各个字段
                resultResponseApi = ParseAndSimplifyResponse(context.Request.InputStream, MAX_FIELD_SIZE)

                If resultResponseApi Is Nothing Then
                    ' 如果解析失败,创建最基本的简化响应
                    resultResponseApi = New Judge0ResultQueue.ApiResponse With {
                    .token = "unknown",
                    .stdout = EXCEED_MSG,
                    .stderr = EXCEED_MSG,
                    .compile_output = EXCEED_MSG,
                    .message = EXCEED_MSG,
                    .time = 0,
                    .memory = 0,
                    .status = New Judge0ResultQueue.StatusStruct With {
                        .id = Judge0ResultState.Unknow,
                        .description = "Failed to parse response (>10MB)"
                    }
                }
                End If
            Else
                ' 正常大小请求,完整读取
                Dim requestBody As String = Nothing
                Using reader As New IO.StreamReader(context.Request.InputStream, context.Request.ContentEncoding)
                    requestBody = reader.ReadToEnd()
                End Using

                If String.IsNullOrEmpty(requestBody) Then
                    Console.WriteLine($"[{DateTime.Now.ToString}] {Judge0ServerIP} Judge0Server.HandleRequest()发生错误:requestBody为空。")
                    resultResponseApi = Nothing
                Else
                    ' 正常反序列化
                    resultResponseApi = JsonSerializer.Deserialize(Of Judge0ResultQueue.ApiResponse)(requestBody)
                End If
            End If

            ' 处理结果
            If resultResponseApi IsNot Nothing Then
                ThreadPool.QueueUserWorkItem(Sub(state)
                                                 Try
                                                     ProcessResult(resultResponseApi)
                                                 Catch ex As Exception
                                                     Console.WriteLine($"[{DateTime.Now.ToString}] {Judge0ServerIP} Judge0Server.HandleRequest()发生错误: " & vbCrLf & ex.ToString)
                                                 End Try
                                             End Sub)
            End If

            ' 立即响应客户端
            Dim responseBuffer As Byte() = Encoding.UTF8.GetBytes("Callback received successfully.")
            context.Response.ContentLength64 = responseBuffer.Length
            context.Response.StatusCode = 200
            context.Response.OutputStream.Write(responseBuffer, 0, responseBuffer.Length)
            context.Response.Close()

        Catch ex As Exception
            Console.WriteLine($"[{DateTime.Now.ToString}] {Judge0ServerIP}HandleRequest发生错误:" & ex.ToString)
            If context IsNot Nothing AndAlso context.Response IsNot Nothing Then
                Try
                    context.Response.StatusCode = 500
                    context.Response.Close()
                Catch
                End Try
            End If
        End Try
    End Sub

        至此,整个工作流程就非常清晰了:首先,轮询数据库,当有评测任务时进行一些预处理(我使用了本地编译,它可以有效的过滤掉那些无法编译的代码,给Judge0减轻负担;实际上也可以单独测试测试点1,如果它产生溢出之类的错误,后续也没有必要评测。),然后就可以把每个测试点放入“待评测队列”;服务器管理器会提示检查任务,并且自动分配;当得到一个评测结果之后立即放入到返回结果队列,而后从结果队列中摘取再分配给每次代码提交,当代码提交记录的token都返回或者返回足够个数即可认定评测完成。这样做的好处是能够立即响应网络请求,每次提交的若干测试点多数时候被分配给多个服务器——充分利用每个服务器那么用户感知评测就比较快;但也有一些问题,这个过程中记录了很多中间结果,在大量评测同时进行时(例如重测某些题目或比赛)OJ服务器本身压力较大,需要足够的内存。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

清晨曦月

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值