在服务器端,有很多事情要做:
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服务器本身压力较大,需要足够的内存。

被折叠的 条评论
为什么被折叠?



