From 936a485195148cf2d38b1e72051caccc9318a154 Mon Sep 17 00:00:00 2001 From: XuShanQiXun <3401460572@qq.com> Date: Sun, 22 Jun 2025 00:14:43 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=97=B6=E9=97=B4=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F=E5=8C=96=E5=92=8C=E5=8E=86=E5=8F=B2=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 `ChatDataModel.cs` 中添加 `TimeFormatConverter` 类,用于格式化本地时间,并在 `MainWindow.xaml` 中应用该转换器。 在 `chatapi.cs` 中新增 `HistoryRequest` 和 `HistoryResponse` 类以处理历史记录请求和响应。 修改 `LoginWindow.xaml.cs` 中的数据发送方式,使用 `SendWithPrefix` 方法以支持数据压缩和长度前缀。 在 `MainWindow.xaml.cs` 中添加 `LoadHistoryMessages` 方法以加载历史消息,并在接收到响应时更新消息列表。 在 `Program.cs` 中实现数据压缩和解压缩方法,提升网络传输效率。 新增消息表和索引以支持消息存储和查询。 更新日志记录以提供更详细的操作信息和错误处理。 --- chatclient/Data/ChatDataModel.cs | 73 ++++++++++ chatclient/Data/chatapi.cs | 25 +++- chatclient/LoginWindow.xaml.cs | 8 +- chatclient/MainWindow.xaml | 6 +- chatclient/MainWindow.xaml.cs | 201 +++++++++++++++++++++++---- chatserver/Data/chatapi.cs | 42 ++++++ chatserver/Program.cs | 225 ++++++++++++++++++++++++++++--- 7 files changed, 523 insertions(+), 57 deletions(-) diff --git a/chatclient/Data/ChatDataModel.cs b/chatclient/Data/ChatDataModel.cs index 3e773cf..af99423 100644 --- a/chatclient/Data/ChatDataModel.cs +++ b/chatclient/Data/ChatDataModel.cs @@ -8,6 +8,8 @@ using System.Windows; using System.Windows.Media.Imaging; using System.Windows.Controls; using System.Windows.Input; +using System.Globalization; +using System.Windows.Data; namespace chatclient.Data { @@ -60,4 +62,75 @@ namespace chatclient.Data public string? UserName { get; set; } public string? UserPassword { get; set; } } + /// + /// 时间格式转换器类 + /// + public class TimeFormatConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is DateTime timestamp) + { + // 获取本地时区信息 + TimeZoneInfo localTimeZone = TimeZoneInfo.Local; + bool isLocalTimeZoneUtcPlus8 = localTimeZone.BaseUtcOffset == TimeSpan.FromHours(8); + DateTime localTime; + if (timestamp.Kind == DateTimeKind.Utc) + { + localTime = timestamp.ToLocalTime(); + } + else if (timestamp.Kind == DateTimeKind.Unspecified) + { + // 假设未指定时间是UTC时间(常见实践) + localTime = DateTime.SpecifyKind(timestamp, DateTimeKind.Utc).ToLocalTime(); + } + else + { + localTime = timestamp; + } + + var now = DateTime.Now; + var today = now.Date; + var yesterday = today.AddDays(-1); + var localTimeDate = localTime.Date; + + // 格式化时间字符串 + string formattedTime; + if (localTimeDate == today) + { + formattedTime = localTime.ToString("HH:mm:ss"); + } + else if (localTimeDate == yesterday) + { + formattedTime = "昨天 " + localTime.ToString("HH:mm"); + } + else if (localTime.Year == now.Year) + { + formattedTime = localTime.ToString("MM/dd HH:mm"); + } + else + { + formattedTime = localTime.ToString("yy/MM/dd HH:mm"); + } + + // 仅在非UTC+8时区显示时区信息 + if (!isLocalTimeZoneUtcPlus8) + { + // 获取原始时间的时区信息 + string timeZoneInfo = timestamp.Kind == DateTimeKind.Utc + ? "UTC" + : "UTC"+localTime.ToString("zzz"); + + return $"{timeZoneInfo} {formattedTime}"; + } + return formattedTime; + } + return value; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } } diff --git a/chatclient/Data/chatapi.cs b/chatclient/Data/chatapi.cs index 6dba7a3..724429a 100644 --- a/chatclient/Data/chatapi.cs +++ b/chatclient/Data/chatapi.cs @@ -5,8 +5,8 @@ namespace chatclient.Data internal class Server { public const string ServerUrl = "http://127.0.0.1:5001"; - //public const string ServerIP = "175.24.191.172"; - public const string ServerIP = "127.0.0.1"; + public const string ServerIP = "175.24.191.172"; + //public const string ServerIP = "127.0.0.1"; public const int ServerPort = 52006; } internal class LoginData @@ -57,4 +57,25 @@ namespace chatclient.Data public required string userid { get; set; } = "Unid"; public string? token { get; set; } = null; // 添加token字段 } + /// + /// 历史记录请求类 + /// + internal class HistoryRequest + { + public string type { get; set; } = "history"; + public int offset { get; set; } = 0; // 分页偏移量 + public int count { get; set; } = 10; // 请求数量 + public string? chat_type { get; set; } = "group"; // group/private + public string? room_id { get; set; } = "global"; // 群聊房间ID + public string? receiver_id { get; set; } = null; // 私聊接收者ID + } + + /// + /// 历史记录响应类 + /// + internal class HistoryResponse + { + public List history { get; set; } = new List(); + public int total_count { get; set; } // 总消息数 + } } diff --git a/chatclient/LoginWindow.xaml.cs b/chatclient/LoginWindow.xaml.cs index 5b1e999..32f955e 100644 --- a/chatclient/LoginWindow.xaml.cs +++ b/chatclient/LoginWindow.xaml.cs @@ -150,7 +150,7 @@ namespace chatclient // 检查Socket是否可用 if (MainWindow.Client?.Connected == true) { - MainWindow.Client.Send(dataBytes); + MainWindow.SendWithPrefix(dataBytes); return; } log.Info("未连接服务器,尝试异步连接"); @@ -162,7 +162,7 @@ namespace chatclient MainWindow.Client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); MainWindow.Client?.Connect(IPAddress.Parse(Server.ServerIP), Server.ServerPort); MainWindow.StartReceive(); - MainWindow.Client?.Send(dataBytes); + MainWindow.SendWithPrefix(dataBytes); } catch (Exception ex) { @@ -235,7 +235,7 @@ namespace chatclient // 检查Socket是否可用 if (MainWindow.Client?.Connected == true) { - MainWindow.Client.Send(dataBytes); + MainWindow.SendWithPrefix(dataBytes); return; } log.Info("未连接服务器,尝试异步连接"); @@ -247,7 +247,7 @@ namespace chatclient MainWindow.Client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); MainWindow.Client?.Connect(IPAddress.Parse(Server.ServerIP), Server.ServerPort); MainWindow.StartReceive(); - MainWindow.Client?.Send(dataBytes); + MainWindow.SendWithPrefix(dataBytes); } catch (Exception ex) { diff --git a/chatclient/MainWindow.xaml b/chatclient/MainWindow.xaml index 2b5eca4..144a7c0 100644 --- a/chatclient/MainWindow.xaml +++ b/chatclient/MainWindow.xaml @@ -5,10 +5,14 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:local="clr-namespace:chatclient" + xmlns:data="clr-namespace:chatclient.Data" xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls" x:Class="chatclient.MainWindow" mc:Ignorable="d" Title="ChatWindow" Height="450" Width="800" MinHeight="240" MinWidth="380" Style="{StaticResource MaterialDesignWindow}" Closed="MainWindow_Closed" Loaded="MainWindow_Loaded"> + + + @@ -75,7 +79,7 @@ - + diff --git a/chatclient/MainWindow.xaml.cs b/chatclient/MainWindow.xaml.cs index 11b2f64..792ffea 100644 --- a/chatclient/MainWindow.xaml.cs +++ b/chatclient/MainWindow.xaml.cs @@ -22,6 +22,7 @@ using System.Collections.ObjectModel; using System.Windows.Threading; using System.Collections.Specialized; using Hardcodet.Wpf.TaskbarNotification; +using System.IO.Compression; [assembly: XmlConfigurator(ConfigFile = "config/log4net.config", Watch = true)] namespace chatclient @@ -32,10 +33,11 @@ namespace chatclient public partial class MainWindow : Window, INotifyPropertyChanged { LoginWindow Login = new(); - static string? receive; + //static string? receive; public static string UserName { get; set; } = "?"; public static string? token = null; public static string? UserId = null; + //private bool isLoadingHistory = false; private static readonly ILog log = LogManager.GetLogger(typeof(MainWindow)); public static Socket? Client; public static readonly HttpClient HttpClient = new HttpClient(); @@ -87,8 +89,6 @@ namespace chatclient MessageScroller.ScrollToEnd(); }), DispatcherPriority.ContextIdle); }; - //Loaded += MainWindow_Loaded; - //Closed += MainWindow_Closed; } public static void StartReceive() { @@ -101,29 +101,64 @@ namespace chatclient } static void Receive() { - byte[] buffer = new byte[1024]; + const int prefixSize = sizeof(int); + byte[] prefixBuffer = new byte[prefixSize]; + MemoryStream receivedStream = new MemoryStream(); try { while (true) { - int num = Client!.Receive(buffer); - if (num == 0) break; - if (Client.Poll(100, SelectMode.SelectRead) && Client.Available == 0 || !Client.Connected) + //接收前缀长度 + int prefixBytesRead = 0; + while (prefixBytesRead < prefixSize) { - log.Error("连接已断开"); - break; + int bytesRead = Client!.Receive(prefixBuffer, prefixBytesRead, prefixSize - prefixBytesRead, SocketFlags.None); + if (bytesRead == 0) + { + log.Info("连接已关闭"); + return; + } + prefixBytesRead += bytesRead; } - receive = Encoding.UTF8.GetString(buffer, 0, num); - response(receive); + //解析消息长度 + int messageLength = BitConverter.ToInt32(prefixBuffer, 0); + if (messageLength <= 0 || messageLength > 10 * 1024 * 1024) + { + log.Error($"无效消息长度: {messageLength}"); + continue; + } + //读取完整消息 + byte[] messageBuffer = new byte[messageLength]; + int totalBytesRead = 0; + while (totalBytesRead < messageLength) + { + int bytesRead = Client!.Receive(messageBuffer, totalBytesRead, messageLength - totalBytesRead, SocketFlags.None); + if (bytesRead == 0) + { + log.Info("连接已关闭"); + return; + } + totalBytesRead += bytesRead; + } + //解压缩 + byte[] decompressedData = Decompress(messageBuffer); + //处理消息 + string message = Encoding.UTF8.GetString(decompressedData); + response(message); } } + catch (SocketException ex) + { + log.Error($"Socket错误: {ex.SocketErrorCode}"); + } catch (Exception ex) { - log.Error(ex); + log.Error($"接收错误: {ex.Message}"); } finally { Client?.Close(); + receivedStream.Dispose(); } } static void response(string msg) @@ -165,6 +200,7 @@ namespace chatclient mainWindow?.Messages.Add(chatmessage); } }); + LoadHistoryMessages(); log.Info($"用户 {UserName} 登录成功(token:{token},userid:{UserId})"); } else if (LoginResponse!.status == "error_0") @@ -325,6 +361,45 @@ namespace chatclient log.Error("反序列化聊天数据时返回了 null"); } } + else if (Type.type == "history") + { + var historyResponse = JsonSerializer.Deserialize(msg); + Application.Current.Dispatcher.Invoke(() => + { + var mainWindow = Application.Current.Windows.OfType().FirstOrDefault(); + if (mainWindow == null) return; + if (historyResponse?.history != null) + { + foreach (var msgItem in historyResponse.history) + { + try + { + mainWindow.Dispatcher.Invoke(() => + { + var chatmessage = new ChatMessage + { + Sender = msgItem.user ?? "未知用户", + MsgType = MessageType.Text, + Image = new BitmapImage(new Uri( + "pack://application:,,,/resource/user.png", + UriKind.Absolute)), + Content = msgItem.message ?? "(无内容)", + Timestamp = msgItem.timestamp ?? DateTime.Now, + Alignment = msgItem.userid == UserId ? + HorizontalAlignment.Right : + HorizontalAlignment.Left + }; + mainWindow.Messages.Insert(0, chatmessage); + }); + } + catch (Exception ex) + { + log.Error($"添加历史消息失败: {ex.Message}"); + } + } + } + }); + } else if (Type.type == "ping") { } else { @@ -346,6 +421,58 @@ namespace chatclient log.Error("处理响应时发生错误", ex); } } + /// + /// 压缩数据 + /// + /// + /// + private static byte[] Compress(byte[] data) + { + if (data.Length < 256) + return data; + + using (var compressedStream = new MemoryStream()) + { + using (var zipStream = new GZipStream(compressedStream, CompressionMode.Compress)) + { + zipStream.Write(data, 0, data.Length); + } + return compressedStream.ToArray(); + } + } + /// + /// 解压缩数据 + /// + /// + /// + private static byte[] Decompress(byte[] compressedData) + { + if (compressedData.Length < 2 || compressedData[0] != 0x1F || compressedData[1] != 0x8B) + return compressedData; // 未压缩数据 + + using (var compressedStream = new MemoryStream(compressedData)) + using (var zipStream = new GZipStream(compressedStream, CompressionMode.Decompress)) + using (var resultStream = new MemoryStream()) + { + zipStream.CopyTo(resultStream); + return resultStream.ToArray(); + } + } + /// + /// 发送消息 + /// + /// + public static void SendWithPrefix(byte[] data) + { + byte[] compressedData = Compress(data); + byte[] lengthPrefix = BitConverter.GetBytes(compressedData.Length); + byte[] fullMessage = new byte[lengthPrefix.Length + compressedData.Length]; + + Buffer.BlockCopy(lengthPrefix, 0, fullMessage, 0, lengthPrefix.Length); + Buffer.BlockCopy(compressedData, 0, fullMessage, lengthPrefix.Length, compressedData.Length); + log.Info($"发送数据(长度:{data.Length},压缩后长度:{lengthPrefix.Length},总体长度:{fullMessage.Length})"); + Client?.Send(fullMessage); + } private async void SendMessage_Click(object sender, RoutedEventArgs e) { await SendMessage(); @@ -354,24 +481,10 @@ namespace chatclient { if (string.IsNullOrWhiteSpace(txtMessage.Text)) return; - // 获取当前选中的联系人 //var contact = cmbContacts.SelectedItem as Contact; // 判断是否为群组,若是则收件人设为“所有人”,否则为联系人显示名 //string recipient = contact?.IsGroup == true ? "所有人" : contact?.DisplayName; - - // 弃用的方法 - // 创建新消息 - //var newMessage = new ChatMessage - //{ - // Sender = "我", - // Type = MessageType.Text, - // Image = new BitmapImage(new Uri("pack://application:,,,/resource/user.png", UriKind.Absolute)), // 默认头像 - // Content = txtMessage.Text, - // Timestamp = DateTime.Now, - // Alignment = HorizontalAlignment.Right, // 自己发送的消息靠右 - // SenderColor = new SolidColorBrush(Colors.Blue) - //}; var newChatMessage = new ChatData { type = "chat", @@ -386,7 +499,7 @@ namespace chatclient // 检查Socket是否可用 if (Client?.Connected == true) { - Client.Send(dataBytes); + SendWithPrefix(dataBytes); Application.Current.Dispatcher.Invoke(() => { txtMessage.Clear(); @@ -402,7 +515,7 @@ namespace chatclient Client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); Client?.Connect(IPAddress.Parse(Server.ServerIP), Server.ServerPort); StartReceive(); - Client?.Send(dataBytes); + SendWithPrefix(dataBytes); Application.Current.Dispatcher.Invoke(() => { txtMessage.Clear(); @@ -425,6 +538,38 @@ namespace chatclient // 添加到消息列表 //Messages.Add(newMessage); } + private static void LoadHistoryMessages(int offset = 0) + { + var historyRequest = new HistoryRequest + { + offset = offset, + count = 10, + chat_type = "group", + room_id = "global" + }; + string requestData = JsonSerializer.Serialize(historyRequest); + byte[] data = Encoding.UTF8.GetBytes(requestData); + if (Client?.Connected == true) + { + try + { + SendWithPrefix(data); + } + catch (SocketException ex) + { + log.Error($"发送历史请求失败: {ex.SocketErrorCode}"); + } + catch (Exception ex) + { + log.Error($"发送历史请求异常: {ex.Message}"); + } + } + else + { + // 处理断开连接的情况 + log.Warn("发送历史请求时客户端未连接"); + } + } private void QueueMessage(string message) { if (SnackbarThree.MessageQueue is { } messageQueue) diff --git a/chatserver/Data/chatapi.cs b/chatserver/Data/chatapi.cs index 5c9e194..7c6b583 100644 --- a/chatserver/Data/chatapi.cs +++ b/chatserver/Data/chatapi.cs @@ -6,12 +6,18 @@ using System.Threading.Tasks; namespace chatserver.Data { + /// + /// 登录请求类 + /// internal class LoginData { public string? username { get; set; } = null; public string? password { get; set; } = null; public string? token { get; set; } = null; } + /// + /// 登录响应类 + /// internal class LoginResultData { public required string type { get; set; } = "login"; @@ -21,21 +27,33 @@ namespace chatserver.Data public string? token { get; set; } public string? username { get; set; } } + /// + /// 注册请求类 + /// internal class SignData { public string? username { get; set; } = null; public string? password { get; set; } = null; } + /// + /// 注册响应类 + /// internal class SignResultData { public required string type { get; set; } = "sign"; public string? status { get; set; } = null; public string? message { get; set; } = null; } + /// + /// 类型数据类 + /// internal class TypeData { public string? type { get; set; } } + /// + /// 聊天数据响应类 + /// internal class ChatRegisterData { public required string type { get; set; } = "chat"; @@ -47,6 +65,9 @@ namespace chatserver.Data public MessageType? msgtype { get; set; } = MessageType.Text; public DateTime? timestamp { get; set; } = DateTime.Now; } + /// + /// 聊天数据请求类 + /// internal class ChatData { public required string message { get; set; } = "message"; @@ -54,4 +75,25 @@ namespace chatserver.Data public required string userid { get; set; } = "Unid"; public string? token { get; set; } = null; // 添加token字段 } + /// + /// 历史记录请求类 + /// + internal class HistoryRequest + { + public int offset { get; set; } = 0; // 分页偏移量 + public int count { get; set; } = 10; // 请求数量 + public string? chat_type { get; set; } = "group"; // group/private + public string? room_id { get; set; } = "global"; // 群聊房间ID + public string? receiver_id { get; set; } = null; // 私聊接收者ID + } + + /// + /// 历史记录响应类 + /// + internal class HistoryResponse + { + public string type { get; set; } = "history"; + public List history { get; set; } = new List(); + public int total_count { get; set; } // 总消息数 + } } diff --git a/chatserver/Program.cs b/chatserver/Program.cs index 1da1fd7..6ed120c 100644 --- a/chatserver/Program.cs +++ b/chatserver/Program.cs @@ -8,6 +8,7 @@ using chatserver.Data; using System.Text.Json; using System.Reflection; using static log4net.Appender.FileAppender; +using System.IO.Compression; [assembly: XmlConfigurator(ConfigFile = "config/log4net.config", Watch = true)] namespace chatserver @@ -51,20 +52,47 @@ namespace chatserver log.Info("正在打开数据库连接..."); User_db = new SQLiteConnection("Data Source=ServerUser.db;Version=3;"); //没有数据库则自动创建 User_db.Open(); - EnsureUsersTableExists(); // 确保users表存在 + InitializeTable(); log.Info("数据库连接已打开"); } static void HandleClient(Socket socket) { + const int prefixSize = sizeof(int); + byte[] prefixBuffer = new byte[prefixSize]; try { while (true) - { - byte[] buffer = new byte[1024]; - int received = socket.Receive(buffer); - if (received == 0) break; // 客户端断开连接 - string message = System.Text.Encoding.UTF8.GetString(buffer, 0, received); + //读取长度前缀 + int prefixBytesRead = 0; + while (prefixBytesRead < prefixSize) + { + int bytesRead = socket.Receive(prefixBuffer, prefixBytesRead, prefixSize - prefixBytesRead, SocketFlags.None); + if (bytesRead == 0) + return; + prefixBytesRead += bytesRead; + } + //解析消息长度 + int messageLength = BitConverter.ToInt32(prefixBuffer, 0); + if (messageLength <= 0 || messageLength > 10 * 1024 * 1024) + { + log.Error($"无效消息长度: {messageLength}"); + continue; + } + //读取完整消息 + byte[] messageBuffer = new byte[messageLength]; + int totalBytesRead = 0; + while (totalBytesRead < messageLength) + { + int bytesRead = socket.Receive(messageBuffer, totalBytesRead, messageLength - totalBytesRead, SocketFlags.None); + if (bytesRead == 0) + return; + totalBytesRead += bytesRead; + } + //解压缩 + byte[] decompressedData = Decompress(messageBuffer); + string message = System.Text.Encoding.UTF8.GetString(decompressedData); + //处理信息 log.Info("Received message: " + message); var Type = JsonSerializer.Deserialize(message); if (Type != null) @@ -79,28 +107,28 @@ namespace chatserver { log.Warn("用户名或密码不能为空"); var emptyResult = new SignResultData { type = "register", status = "error_-1", message = "用户名或密码不能为空" }; - socket.Send(JsonSerializer.SerializeToUtf8Bytes(emptyResult)); + SendWithPrefix(socket,JsonSerializer.SerializeToUtf8Bytes(emptyResult)); continue; // 如果用户名或密码为空,则跳过注册流程 } if (sginuser.username.Length < 2 || sginuser.username.Length > 20) { log.Warn($"用户注册时 {sginuser.username} 用户名长度不符合要求"); var lengthResult = new SignResultData { type = "register", status = "error_2", message = "用户名长度必须在2到20个字符之间" }; - socket.Send(JsonSerializer.SerializeToUtf8Bytes(lengthResult)); + SendWithPrefix(socket,JsonSerializer.SerializeToUtf8Bytes(lengthResult)); continue; // 如果用户名长度不符合要求,则跳过注册流程 } if (sginuser.password.Length < 4 || sginuser.password.Length > 20) { log.Warn($"用户注册时 {sginuser.username} 密码长度不符合要求"); var weakPwdResult = new SignResultData { type = "register", status = "error_1", message = "密码长度必须在4到20个字符之间" }; - socket.Send(JsonSerializer.SerializeToUtf8Bytes(weakPwdResult)); + SendWithPrefix(socket,JsonSerializer.SerializeToUtf8Bytes(weakPwdResult)); continue; // 如果密码过弱,则跳过注册流程 } if (UserExists(sginuser.username)) { log.Warn($"用户 {sginuser.username} 已存在"); var Result = new SignResultData { type = "register", status = "error_0", message = "用户名已存在" }; - socket.Send(System.Text.Encoding.UTF8.GetBytes(JsonSerializer.Serialize(Result))); + SendWithPrefix(socket,System.Text.Encoding.UTF8.GetBytes(JsonSerializer.Serialize(Result))); continue;// 如果用户名已存在,则跳过注册流程 } var cmd = new SQLiteCommand("INSERT INTO users (userid, username, password) VALUES (@userid, @username, @password)", User_db); @@ -111,7 +139,7 @@ namespace chatserver cmd.ExecuteNonQuery(); log.Info($"用户 {sginuser.username} 注册成功(id:{timedUlid})"); var result = new SignResultData { type = "register", status = "succeed", message = "注册成功" }; - socket.Send(System.Text.Encoding.UTF8.GetBytes(JsonSerializer.Serialize(result))); + SendWithPrefix(socket,System.Text.Encoding.UTF8.GetBytes(JsonSerializer.Serialize(result))); } } else if (Type.type == "login") @@ -123,21 +151,21 @@ namespace chatserver { log.Warn("用户名或密码不能为空"); var emptyResult = new LoginResultData { type = "login", status = "error_-1", message = "用户名或密码不能为空" }; - socket.Send(JsonSerializer.SerializeToUtf8Bytes(emptyResult)); + SendWithPrefix(socket,JsonSerializer.SerializeToUtf8Bytes(emptyResult)); continue; } if (loginData.username.Length < 2 || loginData.username.Length > 20) { log.Warn($"用户登录时 {loginData.username} 用户名长度不符合要求"); var lengthResult = new LoginResultData { type = "login", status = "error_2", message = "用户名长度必须在2到20个字符之间" }; - socket.Send(JsonSerializer.SerializeToUtf8Bytes(lengthResult)); + SendWithPrefix(socket,JsonSerializer.SerializeToUtf8Bytes(lengthResult)); continue; } if (loginData.password.Length > 20) { log.Warn($"用户登录时 {loginData.username} 密码长度不符合要求"); var weakPwdResult = new LoginResultData { type = "login", status = "error_1", message = "密码长度不能超过20个字符" }; - socket.Send(JsonSerializer.SerializeToUtf8Bytes(weakPwdResult)); + SendWithPrefix(socket,JsonSerializer.SerializeToUtf8Bytes(weakPwdResult)); continue; } var Authentication = UserAuthentication(loginData.username, loginData.password); @@ -164,13 +192,13 @@ namespace chatserver username = loginData.username, userid = Authentication }; - socket.Send(System.Text.Encoding.UTF8.GetBytes(JsonSerializer.Serialize(result))); + SendWithPrefix(socket,System.Text.Encoding.UTF8.GetBytes(JsonSerializer.Serialize(result))); } else { log.Warn($"用户 {loginData.username} 登录失败,用户名或密码错误"); var result = new LoginResultData { type = "login", status = "error_0", message = "用户名或密码错误" }; - socket.Send(System.Text.Encoding.UTF8.GetBytes(JsonSerializer.Serialize(result))); + SendWithPrefix(socket,System.Text.Encoding.UTF8.GetBytes(JsonSerializer.Serialize(result))); } } } @@ -179,7 +207,7 @@ namespace chatserver var chatData = JsonSerializer.Deserialize(message); if (chatData != null && chatData.message != null) { - log.Info($"接收到聊天消息: {chatData.message}"); + log.Info($"接收到聊天消息(长度: {chatData.message.Length} )"); if (Client.Count == 0) { log.Warn("没有客户端连接,取消发送消息。"); @@ -201,13 +229,13 @@ namespace chatserver msgtype = MessageType.Text, timestamp = DateTime.Now }; - socket.Send(System.Text.Encoding.UTF8.GetBytes(JsonSerializer.Serialize(errorData))); + SendWithPrefix(socket,System.Text.Encoding.UTF8.GetBytes(JsonSerializer.Serialize(errorData))); continue; } List clientsCopy; List usersCopy; - // 1. 获取集合快照 + // 获取集合快照 lock (Client_lock) { clientsCopy = new List(Client); // 创建客户端列表副本 @@ -223,6 +251,31 @@ namespace chatserver status = "succeed", timestamp = DateTime.Now }; + Task.Run(() => + { + try + { + using (var dbConnection = new SQLiteConnection("Data Source=ServerUser.db;Version=3;")) + { + dbConnection.Open(); + using (var cmd = new SQLiteCommand(@" + INSERT INTO messages (type, room_id, sender_id, receiver_id, content) + VALUES (@type, @room_id, @sender_id, @receiver_id, @content)", dbConnection)) + { + cmd.Parameters.AddWithValue("@type", "group"); + cmd.Parameters.AddWithValue("@room_id", "global"); + cmd.Parameters.AddWithValue("@sender_id", chatData.userid); + cmd.Parameters.AddWithValue("@receiver_id", DBNull.Value); + cmd.Parameters.AddWithValue("@content", chatData.message); + cmd.ExecuteNonQuery(); + } + } + } + catch (Exception ex) + { + log.Error($"存储消息到数据库失败: {ex.Message}"); + } + }); foreach (var user in usersCopy) { // 查找匹配的客户端 @@ -234,7 +287,7 @@ namespace chatserver try { // ==== 修改:添加发送异常处理 ==== - targetClient.Send(System.Text.Encoding.UTF8.GetBytes(JsonSerializer.Serialize(chatRegisterData))); + SendWithPrefix(targetClient,System.Text.Encoding.UTF8.GetBytes(JsonSerializer.Serialize(chatRegisterData))); } catch (SocketException ex) { @@ -252,6 +305,59 @@ namespace chatserver log.Warn("接收到无效的聊天消息"); } } + else if (Type.type == "history") + { + var historyReq = JsonSerializer.Deserialize(message); + if (historyReq != null) + { + var response = new HistoryResponse(); + string query = "SELECT * FROM messages WHERE "; + var parameters = new List(); + + // 构建查询条件 + if (historyReq.chat_type == "group") + { + query += "type = 'group' AND room_id = @room_id "; + parameters.Add(new SQLiteParameter("@room_id", historyReq.room_id ?? "global")); + } + // else if (historyReq.chat_type == "private") + // { + // query += @"(type = 'private' AND + //((sender_id = @userid1 AND receiver_id = @userid2) OR + // (sender_id = @userid2 AND receiver_id = @userid1))) "; + // parameters.Add(new SQLiteParameter("@userid1", historyReq.sender_id)); + // parameters.Add(new SQLiteParameter("@userid2", historyReq.receiver_id)); + // } + + // 获取总消息数 + string countQuery = query.Replace("SELECT *", "SELECT COUNT(*)"); + using var countCmd = new SQLiteCommand(countQuery, User_db); + countCmd.Parameters.AddRange(parameters.ToArray()); + response.total_count = Convert.ToInt32(countCmd.ExecuteScalar()); + + // 获取分页消息 + query += "ORDER BY timestamp DESC LIMIT @count OFFSET @offset"; + using var cmd = new SQLiteCommand(query, User_db); + cmd.Parameters.AddRange(parameters.ToArray()); + cmd.Parameters.AddWithValue("@count", historyReq.count); + cmd.Parameters.AddWithValue("@offset", historyReq.offset); + + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + response.history.Add(new ChatRegisterData + { + type = "chat", + userid = reader["sender_id"].ToString() ?? "Unid", + user = GetUsernameByUserId(reader["sender_id"].ToString() ?? "Unid"), + msgtype = MessageType.Text, + message = reader["content"].ToString(), + timestamp = Convert.ToDateTime(reader["timestamp"]) + }); + } + SendWithPrefix(socket,JsonSerializer.SerializeToUtf8Bytes(response)); + } + } else { log.Warn("未知的请求类型: " + Type.type); @@ -285,6 +391,59 @@ namespace chatserver } } /// + /// 压缩数据 + /// + /// + /// + private static byte[] Compress(byte[] data) + { + if (data.Length < 256) + return data; + + using (var compressedStream = new MemoryStream()) + { + using (var zipStream = new GZipStream(compressedStream, CompressionMode.Compress)) + { + zipStream.Write(data, 0, data.Length); + } + return compressedStream.ToArray(); + } + } + /// + /// 解压缩数据 + /// + /// + /// + private static byte[] Decompress(byte[] compressedData) + { + if (compressedData.Length < 2 || compressedData[0] != 0x1F || compressedData[1] != 0x8B) + return compressedData; + + using (var compressedStream = new MemoryStream(compressedData)) + using (var zipStream = new GZipStream(compressedStream, CompressionMode.Decompress)) + using (var resultStream = new MemoryStream()) + { + zipStream.CopyTo(resultStream); + return resultStream.ToArray(); + } + } + /// + /// 发送数据 + /// + /// + /// + private static void SendWithPrefix(Socket socket, byte[] data) + { + byte[] compressedData = Compress(data); + byte[] lengthPrefix = BitConverter.GetBytes(compressedData.Length); + byte[] fullMessage = new byte[lengthPrefix.Length + compressedData.Length]; + + Buffer.BlockCopy(lengthPrefix, 0, fullMessage, 0, lengthPrefix.Length); + Buffer.BlockCopy(compressedData, 0, fullMessage, lengthPrefix.Length, compressedData.Length); + log.Info($"发送数据(长度:{data.Length},压缩后长度:{lengthPrefix.Length},总体长度:{fullMessage.Length})"); + socket.Send(fullMessage); + } + /// /// 查询User_db是否有相同用户名 /// /// @@ -311,7 +470,7 @@ namespace chatserver return result != null ? result.ToString()! : string.Empty; } // 在ChatServer类中添加一个方法用于初始化users表 - private static void EnsureUsersTableExists() + private static void InitializeTable() { using var cmd = new SQLiteCommand(@" CREATE TABLE IF NOT EXISTS users ( @@ -320,12 +479,34 @@ namespace chatserver password TEXT NOT NULL )", User_db); cmd.ExecuteNonQuery(); + // 新增消息表 + using var cmd2 = new SQLiteCommand(@" + CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type TEXT NOT NULL CHECK(type IN ('group', 'private')), + room_id TEXT, + sender_id TEXT NOT NULL REFERENCES users(userid), + receiver_id TEXT REFERENCES users(userid), + content TEXT NOT NULL, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP + )", User_db); + cmd2.ExecuteNonQuery(); + + // 创建索引提高查询效率 + using var cmd3 = new SQLiteCommand(@" + CREATE INDEX IF NOT EXISTS idx_messages_timestamp + ON messages (timestamp DESC)", User_db); + cmd3.ExecuteNonQuery(); + using var cmd4 = new SQLiteCommand(@" + CREATE INDEX IF NOT EXISTS idx_messages_room + ON messages (room_id, timestamp DESC)", User_db); + cmd4.ExecuteNonQuery(); } /// /// 根据userid查询对应的用户名 /// /// - /// 用户名,如果不存在则返回 + /// 用户名,如果userid为空或为"Unid",返回默认用户名"Unnamed" static string GetUsernameByUserId(string userid) { if (string.IsNullOrEmpty(userid) || userid == "Unid")