(tools, client, server) feat: Complete ProtoBuf message transmission with both TCP and UDP

This commit is contained in:
2025-08-30 21:25:17 +08:00
parent 362aa799b9
commit 450b15e4df
13 changed files with 149 additions and 35 deletions

View File

@ -584,7 +584,7 @@ GameObject:
m_Icon: {fileID: 0} m_Icon: {fileID: 0}
m_NavMeshLayer: 0 m_NavMeshLayer: 0
m_StaticEditorFlags: 0 m_StaticEditorFlags: 0
m_IsActive: 0 m_IsActive: 1
--- !u!4 &1388451206 --- !u!4 &1388451206
Transform: Transform:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0

View File

@ -8,6 +8,8 @@ namespace Network
{ {
public class UnityTcpClient : Singleton<UnityTcpClient>, IDisposable public class UnityTcpClient : Singleton<UnityTcpClient>, IDisposable
{ {
private const int TcpMaxPayloadSize = 1460;
private TcpClient _client; private TcpClient _client;
private bool _disposed; private bool _disposed;
@ -34,9 +36,9 @@ namespace Network
await stream.WriteAsync(data, 0, data.Length); await stream.WriteAsync(data, 0, data.Length);
var buffer = new byte[1024]; var buffer = new byte[TcpMaxPayloadSize];
await stream.ReadAsync(buffer); var len = await stream.ReadAsync(buffer);
return buffer; return buffer[..len];
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -30,11 +30,13 @@ namespace Protocol {
"Eg8KB21lc3NhZ2UYAiABKAkiMwoNU2lnbnVwUmVxdWVzdBIQCgh1c2VybmFt", "Eg8KB21lc3NhZ2UYAiABKAkiMwoNU2lnbnVwUmVxdWVzdBIQCgh1c2VybmFt",
"ZRgBIAEoCRIQCghwYXNzd29yZBgCIAEoCSJKCg5TaWdudXBSZXNwb25zZRIn", "ZRgBIAEoCRIQCghwYXNzd29yZBgCIAEoCSJKCg5TaWdudXBSZXNwb25zZRIn",
"CgZyZXN1bHQYASABKA4yFy5wcm90b2NvbC5SZXF1ZXN0UmVzdWx0Eg8KB21l", "CgZyZXN1bHQYASABKA4yFy5wcm90b2NvbC5SZXF1ZXN0UmVzdWx0Eg8KB21l",
"c3NhZ2UYAiABKAkqJgoNUmVxdWVzdFJlc3VsdBILCgdTdWNjZXNzEAASCAoE", "c3NhZ2UYAiABKAkqWQoLTWVzc2FnZVR5cGUSEAoMbG9naW5SZXF1ZXN0EAAS",
"RmFpbBABYgZwcm90bzM=")); "EQoNbG9naW5SZXNwb25zZRABEhEKDXNpZ251cFJlcXVlc3QQAhISCg5zaWdu",
"dXBSZXNwb25zZRADKiYKDVJlcXVlc3RSZXN1bHQSCwoHU3VjY2VzcxAAEggK",
"BEZhaWwQAWIGcHJvdG8z"));
descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData, descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData,
new pbr::FileDescriptor[] { }, new pbr::FileDescriptor[] { },
new pbr::GeneratedClrTypeInfo(new[] {typeof(global::Protocol.RequestResult), }, null, new pbr::GeneratedClrTypeInfo[] { new pbr::GeneratedClrTypeInfo(new[] {typeof(global::Protocol.MessageType), typeof(global::Protocol.RequestResult), }, null, new pbr::GeneratedClrTypeInfo[] {
new pbr::GeneratedClrTypeInfo(typeof(global::Protocol.LoginRequest), global::Protocol.LoginRequest.Parser, new[]{ "Username", "Password" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::Protocol.LoginRequest), global::Protocol.LoginRequest.Parser, new[]{ "Username", "Password" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::Protocol.LoginResponse), global::Protocol.LoginResponse.Parser, new[]{ "Result", "Message" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::Protocol.LoginResponse), global::Protocol.LoginResponse.Parser, new[]{ "Result", "Message" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::Protocol.SignupRequest), global::Protocol.SignupRequest.Parser, new[]{ "Username", "Password" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::Protocol.SignupRequest), global::Protocol.SignupRequest.Parser, new[]{ "Username", "Password" }, null, null, null, null),
@ -45,6 +47,13 @@ namespace Protocol {
} }
#region Enums #region Enums
public enum MessageType {
[pbr::OriginalName("loginRequest")] LoginRequest = 0,
[pbr::OriginalName("loginResponse")] LoginResponse = 1,
[pbr::OriginalName("signupRequest")] SignupRequest = 2,
[pbr::OriginalName("signupResponse")] SignupResponse = 3,
}
public enum RequestResult { public enum RequestResult {
[pbr::OriginalName("Success")] Success = 0, [pbr::OriginalName("Success")] Success = 0,
[pbr::OriginalName("Fail")] Fail = 1, [pbr::OriginalName("Fail")] Fail = 1,

View File

@ -1,5 +1,7 @@
using Google.Protobuf;
using Network; using Network;
using System.Text; using Protocol;
using System.Collections.Generic;
using UnityEngine; using UnityEngine;
namespace Test namespace Test
@ -8,12 +10,30 @@ namespace Test
{ {
private async void Start() private async void Start()
{ {
var sendBytes = Encoding.UTF8.GetBytes("This is a test string sent via TCP."); var request = new LoginRequest
{
Username = "原神启动通过TCP",
Password = "20200928",
};
var receivedBytes = await UnityTcpClient.Instance.SendAndReceiveData(sendBytes); var requestBytes = new byte[request.CalculateSize()];
var receivedString = Encoding.UTF8.GetString(receivedBytes); request.WriteTo(requestBytes);
Debug.Log($"Received string: {receivedString}"); var sendBytes = new List<byte>
{
(byte)MessageType.LoginRequest
};
sendBytes.AddRange(requestBytes);
var responseBytes = await UnityTcpClient.Instance.SendAndReceiveData(sendBytes.ToArray());
if (responseBytes.Length == 0) return;
else if (responseBytes[0] == (byte)MessageType.LoginResponse)
{
var response = LoginResponse.Parser.ParseFrom(responseBytes[1..]);
Debug.Log($"Received response: {response}");
}
} }
} }
} }

View File

@ -1,5 +1,7 @@
using Google.Protobuf;
using Network; using Network;
using System.Text; using Protocol;
using System.Collections.Generic;
using UnityEngine; using UnityEngine;
namespace Test namespace Test
@ -8,12 +10,30 @@ namespace Test
{ {
private async void Start() private async void Start()
{ {
var sendBytes = Encoding.UTF8.GetBytes("This is a test string sent via UDP."); var request = new LoginRequest
{
Username = "原神启动谁会通过UDP启动啊喂",
Password = "20200928",
};
var receivedBytes = await UnityUdpClient.Instance.SendAndReceiveData(sendBytes); var requestBytes = new byte[request.CalculateSize()];
var receivedString = Encoding.UTF8.GetString(receivedBytes); request.WriteTo(requestBytes);
Debug.Log($"Received string: {receivedString}"); var sendBytes = new List<byte>
{
(byte)MessageType.LoginRequest
};
sendBytes.AddRange(requestBytes);
var responseBytes = await UnityUdpClient.Instance.SendAndReceiveData(sendBytes.ToArray());
if (responseBytes.Length == 0) return;
else if (responseBytes[0] == (byte)MessageType.LoginResponse)
{
var response = LoginResponse.Parser.ParseFrom(responseBytes[1..]);
Debug.Log($"Received response: {response}");
}
} }
} }
} }

View File

@ -1,8 +1,8 @@
mod command_helper; mod command_helper;
mod message_dispatcher;
mod protocol; mod protocol;
mod server_logger; mod server_logger;
mod servers; mod servers;
mod services;
use server_logger::ServerLogger; use server_logger::ServerLogger;
use servers::tcp_server::TCP_SERVER; use servers::tcp_server::TCP_SERVER;

View File

@ -0,0 +1,48 @@
use prost::Message;
use crate::protocol::{
LoginRequest, LoginResponse, MessageType, RequestResult, SignupRequest, SignupResponse,
};
pub(crate) fn dispatch_message(msg_type: u8, msg: &[u8]) -> Vec<u8> {
let mut buf = Vec::new();
match msg_type {
// Owing to Rust disallows converting an integer to an
// enumeration (yet allows in reverse! ಠ_ಠ), we have to
// use such this way to make this pattern matching works.
//
// This seems not as elegable as what we are expected,
// but it could work well, provided that items in
// `MessageType` doesn't excceed 256.
val if val == MessageType::LoginRequest as u8 => {
let msg = LoginRequest::decode(msg).unwrap();
log::info!("{msg:?}");
LoginResponse {
result: RequestResult::Success.into(),
message: "Successfully logged in!".into(),
}
.encode(&mut buf)
.unwrap();
[vec![MessageType::LoginResponse as u8], buf].concat()
}
val if val == MessageType::SignupRequest as u8 => {
let msg = SignupRequest::decode(msg).unwrap();
log::info!("{msg:?}");
SignupResponse {
result: RequestResult::Success.into(),
message: "Successfully signed up!".into(),
}
.encode(&mut buf)
.unwrap();
[vec![MessageType::SignupResponse as u8], buf].concat()
}
_ => unreachable!(),
}
}

View File

@ -2,3 +2,5 @@ pub(crate) mod tcp_server;
pub(crate) mod udp_server; pub(crate) mod udp_server;
const SERVER_ADDR: &str = "127.0.0.1:12345"; const SERVER_ADDR: &str = "127.0.0.1:12345";
const TCP_MAX_PAYLOAD_SIZE: usize = 1460;
const UDP_MAX_PAYLOAD_SIZE: usize = 1472;

View File

@ -5,10 +5,12 @@ use std::sync::LazyLock;
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream}; use tokio::net::{TcpListener, TcpStream};
use tokio::sync::{Mutex, mpsc}; use tokio::sync::Mutex;
use tokio::sync::mpsc::{self, Sender};
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use super::SERVER_ADDR; use super::{SERVER_ADDR, TCP_MAX_PAYLOAD_SIZE};
use crate::message_dispatcher;
pub(crate) static TCP_SERVER: LazyLock<Mutex<TcpServer>> = pub(crate) static TCP_SERVER: LazyLock<Mutex<TcpServer>> =
LazyLock::new(|| Mutex::new(TcpServer::new())); LazyLock::new(|| Mutex::new(TcpServer::new()));
@ -16,7 +18,7 @@ pub(crate) static TCP_SERVER: LazyLock<Mutex<TcpServer>> =
pub(crate) struct TcpServer { pub(crate) struct TcpServer {
is_running: bool, is_running: bool,
clients: HashMap<SocketAddr, JoinHandle<()>>, clients: HashMap<SocketAddr, JoinHandle<()>>,
shutdown_tx: Option<mpsc::Sender<()>>, shutdown_tx: Option<Sender<()>>,
} }
impl TcpServer { impl TcpServer {
@ -31,13 +33,14 @@ impl TcpServer {
pub(crate) async fn start(&mut self) { pub(crate) async fn start(&mut self) {
if self.is_running { if self.is_running {
log::warn!("TCP server is already running"); log::warn!("TCP server is already running");
return; return;
} }
match TcpListener::bind(SERVER_ADDR).await { match TcpListener::bind(SERVER_ADDR).await {
Ok(listener) => { Ok(listener) => {
let (shutdown_tx, shutdown_rx) = mpsc::channel(1);
self.is_running = true; self.is_running = true;
let (shutdown_tx, shutdown_rx) = mpsc::channel(1);
self.shutdown_tx = Some(shutdown_tx); self.shutdown_tx = Some(shutdown_tx);
tokio::spawn(async move { tokio::spawn(async move {
@ -63,6 +66,7 @@ impl TcpServer {
for (addr, connection) in self.clients.drain() { for (addr, connection) in self.clients.drain() {
log::info!("Closing connection to {}", addr); log::info!("Closing connection to {}", addr);
connection.abort(); connection.abort();
} }
} }
@ -79,6 +83,7 @@ impl TcpServer {
if let Err(e) = Self::handle_client(socket, addr).await { if let Err(e) = Self::handle_client(socket, addr).await {
log::error!("Client {addr} error: {e}"); log::error!("Client {addr} error: {e}");
} }
log::info!("Client {addr} disconnected"); log::info!("Client {addr} disconnected");
}); });
@ -91,6 +96,7 @@ impl TcpServer {
_ = shutdown_rx.recv() => { _ = shutdown_rx.recv() => {
log::info!("TCP Server shutting down"); log::info!("TCP Server shutting down");
break; break;
} }
} }
@ -98,18 +104,18 @@ impl TcpServer {
} }
async fn handle_client(mut socket: TcpStream, addr: SocketAddr) -> io::Result<()> { async fn handle_client(mut socket: TcpStream, addr: SocketAddr) -> io::Result<()> {
let mut buffer = [0; 1024];
loop { loop {
let mut buffer = [0; TCP_MAX_PAYLOAD_SIZE];
let len = socket.read(&mut buffer).await?; let len = socket.read(&mut buffer).await?;
if len == 0 { if len == 0 {
break; break;
} }
log::debug!("Received {} bytes from {}", len, addr); log::info!("Received {} bytes from {}", len, addr);
// TODO: Deserialize data let response = message_dispatcher::dispatch_message(buffer[0], &buffer[1..len]);
socket.write_all(&buffer[..len]).await?;
socket.write_all(&response).await?;
} }
let mut server = TCP_SERVER.lock().await; let mut server = TCP_SERVER.lock().await;

View File

@ -4,7 +4,8 @@ use std::sync::LazyLock;
use tokio::net::UdpSocket; use tokio::net::UdpSocket;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use super::SERVER_ADDR; use super::{SERVER_ADDR, UDP_MAX_PAYLOAD_SIZE};
use crate::message_dispatcher;
pub(crate) static UDP_SERVER: LazyLock<Mutex<UdpServer>> = pub(crate) static UDP_SERVER: LazyLock<Mutex<UdpServer>> =
LazyLock::new(|| Mutex::new(UdpServer::new())); LazyLock::new(|| Mutex::new(UdpServer::new()));
@ -21,6 +22,7 @@ impl UdpServer {
pub(crate) async fn start(&mut self) { pub(crate) async fn start(&mut self) {
if self.is_running { if self.is_running {
log::warn!("UDP server is already running"); log::warn!("UDP server is already running");
return; return;
} }
@ -50,17 +52,17 @@ impl UdpServer {
async fn handle_client(socket: &UdpSocket) -> io::Result<()> { async fn handle_client(socket: &UdpSocket) -> io::Result<()> {
loop { loop {
let mut buffer = [0; 1024]; let mut buffer = [0; UDP_MAX_PAYLOAD_SIZE];
let (len, addr) = socket.recv_from(&mut buffer).await?; let (len, addr) = socket.recv_from(&mut buffer).await?;
if len == 0 { if len == 0 {
break; break;
} }
log::info!("Received message from client {addr}"); log::info!("Received {} bytes from {}", len, addr);
// TODO: Deserialize data let response = message_dispatcher::dispatch_message(buffer[0], &buffer[1..len]);
let buffer = &buffer[..len];
socket.send_to(buffer, addr).await?; socket.send_to(&response, addr).await?;
} }
Ok(()) Ok(())

View File

@ -1 +0,0 @@
pub(crate) mod game_service;

View File

@ -1 +0,0 @@

View File

@ -2,6 +2,13 @@ syntax = "proto3";
package protocol; package protocol;
enum MessageType {
loginRequest = 0;
loginResponse = 1;
signupRequest = 2;
signupResponse = 3;
}
enum RequestResult { enum RequestResult {
Success = 0; Success = 0;
Fail = 1; Fail = 1;