添加时间格式化和历史记录功能
在 `ChatDataModel.cs` 中添加 `TimeFormatConverter` 类,用于格式化本地时间,并在 `MainWindow.xaml` 中应用该转换器。 在 `chatapi.cs` 中新增 `HistoryRequest` 和 `HistoryResponse` 类以处理历史记录请求和响应。 修改 `LoginWindow.xaml.cs` 中的数据发送方式,使用 `SendWithPrefix` 方法以支持数据压缩和长度前缀。 在 `MainWindow.xaml.cs` 中添加 `LoadHistoryMessages` 方法以加载历史消息,并在接收到响应时更新消息列表。 在 `Program.cs` 中实现数据压缩和解压缩方法,提升网络传输效率。 新增消息表和索引以支持消息存储和查询。 更新日志记录以提供更详细的操作信息和错误处理。
This commit is contained in:
parent
b5ebe51aa1
commit
936a485195
@ -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; }
|
||||
}
|
||||
/// <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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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字段
|
||||
}
|
||||
/// <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; } // 总消息数
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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">
|
||||
<Window.Resources>
|
||||
<data:TimeFormatConverter x:Key="TimeConverter"/>
|
||||
</Window.Resources>
|
||||
<Grid>
|
||||
<materialDesign:Card>
|
||||
<TabControl x:Name="TabControl" VerticalContentAlignment="Bottom" materialDesign:ColorZoneAssist.Mode="PrimaryMid" Style="{StaticResource MaterialDesignNavigationRailTabControl}">
|
||||
@ -75,7 +79,7 @@
|
||||
<StackPanel>
|
||||
<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 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>
|
||||
</materialDesign:Card>
|
||||
<!-- 右侧头像(仅当Alignment=Right时显示) -->
|
||||
|
@ -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<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
|
||||
{
|
||||
@ -346,6 +421,58 @@ namespace chatclient
|
||||
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)
|
||||
{
|
||||
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)
|
||||
|
@ -6,12 +6,18 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace chatserver.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// 登录请求类
|
||||
/// </summary>
|
||||
internal class LoginData
|
||||
{
|
||||
public string? username { get; set; } = null;
|
||||
public string? password { get; set; } = null;
|
||||
public string? token { get; set; } = null;
|
||||
}
|
||||
/// <summary>
|
||||
/// 登录响应类
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
/// <summary>
|
||||
/// 注册请求类
|
||||
/// </summary>
|
||||
internal class SignData
|
||||
{
|
||||
public string? username { get; set; } = null;
|
||||
public string? password { get; set; } = null;
|
||||
}
|
||||
/// <summary>
|
||||
/// 注册响应类
|
||||
/// </summary>
|
||||
internal class SignResultData
|
||||
{
|
||||
public required string type { get; set; } = "sign";
|
||||
public string? status { get; set; } = null;
|
||||
public string? message { get; set; } = null;
|
||||
}
|
||||
/// <summary>
|
||||
/// 类型数据类
|
||||
/// </summary>
|
||||
internal class TypeData
|
||||
{
|
||||
public string? type { get; set; }
|
||||
}
|
||||
/// <summary>
|
||||
/// 聊天数据响应类
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
/// <summary>
|
||||
/// 聊天数据请求类
|
||||
/// </summary>
|
||||
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字段
|
||||
}
|
||||
/// <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; } // 总消息数
|
||||
}
|
||||
}
|
||||
|
@ -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<TypeData>(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<ChatData>(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<Socket> clientsCopy;
|
||||
List<User> usersCopy;
|
||||
// 1. 获取集合快照
|
||||
// 获取集合快照
|
||||
lock (Client_lock)
|
||||
{
|
||||
clientsCopy = new List<Socket>(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<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
|
||||
{
|
||||
log.Warn("未知的请求类型: " + Type.type);
|
||||
@ -285,6 +391,59 @@ namespace chatserver
|
||||
}
|
||||
}
|
||||
/// <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是否有相同用户名
|
||||
/// </summary>
|
||||
/// <param name="username"></param>
|
||||
@ -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();
|
||||
}
|
||||
/// <summary>
|
||||
/// 根据userid查询对应的用户名
|
||||
/// </summary>
|
||||
/// <param name="userid"></param>
|
||||
/// <returns>用户名,如果不存在则返回</returns>
|
||||
/// <returns>用户名,如果userid为空或为"Unid",返回默认用户名"Unnamed"</returns>
|
||||
static string GetUsernameByUserId(string userid)
|
||||
{
|
||||
if (string.IsNullOrEmpty(userid) || userid == "Unid")
|
||||
|
Loading…
x
Reference in New Issue
Block a user