在 `MainWindow.xaml.cs` 中,调整了消息对齐逻辑,新增 `SenderColor` 属性以根据用户 ID 设置消息颜色。发送者为当前用户时,消息右对齐并显示蓝色;否则左对齐并显示黑色。 在 `Program.cs` 中,移除了对 HTTP 请求的处理逻辑,简化了客户端连接处理。同时,更新了日志记录,确保准确反映压缩数据的实际长度。
604 lines
28 KiB
C#
604 lines
28 KiB
C#
using System.Text;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Documents;
|
|
using System.Windows.Input;
|
|
using System.Windows.Media;
|
|
using System.Windows.Media.Imaging;
|
|
using System.Windows.Navigation;
|
|
using System.Windows.Shapes;
|
|
using System.Net.Sockets;//socket库
|
|
using System.Net.Http;
|
|
using System.Net;
|
|
using System.IO;
|
|
using System;
|
|
using System.Security.Policy;
|
|
using log4net;
|
|
using log4net.Config;
|
|
using System.Text.Json;
|
|
using chatclient.Data;
|
|
using System.ComponentModel;
|
|
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
|
|
{
|
|
/// <summary>
|
|
/// Interaction logic for MainWindow.xaml
|
|
/// </summary>
|
|
public partial class MainWindow : Window, INotifyPropertyChanged
|
|
{
|
|
LoginWindow Login = new();
|
|
//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();
|
|
public event PropertyChangedEventHandler? PropertyChanged;
|
|
private string? _Username;
|
|
public string? Username
|
|
{
|
|
get { return _Username; }
|
|
set
|
|
{
|
|
_Username = value;
|
|
Update("Username");
|
|
}
|
|
}
|
|
// 消息列表
|
|
public ObservableCollection<ChatMessage> Messages { get; } = new ObservableCollection<ChatMessage>();
|
|
private ItemsControl MessageList => messageList;
|
|
private ScrollViewer MessageScroller => messageScroller;
|
|
private void Update(string UpdateName)
|
|
{
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(UpdateName));
|
|
}
|
|
private TrayIconManager? _trayManager;
|
|
public MainWindow()
|
|
{
|
|
InitializeComponent();
|
|
this.DataContext = this;
|
|
log.Info("Hello World!");
|
|
this.Hide();
|
|
Client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
|
try
|
|
{
|
|
log.Info($"连接服务器 {Server.ServerIP}:{Server.ServerPort} ");
|
|
Client.Connect(Server.ServerIP, Server.ServerPort);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Client.Close();
|
|
log.Error(ex);
|
|
}
|
|
Login.Show();
|
|
if(Client != null && Client.Connected == true) StartReceive();
|
|
MessageList.ItemsSource = Messages;
|
|
((INotifyCollectionChanged)MessageList.Items).CollectionChanged += (s, e) =>
|
|
{
|
|
// 确保有足够的时间让UI更新
|
|
Dispatcher.BeginInvoke(new Action(() =>
|
|
{
|
|
MessageScroller.ScrollToEnd();
|
|
}), DispatcherPriority.ContextIdle);
|
|
};
|
|
}
|
|
public static void StartReceive()
|
|
{
|
|
if (Client != null && Client.Connected == true)
|
|
{
|
|
Thread th = new Thread(Receive);
|
|
th.Start();
|
|
}
|
|
else { log.Fatal("在Client为NULL或未连接服务器时被调用StartReceive()"); }
|
|
}
|
|
static void Receive()
|
|
{
|
|
const int prefixSize = sizeof(int);
|
|
byte[] prefixBuffer = new byte[prefixSize];
|
|
MemoryStream receivedStream = new MemoryStream();
|
|
try
|
|
{
|
|
while (true)
|
|
{
|
|
//接收前缀长度
|
|
int prefixBytesRead = 0;
|
|
while (prefixBytesRead < prefixSize)
|
|
{
|
|
int bytesRead = Client!.Receive(prefixBuffer, prefixBytesRead, prefixSize - prefixBytesRead, SocketFlags.None);
|
|
if (bytesRead == 0)
|
|
{
|
|
log.Info("连接已关闭");
|
|
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 = 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.Message}");
|
|
}
|
|
finally
|
|
{
|
|
Client?.Close();
|
|
receivedStream.Dispose();
|
|
}
|
|
}
|
|
static void response(string msg)
|
|
{
|
|
log.Info($"收到服务器消息: {msg}");
|
|
try
|
|
{
|
|
var Type = JsonSerializer.Deserialize<RegisterData>(msg);
|
|
if (Type != null)
|
|
{
|
|
if (Type.type == "login")
|
|
{
|
|
var LoginResponse = JsonSerializer.Deserialize<LoginResultData>(msg);
|
|
if (LoginResponse!.status == "succeed" && LoginResponse != null)
|
|
{
|
|
UserId = LoginResponse.userid ?? "Unid";
|
|
UserName = LoginResponse.username ?? "Unnamed";
|
|
token = LoginResponse.token ?? "NoToken";
|
|
Application.Current.Dispatcher.Invoke(() =>
|
|
{
|
|
var mainWindow = Application.Current.Windows.OfType<MainWindow>().FirstOrDefault();
|
|
if (mainWindow != null)
|
|
{
|
|
mainWindow.Username = UserName;
|
|
mainWindow.Show();
|
|
var loginWindow = Application.Current.Windows.OfType<LoginWindow>().FirstOrDefault();
|
|
loginWindow?.Close();
|
|
mainWindow.Activate();
|
|
var chatmessage = new ChatMessage
|
|
{
|
|
Sender = "System",
|
|
MsgType = MessageType.System,
|
|
Image = new BitmapImage(new Uri("pack://application:,,,/resource/user.png")),
|
|
Content = $"你好 {UserName} (id: {UserId} )!",
|
|
Timestamp = DateTime.Now,
|
|
Alignment = HorizontalAlignment.Center,
|
|
SenderColor = new SolidColorBrush(Colors.Gray)
|
|
};
|
|
mainWindow?.Messages.Add(chatmessage);
|
|
}
|
|
});
|
|
LoadHistoryMessages();
|
|
log.Info($"用户 {UserName} 登录成功(token:{token},userid:{UserId})");
|
|
}
|
|
else if (LoginResponse!.status == "error_0")
|
|
{
|
|
log.Warn($"登录失败: {LoginResponse!.message}\nMsg:{msg}");
|
|
Application.Current.Dispatcher.Invoke(() =>
|
|
{
|
|
var loginWindow = Application.Current.Windows.OfType<LoginWindow>().FirstOrDefault();
|
|
if (loginWindow != null)
|
|
{
|
|
loginWindow.LoginMsg = "用户名或密码错误";
|
|
}
|
|
});
|
|
}
|
|
else if (LoginResponse!.status == "error_2")
|
|
{
|
|
log.Warn($"登录失败: {LoginResponse!.message}\nMsg:{msg}");
|
|
Application.Current.Dispatcher.Invoke(() =>
|
|
{
|
|
var loginWindow = Application.Current.Windows.OfType<LoginWindow>().FirstOrDefault();
|
|
if (loginWindow != null)
|
|
{
|
|
loginWindow.LoginMsg = "用户名长度必须在2到20个字符之间";
|
|
}
|
|
});
|
|
}
|
|
else
|
|
{
|
|
Application.Current.Dispatcher.Invoke(() =>
|
|
{
|
|
var loginWindow = Application.Current.Windows.OfType<LoginWindow>().FirstOrDefault();
|
|
if (loginWindow != null)
|
|
{
|
|
loginWindow.LoginMsg = LoginResponse != null ? LoginResponse.message : "服务器返回错误";
|
|
}
|
|
});
|
|
}
|
|
}
|
|
else if (Type.type == "register")
|
|
{
|
|
var SignResponse = JsonSerializer.Deserialize<SignResultData>(msg);
|
|
if (SignResponse!.status == "success")
|
|
{
|
|
log.Warn($"注册成功\nMsg:{msg}");
|
|
Application.Current.Dispatcher.Invoke(async () =>
|
|
{
|
|
//var loginWindow = Application.Current.Windows.OfType<LoginWindow>().FirstOrDefault();
|
|
await LoginWindow.Login(true, LoginWindow.SignName, LoginWindow.SignPassword1);
|
|
});
|
|
}
|
|
else if (SignResponse!.status == "error_1")
|
|
{
|
|
log.Warn($"注册失败: {SignResponse!.message}\nMsg:{msg}");
|
|
Application.Current.Dispatcher.Invoke(() =>
|
|
{
|
|
var loginWindow = Application.Current.Windows.OfType<LoginWindow>().FirstOrDefault();
|
|
if (loginWindow != null)
|
|
{
|
|
loginWindow.SignMsg = "密码长度必须在4到20个字符之间";
|
|
}
|
|
});
|
|
}
|
|
else if (SignResponse!.status == "error_2")
|
|
{
|
|
log.Warn($"注册失败: {SignResponse!.message}\nMsg:{msg}");
|
|
Application.Current.Dispatcher.Invoke(() =>
|
|
{
|
|
var loginWindow = Application.Current.Windows.OfType<LoginWindow>().FirstOrDefault();
|
|
if (loginWindow != null)
|
|
{
|
|
loginWindow.SignMsg = "用户名长度必须在2到20个字符之间";
|
|
}
|
|
});
|
|
}
|
|
else if (SignResponse!.status == "error_3")
|
|
{
|
|
log.Warn($"注册失败: {SignResponse!.message}\nMsg:{msg}");
|
|
{
|
|
Application.Current.Dispatcher.Invoke(() =>
|
|
{
|
|
var loginWindow = Application.Current.Windows.OfType<LoginWindow>().FirstOrDefault();
|
|
if (loginWindow != null)
|
|
{
|
|
loginWindow.SignMsg = "用户名已存在";
|
|
}
|
|
});
|
|
}
|
|
}
|
|
else
|
|
{
|
|
log.Error($"注册失败: {SignResponse!.message}\nMsg:{msg}");
|
|
Application.Current.Dispatcher.Invoke(() =>
|
|
{
|
|
var loginWindow = Application.Current.Windows.OfType<LoginWindow>().FirstOrDefault();
|
|
if (loginWindow != null)
|
|
{
|
|
loginWindow.SignMsg = SignResponse != null ? SignResponse.message : "服务器返回错误";
|
|
}
|
|
});
|
|
}
|
|
}
|
|
else if (Type.type == "chat")
|
|
{
|
|
var chat = JsonSerializer.Deserialize<ChatRegisterData>(msg);
|
|
if (chat != null)
|
|
{
|
|
Application.Current.Dispatcher.Invoke(() =>
|
|
{
|
|
if (chat.status == "succeed")
|
|
{
|
|
// 处理聊天消息
|
|
if (chat.userid == UserId)
|
|
{
|
|
var chatmessage = new ChatMessage
|
|
{
|
|
Sender = chat.user ?? "未知用户",
|
|
MsgType = MessageType.Text,
|
|
Image = new BitmapImage(new Uri(chat.avatar ?? "pack://application:,,,/resource/user.png")),
|
|
Content = chat.message ?? "(无内容)",
|
|
Timestamp = chat.timestamp ?? DateTime.Now,
|
|
Alignment = HorizontalAlignment.Right,
|
|
SenderColor = new SolidColorBrush(Colors.Blue)
|
|
};
|
|
var mainWindow = Application.Current.Windows.OfType<MainWindow>().FirstOrDefault();
|
|
mainWindow?.Messages.Add(chatmessage);
|
|
}
|
|
else
|
|
{
|
|
var chatmessage = new ChatMessage
|
|
{
|
|
Sender = chat.user ?? "未知用户",
|
|
MsgType = MessageType.Text,
|
|
Image = new BitmapImage(new Uri(chat.avatar ?? "pack://application:,,,/resource/user.png")),
|
|
Content = chat.message ?? "(无内容)",
|
|
Timestamp = chat.timestamp ?? DateTime.Now,
|
|
Alignment = HorizontalAlignment.Left,
|
|
SenderColor = new SolidColorBrush(Colors.Black)
|
|
};
|
|
var mainWindow = Application.Current.Windows.OfType<MainWindow>().FirstOrDefault();
|
|
mainWindow?.Messages.Add(chatmessage);
|
|
}
|
|
}
|
|
else if (chat.status == "error_0")
|
|
{
|
|
var mainWindow = Application.Current.Windows.OfType<MainWindow>().FirstOrDefault();
|
|
mainWindow?.QueueMessage("登录已退出");
|
|
}
|
|
else
|
|
{
|
|
log.Error("反序列化聊天数据时返回了 null");
|
|
var mainWindow = Application.Current.Windows.OfType<MainWindow>().FirstOrDefault();
|
|
mainWindow?.QueueMessage("服务器返回了错误的聊天数据");
|
|
}
|
|
});
|
|
}
|
|
else
|
|
{
|
|
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,
|
|
SenderColor = msgItem.userid == UserId ?
|
|
new SolidColorBrush(Colors.Blue) :
|
|
new SolidColorBrush(Colors.Black)
|
|
};
|
|
mainWindow.Messages.Insert(0, chatmessage);
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Error($"添加历史消息失败: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
else if (Type.type == "ping") { }
|
|
else
|
|
{
|
|
log.Error($"未知的消息类型: {Type.type},请检查服务器响应格式");
|
|
Application.Current.Dispatcher.Invoke(() =>
|
|
{
|
|
var loginWindow = Application.Current.Windows.OfType<LoginWindow>().FirstOrDefault();
|
|
if (loginWindow != null) loginWindow.LoginMsg = "服务器返回了错误的值";
|
|
});
|
|
}
|
|
}
|
|
}
|
|
catch (JsonException ex)
|
|
{
|
|
log.Error("JSON解析错误", ex);
|
|
}
|
|
catch (Exception 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},压缩后长度:{compressedData.Length},总体长度:{fullMessage.Length})");
|
|
Client?.Send(fullMessage);
|
|
}
|
|
private async void SendMessage_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
await SendMessage();
|
|
}
|
|
private async Task SendMessage()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(txtMessage.Text))
|
|
return;
|
|
// 获取当前选中的联系人
|
|
//var contact = cmbContacts.SelectedItem as Contact;
|
|
// 判断是否为群组,若是则收件人设为“所有人”,否则为联系人显示名
|
|
//string recipient = contact?.IsGroup == true ? "所有人" : contact?.DisplayName;
|
|
var newChatMessage = new ChatData
|
|
{
|
|
type = "chat",
|
|
message = txtMessage.Text,
|
|
msgtype = MessageType.Text,
|
|
userid = UserId ?? "Unid", // 使用UserId作为发送者ID
|
|
token = token
|
|
};
|
|
string ChatJsonData = JsonSerializer.Serialize(newChatMessage);
|
|
byte[] dataBytes = Encoding.UTF8.GetBytes(ChatJsonData);
|
|
log.Info($"向服务器聊天信息(长度:{dataBytes.Length})");
|
|
// 检查Socket是否可用
|
|
if (Client?.Connected == true)
|
|
{
|
|
SendWithPrefix(dataBytes);
|
|
Application.Current.Dispatcher.Invoke(() =>
|
|
{
|
|
txtMessage.Clear();
|
|
});
|
|
return;
|
|
}
|
|
log.Info("未连接服务器,尝试异步连接");
|
|
// 异步连接操作
|
|
await Task.Run(() =>
|
|
{
|
|
try
|
|
{
|
|
Client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
|
Client?.Connect(IPAddress.Parse(Server.ServerIP), Server.ServerPort);
|
|
StartReceive();
|
|
SendWithPrefix(dataBytes);
|
|
Application.Current.Dispatcher.Invoke(() =>
|
|
{
|
|
txtMessage.Clear();
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Error($"连接失败: {ex.Message}");
|
|
Client?.Close();
|
|
Application.Current.Dispatcher.Invoke(() =>
|
|
{
|
|
var mainWindow = Application.Current.Windows.OfType<MainWindow>().FirstOrDefault();
|
|
if (mainWindow != null)
|
|
{
|
|
QueueMessage("连接失败,请检查网络设置或服务器状态。");
|
|
}
|
|
});
|
|
}
|
|
});
|
|
// 添加到消息列表
|
|
//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)
|
|
{
|
|
//use the message queue to send a message.
|
|
//the message queue can be called from any thread
|
|
Task.Factory.StartNew(() => messageQueue.Enqueue(message));
|
|
}
|
|
}
|
|
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
|
|
{
|
|
// 初始化托盘管理器
|
|
_trayManager = new TrayIconManager(this);
|
|
}
|
|
private void MainWindow_Closed(object sender, System.EventArgs e)
|
|
{
|
|
log.Info("MainWindow 关闭事件触发,清理资源");
|
|
// 清理资源
|
|
if (Client!.Connected) Client?.Shutdown(SocketShutdown.Both);
|
|
Client?.Close();
|
|
log.Info("关闭Socket连接");
|
|
Client?.Dispose();
|
|
token = null;
|
|
_trayManager?.Dispose();
|
|
log.Info("托盘图标管理器已释放资源");
|
|
log.Info("Bye!");
|
|
}
|
|
}
|
|
} |