添加时间格式化和历史记录功能

在 `ChatDataModel.cs` 中添加 `TimeFormatConverter` 类,用于格式化本地时间,并在 `MainWindow.xaml` 中应用该转换器。
在 `chatapi.cs` 中新增 `HistoryRequest` 和 `HistoryResponse` 类以处理历史记录请求和响应。
修改 `LoginWindow.xaml.cs` 中的数据发送方式,使用 `SendWithPrefix` 方法以支持数据压缩和长度前缀。
在 `MainWindow.xaml.cs` 中添加 `LoadHistoryMessages` 方法以加载历史消息,并在接收到响应时更新消息列表。
在 `Program.cs` 中实现数据压缩和解压缩方法,提升网络传输效率。
新增消息表和索引以支持消息存储和查询。
更新日志记录以提供更详细的操作信息和错误处理。
This commit is contained in:
绪山七寻 2025-06-22 00:14:43 +08:00
parent b5ebe51aa1
commit 936a485195
7 changed files with 523 additions and 57 deletions

View File

@ -8,6 +8,8 @@ using System.Windows;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
using System.Globalization;
using System.Windows.Data;
namespace chatclient.Data namespace chatclient.Data
{ {
@ -60,4 +62,75 @@ namespace chatclient.Data
public string? UserName { get; set; } public string? UserName { get; set; }
public string? UserPassword { get; set; } public string? UserPassword { get; set; }
} }
/// <summary>
/// 时间格式转换器类
/// </summary>
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();
}
}
} }

View File

@ -5,8 +5,8 @@ namespace chatclient.Data
internal class Server internal class Server
{ {
public const string ServerUrl = "http://127.0.0.1:5001"; public const string ServerUrl = "http://127.0.0.1:5001";
//public const string ServerIP = "175.24.191.172"; public const string ServerIP = "175.24.191.172";
public const string ServerIP = "127.0.0.1"; //public const string ServerIP = "127.0.0.1";
public const int ServerPort = 52006; public const int ServerPort = 52006;
} }
internal class LoginData internal class LoginData
@ -57,4 +57,25 @@ namespace chatclient.Data
public required string userid { get; set; } = "Unid"; public required string userid { get; set; } = "Unid";
public string? token { get; set; } = null; // 添加token字段 public string? token { get; set; } = null; // 添加token字段
} }
/// <summary>
/// 历史记录请求类
/// </summary>
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
}
/// <summary>
/// 历史记录响应类
/// </summary>
internal class HistoryResponse
{
public List<ChatRegisterData> history { get; set; } = new List<ChatRegisterData>();
public int total_count { get; set; } // 总消息数
}
} }

View File

@ -150,7 +150,7 @@ namespace chatclient
// 检查Socket是否可用 // 检查Socket是否可用
if (MainWindow.Client?.Connected == true) if (MainWindow.Client?.Connected == true)
{ {
MainWindow.Client.Send(dataBytes); MainWindow.SendWithPrefix(dataBytes);
return; return;
} }
log.Info("未连接服务器,尝试异步连接"); log.Info("未连接服务器,尝试异步连接");
@ -162,7 +162,7 @@ namespace chatclient
MainWindow.Client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); MainWindow.Client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
MainWindow.Client?.Connect(IPAddress.Parse(Server.ServerIP), Server.ServerPort); MainWindow.Client?.Connect(IPAddress.Parse(Server.ServerIP), Server.ServerPort);
MainWindow.StartReceive(); MainWindow.StartReceive();
MainWindow.Client?.Send(dataBytes); MainWindow.SendWithPrefix(dataBytes);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -235,7 +235,7 @@ namespace chatclient
// 检查Socket是否可用 // 检查Socket是否可用
if (MainWindow.Client?.Connected == true) if (MainWindow.Client?.Connected == true)
{ {
MainWindow.Client.Send(dataBytes); MainWindow.SendWithPrefix(dataBytes);
return; return;
} }
log.Info("未连接服务器,尝试异步连接"); log.Info("未连接服务器,尝试异步连接");
@ -247,7 +247,7 @@ namespace chatclient
MainWindow.Client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); MainWindow.Client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
MainWindow.Client?.Connect(IPAddress.Parse(Server.ServerIP), Server.ServerPort); MainWindow.Client?.Connect(IPAddress.Parse(Server.ServerIP), Server.ServerPort);
MainWindow.StartReceive(); MainWindow.StartReceive();
MainWindow.Client?.Send(dataBytes); MainWindow.SendWithPrefix(dataBytes);
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -5,10 +5,14 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:local="clr-namespace:chatclient" xmlns:local="clr-namespace:chatclient"
xmlns:data="clr-namespace:chatclient.Data"
xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls" x:Class="chatclient.MainWindow" xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls" x:Class="chatclient.MainWindow"
mc:Ignorable="d" mc:Ignorable="d"
Title="ChatWindow" Height="450" Width="800" MinHeight="240" MinWidth="380" Title="ChatWindow" Height="450" Width="800" MinHeight="240" MinWidth="380"
Style="{StaticResource MaterialDesignWindow}" Closed="MainWindow_Closed" Loaded="MainWindow_Loaded"> Style="{StaticResource MaterialDesignWindow}" Closed="MainWindow_Closed" Loaded="MainWindow_Loaded">
<Window.Resources>
<data:TimeFormatConverter x:Key="TimeConverter"/>
</Window.Resources>
<Grid> <Grid>
<materialDesign:Card> <materialDesign:Card>
<TabControl x:Name="TabControl" VerticalContentAlignment="Bottom" materialDesign:ColorZoneAssist.Mode="PrimaryMid" Style="{StaticResource MaterialDesignNavigationRailTabControl}"> <TabControl x:Name="TabControl" VerticalContentAlignment="Bottom" materialDesign:ColorZoneAssist.Mode="PrimaryMid" Style="{StaticResource MaterialDesignNavigationRailTabControl}">
@ -75,7 +79,7 @@
<StackPanel> <StackPanel>
<TextBlock Text="{Binding Sender}" x:Name="Sender" FontWeight="Bold" Foreground="{Binding SenderColor}"/> <TextBlock Text="{Binding Sender}" x:Name="Sender" FontWeight="Bold" Foreground="{Binding SenderColor}"/>
<TextBlock Text="{Binding Content}" TextWrapping="Wrap" Margin="0,5,0,0"/> <TextBlock Text="{Binding Content}" TextWrapping="Wrap" Margin="0,5,0,0"/>
<TextBlock Text="{Binding Timestamp, StringFormat='HH:mm:ss'}" x:Name="Timestamp" Foreground="Gray" FontSize="10" HorizontalAlignment="Right" Margin="0,5,0,0"/> <TextBlock Text="{Binding Timestamp, Converter={StaticResource TimeConverter}}" x:Name="Timestamp" Foreground="Gray" FontSize="10" HorizontalAlignment="Right" Margin="0,5,0,0"/>
</StackPanel> </StackPanel>
</materialDesign:Card> </materialDesign:Card>
<!-- 右侧头像仅当Alignment=Right时显示 --> <!-- 右侧头像仅当Alignment=Right时显示 -->

View File

@ -22,6 +22,7 @@ using System.Collections.ObjectModel;
using System.Windows.Threading; using System.Windows.Threading;
using System.Collections.Specialized; using System.Collections.Specialized;
using Hardcodet.Wpf.TaskbarNotification; using Hardcodet.Wpf.TaskbarNotification;
using System.IO.Compression;
[assembly: XmlConfigurator(ConfigFile = "config/log4net.config", Watch = true)] [assembly: XmlConfigurator(ConfigFile = "config/log4net.config", Watch = true)]
namespace chatclient namespace chatclient
@ -32,10 +33,11 @@ namespace chatclient
public partial class MainWindow : Window, INotifyPropertyChanged public partial class MainWindow : Window, INotifyPropertyChanged
{ {
LoginWindow Login = new(); LoginWindow Login = new();
static string? receive; //static string? receive;
public static string UserName { get; set; } = "?"; public static string UserName { get; set; } = "?";
public static string? token = null; public static string? token = null;
public static string? UserId = null; public static string? UserId = null;
//private bool isLoadingHistory = false;
private static readonly ILog log = LogManager.GetLogger(typeof(MainWindow)); private static readonly ILog log = LogManager.GetLogger(typeof(MainWindow));
public static Socket? Client; public static Socket? Client;
public static readonly HttpClient HttpClient = new HttpClient(); public static readonly HttpClient HttpClient = new HttpClient();
@ -87,8 +89,6 @@ namespace chatclient
MessageScroller.ScrollToEnd(); MessageScroller.ScrollToEnd();
}), DispatcherPriority.ContextIdle); }), DispatcherPriority.ContextIdle);
}; };
//Loaded += MainWindow_Loaded;
//Closed += MainWindow_Closed;
} }
public static void StartReceive() public static void StartReceive()
{ {
@ -101,29 +101,64 @@ namespace chatclient
} }
static void Receive() static void Receive()
{ {
byte[] buffer = new byte[1024]; const int prefixSize = sizeof(int);
byte[] prefixBuffer = new byte[prefixSize];
MemoryStream receivedStream = new MemoryStream();
try try
{ {
while (true) while (true)
{ {
int num = Client!.Receive(buffer); //接收前缀长度
if (num == 0) break; int prefixBytesRead = 0;
if (Client.Poll(100, SelectMode.SelectRead) && Client.Available == 0 || !Client.Connected) while (prefixBytesRead < prefixSize)
{ {
log.Error("连接已断开"); int bytesRead = Client!.Receive(prefixBuffer, prefixBytesRead, prefixSize - prefixBytesRead, SocketFlags.None);
break; 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) catch (Exception ex)
{ {
log.Error(ex); log.Error($"接收错误: {ex.Message}");
} }
finally finally
{ {
Client?.Close(); Client?.Close();
receivedStream.Dispose();
} }
} }
static void response(string msg) static void response(string msg)
@ -165,6 +200,7 @@ namespace chatclient
mainWindow?.Messages.Add(chatmessage); mainWindow?.Messages.Add(chatmessage);
} }
}); });
LoadHistoryMessages();
log.Info($"用户 {UserName} 登录成功(token:{token},userid:{UserId})"); log.Info($"用户 {UserName} 登录成功(token:{token},userid:{UserId})");
} }
else if (LoginResponse!.status == "error_0") else if (LoginResponse!.status == "error_0")
@ -325,6 +361,45 @@ namespace chatclient
log.Error("反序列化聊天数据时返回了 null"); log.Error("反序列化聊天数据时返回了 null");
} }
} }
else if (Type.type == "history")
{
var historyResponse = JsonSerializer.Deserialize<HistoryResponse>(msg);
Application.Current.Dispatcher.Invoke(() =>
{
var mainWindow = Application.Current.Windows.OfType<MainWindow>().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 if (Type.type == "ping") { }
else else
{ {
@ -346,6 +421,58 @@ namespace chatclient
log.Error("处理响应时发生错误", ex); log.Error("处理响应时发生错误", ex);
} }
} }
/// <summary>
/// 压缩数据
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
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();
}
}
/// <summary>
/// 解压缩数据
/// </summary>
/// <param name="compressedData"></param>
/// <returns></returns>
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();
}
}
/// <summary>
/// 发送消息
/// </summary>
/// <param name="data"></param>
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) private async void SendMessage_Click(object sender, RoutedEventArgs e)
{ {
await SendMessage(); await SendMessage();
@ -354,24 +481,10 @@ namespace chatclient
{ {
if (string.IsNullOrWhiteSpace(txtMessage.Text)) if (string.IsNullOrWhiteSpace(txtMessage.Text))
return; return;
// 获取当前选中的联系人 // 获取当前选中的联系人
//var contact = cmbContacts.SelectedItem as Contact; //var contact = cmbContacts.SelectedItem as Contact;
// 判断是否为群组,若是则收件人设为“所有人”,否则为联系人显示名 // 判断是否为群组,若是则收件人设为“所有人”,否则为联系人显示名
//string recipient = contact?.IsGroup == true ? "所有人" : contact?.DisplayName; //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 var newChatMessage = new ChatData
{ {
type = "chat", type = "chat",
@ -386,7 +499,7 @@ namespace chatclient
// 检查Socket是否可用 // 检查Socket是否可用
if (Client?.Connected == true) if (Client?.Connected == true)
{ {
Client.Send(dataBytes); SendWithPrefix(dataBytes);
Application.Current.Dispatcher.Invoke(() => Application.Current.Dispatcher.Invoke(() =>
{ {
txtMessage.Clear(); txtMessage.Clear();
@ -402,7 +515,7 @@ namespace chatclient
Client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); Client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
Client?.Connect(IPAddress.Parse(Server.ServerIP), Server.ServerPort); Client?.Connect(IPAddress.Parse(Server.ServerIP), Server.ServerPort);
StartReceive(); StartReceive();
Client?.Send(dataBytes); SendWithPrefix(dataBytes);
Application.Current.Dispatcher.Invoke(() => Application.Current.Dispatcher.Invoke(() =>
{ {
txtMessage.Clear(); txtMessage.Clear();
@ -425,6 +538,38 @@ namespace chatclient
// 添加到消息列表 // 添加到消息列表
//Messages.Add(newMessage); //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) private void QueueMessage(string message)
{ {
if (SnackbarThree.MessageQueue is { } messageQueue) if (SnackbarThree.MessageQueue is { } messageQueue)

View File

@ -6,12 +6,18 @@ using System.Threading.Tasks;
namespace chatserver.Data namespace chatserver.Data
{ {
/// <summary>
/// 登录请求类
/// </summary>
internal class LoginData internal class LoginData
{ {
public string? username { get; set; } = null; public string? username { get; set; } = null;
public string? password { get; set; } = null; public string? password { get; set; } = null;
public string? token { get; set; } = null; public string? token { get; set; } = null;
} }
/// <summary>
/// 登录响应类
/// </summary>
internal class LoginResultData internal class LoginResultData
{ {
public required string type { get; set; } = "login"; public required string type { get; set; } = "login";
@ -21,21 +27,33 @@ namespace chatserver.Data
public string? token { get; set; } public string? token { get; set; }
public string? username { get; set; } public string? username { get; set; }
} }
/// <summary>
/// 注册请求类
/// </summary>
internal class SignData internal class SignData
{ {
public string? username { get; set; } = null; public string? username { get; set; } = null;
public string? password { get; set; } = null; public string? password { get; set; } = null;
} }
/// <summary>
/// 注册响应类
/// </summary>
internal class SignResultData internal class SignResultData
{ {
public required string type { get; set; } = "sign"; public required string type { get; set; } = "sign";
public string? status { get; set; } = null; public string? status { get; set; } = null;
public string? message { get; set; } = null; public string? message { get; set; } = null;
} }
/// <summary>
/// 类型数据类
/// </summary>
internal class TypeData internal class TypeData
{ {
public string? type { get; set; } public string? type { get; set; }
} }
/// <summary>
/// 聊天数据响应类
/// </summary>
internal class ChatRegisterData internal class ChatRegisterData
{ {
public required string type { get; set; } = "chat"; public required string type { get; set; } = "chat";
@ -47,6 +65,9 @@ namespace chatserver.Data
public MessageType? msgtype { get; set; } = MessageType.Text; public MessageType? msgtype { get; set; } = MessageType.Text;
public DateTime? timestamp { get; set; } = DateTime.Now; public DateTime? timestamp { get; set; } = DateTime.Now;
} }
/// <summary>
/// 聊天数据请求类
/// </summary>
internal class ChatData internal class ChatData
{ {
public required string message { get; set; } = "message"; public required string message { get; set; } = "message";
@ -54,4 +75,25 @@ namespace chatserver.Data
public required string userid { get; set; } = "Unid"; public required string userid { get; set; } = "Unid";
public string? token { get; set; } = null; // 添加token字段 public string? token { get; set; } = null; // 添加token字段
} }
/// <summary>
/// 历史记录请求类
/// </summary>
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
}
/// <summary>
/// 历史记录响应类
/// </summary>
internal class HistoryResponse
{
public string type { get; set; } = "history";
public List<ChatRegisterData> history { get; set; } = new List<ChatRegisterData>();
public int total_count { get; set; } // 总消息数
}
} }

View File

@ -8,6 +8,7 @@ using chatserver.Data;
using System.Text.Json; using System.Text.Json;
using System.Reflection; using System.Reflection;
using static log4net.Appender.FileAppender; using static log4net.Appender.FileAppender;
using System.IO.Compression;
[assembly: XmlConfigurator(ConfigFile = "config/log4net.config", Watch = true)] [assembly: XmlConfigurator(ConfigFile = "config/log4net.config", Watch = true)]
namespace chatserver namespace chatserver
@ -51,20 +52,47 @@ namespace chatserver
log.Info("正在打开数据库连接..."); log.Info("正在打开数据库连接...");
User_db = new SQLiteConnection("Data Source=ServerUser.db;Version=3;"); //没有数据库则自动创建 User_db = new SQLiteConnection("Data Source=ServerUser.db;Version=3;"); //没有数据库则自动创建
User_db.Open(); User_db.Open();
EnsureUsersTableExists(); // 确保users表存在 InitializeTable();
log.Info("数据库连接已打开"); log.Info("数据库连接已打开");
} }
static void HandleClient(Socket socket) static void HandleClient(Socket socket)
{ {
const int prefixSize = sizeof(int);
byte[] prefixBuffer = new byte[prefixSize];
try try
{ {
while (true) while (true)
{ {
byte[] buffer = new byte[1024]; //读取长度前缀
int received = socket.Receive(buffer); int prefixBytesRead = 0;
if (received == 0) break; // 客户端断开连接 while (prefixBytesRead < prefixSize)
string message = System.Text.Encoding.UTF8.GetString(buffer, 0, received); {
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); log.Info("Received message: " + message);
var Type = JsonSerializer.Deserialize<TypeData>(message); var Type = JsonSerializer.Deserialize<TypeData>(message);
if (Type != null) if (Type != null)
@ -79,28 +107,28 @@ namespace chatserver
{ {
log.Warn("用户名或密码不能为空"); log.Warn("用户名或密码不能为空");
var emptyResult = new SignResultData { type = "register", status = "error_-1", message = "用户名或密码不能为空" }; var emptyResult = new SignResultData { type = "register", status = "error_-1", message = "用户名或密码不能为空" };
socket.Send(JsonSerializer.SerializeToUtf8Bytes(emptyResult)); SendWithPrefix(socket,JsonSerializer.SerializeToUtf8Bytes(emptyResult));
continue; // 如果用户名或密码为空,则跳过注册流程 continue; // 如果用户名或密码为空,则跳过注册流程
} }
if (sginuser.username.Length < 2 || sginuser.username.Length > 20) if (sginuser.username.Length < 2 || sginuser.username.Length > 20)
{ {
log.Warn($"用户注册时 {sginuser.username} 用户名长度不符合要求"); log.Warn($"用户注册时 {sginuser.username} 用户名长度不符合要求");
var lengthResult = new SignResultData { type = "register", status = "error_2", message = "用户名长度必须在2到20个字符之间" }; var lengthResult = new SignResultData { type = "register", status = "error_2", message = "用户名长度必须在2到20个字符之间" };
socket.Send(JsonSerializer.SerializeToUtf8Bytes(lengthResult)); SendWithPrefix(socket,JsonSerializer.SerializeToUtf8Bytes(lengthResult));
continue; // 如果用户名长度不符合要求,则跳过注册流程 continue; // 如果用户名长度不符合要求,则跳过注册流程
} }
if (sginuser.password.Length < 4 || sginuser.password.Length > 20) if (sginuser.password.Length < 4 || sginuser.password.Length > 20)
{ {
log.Warn($"用户注册时 {sginuser.username} 密码长度不符合要求"); log.Warn($"用户注册时 {sginuser.username} 密码长度不符合要求");
var weakPwdResult = new SignResultData { type = "register", status = "error_1", message = "密码长度必须在4到20个字符之间" }; var weakPwdResult = new SignResultData { type = "register", status = "error_1", message = "密码长度必须在4到20个字符之间" };
socket.Send(JsonSerializer.SerializeToUtf8Bytes(weakPwdResult)); SendWithPrefix(socket,JsonSerializer.SerializeToUtf8Bytes(weakPwdResult));
continue; // 如果密码过弱,则跳过注册流程 continue; // 如果密码过弱,则跳过注册流程
} }
if (UserExists(sginuser.username)) if (UserExists(sginuser.username))
{ {
log.Warn($"用户 {sginuser.username} 已存在"); log.Warn($"用户 {sginuser.username} 已存在");
var Result = new SignResultData { type = "register", status = "error_0", message = "用户名已存在" }; 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;// 如果用户名已存在,则跳过注册流程 continue;// 如果用户名已存在,则跳过注册流程
} }
var cmd = new SQLiteCommand("INSERT INTO users (userid, username, password) VALUES (@userid, @username, @password)", User_db); var cmd = new SQLiteCommand("INSERT INTO users (userid, username, password) VALUES (@userid, @username, @password)", User_db);
@ -111,7 +139,7 @@ namespace chatserver
cmd.ExecuteNonQuery(); cmd.ExecuteNonQuery();
log.Info($"用户 {sginuser.username} 注册成功(id:{timedUlid})"); log.Info($"用户 {sginuser.username} 注册成功(id:{timedUlid})");
var result = new SignResultData { type = "register", status = "succeed", message = "注册成功" }; 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") else if (Type.type == "login")
@ -123,21 +151,21 @@ namespace chatserver
{ {
log.Warn("用户名或密码不能为空"); log.Warn("用户名或密码不能为空");
var emptyResult = new LoginResultData { type = "login", status = "error_-1", message = "用户名或密码不能为空" }; var emptyResult = new LoginResultData { type = "login", status = "error_-1", message = "用户名或密码不能为空" };
socket.Send(JsonSerializer.SerializeToUtf8Bytes(emptyResult)); SendWithPrefix(socket,JsonSerializer.SerializeToUtf8Bytes(emptyResult));
continue; continue;
} }
if (loginData.username.Length < 2 || loginData.username.Length > 20) if (loginData.username.Length < 2 || loginData.username.Length > 20)
{ {
log.Warn($"用户登录时 {loginData.username} 用户名长度不符合要求"); log.Warn($"用户登录时 {loginData.username} 用户名长度不符合要求");
var lengthResult = new LoginResultData { type = "login", status = "error_2", message = "用户名长度必须在2到20个字符之间" }; var lengthResult = new LoginResultData { type = "login", status = "error_2", message = "用户名长度必须在2到20个字符之间" };
socket.Send(JsonSerializer.SerializeToUtf8Bytes(lengthResult)); SendWithPrefix(socket,JsonSerializer.SerializeToUtf8Bytes(lengthResult));
continue; continue;
} }
if (loginData.password.Length > 20) if (loginData.password.Length > 20)
{ {
log.Warn($"用户登录时 {loginData.username} 密码长度不符合要求"); log.Warn($"用户登录时 {loginData.username} 密码长度不符合要求");
var weakPwdResult = new LoginResultData { type = "login", status = "error_1", message = "密码长度不能超过20个字符" }; var weakPwdResult = new LoginResultData { type = "login", status = "error_1", message = "密码长度不能超过20个字符" };
socket.Send(JsonSerializer.SerializeToUtf8Bytes(weakPwdResult)); SendWithPrefix(socket,JsonSerializer.SerializeToUtf8Bytes(weakPwdResult));
continue; continue;
} }
var Authentication = UserAuthentication(loginData.username, loginData.password); var Authentication = UserAuthentication(loginData.username, loginData.password);
@ -164,13 +192,13 @@ namespace chatserver
username = loginData.username, username = loginData.username,
userid = Authentication userid = Authentication
}; };
socket.Send(System.Text.Encoding.UTF8.GetBytes(JsonSerializer.Serialize(result))); SendWithPrefix(socket,System.Text.Encoding.UTF8.GetBytes(JsonSerializer.Serialize(result)));
} }
else else
{ {
log.Warn($"用户 {loginData.username} 登录失败,用户名或密码错误"); log.Warn($"用户 {loginData.username} 登录失败,用户名或密码错误");
var result = new LoginResultData { type = "login", status = "error_0", message = "用户名或密码错误" }; 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<ChatData>(message); var chatData = JsonSerializer.Deserialize<ChatData>(message);
if (chatData != null && chatData.message != null) if (chatData != null && chatData.message != null)
{ {
log.Info($"接收到聊天消息: {chatData.message}"); log.Info($"接收到聊天消息(长度: {chatData.message.Length} )");
if (Client.Count == 0) if (Client.Count == 0)
{ {
log.Warn("没有客户端连接,取消发送消息。"); log.Warn("没有客户端连接,取消发送消息。");
@ -201,13 +229,13 @@ namespace chatserver
msgtype = MessageType.Text, msgtype = MessageType.Text,
timestamp = DateTime.Now timestamp = DateTime.Now
}; };
socket.Send(System.Text.Encoding.UTF8.GetBytes(JsonSerializer.Serialize(errorData))); SendWithPrefix(socket,System.Text.Encoding.UTF8.GetBytes(JsonSerializer.Serialize(errorData)));
continue; continue;
} }
List<Socket> clientsCopy; List<Socket> clientsCopy;
List<User> usersCopy; List<User> usersCopy;
// 1. 获取集合快照 // 获取集合快照
lock (Client_lock) lock (Client_lock)
{ {
clientsCopy = new List<Socket>(Client); // 创建客户端列表副本 clientsCopy = new List<Socket>(Client); // 创建客户端列表副本
@ -223,6 +251,31 @@ namespace chatserver
status = "succeed", status = "succeed",
timestamp = DateTime.Now 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) foreach (var user in usersCopy)
{ {
// 查找匹配的客户端 // 查找匹配的客户端
@ -234,7 +287,7 @@ namespace chatserver
try try
{ {
// ==== 修改:添加发送异常处理 ==== // ==== 修改:添加发送异常处理 ====
targetClient.Send(System.Text.Encoding.UTF8.GetBytes(JsonSerializer.Serialize(chatRegisterData))); SendWithPrefix(targetClient,System.Text.Encoding.UTF8.GetBytes(JsonSerializer.Serialize(chatRegisterData)));
} }
catch (SocketException ex) catch (SocketException ex)
{ {
@ -252,6 +305,59 @@ namespace chatserver
log.Warn("接收到无效的聊天消息"); log.Warn("接收到无效的聊天消息");
} }
} }
else if (Type.type == "history")
{
var historyReq = JsonSerializer.Deserialize<HistoryRequest>(message);
if (historyReq != null)
{
var response = new HistoryResponse();
string query = "SELECT * FROM messages WHERE ";
var parameters = new List<SQLiteParameter>();
// 构建查询条件
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 else
{ {
log.Warn("未知的请求类型: " + Type.type); log.Warn("未知的请求类型: " + Type.type);
@ -285,6 +391,59 @@ namespace chatserver
} }
} }
/// <summary> /// <summary>
/// 压缩数据
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
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();
}
}
/// <summary>
/// 解压缩数据
/// </summary>
/// <param name="compressedData"></param>
/// <returns></returns>
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();
}
}
/// <summary>
/// 发送数据
/// </summary>
/// <param name="socket"></param>
/// <param name="data"></param>
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);
}
/// <summary>
/// 查询User_db是否有相同用户名 /// 查询User_db是否有相同用户名
/// </summary> /// </summary>
/// <param name="username"></param> /// <param name="username"></param>
@ -311,7 +470,7 @@ namespace chatserver
return result != null ? result.ToString()! : string.Empty; return result != null ? result.ToString()! : string.Empty;
} }
// 在ChatServer类中添加一个方法用于初始化users表 // 在ChatServer类中添加一个方法用于初始化users表
private static void EnsureUsersTableExists() private static void InitializeTable()
{ {
using var cmd = new SQLiteCommand(@" using var cmd = new SQLiteCommand(@"
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
@ -320,12 +479,34 @@ namespace chatserver
password TEXT NOT NULL password TEXT NOT NULL
)", User_db); )", User_db);
cmd.ExecuteNonQuery(); 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();
} }
/// <summary> /// <summary>
/// 根据userid查询对应的用户名 /// 根据userid查询对应的用户名
/// </summary> /// </summary>
/// <param name="userid"></param> /// <param name="userid"></param>
/// <returns>用户名,如果不存在则返回</returns> /// <returns>用户名,如果userid为空或为"Unid",返回默认用户名"Unnamed"</returns>
static string GetUsernameByUserId(string userid) static string GetUsernameByUserId(string userid)
{ {
if (string.IsNullOrEmpty(userid) || userid == "Unid") if (string.IsNullOrEmpty(userid) || userid == "Unid")