This commit is contained in:
龙腾猫跃
2025-11-01 11:51:54 +08:00
parent a067271995
commit efd480f06c
24 changed files with 1031 additions and 431 deletions
@@ -1,4 +1,6 @@
Public Class PageLinkMain
Imports System.Net.Sockets
Public Class PageLinkMain
'===============================
' 状态机与前端页面
@@ -6,33 +8,33 @@
#Region "状态管理"
Private Enum States
Public Enum LinkStates
Waiting
Loading
Failed
Finished
End Enum
Private State As States = States.Waiting
Public Shared LinkState As LinkStates = LinkStates.Waiting
''' <summary>
''' 切换到指定的状态。
''' </summary>
Private Sub ChangeState(NewState As States)
If State = NewState Then Return
Dim OldState = State
State = NewState
Private Sub ChangeState(NewState As LinkStates)
If LinkState = NewState Then Return
Dim OldState = LinkState
LinkState = NewState
Log($"[Link] 主状态由 {GetStringFromEnum(OldState)} 变更为 {GetStringFromEnum(NewState)}")
'触发状态切换
RunInUi(
Sub()
Select Case NewState
Case States.Waiting
Case LinkStates.Waiting
SwitchToWaiting(OldState)
Case States.Loading
Case LinkStates.Loading
SwitchToLoading(OldState)
Case States.Failed
Case LinkStates.Failed
SwitchToFailed(OldState)
Case States.Finished
Case LinkStates.Finished
SwitchToFinished(OldState)
End Select
End Sub)
@@ -45,18 +47,18 @@
Sub(Loader As LoaderBase, NewState As LoadState, OldState As LoadState)
Select Case NewState
Case LoadState.Finished
ChangeState(States.Finished)
ChangeState(LinkStates.Finished)
Case LoadState.Failed
Telemetry("联机失败", "Exception", FilterUserName(Loader.Error.GetDetail, "*"))
ChangeState(States.Failed)
ChangeState(LinkStates.Failed)
End Select
End Sub
End Sub
'页面动画兼容
Private Sub UpdatePanelVisibility() Handles Me.PageEnter
FrmLinkMain.PanSelect.Visibility = If(State = States.Waiting, Visibility.Visible, Visibility.Collapsed)
FrmLinkMain.PanFinish.Visibility = If(State = States.Finished, Visibility.Visible, Visibility.Collapsed)
FrmLinkMain.PanSelect.Visibility = If(LinkState = LinkStates.Waiting, Visibility.Visible, Visibility.Collapsed)
FrmLinkMain.PanFinish.Visibility = If(LinkState = LinkStates.Finished, Visibility.Visible, Visibility.Collapsed)
End Sub
#End Region
@@ -64,7 +66,7 @@
#Region "初始页面 (Waiting)"
'由任意状态切换到 Waiting
Private Sub SwitchToWaiting(OldState As States)
Private Sub SwitchToWaiting(OldState As LinkStates)
'加载器与进程状态
LinkLoader.Abort()
SyncLock LinkLoader.LockState
@@ -72,7 +74,7 @@
End SyncLock
ProcessStop()
'页面切换
If OldState <> States.Finished Then
If OldState <> LinkStates.Finished Then
'从完成页面退出时,PageOnContentExit 结束会触发 PageEnter 事件,这会调用 UpdatePanelVisibility
'如果在现在提前调用了,会丢失完成页面的退出动画
UpdatePanelVisibility()
@@ -87,7 +89,7 @@
'创建
Private Sub Create_MouseLeftButtonUp() Handles PanSelectCreate.MouseLeftButtonUp
'输入端口号
Dim Port As String = MyMsgBoxInput("输入端口", $"在单人游戏的暂停菜单选择 {vbLQ}对局域网开放{vbRQ},然后输入端口数字。{vbCrLf}其实也可以输入其他游戏的端口……",
Dim Port As String = MyMsgBoxInput("输入端口", $"在单人游戏的暂停菜单选择 {vbLQ}对局域网开放{vbRQ},然后输入端口数字。{vbCrLf}甚至可以输入其他游戏的端口……嗯……",
ValidateRules:=New ObjectModel.Collection(Of Validate) From {New ValidateInteger(1024, 65535)},
HintText:="端口号")
If Port Is Nothing Then Return
@@ -106,50 +108,31 @@
NetworkSecret = GenerateRandomCode()
Log($"[Link] 尝试创建房间,网络名 {NetworkName},网络密码 {NetworkSecret},端口 {ServerPort}")
'启动
ChangeState(States.Loading)
ChangeState(LinkStates.Loading)
End Sub
'加入
Private LastCode As String = Nothing
Private Sub Join_MouseLeftButtonUp() Handles PanSelectJoin.MouseLeftButtonUp
'输入邀请码
Private Shared LastCode As String = Nothing
''' <summary>
''' 输入邀请码,切换到联机页并立即加入房间。
''' </summary>
Public Shared Sub Join() Handles PanSelectJoin.MouseLeftButtonUp
Dim Code As String = MyMsgBoxInput("输入邀请码", "输入房主发给你的邀请码。",
HintText:=If(String.IsNullOrEmpty(LastCode), "", "使用上一次的邀请码:" & LastCode))
HintText:=If(String.IsNullOrEmpty(LastCode), "", "使用上一次的邀请码:" & LastCode))
If Not String.IsNullOrEmpty(LastCode) AndAlso Code IsNot Nothing AndAlso Code = "" Then Code = LastCode
If Code Is Nothing Then Return
Join(Code)
End Sub
Public Sub JoinInternal(Code As String)
If Code Is Nothing Then Return
'基础格式校验
Code = Code.Between("【", "】").Between("[", "]") '从完整消息中提取
Code = Code.ToUpper.Replace("O", "0").Replace("I", "1") '输入修正
If Not (Code.Length >= 14 AndAlso Code(0) = "P"c AndAlso Code(5) = "-"c AndAlso Code(11) = "-"c) Then
If Code.StartsWithF("U/") Then 'HMCL
Hint("请让房主使用 PCL 创建房间!", HintType.Red)
ElseIf Code.Length = 10 Then 'PCL CE
Hint("请让房主使用非社区版的 PCL 创建房间!", HintType.Red)
ElseIf Code.StartsWithF("P") Then '远古版 PCL
Hint("你的 PCL 版本与房主的 PCL 版本不一致,或输入的邀请码有误!", HintType.Red)
Else
Hint("邀请码有误,请让房主使用 PCL 创建房间!", HintType.Red)
End If
Dim ValidateResult = ValidateCodeFormat(Code)
If ValidateResult IsNot Nothing Then
Hint(ValidateResult, HintType.Red)
Return
End If
'离线登录警告
If McLoginLoader.State = LoadState.Finished AndAlso McLoginLoader.Output.Type = "Legacy" AndAlso Not Setup.Get("LinkOfflineHint") Then
Dim Solution As String
If McVersionCurrent IsNot Nothing AndAlso McVersionCurrent.Version.McVersion >= New Version(1, 21, 5) Then
Solution = $"请换用正版登录,或是让房主安装 {vbLQ}更高级联机设置{vbRQ} Mod 并禁用正版验证。"
ElseIf McVersionCurrent IsNot Nothing AndAlso McVersionCurrent.Version.McVersion >= New Version(1, 12, 0) Then
Solution = $"请换用正版登录,或是让房主安装 {vbLQ}自定义局域网联机{vbRQ} Mod 并关闭在线模式。"
Else
Solution = $"请换用正版登录,或是让房主用服务端开服,关闭 online-mode,然后在创建房间时输入服务端的端口号。"
End If
Select Case MyMsgBox($"如果不用正版登录可能会进不了服,显示 {vbLQ}无效会话{vbRQ}。" & vbCrLf & Solution,
"离线登录警告", "继续", "继续且不再提示", "取消", IsWarn:=True)
Case 2
Setup.Set("LinkOfflineHint", True)
Case 3
Return
End Select
End If
'基础信息
IsServerSide = False
ServerPort = RadixConvert(Code.Substring(1, 4), 16, 10)
@@ -158,7 +141,45 @@
Log($"[Link] 尝试加入房间,网络名 {NetworkName},网络密码 {NetworkSecret},端口 {ServerPort}")
'启动
LastCode = Code
ChangeState(States.Loading)
ChangeState(LinkStates.Loading)
End Sub
Public Shared Function ValidateCodeFormat(Code As String) As String
If Code Is Nothing Then Return "邀请码为空!"
Code = Code.Between("【", "】").Between("[", "]") '从完整消息中提取
Code = Code.ToUpper.Replace("O", "0").Replace("I", "1") '输入修正
If Not (Code.Length >= 14 AndAlso Code(0) = "P"c AndAlso Code(5) = "-"c AndAlso Code(11) = "-"c) Then
If Code.StartsWithF("U/") Then 'HMCL
Return "请让房主使用 PCL 创建房间!"
ElseIf Code.Length = 10 Then 'PCL CE
Return "请让房主使用非社区版的 PCL 创建房间!"
Else
Return "邀请码有误,请让房主使用 PCL 创建房间!"
End If
End If
Return Nothing
End Function
'自动加入
''' <summary>
''' 切换到联机页并立即加入指定房间。
''' </summary>
Public Shared Sub Join(Code As String)
If LinkState <> LinkStates.Waiting Then
Hint("你已经在联机房间中了!", HintType.Red)
ElseIf FrmMain.PageCurrent = FormMain.PageType.Link Then
FrmLinkMain.JoinInternal(Code)
Else
AutoJoinCode = Code
FrmMain.PageChange(FormMain.PageType.Link, FormMain.PageSubType.LinkMain)
End If
End Sub
'在进入页面时自动尝试加入房间
Private Shared AutoJoinCode As String = Nothing
Private Sub PanSelectJoin_Loaded() Handles PanSelectJoin.Loaded
If AutoJoinCode Is Nothing Then Return
If LinkState = LinkStates.Waiting Then JoinInternal(AutoJoinCode)
AutoJoinCode = Nothing
End Sub
#End Region
@@ -166,7 +187,7 @@
#Region "加载页面 (Loading | Failed)"
'由 Waiting 或 Failed 状态切换到 Loading
Private Sub SwitchToLoading(OldState As States)
Private Sub SwitchToLoading(OldState As LinkStates)
'清理变量
Peers = Nothing
FailCount = 0
@@ -180,7 +201,7 @@
End Sub
'由 Loading 状态切换到 Failed
Private Sub SwitchToFailed(OldState As States)
Private Sub SwitchToFailed(OldState As LinkStates)
'加载器与进程状态
LinkLoader.Abort()
ProcessStop()
@@ -217,12 +238,12 @@
'取消加载
Private Sub BtnLoadCancel_Click() Handles BtnLoadCancel.Click
ChangeState(States.Waiting)
ChangeState(LinkStates.Waiting)
End Sub
'点击重试
Private Sub CardLoad_MouseLeftButtonUp() Handles CardLoad.MouseLeftButtonUp
ChangeState(States.Loading)
ChangeState(LinkStates.Loading)
End Sub
'进度条
@@ -252,7 +273,7 @@
#Region "完成页面 (Finished)"
'由 Loading 状态切换到 Finished
Private Sub SwitchToFinished(OldState As States)
Private Sub SwitchToFinished(OldState As LinkStates)
'由于只可能从加载器的完成事件触发,不需要管加载器
UpdatePanelVisibility() '页面的实际切换会由 Loader 调用 MyPageRight 来触发
@@ -260,26 +281,27 @@
If IsServerSide Then
LabFinishTitle.Text = "已创建房间"
LabFinishDesc.Text = $"把邀请码发给朋友,让大家加入房间吧!{vbCrLf}邀请码:{NetworkName}-{NetworkSecret}"
BtnFinishCopy.Text = "复制邀请码"
BtnFinishExit.Text = "关闭"
BtnFinishCopy.Visibility = Visibility.Visible
Copy() '立即复制邀请码
'下边栏
BtnFinishPing.ToolTip = "网络延迟"
LineFinishPort.Visibility = Visibility.Visible
BtnFinishPort.Visibility = Visibility.Visible
LabFinishPort.Visibility = Visibility.Visible
BtnFinishIp.Visibility = Visibility.Collapsed
LabFinishIp.Visibility = Visibility.Collapsed
LabFinishPort.Text = ServerPort
Else
LabFinishTitle.Text = "已加入房间"
LabFinishDesc.Text = $"在多人游戏中选择直接连接,粘贴地址即可加入游戏!{vbCrLf}服务器地址:{ClientAddress}" '"如果提示无效会话,不要退出联机房间,重新启动游戏即可!"
BtnFinishCopy.Text = "复制地址"
LabFinishDesc.Text = $"在多人游戏页面的最下方就能找到联机房间!{vbCrLf}注意:使用离线登录时不要手动输入 IP!"
BtnFinishExit.Text = "离开"
BtnFinishCopy.Visibility = Visibility.Collapsed
'下边栏
BtnFinishPing.ToolTip = "与房主的延迟"
LineFinishPort.Visibility = Visibility.Collapsed
BtnFinishPort.Visibility = Visibility.Collapsed
LabFinishPort.Visibility = Visibility.Collapsed
LabFinishIp.Text = ClientAddress
BtnFinishIp.Visibility = Visibility.Visible
LabFinishIp.Visibility = Visibility.Visible
End If
Copy() '复制信息
Update() '立即刷新
End Sub
@@ -289,30 +311,37 @@
'退出
Private Sub BtnFinishExit_Click() Handles BtnFinishExit.Click
TryExit(True)
TryExit(False, False)
End Sub
''' <summary>
''' 退出联机。
''' 返回是否弹出了警告窗口并且玩家选择了取消。
''' </summary>
Public Function TryExit(SendWarning As Boolean) As Boolean
If State = States.Waiting OrElse State = States.Failed Then Return False
If IsServerSide AndAlso PeopleCount > 1 Then
If MyMsgBox("你确定要关闭联机房间吗?" & vbCrLf & "所有玩家都需要重新输入邀请码才可加入游戏!", "关闭房间", "确定", "取消", IsWarn:=True) = 2 Then Return True
Public Function TryExit(Slient As Boolean, Closing As Boolean) As Boolean
If LinkState = LinkStates.Waiting OrElse LinkState = LinkStates.Failed Then Return False
If Not Slient Then
If IsServerSide AndAlso PeopleCount > 1 Then
If MyMsgBox("你确定要关闭联机房间吗?" & vbCrLf & "所有玩家都需要重新输入邀请码才可加入游戏!", "退出联机", "确定", "取消", IsWarn:=True) = 2 Then Return True
ElseIf Closing Then
If MyMsgBox(If(IsServerSide, "你确定要关闭联机房间吗?", "你确定要离开联机房间吗?"), "退出联机", "确定", "取消") = 2 Then Return True
End If
End If
ChangeState(States.Waiting)
ChangeState(LinkStates.Waiting)
Return False
End Function
'复制
'复制邀请码
Private Sub Copy() Handles BtnFinishCopy.Click
If IsServerSide Then
ClipboardSet($"在 PCL 启动器中输入邀请码【{NetworkName}-{NetworkSecret}】,即可加入联机房间!", False)
Hint("已复制邀请码!", HintType.Green)
Else
ClipboardSet(ClientAddress, False)
Hint("已复制服务器地址!", HintType.Green)
End If
Dim CodeText As String = $"在 PCL 启动器中输入邀请码【{NetworkName}-{NetworkSecret}】,即可加入联机房间!"
ClipboardSet(CodeText, False)
Setup.Set("LinkLastAutoJoinInviteCode", CodeText)
Hint("已复制邀请码!", HintType.Green)
End Sub
'复制 IP
Private Sub BtnFinishIp_MouseLeftButtonUp(sender As Object, e As MouseButtonEventArgs) Handles BtnFinishIp.MouseLeftButtonUp
ClipboardSet(ClientAddress, False)
Hint("已复制服务器地址!", HintType.Green)
End Sub
#End Region
@@ -334,6 +363,14 @@
''' </summary>
Private ServerPort As Integer
''' <summary>
''' 客户端映射后的端口号。
''' </summary>
Private ClientPort As Integer
''' <summary>
''' RPC 端口号。
''' </summary>
Private RPCPort As Integer
''' <summary>
''' 在客户端映射后的地址。
''' 除非已作为客户端启动 EasyTier,否则一直为 Nothing。
''' </summary>
@@ -414,7 +451,7 @@
'解压
Dim ExtractPath As String = RequestTaskTempFolder()
ExtractFile(PathEasyTier & "EasyTier.zip", ExtractPath,
ProgressIncrementHandler:=Sub(Progress) Task.Progress += Progress * 0.06)
ProgressIncrementHandler:=Sub(Progress) Task.Progress += Progress * 0.05)
Dim ExtractedPath As String = New DirectoryInfo(ExtractPath).EnumerateDirectories.FirstOrDefault?.FullName
Log("[Link] 联机模块解压时的临时路径:" & ExtractedPath)
CopyDirectory(ExtractedPath, PathEasyTier)
@@ -427,28 +464,42 @@
File.Delete(PathEasyTier & "EasyTier.zip")
Setup.Set("LinkEasyTierVersion", ServerVersion)
End If
Task.Progress = 0.1
'获取启动参数
Task.Progress = 0.07
'获取节点列表
Dim Peers As List(Of String)
Dim CustomPeers As String = Setup.Get("LinkCustomPeer")
If String.IsNullOrWhiteSpace(CustomPeers) Then
Peers = GetOnlinePeers()
Else
Peers = CustomPeers.Split(",,".ToCharArray).Select(Function(p) p.Trim).Where(Function(p) Not String.IsNullOrEmpty(p)).ToList()
Log("[Link] 使用自定义节点")
End If
Task.Progress = 0.13
'获取空闲端口
UpdateLoadingPage("正在启动联机模块……", "启动联机模块")
Dim FreePorts = FindFreePorts(5, ServerPort)
ClientPort = FreePorts(0)
RPCPort = FreePorts(1)
Dim ListenersPort As Integer = FreePorts(2) '它会占用【当前、当前 +1、当前 +2】共三个端口
'获取启动参数
Dim Arguments As String = ServerConfig("Link")("Argument")
Arguments += $" --network-name={NetworkName} --network-secret={NetworkSecret}"
Arguments += $" --network-name={NetworkName} --network-secret={NetworkSecret} --listeners {ListenersPort} --rpc-portal {RPCPort}"
HostName = If(IsServerSide, "Server-", "Client-") & RadixConvert(Math.Abs(Identify.GetHashCode), 10, 36)
If IsServerSide Then
Arguments += $" -i 10.114.114.114 --hostname={HostName} --tcp-whitelist={ServerPort} --udp-whitelist={ServerPort}"
Else
Arguments += $" -d --hostname={HostName} --tcp-whitelist=0 --udp-whitelist=0"
'端口转发
Dim Port As Integer = GetAvailablePort()
ClientAddress = $"localhost:{Port}"
Arguments += $" --port-forward tcp://[{IPAddress.IPv6Loopback}]:{Port}/10.114.114.114:{ServerPort}"
Arguments += $" --port-forward udp://[{IPAddress.IPv6Loopback}]:{Port}/10.114.114.114:{ServerPort}"
Arguments += $" --port-forward tcp://{IPAddress.Loopback}:{Port}/10.114.114.114:{ServerPort}"
Arguments += $" --port-forward udp://{IPAddress.Loopback}:{Port}/10.114.114.114:{ServerPort}"
ClientAddress = $"localhost:{ClientPort}"
Arguments += $" --port-forward tcp://[{IPAddress.IPv6Loopback}]:{ClientPort}/10.114.114.114:{ServerPort}"
Arguments += $" --port-forward udp://[{IPAddress.IPv6Loopback}]:{ClientPort}/10.114.114.114:{ServerPort}"
Arguments += $" --port-forward tcp://{IPAddress.Loopback}:{ClientPort}/10.114.114.114:{ServerPort}"
Arguments += $" --port-forward udp://{IPAddress.Loopback}:{ClientPort}/10.114.114.114:{ServerPort}"
End If
For Each Peer As String In ServerConfig("Link")("Peers")
For Each Peer As String In Peers
Arguments += $" -p=""{Peer}"""
Next
If Not Setup.Get("LinkShareMode") Then Arguments += " --private-mode true"
Arguments += " --private-mode true" '老好人模式现在莫得用:If Not Setup.Get("LinkShareMode") Then
'启动进程
ProcessStart(Arguments)
Task.Progress = 0.15
@@ -482,9 +533,9 @@
'超时判定
If LastProgress <> Task.Progress Then
TimeoutCounter = 0
ElseIf TimeoutCounter < 20 Then
ElseIf TimeoutCounter < 30 * 2 Then
TimeoutCounter += 1
Else '进度停滞超过 10s
Else '进度停滞超过 30s
Select Case PeerCount
Case -1 'CLI 无返回
Panic("无法启动联机模块", "近期日志:" & vbCrLf & LogHistory.Join(vbCrLf))
@@ -497,6 +548,49 @@
Loop Until Task.IsAborted
If Task.IsAborted Then Throw New ThreadInterruptedException
End Sub
''' <summary>
''' 从在线配置和 API 获取节点列表。
''' </summary>
Private Function GetOnlinePeers() As List(Of String)
Dim Peers As List(Of String) = ServerConfig("Link")("Peers").Select(Function(p) p.ToString).ToList()
Try
'从 API 获取节点列表
Dim BlackList As List(Of String) = ServerConfig("Link")("PeersBlackList").Select(Function(p) p.ToString).ToList() '黑名单
Dim CentralNodes As New List(Of String)
Dim RandomNodes As New List(Of Tuple(Of String, Double))
For Each Node As JObject In CType(GetJson(NetRequestByClientRetry("https://uptime.easytier.cn/api/nodes?page=1&per_page=200")), JObject)("data")("items")
'状态检查
If Node("protocol").ToString <> "tcp" Then Continue For
If Node("current_health_status").ToString <> "healthy" Then Continue For
If Not Node("is_active").ToObject(Of Boolean) Then Continue For
If Not Node("is_approved").ToObject(Of Boolean) Then Continue For
Dim Tags = Node("tags").Select(Function(t) t.ToString).ToList
If Not Tags.Contains("国内") Then Continue For
If Tags.Contains("即将下线") Then Continue For
Dim Address As String = Node("address").ToString
If BlackList.Contains(Address) Then Continue For
'添加节点
If Tags.Contains("MC") Then
CentralNodes.Add(Address)
Else
If Not Node("allow_relay").ToObject(Of Boolean) Then Continue For
If Node("usage_percentage").ToObject(Of Double) = 0 Then Continue For '或许节点有问题才导致是 0 负载
RandomNodes.Add(New Tuple(Of String, Double)(
Address,
Node("usage_percentage").ToObject(Of Double) * (103 - Node("health_percentage_24h").ToObject(Of Double)))) '负载,越低越好
End If
Next
RandomNodes = RandomNodes.OrderBy(Function(n) n.Item2).ToList()
Log($"[Link] 获取到 {CentralNodes.Count} 个中心节点,{RandomNodes.Count} 个随机节点")
'选择节点
Dim RandomCount As Integer = ServerConfig("Link")("RandomPeer").ToObject(Of Integer)
If RandomNodes.Count < RandomCount Then Throw New Exception($"可用的随机节点数量不足,需要 {RandomCount} 个,实际 {RandomNodes.Count} 个")
Peers = CentralNodes.Concat(RandomNodes.Take(RandomCount).Select(Function(n) n.Item1)).ToList()
Catch ex As Exception
Log(ex, "获取节点列表失败,联机质量可能受到影响", LogLevel.Hint)
End Try
Return Peers
End Function
#End Region
@@ -544,26 +638,14 @@
''' 出现意外错误,给出错误信息并结束联机。
''' </summary>
Private Sub Panic(Brief As String, Detail As String)
If State = States.Loading Then
If LinkState = LinkStates.Loading Then
FailReason = Brief
LinkLoader.Failed(New Exception(Detail))
Else
MyMsgBox(Detail, "联机出错:" & Brief, IsWarn:=True)
ChangeState(States.Waiting)
ChangeState(LinkStates.Waiting)
Telemetry("联机失败", "Exception", FilterUserName(Brief & ":" & Detail, "*"))
End If
'收集到的 CLI 错误:
'Error: Timeout : deadline has elapsed
'Caused by
' deadline has elapsed
'Error: failed to get peer manager client
'Caused by
' 0: rust tun error failed to connect to server: Url {scheme: "tcp", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("127.0.0.1")), port: Some(15888), path: "", query: None, fragment: None }
' 1: failed to connect to server: Url {scheme: "tcp", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("127.0.0.1")), port: Some(15888), path: "", query: None, fragment: None }
' 2: IO error
' 3: 由于目标计算机积极拒绝, 无法连接。 (os error 10061)
End Sub
''' <summary>
@@ -656,32 +738,33 @@
''' 名称。
''' </summary>
Public ReadOnly Name As String
''' <summary>
''' 连接方式。
''' </summary>
Public ReadOnly Cost As String
''' <summary>
''' 从 CLI 给出的信息行分析对应的数据。
''' 从 CLI 给出的信息分析对应的数据。
''' </summary>
Public Sub New(Info As String)
'具体每个部分的索引见 GetPeerData()
Dim Parts = Info.Split("|").Select(Function(s) If(s.All(Function(c) c = " "c), s, s.Trim)).Where(Function(s) s <> "").ToList
If Parts.Count <> 10 Then Throw New Exception($"信息列数有误")
'延迟
Ping = If(Parts(3) = "-", 0, Parts(3))
Public Sub New(Info As JObject)
'类别
Dim PeerName = Parts(1)
Dim PeerName = Info("hostname").ToString
If PeerName = HostName Then
Type = Types.Self
ElseIf PeerName.StartsWithF("Client") Then
Type = Types.Client
ElseIf Parts(0).StartsWithF("10.114.114.114") Then '只允许这个 IP 作为房主,避免网络被多个房主搞炸的情况
ElseIf Info("ipv4") = "10.114.114.114" Then '只允许这个 IP 作为房主,避免网络被多个房主搞炸的情况
Type = Types.Server
Else
Type = Types.Misc
End If
'主机名
'基础信息
Ping = If(Info("lat_ms").ToString = "-", 0, Info("lat_ms").ToString)
Name = PeerName
Cost = Info("cost")
End Sub
Public Overrides Function ToString() As String
Return $"{Type} - {Name} - Ping {Ping:0.0}ms"
Return $"{Type} - {Name} - Ping {Ping:0.0}ms [{Cost}]"
End Function
End Class
@@ -705,12 +788,12 @@
'| 10.114.114.114/24 | Server-J6P6IW | p2p | 5.63 | 0.0% | 1.65 kB | 1.64 kB | udp | PortRestricted | 2.4.5-4c4d172e |
'| 10.114.114.114/24 | Server-J6PHIW (连接中) | relay(2) | 1000.00 | 0.0% | 0 B | 0 B | | PortRestricted | 2.4.5-4c4d172e |
Try
Dim CliResult = StartProcessAndGetOutput(PathEasyTier & "联机模块 CLI.exe", "peer", 2000, Encoding:=Encoding.UTF8, PrintLog:=False)
Dim CliResult = StartProcessAndGetOutput(PathEasyTier & "联机模块 CLI.exe", $"-o json -p 127.0.0.1:{RPCPort} peer", 2000, Encoding:=Encoding.UTF8, PrintLog:=False)
'解析
If Not CliResult.Contains("lat(ms)") Then Throw New Exception("CLI 调用失败:" & vbCrLf & CliResult)
If GetUuid() Mod If(ModeDebug, 4, 30) = 0 Then Log("[EasyTier] CLI 输出抽样:" & vbCrLf & CliResult)
If Not CliResult.Contains("lat_ms") Then Throw New Exception("CLI 调用失败:" & vbCrLf & CliResult)
If GetUuid() Mod If(ModeDebug, 23, 103) = 0 Then Log("[EasyTier] CLI 输出抽样:" & vbCrLf & CliResult)
Dim NewPeers As New List(Of Peer)
For Each Line In CliResult.Split(vbCrLf.ToCharArray, StringSplitOptions.RemoveEmptyEntries).Skip(2)
For Each Line As JObject In CType(GetJson(CliResult), JArray).Skip(1)
Try
Dim Peer = New Peer(Line)
If Peer.Type = Peer.Types.Self Then Continue For '自己
@@ -724,7 +807,7 @@
FailCount = 0
Catch ex As Exception
Log(ex, "获取节点信息失败")
If State = States.Finished Then
If LinkState = LinkStates.Finished Then
FailCount += 1
If FailCount >= 4 Then Panic("获取节点信息失败", ex.Message)
End If
@@ -741,18 +824,26 @@
''' 0 代表尚不可用。
''' </summary>
Private Function GetPeerPing() As Double
If Peers Is Nothing Then Return 0
Dim PingTargets As IEnumerable(Of Peer)
Dim Peer = GetTargetPeer()
If Peer Is Nothing Then Return 0
Return Peer.Ping
End Function
''' <summary>
''' 服务端会返回所有节点中 Ping 的那一个,客户端会返回服务端。
''' 若没有则为 Nothing。
''' </summary>
Private Function GetTargetPeer() As Peer
If Peers Is Nothing Then Return Nothing
Dim Targets As IEnumerable(Of Peer)
If IsServerSide Then
PingTargets = Peers
Targets = Peers.Where(Function(p) p.Ping > 0)
Else
PingTargets = Peers.Where(Function(p) p.Type = Peer.Types.Server)
End If
If Not PingTargets.Any Then
Return 0
Else
Return Math.Max(1, PingTargets.Select(Function(p) p.Ping).Min())
Targets = Peers.Where(Function(p) p.Type = Peer.Types.Server AndAlso p.Ping > 0)
End If
If Not Targets.Any Then Return Nothing
'返回 Ping 大于 0 且最低的那个
Dim MinPing = Targets.Min(Function(p) p.Ping)
Return Targets.First(Function(p) p.Ping = MinPing)
End Function
''' <summary>
@@ -768,7 +859,7 @@
#End Region
#Region "定时状态更新"
#Region "定时任务"
'启动
Private IsTimerStarted As Boolean = False
@@ -781,13 +872,13 @@
Try
Thread.Sleep(200)
Counter += 200
'每 4 秒执行一次
If Counter >= 4000 Then
'每秒执行一次
If Counter >= 1000 Then
Counter = 0
Update()
End If
'每 200ms 更新进度条
If State = States.Loading Then RunInUi(AddressOf UpdateProgressBar)
If LinkState = LinkStates.Loading Then RunInUi(AddressOf UpdateProgressBar)
Catch ex As Exception
Log(ex, "联机模块主时钟出错", LogLevel.Feedback)
Thread.Sleep(10000)
@@ -796,9 +887,10 @@
End Sub, "Link Timer")
End Sub
'每 4 秒或进入页面时更新一次信息
'每秒或进入页面时触发
Private BroadcastSocket As New Socket(SocketType.Dgram, ProtocolType.Udp)
Private Sub Update() Handles Me.Loaded
If State <> States.Finished Then Return
If LinkState <> LinkStates.Finished Then Return
'重新获取信息
SyncLock RefreshPeerLoader.LockState
If RefreshPeerLoader.State <> LoadState.Loading Then RefreshPeerLoader.Start(IsForceRestart:=True)
@@ -808,13 +900,25 @@
RunInUi(
Sub()
Dim Ping As Double = GetPeerPing()
Dim RelayLayer As Integer = If(GetTargetPeer().Cost.RegexSeek("(?<=relay\()\d+") Is Nothing,
0, Val(GetTargetPeer().Cost.RegexSeek("(?<=relay\()\d+")) - 1)
'更新 Ping 显示
If Ping Mod 500 = 0 OrElse $"{Ping:0.0}" = "1.0" OrElse FailCount > 0 Then
LabFinishPing.Text = "连接中"
AniStop("Link Logo Rotation")
Else
LabFinishPing.Text = If(Ping >= 10, $"{Ping:0} ms", $"{Ping:0.0} ms")
LabFinishPing.Text =
If(RelayLayer > 0, If(RelayLayer > 1, $"中继 {RelayLayer} · ", "中继 · "), "") & '使用中继连接时显示 “中继” 前缀
If(Ping >= 10, $"{Ping:0} ms", $"{Ping:0.0} ms")
AniStart(AaRotateTransform(ImgFinishLogo, 500, 5000), "Link Logo Rotation") 'Logo 旋转动画
End If
'更新 Ping 的 Tooltip 显示
Dim Tooltip As String = If(IsServerSide, "网络延迟", "与房主的延迟")
If RelayLayer Then Tooltip &= If(IsServerSide,
$"(你的网络环境较差,正经过 {RelayLayer} 层中继,可能会有点卡)",
$"(你或者房主的网络环境较差,正经过 {RelayLayer} 层中继,可能会有点卡)")
BtnFinishPing.ToolTip = Tooltip
'更新人数显示
LabFinishPlayer.Text = PeopleCount & "  人"
End Sub)
End If
@@ -827,9 +931,20 @@
End If
If Not IsServerSide AndAlso Not Peers.Any(Function(p) p.Type = Peer.Types.Server) Then
MyMsgBox("房主已离开房间!", "联机结束")
ChangeState(States.Waiting)
ChangeState(LinkStates.Waiting)
Return
End If
'广播联机房间端口
If Not IsServerSide Then
Try
BroadcastSocket.SendTo(
Encoding.UTF8.GetBytes($"[MOTD]PCL 联机房间[/MOTD][AD]{ClientPort}[/AD]"),
SocketFlags.None,
New IPEndPoint(IPAddress.Loopback, 4445))
Catch ex As Exception
Log(ex, "广播联机房间端口失败")
End Try
End If
End Sub
''' <summary>
''' 检查联机模块是否崩溃。