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

523 lines
28 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Net.Sockets;
using System.Net.Http;
using log4net;
using log4net.Config;
using System.Net;
using System.Data.SQLite;
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
{
internal class ChatServer
{
private static readonly ILog log = LogManager.GetLogger(typeof(ChatServer));
private static readonly object Client_lock = new object();
private static List<Socket> Client = new();
private static List<User> LoginUser = new();
private static Socket? Server;
private static SQLiteConnection? User_db;
static void Main(string[] args)
{
//XmlConfigurator.Configure();
log.Info("Hello World!");
Server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
Server.Bind(new IPEndPoint(IPAddress.Any, 52006));
Server.Listen(20);
OpenUser_db();
log.Info("服务器以在52006端口监听");
while (true)
{
try
{
Socket client = Server.Accept();
lock (Client_lock) { Client.Add(client); }
log.Info($"用户 {client.RemoteEndPoint} 连接");
Thread thread = new Thread(() => HandleClient(client));
thread.Start();
}
catch (Exception ex)
{
log.Error("Error accepting client connection: " + ex.Message);
}
}
}
public static void OpenUser_db()
{
log.Info("正在打开数据库连接...");
User_db = new SQLiteConnection("Data Source=ServerUser.db;Version=3;"); //没有数据库则自动创建
User_db.Open();
InitializeTable();
log.Info("数据库连接已打开");
}
static void HandleClient(Socket socket)
{
const int prefixSize = sizeof(int);
byte[] prefixBuffer = new byte[prefixSize];
try
{
while (true)
{
//读取长度前缀
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)
{
if (Type.type == "register")
{
var sginuser = JsonSerializer.Deserialize<SignData>(message);
if (sginuser != null && sginuser.username != null && sginuser.password != null)
{
log.Info($"用户 {sginuser.username} 正在注册");
if (string.IsNullOrEmpty(sginuser.username) || string.IsNullOrEmpty(sginuser.password))
{
log.Warn("用户名或密码不能为空");
var emptyResult = new SignResultData { type = "register", status = "error_-1", message = "用户名或密码不能为空" };
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个字符之间" };
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个字符之间" };
SendWithPrefix(socket,JsonSerializer.SerializeToUtf8Bytes(weakPwdResult));
continue; // 如果密码过弱,则跳过注册流程
}
if (UserExists(sginuser.username))
{
log.Warn($"用户 {sginuser.username} 已存在");
var Result = new SignResultData { type = "register", status = "error_0", message = "用户名已存在" };
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);
var timedUlid = Ulid.NewUlid(DateTimeOffset.UtcNow);
cmd.Parameters.AddWithValue("@userid", timedUlid);
cmd.Parameters.AddWithValue("@username", sginuser.username);
cmd.Parameters.AddWithValue("@password", sginuser.password);
cmd.ExecuteNonQuery();
log.Info($"用户 {sginuser.username} 注册成功(id:{timedUlid})");
var result = new SignResultData { type = "register", status = "succeed", message = "注册成功" };
SendWithPrefix(socket,System.Text.Encoding.UTF8.GetBytes(JsonSerializer.Serialize(result)));
}
}
else if (Type.type == "login")
{
var loginData = JsonSerializer.Deserialize<LoginData>(message);
if (loginData != null)
{
if (string.IsNullOrEmpty(loginData.username) || string.IsNullOrEmpty(loginData.password))
{
log.Warn("用户名或密码不能为空");
var emptyResult = new LoginResultData { type = "login", status = "error_-1", message = "用户名或密码不能为空" };
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个字符之间" };
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个字符" };
SendWithPrefix(socket,JsonSerializer.SerializeToUtf8Bytes(weakPwdResult));
continue;
}
var Authentication = UserAuthentication(loginData.username, loginData.password);
if (Authentication != "")
{
log.Info($"用户 {loginData.username} 登录成功 (id:{Authentication})");
var timedUlid = Ulid.NewUlid(DateTimeOffset.UtcNow);
lock (Client_lock)
{
LoginUser.Add(new User
{
UserId = Authentication,
LoginIP = socket.RemoteEndPoint?.ToString() ?? "Unknown:0",
Avatar = null,
token = timedUlid.ToString()
});
}
var result = new LoginResultData
{
type = "login",
status = "succeed",
message = "登录成功",
token = timedUlid.ToString(),
username = loginData.username,
userid = Authentication
};
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 = "用户名或密码错误" };
SendWithPrefix(socket,System.Text.Encoding.UTF8.GetBytes(JsonSerializer.Serialize(result)));
}
}
}
else if (Type.type == "chat")
{
var chatData = JsonSerializer.Deserialize<ChatData>(message);
if (chatData != null && chatData.message != null)
{
log.Info($"接收到聊天消息(长度: {chatData.message.Length} )");
if (Client.Count == 0)
{
log.Warn("没有客户端连接,取消发送消息。");
return;
}
// 获取发送者完整端点IP: 端口)
string senderEndpoint = socket.RemoteEndPoint?.ToString() ?? "Unknown:0";
// ==== 修改使用完整端点验证非IP部分====
var loginUser = LoginUser.FirstOrDefault(u => u.LoginIP == senderEndpoint && u.UserId == chatData.userid);
if (loginUser == null || loginUser.token != chatData.token)
{
log.Warn($"聊天消息验证失败:端点 {senderEndpoint} 未登录或 token 不匹配");
var errorData = new ChatRegisterData
{
type = "chat",
userid = chatData.userid ?? "Unid",
status = "error_0",
message = "未登录或token无效无法发送消息",
msgtype = MessageType.Text,
timestamp = DateTime.Now
};
SendWithPrefix(socket,System.Text.Encoding.UTF8.GetBytes(JsonSerializer.Serialize(errorData)));
continue;
}
List<Socket> clientsCopy;
List<User> usersCopy;
// 获取集合快照
lock (Client_lock)
{
clientsCopy = new List<Socket>(Client); // 创建客户端列表副本
usersCopy = new List<User>(LoginUser); // 创建用户列表副本
}
var chatRegisterData = new ChatRegisterData
{
type = "chat",
userid = chatData.userid ?? "Unid",
user = GetUsernameByUserId(chatData.userid!),
message = chatData.message,
msgtype = chatData.msgtype ?? MessageType.Text,
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)
{
// 查找匹配的客户端
var targetClient = clientsCopy.FirstOrDefault(c =>
c.RemoteEndPoint?.ToString() == user.LoginIP);
if (targetClient != null)
{
try
{
// ==== 修改:添加发送异常处理 ====
SendWithPrefix(targetClient,System.Text.Encoding.UTF8.GetBytes(JsonSerializer.Serialize(chatRegisterData)));
}
catch (SocketException ex)
{
log.Error($"发送消息到 {user.LoginIP} 失败: {ex.SocketErrorCode}");
}
catch (Exception ex)
{
log.Error($"发送消息异常: {ex.Message}");
}
}
}
}
else
{
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);
}
}
}
}
catch (SocketException ex)
{
log.Error("Socket error: " + ex.Message);
}
catch (JsonException ex)
{
log.Error("JSON parsing error: " + ex.Message);
}
catch (Exception ex)
{
log.Error("Error handling client: " + ex.Message);
}
finally
{
lock (Client_lock)
{
log.Info($"用户 {socket.RemoteEndPoint} 已断开连接");
var disconnectedIp = socket.RemoteEndPoint?.ToString();
if (!string.IsNullOrEmpty(disconnectedIp))
{
LoginUser.RemoveAll(u => u.LoginIP.StartsWith(disconnectedIp));
}
}
}
}
/// <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>
/// <returns></returns>
static bool UserExists(string username)
{
using var cmd = new SQLiteCommand("SELECT COUNT(*) FROM users WHERE username = @username", User_db);
cmd.Parameters.AddWithValue("@username", username);
var count = Convert.ToInt32(cmd.ExecuteScalar());
return count > 0;
}
/// <summary>
/// 验证用户登录信息并返回userid结果
/// </summary>
/// <param name="username"></param>
/// <param name="password"></param>
/// <returns></returns>
static string UserAuthentication(string username, string password)
{
using var cmd = new SQLiteCommand("SELECT userid FROM users WHERE username = @username AND password = @password", User_db);
cmd.Parameters.AddWithValue("@username", username);
cmd.Parameters.AddWithValue("@password", password);
var result = cmd.ExecuteScalar();
return result != null ? result.ToString()! : string.Empty;
}
// 在ChatServer类中添加一个方法用于初始化users表
private static void InitializeTable()
{
using var cmd = new SQLiteCommand(@"
CREATE TABLE IF NOT EXISTS users (
userid TEXT PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
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>用户名如果userid为空或为"Unid",返回默认用户名"Unnamed"</returns>
static string GetUsernameByUserId(string userid)
{
if (string.IsNullOrEmpty(userid) || userid == "Unid")
{
return "Unnamed"; // 如果userid为空或为"Unid",返回默认用户名
}
using var cmd = new SQLiteCommand("SELECT username FROM users WHERE userid = @userid", User_db);
cmd.Parameters.AddWithValue("@userid", userid);
var result = cmd.ExecuteScalar();
return result != null ? result.ToString()! : "Unnamed";
}
}
}