上一篇介绍了整体构架这一篇介绍如何实现心跳程序。在ubuntu上使用.net程序有不少方法我们采用使用标准框架的方式。一、工程配置1、类型控制台应用——莫担心它会得到一个dll部署这个dll即可2、目标框架.net 8.03、目标OS无二、心跳客户端在分布式评测系统中Judge0服务器节点需要向管理端位于OJ服务器上Manager报告自己的状态。并且需要1、尽可能占用较少资源不影响Judge0评测性能2、Manager故障不影响心跳发送3、支持多个Manager地址和参数调账4、跨平台2.1配置文件管理Imports System.IO Imports System.Xml.Serialization Serializable Public Structure Judge0ServerConfig Public OJURL() As String Manager服务器地址列表 Public Judge0Port As Integer Judge0服务端口 Public Group() As Integer 服务器组别 (0低性能, 1高性能) Public QueueMaxSize As Integer 队列最大容量 Public RequestCountSize As Integer 最大并行请求数 Public AvailableMemoryThresholdOfMB As Double 内存保护阈值(MB) Sub New(OJURL() As String, Judge0Port As Integer, Group() As Integer, QueueMaxSize As Integer, RequestCountSize As Integer, AvailableMemoryThresholdOfMB As Double) Me.OJURL OJURL Me.Judge0Port Judge0Port Me.Group Group Me.QueueMaxSize QueueMaxSize Me.RequestCountSize RequestCountSize Me.AvailableMemoryThresholdOfMB AvailableMemoryThresholdOfMB End Sub End Structure Public Class Config Private Shared filePath As String AIOJ_Judge0.conf summary 序列化配置文件首次运行生成示例配置 /summary Friend Shared Sub SerializeJudge0Config() 示例包含内网和外网两个Manager地址 Dim config As New Judge0ServerConfig( {http://192.31.116.222:6666, http://xxx.xxx.xxx.xxx:6666}, 2358, {0, 1}, 100, 4, 200 ) Dim serializer As New XmlSerializer(GetType(Judge0ServerConfig), {GetType(String())}) Using writer As New StreamWriter(filePath) serializer.Serialize(writer, config) End Using Console.WriteLine($[{DateTime.Now}] 配置文件已生成: {filePath}) End Sub summary 反序列化配置文件 /summary Friend Shared Function DeserializeJudge0Config() As Judge0ServerConfig Try Dim serializer As New XmlSerializer(GetType(Judge0ServerConfig), {GetType(String())}) Using reader As New StreamReader(filePath) Return CType(serializer.Deserialize(reader), Judge0ServerConfig) End Using Catch ex As Exception Console.WriteLine($[{DateTime.Now}] 读取配置文件失败: {ex.Message}) Console.WriteLine(请确保配置文件存在或运行 SerializeJudge0Config 生成示例配置) Return New Judge0ServerConfig(Nothing, -1, {-1, -1}, -1, -1, -1) End Try End Function End Class2.2心跳消息发送Imports System.Net.Http Imports System.Text Imports System.Text.Json Imports System.Threading 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 所属组别 Public Property AvailableMemoryOfMB As Double 可用内存大小(MB) Public Property AvailableMemoryThresholdOfMB As Double 内存保护阈值 End Class Public Class Heartbeat Private Shared ReadOnly _httpClient As New HttpClient() summary 发送心跳请求 /summary param namePortJudge0端口/param param namegroup组别/param param namesubmissionURLManager接收地址/param param namequeueMaxSize队列最大容量/param param namerequestCountSize最大并行数/param param nameavailableMemoryMB可用内存/param param nameavailableMemoryThresholdOfMB内存阈值/param param nametimeoutSeconds超时时间(秒)/param returns0成功, 负数错误码/returns Public Shared Async Function SendRequest( Port As Integer, group As Integer, submissionURL As String, queueMaxSize As Integer, requestCountSize As Integer, availableMemoryMB As Double, availableMemoryThresholdOfMB As Double, timeoutSeconds As Integer ) As Task(Of Integer) 构建心跳消息 Dim jsonContent As New HeartbeatMessage With { .Port Port, .Group group, .QueueMaxSize queueMaxSize, .RequestCountSize requestCountSize, .AvailableMemoryOfMB availableMemoryMB, .AvailableMemoryThresholdOfMB availableMemoryThresholdOfMB } 序列化为JSON Dim content As New StringContent( JsonSerializer.Serialize(jsonContent), Encoding.UTF8, application/json ) Using cts As New CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds)) Try 发送POST请求 Dim response As HttpResponseMessage Await _httpClient.PostAsync( submissionURL, content, cts.Token ) If response.IsSuccessStatusCode Then Return 0 成功 Else Console.WriteLine($[{DateTime.Now}] {submissionURL} 请求失败状态码: {response.StatusCode}) Return -2001 End If Catch ex As TaskCanceledException When cts.Token.IsCancellationRequested 超时处理 Console.WriteLine($[{DateTime.Now}] {submissionURL} 请求超时{timeoutSeconds}秒) Return -3001 Catch ex As Exception Console.WriteLine($[{DateTime.Now}] {submissionURL} 发送错误: {ex.Message}) Return -1001 End Try End Using End Function End Class2.3系统监控模块Imports System.Diagnostics Imports System.IO Public Class ServerMonitor Private Shared ReadOnly _memInfoPath As String /proc/meminfo summary 获取Ubuntu系统可用内存单位MB 直接从 /proc/meminfo 读取性能优于调用系统命令 /summary returns可用内存(MB)失败返回-1/returns Public Shared Function GetUbuntuAvailableMemoryMB() As Double Try 检查文件是否存在 If Not File.Exists(_memInfoPath) Then Console.WriteLine($错误: {_memInfoPath} 文件不存在) Return -1 End If 读取内存信息 Dim memInfoLines As String() File.ReadAllLines(_memInfoPath) Dim availableKb As Long -1 解析 MemAvailable 字段Linux 3.14 内核支持 For Each line As String In memInfoLines If line.StartsWith(MemAvailable:) Then Dim parts As String() line.Split(New Char() { c}, StringSplitOptions.RemoveEmptyEntries) If parts.Length 2 AndAlso Long.TryParse(parts(1), availableKb) Then Exit For End If End If Next 如果找不到 MemAvailable尝试使用 MemFree Buffers Cached If availableKb -1 Then Console.WriteLine(警告: 未找到 MemAvailable尝试计算可用内存) availableKb CalculateAvailableMemoryFromMeminfo(memInfoLines) End If If availableKb -1 Then Dim availableMB As Double availableKb / 1024.0 Return Math.Round(availableMB, 2) Else Console.WriteLine(错误: 无法解析内存信息) Return -1 End If Catch ex As UnauthorizedAccessException Console.WriteLine($错误: 没有权限读取 {_memInfoPath}) Return -1 Catch ex As Exception Console.WriteLine($读取内存信息异常: {ex.Message}) Return -1 End Try End Function summary 备用方法通过 MemFree Buffers Cached 计算可用内存 /summary Private Shared Function CalculateAvailableMemoryFromMeminfo(lines As String()) As Long Dim memFree As Long 0 Dim buffers As Long 0 Dim cached As Long 0 For Each line As String In lines If line.StartsWith(MemFree:) Then Dim parts line.Split(New Char() { c}, StringSplitOptions.RemoveEmptyEntries) If parts.Length 2 Then Long.TryParse(parts(1), memFree) ElseIf line.StartsWith(Buffers:) Then Dim parts line.Split(New Char() { c}, StringSplitOptions.RemoveEmptyEntries) If parts.Length 2 Then Long.TryParse(parts(1), buffers) ElseIf line.StartsWith(Cached:) Then Dim parts line.Split(New Char() { c}, StringSplitOptions.RemoveEmptyEntries) If parts.Length 2 Then Long.TryParse(parts(1), cached) End If Next If memFree 0 OrElse buffers 0 OrElse cached 0 Then Return memFree buffers cached End If Return -1 End Function summary 获取CPU负载扩展功能 /summary Public Shared Function GetCpuLoad() As Double Try 读取 /proc/loadavg 获取系统负载 Dim loadAvgContent File.ReadAllText(/proc/loadavg) Dim parts loadAvgContent.Split(New Char() { c}, StringSplitOptions.RemoveEmptyEntries) If parts.Length 3 Then Dim load1Min As Double If Double.TryParse(parts(0), load1Min) Then Return load1Min End If End If Catch ex As Exception 忽略错误返回 -1 End Try Return -1 End Function summary 获取Judge0进程状态扩展功能 /summary Public Shared Function IsJudge0Running() As Boolean Try Dim processes Process.GetProcessesByName(judge0) Return processes.Length 0 Catch Return False End Try End Function End Class2.4主程序入口Imports System Imports System.Net Imports System.Net.Http Imports System.Threading Module Program Private OJ As Judge0ServerConfig Private Const HEARTBEAT_INTERVAL_MS As Integer 10000 10秒 Private Const JUDGE0_READY_TIMEOUT_MINUTES As Integer 6 Private Const JUDGE0_PORT As Integer 2358 Sub Main(args As String()) 处理命令行参数 If args.Length 0 AndAlso args(0) --generate-config Then Config.SerializeJudge0Config() Console.WriteLine(配置文件已生成请根据需要修改后重新启动) Return End If Console.WriteLine($[{DateTime.Now}] ) Console.WriteLine($[{DateTime.Now}] AIOJ_Judge0 心跳客户端启动) Console.WriteLine($[{DateTime.Now}] 版本: 1.0.0) Console.WriteLine($[{DateTime.Now}] ) 等待 Judge0 服务就绪 If Not WaitForJudge0Ready() Then Console.WriteLine($[{DateTime.Now}] 警告: Judge0 服务未就绪继续启动心跳...) End If 读取配置 OJ Config.DeserializeJudge0Config() If OJ.OJURL Is Nothing OrElse OJ.Judge0Port -1 Then Console.WriteLine($[{DateTime.Now}] 错误: 无法读取配置文件) Console.WriteLine($[{DateTime.Now}] 请运行: {AppDomain.CurrentDomain.FriendlyName} --generate-config) Console.ReadKey() Return End If 显示配置信息 DisplayConfiguration() 开始心跳循环 RunHeartbeatLoop() Console.WriteLine($[{DateTime.Now}] 心跳客户端已停止) End Sub summary 等待 Judge0 服务就绪 /summary Private Function WaitForJudge0Ready() As Boolean Console.WriteLine($[{DateTime.Now}] 等待 Judge0 服务就绪...) Dim maxRetries As Integer (JUDGE0_READY_TIMEOUT_MINUTES * 60) / (HEARTBEAT_INTERVAL_MS / 1000) Dim retryCount As Integer 0 While retryCount maxRetries If CheckJudge0Service() Then Console.WriteLine($[{DateTime.Now}] ✓ Judge0 服务已就绪) Return True End If retryCount 1 If retryCount Mod 6 0 Then 每分钟输出一次 Console.WriteLine($[{DateTime.Now}] 等待中... ({retryCount * 10}秒/{maxRetries * 10}秒)) End If Thread.Sleep(HEARTBEAT_INTERVAL_MS) End While Console.WriteLine($[{DateTime.Now}] ✗ Judge0 服务超时{JUDGE0_READY_TIMEOUT_MINUTES}分钟) Return False End Function summary 检查 Judge0 服务是否可用 /summary Private Function CheckJudge0Service() As Boolean Try Using hc As New HttpClient() hc.Timeout TimeSpan.FromSeconds(5) Dim response As HttpResponseMessage hc.GetAsync( $http://localhost:{JUDGE0_PORT}/about ).GetAwaiter().GetResult() Return response.IsSuccessStatusCode End Using Catch ex As Exception Return False End Try End Function summary 显示当前配置 /summary Private Sub DisplayConfiguration() Console.WriteLine($[{DateTime.Now}] 配置信息:) Console.WriteLine($ - Manager地址: {String.Join(, , OJ.OJURL)}) Console.WriteLine($ - Judge0端口: {OJ.Judge0Port}) Console.WriteLine($ - 组别: {String.Join(, , OJ.Group)}) Console.WriteLine($ - 队列容量: {OJ.QueueMaxSize}) Console.WriteLine($ - 并行数: {OJ.RequestCountSize}) Console.WriteLine($ - 内存阈值: {OJ.AvailableMemoryThresholdOfMB} MB) Console.WriteLine($ - 心跳间隔: {HEARTBEAT_INTERVAL_MS / 1000}秒) End Sub summary 心跳主循环 /summary Private Sub RunHeartbeatLoop() Dim heartbeatCount As Integer 0 Dim lastMemoryLog As DateTime DateTime.MinValue While True Try heartbeatCount 1 获取系统可用内存 Dim availableMemoryMB As Double ServerMonitor.GetUbuntuAvailableMemoryMB() 内存获取失败时的处理 If availableMemoryMB 0 Then 使用上次成功获取的值或默认值 availableMemoryMB 1024 默认1GB End If 每60秒输出一次内存日志 If (DateTime.Now - lastMemoryLog).TotalSeconds 60 Then Console.WriteLine($[{DateTime.Now}] 系统可用内存: {availableMemoryMB:F2} MB) lastMemoryLog DateTime.Now 检查CPU负载可选 Dim cpuLoad As Double ServerMonitor.GetCpuLoad() If cpuLoad 0 Then Console.WriteLine($[{DateTime.Now}] 系统负载: {cpuLoad:F2}) End If End If 向所有Manager发送心跳 Dim successCount As Integer 0 For i As Integer 0 To OJ.OJURL.Length - 1 Dim url OJ.OJURL(i) Dim group OJ.Group(i) 异步发送心跳 Dim task Heartbeat.SendRequest( OJ.Judge0Port, group, url, OJ.QueueMaxSize, OJ.RequestCountSize, availableMemoryMB, OJ.AvailableMemoryThresholdOfMB, 10 ) 等待结果同步等待确保顺序 Dim result task.GetAwaiter().GetResult() If result 0 Then successCount 1 Else 仅在首次失败或错误码变化时输出日志 Console.WriteLine($[{DateTime.Now}] 心跳失败 [{url}]: 错误码 {result}) End If Next 所有心跳都失败时的特殊处理 If successCount 0 AndAlso heartbeatCount Mod 6 0 Then Console.WriteLine($[{DateTime.Now}] 警告: 所有Manager均无法连接) End If 等待下一个心跳周期 Thread.Sleep(HEARTBEAT_INTERVAL_MS) Catch ex As Exception Console.WriteLine($[{DateTime.Now}] 心跳循环异常: {ex.Message}) Thread.Sleep(HEARTBEAT_INTERVAL_MS) End Try End While End Sub End Module三、配置文件示例?xml version1.0 encodingutf-8? Judge0ServerConfig xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xmlns:xsdhttp://www.w3.org/2001/XMLSchema OJURL stringhttp://192.31.116.222:6666/string stringhttp://xxx.xxx.xxx.xxx:6666/string /OJURL Judge0Port2358/Judge0Port Group int0/int int1/int /Group QueueMaxSize100/QueueMaxSize RequestCountSize4/RequestCountSize AvailableMemoryThresholdOfMB200/AvailableMemoryThresholdOfMB /Judge0ServerConfig四、部署和运行4.1编译和发布编译得到dll将整个文件夹复制到服务器4.2启动服务# 生成配置文件首次运行 ./AIOJ_Judge0 --generate-config # 编辑配置文件 vim AIOJ_Judge0.conf # 启动心跳客户端 ./AIOJ_Judge04.3配置为系统服务[Unit] DescriptionJudge0 Heartbeat Client Afternetwork.target judge0.service [Service] Typesimple Userjudge0 WorkingDirectory/opt/judge0-heartbeat ExecStart/opt/judge0-heartbeat/AIOJ_Judge0 Restartalways RestartSec10 StandardOutputappend:/var/log/judge0-heartbeat.log StandardErrorappend:/var/log/judge0-heartbeat-error.log [Install] WantedBymulti-user.target4.4启动服务sudo systemctl daemon-reload sudo systemctl enable judge0-heartbeat sudo systemctl start judge0-heartbeat sudo systemctl status judge0-heartbeatPS如果安装.NET8运行时跑不起来可以尝试安装.NET 8 SDK