使用delphi12+mysql+nginx+php+indy+dev+ehlib 作为开发环境 开发一个C/S架构的局域网即时通讯系统

作者: koic 分类: 我的教程 发布时间: 2025-06-23 13:56

使用delphi12+mysql+nginx+php+indy+dev+ehlib 作为开发环境 开发一个C/S架构的局域网即时通讯系统;

主要架构,A客户端>服务器>B客户端,可以互发文件,文字,图片,表情等;

服务端可以进行聊天记录查询,组织架构管理,数据库备份,文件查找等;

客户端可以采用exe+ini+access/sqlite 将聊天记录留存本地数据库,将文件留存软件根目录file/对方用户名/下;

我不太熟悉delphi我需要详细的数据库建表文件,delphi项目新建、窗体新建、控件拖拽的详细说明


要开发一个C/S 架构的局域网即时通讯系统,需分服务器端(负责转发消息、管理数据)和客户端(负责用户交互、本地存储)两部分。以下是详细的数据库设计、Delphi 项目搭建、窗体设计及核心代码说明,适合 Delphi 新手参考。

 

一、系统架构与技术栈

  • 架构:C/S(客户端 – 服务器),客户端通过 TCP 连接服务器,服务器转发消息给目标客户端。
  • 技术栈
    • 服务器端:Delphi 12 + Indy(TCP 通信) + MySQL(数据存储) + Nginx(可选,反向代理) + PHP(可选,辅助管理)。
    • 客户端:Delphi 12 + Indy(TCP 通信) + SQLite/Access(本地聊天记录存储) + INI(配置文件)。
    • 第三方组件:DevExpress(界面美化,可选)、EhLib(数据表格,可选)。

 

二、数据库设计(MySQL)

需创建用户表、部门表、聊天记录表、文件传输表、备份记录表,以下是详细建表 SQL:

1. 部门表(tb_department):存储组织架构

CREATE TABLE `tb_department` (
  `department_id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '部门ID',
  `department_name` VARCHAR(100) NOT NULL COMMENT '部门名称',
  `parent_id` INT DEFAULT 0 COMMENT '父部门ID(0为根部门)',
  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
) COMMENT '部门表';

2. 用户表(tb_user):存储用户信息

CREATE TABLE `tb_user` (
  `user_id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID',
  `username` VARCHAR(50) UNIQUE NOT NULL COMMENT '登录用户名',
  `password` VARCHAR(100) NOT NULL COMMENT '哈希后的密码(如SHA256)',
  `nickname` VARCHAR(50) NOT NULL COMMENT '昵称',
  `department_id` INT COMMENT '所属部门ID',
  `avatar` VARCHAR(200) DEFAULT 'default_avatar.png' COMMENT '头像路径',
  `status` TINYINT DEFAULT 0 COMMENT '在线状态(0:离线,1:在线)',
  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  FOREIGN KEY (`department_id`) REFERENCES `tb_department`(`department_id`)
) COMMENT '用户表';

3. 聊天记录表(tb_chat_log):存储全局聊天记录

CREATE TABLE `tb_chat_log` (
  `msg_id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '消息ID',
  `sender_id` INT NOT NULL COMMENT '发送者ID',
  `receiver_id` INT NOT NULL COMMENT '接收者ID',
  `msg_type` TINYINT NOT NULL COMMENT '消息类型(1:文字,2:图片,3:文件,4:表情)',
  `msg_content` VARCHAR(2000) NOT NULL COMMENT '消息内容(文字/文件路径)',
  `send_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '发送时间',
  `is_read` TINYINT DEFAULT 0 COMMENT '是否已读(0:未读,1:已读)',
  FOREIGN KEY (`sender_id`) REFERENCES `tb_user`(`user_id`),
  FOREIGN KEY (`receiver_id`) REFERENCES `tb_user`(`user_id`)
) COMMENT '聊天记录表';

4. 文件传输表(tb_file_transfer):存储文件传输记录

CREATE TABLE `tb_file_transfer` (
  `file_id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '文件ID',
  `sender_id` INT NOT NULL COMMENT '发送者ID',
  `receiver_id` INT NOT NULL COMMENT '接收者ID',
  `file_name` VARCHAR(200) NOT NULL COMMENT '文件名',
  `file_path` VARCHAR(500) NOT NULL COMMENT '服务器存储路径',
  `file_size` BIGINT NOT NULL COMMENT '文件大小(字节)',
  `transfer_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '传输时间',
  `transfer_status` TINYINT DEFAULT 0 COMMENT '传输状态(0:未完成,1:完成,2:失败)',
  FOREIGN KEY (`sender_id`) REFERENCES `tb_user`(`user_id`),
  FOREIGN KEY (`receiver_id`) REFERENCES `tb_user`(`user_id`)
) COMMENT '文件传输表';

5. 备份记录表(tb_backup_log):存储数据库备份记录

CREATE TABLE `tb_backup_log` (
  `backup_id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '备份ID',
  `backup_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '备份时间',
  `backup_path` VARCHAR(500) NOT NULL COMMENT '备份文件路径',
  `backup_size` BIGINT NOT NULL COMMENT '备份文件大小(字节)',
  `backup_status` TINYINT DEFAULT 1 COMMENT '备份状态(1:成功,0:失败)'
) COMMENT '备份记录表';

三、服务器端开发(Delphi 12)

 

服务器端负责管理客户端连接、转发消息、处理文件传输、维护组织架构

1. 新建服务器端项目

  • 打开 Delphi 12,点击「File」→「New」→「VCL Application」,保存项目为IMServer.dpr,窗体保存为MainForm.pas

 

2. 设计服务器端界面

从「Tool Palette」(工具箱)拖拽以下控件到窗体(MainForm):
控件名称 控件类型 属性设置 说明
IdTCPServer1 TIdTCPServer DefaultPort=8080;Active=False TCP 服务器组件,监听 8080 端口
BtnStartStop TButton Caption=「启动服务器」;Left=10;Top=10 启动 / 停止服务器按钮
MemoLog TMemo Align=alBottom;ReadOnly=True 显示服务器日志
LabelStatus TLabel Caption=「状态:未启动」;Left=100;Top=15 显示服务器运行状态
FDConnection1 TFDConnection 连接 MySQL 数据库(需配置 Driver、Server、Database 等) FireDAC 数据库连接组件
FDQuery1 TFDQuery 关联FDConnection1 执行 SQL 查询

 

3. 核心代码实现

(1)启动 / 停止服务器(BtnStartStop点击事件)
procedure TMainForm.BtnStartStopClick(Sender: TObject);
begin
  if not IdTCPServer1.Active then
  begin
    try
      IdTCPServer1.Active := True;
      BtnStartStop.Caption := '停止服务器';
      LabelStatus.Caption := '状态:运行中';
      MemoLog.Lines.Add('服务器启动成功,端口:' + IntToStr(IdTCPServer1.DefaultPort));
    except
      on E: Exception do
        MemoLog.Lines.Add('服务器启动失败:' + E.Message);
    end;
  end
  else
  begin
    IdTCPServer1.Active := False;
    BtnStartStop.Caption := '启动服务器';
    LabelStatus.Caption := '状态:未启动';
    MemoLog.Lines.Add('服务器停止成功');
  end;
end;
(2)处理客户端连接(IdTCPServer1OnExecute事件)
每个客户端连接会触发此事件,需异步接收消息
procedure TMainForm.IdTCPServer1Execute(AContext: TIdContext);
var
  Msg: string;
  MsgObj: TJSONObject;
begin
  try
    if AContext.Connection.IOHandler.InputBuffer.Size > 0 then
    begin
      Msg := AContext.Connection.IOHandler.ReadLn; // 读取客户端消息(UTF-8编码)
      MemoLog.Lines.Add('收到消息:' + Msg);
      
      // 解析JSON消息(需引用System.JSON单元)
      MsgObj := TJSONObject.ParseJSONValue(Msg) as TJSONObject;
      try
        case StrToInt(MsgObj.GetValue('type').Value) of
          1: ProcessLogin(AContext, MsgObj); // 处理登录请求(type=1)
          2: ProcessChatMsg(AContext, MsgObj); // 处理聊天消息(type=2)
          3: ProcessFileTransfer(AContext, MsgObj); // 处理文件传输(type=3)
        end;
      finally
        MsgObj.Free;
      end;
    end;
  except
    on E: Exception do
    begin
      MemoLog.Lines.Add('客户端处理错误:' + E.Message);
      AContext.Connection.Disconnect; // 断开异常连接
    end;
  end;
end;
(3)处理登录请求(ProcessLogin函数)
验证用户身份,并记录客户端连接:
procedure TMainForm.ProcessLogin(AContext: TIdContext; MsgObj: TJSONObject);
var
  Username, Password: string;
  UserID: Integer;
  ResponseObj: TJSONObject;
begin
  Username := MsgObj.GetValue('username').Value;
  Password := MsgObj.GetValue('password').Value; // 实际应传输哈希后的密码(如SHA256)
  
  // 验证用户(从MySQL查询)
  FDQuery1.SQL.Text := 'SELECT user_id FROM tb_user WHERE username=:username AND password=:password';
  FDQuery1.ParamByName('username').Value := Username;
  FDQuery1.ParamByName('password').Value := Password;
  FDQuery1.Open;
  
  try
    if FDQuery1.IsEmpty then
    begin
      // 登录失败:用户不存在或密码错误
      ResponseObj := TJSONObject.Create;
      ResponseObj.AddPair('code', TJSONNumber.Create(401));
      ResponseObj.AddPair('msg', '用户名或密码错误');
      AContext.Connection.IOHandler.WriteLn(ResponseObj.ToString);
      ResponseObj.Free;
      MemoLog.Lines.Add('登录失败:' + Username);
    end
    else
    begin
      // 登录成功:记录客户端连接(需用临界区保证线程安全)
      UserID := FDQuery1.FieldByName('user_id').AsInteger;
      CS.Enter; // CS是全局TCriticalSection变量,用于保护ClientList
      try
        if ClientList.ContainsKey(UserID) then
          ClientList.Remove(UserID);
        ClientList.Add(UserID, AContext); // ClientList是TDictionary<Integer, TIdContext>,存储用户ID与连接的映射
      finally
        CS.Leave;
      end;
      
      // 发送成功响应
      ResponseObj := TJSONObject.Create;
      ResponseObj.AddPair('code', TJSONNumber.Create(200));
      ResponseObj.AddPair('msg', '登录成功');
      ResponseObj.AddPair('user_id', TJSONNumber.Create(UserID));
      AContext.Connection.IOHandler.WriteLn(ResponseObj.ToString);
      ResponseObj.Free;
      MemoLog.Lines.Add('登录成功:' + Username + '(ID:' + IntToStr(UserID) + ')');
      
      // 更新用户在线状态
      FDQuery1.SQL.Text := 'UPDATE tb_user SET status=1 WHERE user_id=:user_id';
      FDQuery1.ParamByName('user_id').Value := UserID;
      FDQuery1.ExecSQL;
    end;
  finally
    FDQuery1.Close;
  end;
end;

四、客户端开发(Delphi 12)

客户端负责用户登录、发送 / 接收消息、本地存储聊天记录、文件传输

1. 新建客户端项目

  • 打开 Delphi 12,点击「File」→「New」→「VCL Application」,保存项目为IMClient.dpr,窗体保存为LoginForm.pas(登录窗体)和MainForm.pas(主聊天窗体)。

2. 设计登录窗体(LoginForm

拖拽以下控件到窗体:
控件名称 控件类型 属性设置 说明
EditServerIP TEdit Text=「127.0.0.1」;Left=100;Top=10 服务器 IP 输入框
EditServerPort TEdit Text=「8080」;Left=100;Top=40 服务器端口输入框
EditUsername TEdit Left=100;Top=70 用户名输入框
EditPassword TEdit PasswordChar=「*」;Left=100;Top=100 密码输入框
BtnLogin TButton Caption=「登录」;Left=100;Top=130 登录按钮
IdTCPClient1 TIdTCPClient 初始Host= 空;Port=0 TCP 客户端组件,连接服务器

 

3. 设计主聊天窗体(MainForm

拖拽以下控件到窗体:
控件名称 控件类型 属性设置 说明
TreeOrg TTreeView Left=10;Top=10;Width=200;Height=400 显示组织架构(部门 – 用户)
ListContacts TListBox Left=220;Top=10;Width=200;Height=400 显示联系人列表
MemoChatLog TMemo Align=alClient;ReadOnly=True 显示聊天记录
EditMsg TEdit Align=alBottom;Height=30 消息输入框
BtnSend TButton Caption=「发送」;Align=alBottom;Width=80 发送消息按钮
BtnSendFile TButton Caption=「发送文件」;Left=10;Top=420 发送文件按钮
FDConnection1 TFDConnection 连接 SQLite 数据库(Driver=SQLite;Database=local.db) 本地聊天记录存储
FDQuery1 TFDQuery 关联FDConnection1 执行本地 SQL 查询

 

4. 核心代码实现

(1)登录逻辑(LoginFormBtnLogin点击事件)
procedure TLoginForm.BtnLoginClick(Sender: TObject);
var
  LoginMsg: TJSONObject;
  Response: string;
  ResponseObj: TJSONObject;
begin
  // 验证输入
  if EditUsername.Text = '' then
  begin
    ShowMessage('请输入用户名');
    EditUsername.SetFocus;
    Exit;
  end;
  if EditPassword.Text = '' then
  begin
    ShowMessage('请输入密码');
    EditPassword.SetFocus;
    Exit;
  end;
  
  // 连接服务器
  try
    IdTCPClient1.Host := EditServerIP.Text;
    IdTCPClient1.Port := StrToInt(EditServerPort.Text);
    IdTCPClient1.Connect; // 阻塞调用,需用TIdAntiFreeze防止界面冻结
  except
    on E: Exception do
    begin
      ShowMessage('连接服务器失败:' + E.Message);
      Exit;
    end;
  end;
  
  // 发送登录消息(JSON格式)
  LoginMsg := TJSONObject.Create;
  try
    LoginMsg.AddPair('type', TJSONNumber.Create(1)); // 消息类型:登录
    LoginMsg.AddPair('username', EditUsername.Text);
    LoginMsg.AddPair('password', EditPassword.Text); // 实际应哈希(如SHA256)
    IdTCPClient1.IOHandler.WriteLn(LoginMsg.ToString); // 发送消息
    
    // 接收服务器响应
    Response := IdTCPClient1.IOHandler.ReadLn;
    ResponseObj := TJSONObject.ParseJSONValue(Response) as TJSONObject;
    try
      if ResponseObj.GetValue('code').Value = '200' then
      begin
        // 登录成功:打开主窗体
        MainForm := TMainForm.Create(Self);
        MainForm.CurrentUserID := StrToInt(ResponseObj.GetValue('user_id').Value);
        MainForm.CurrentUsername := EditUsername.Text;
        MainForm.Show;
        Self.Hide;
      end
      else
      begin
        ShowMessage('登录失败:' + ResponseObj.GetValue('msg').Value);
        IdTCPClient1.Disconnect;
      end;
    finally
      ResponseObj.Free;
    end;
  finally
    LoginMsg.Free;
  end;
end;
(2)发送文字消息(MainFormBtnSend点击事件)
procedure TMainForm.BtnSendClick(Sender: TObject);
var
  ChatMsg: TJSONObject;
  ReceiverID: Integer;
begin
  // 获取接收者ID(从联系人列表选择)
  if ListContacts.ItemIndex = -1 then
  begin
    ShowMessage('请选择联系人');
    Exit;
  end;
  ReceiverID := StrToInt(ListContacts.Items.Objects[ListContacts.ItemIndex].ToString); // 假设联系人列表存储了用户ID
  
  // 构造聊天消息(JSON格式)
  ChatMsg := TJSONObject.Create;
  try
    ChatMsg.AddPair('type', TJSONNumber.Create(2)); // 消息类型:聊天
    ChatMsg.AddPair('sender_id', TJSONNumber.Create(CurrentUserID));
    ChatMsg.AddPair('receiver_id', TJSONNumber.Create(ReceiverID));
    ChatMsg.AddPair('msg_type', TJSONNumber.Create(1)); // 消息子类型:文字
    ChatMsg.AddPair('msg_content', EditMsg.Text);
    IdTCPClient1.IOHandler.WriteLn(ChatMsg.ToString); // 发送消息
    
    // 保存到本地数据库(SQLite)
    FDQuery1.SQL.Text := 'INSERT INTO local_chat_log (sender_id, sender_username, receiver_id, receiver_username, msg_type, msg_content, send_time) VALUES (:sender_id, :sender_username, :receiver_id, :receiver_username, :msg_type, :msg_content, :send_time)';
    FDQuery1.ParamByName('sender_id').Value := CurrentUserID;
    FDQuery1.ParamByName('sender_username').Value := CurrentUsername;
    FDQuery1.ParamByName('receiver_id').Value := ReceiverID;
    FDQuery1.ParamByName('receiver_username').Value := ListContacts.Items[ListContacts.ItemIndex];
    FDQuery1.ParamByName('msg_type').Value := 1;
    FDQuery1.ParamByName('msg_content').Value := EditMsg.Text;
    FDQuery1.ParamByName('send_time').Value := Now;
    FDQuery1.ExecSQL;
    
    // 清空输入框
    EditMsg.Clear;
  finally
    ChatMsg.Free;
  end;
end;
(3)接收消息(客户端需异步线程)
客户端需用线程接收服务器消息,避免界面冻结。创建TReceiveThread线程类:
type
  TReceiveThread = class(TThread)
  private
    FClient: TIdTCPClient;
    FMainForm: TMainForm;
  protected
    procedure Execute; override;
  public
    constructor Create(AClient: TIdTCPClient; AMainForm: TMainForm);
  end;

constructor TReceiveThread.Create(AClient: TIdTCPClient; AMainForm: TMainForm);
begin
  inherited Create(False); // 立即启动线程
  FClient := AClient;
  FMainForm := AMainForm;
  FreeOnTerminate := True; // 线程结束后自动释放
end;

procedure TReceiveThread.Execute;
var
  Msg: string;
  MsgObj: TJSONObject;
begin
  while not Terminated and FClient.Connected do
  begin
    try
      Msg := FClient.IOHandler.ReadLn; // 读取消息
      if Msg = '' then
        Continue;
      
      // 解析消息
      MsgObj := TJSONObject.ParseJSONValue(Msg) as TJSONObject;
      try
        case StrToInt(MsgObj.GetValue('type').Value) of
          2: FMainForm.ProcessChatMsg(MsgObj); // 处理聊天消息
          3: FMainForm.ProcessFileMsg(MsgObj); // 处理文件消息
        end;
      finally
        MsgObj.Free;
      end;
    except
      on E: Exception do
      begin
        FMainForm.MemoChatLog.Lines.Add('接收消息错误:' + E.Message);
        FClient.Disconnect;
      end;
    end;
  end;
end;

主窗体创建时启动线程

procedure TMainForm.FormCreate(Sender: TObject);
begin
  // 启动接收消息线程
  FReceiveThread := TReceiveThread.Create(IdTCPClient1, Self);
end;

五、关键功能补充

1. 文件传输(客户端)

  • 发送文件:点击「发送文件」按钮,用TOpenDialog选择文件,发送文件信息(文件名、大小、发送者、接收者)给服务器,服务器转发给接收者。接收者确认后,发送者分块传输文件数据(每块 1024 字节)。
  • 接收文件:接收服务器转发的文件信息,创建本地文件(路径:file/对方用户名/文件名),接收分块数据并写入文件,完成后标记传输状态。

 

2. 本地存储(SQLite)

客户端需创建本地聊天记录表local_chat_log),结构与服务器tb_chat_log类似,用于离线时查看聊天记录。用TFDConnection连接 SQLite 数据库(Database属性设为local.db)。

3. INI 配置文件

TIniFile存储客户端配置(服务器 IP、端口、用户名、密码哈希),示例:
var
  Ini: TIniFile;
begin
  Ini := TIniFile.Create(ExtractFilePath(Application.ExeName) + 'config.ini');
  try
    // 读取配置
    EditServerIP.Text := Ini.ReadString('Server', 'IP', '127.0.0.1');
    EditServerPort.Text := Ini.ReadInteger('Server', 'Port', 8080).ToString;
    // 写入配置
    Ini.WriteString('User', 'Username', EditUsername.Text);
    Ini.WriteString('User', 'Password', SHA256Hash(EditPassword.Text)); // 哈希后存储
  finally
    Ini.Free;
  end;
end;

六、测试与调试

  1. 服务器端:启动服务器,查看MemoLog是否显示「服务器启动成功」。
  2. 客户端:运行客户端,输入服务器 IP(127.0.0.1)、端口(8080)、用户名 / 密码(需提前在 MySQL 中插入测试数据),点击「登录」。
  3. 功能测试
    • 文字聊天:选择联系人,输入消息,点击「发送」,查看对方客户端是否收到消息。
    • 文件传输:点击「发送文件」,选择文件,查看对方客户端是否接收并保存到file/对方用户名/目录。
    • 本地存储:关闭客户端,重新打开,查看本地聊天记录是否存在。

七、注意事项

  • 密码安全:客户端传输密码时需用哈希(如 SHA256),服务器存储哈希后的密码,避免明文传输。
  • 多线程安全:服务器端ClientList(客户端连接列表)需用TCriticalSection保护,避免多线程冲突。
  • 文件分块:大文件需分块传输(如 1024 字节 / 块),避免占用过多内存。
  • 界面响应:客户端接收消息需用线程,避免ReadLn阻塞界面。
  • 数据库备份:服务器端用mysqldump命令实现数据库备份,示例:
procedure TMainForm.BtnBackupClick(Sender: TObject);
var
  BackupPath: string;
  Cmd: string;
begin
  BackupPath := ExtractFilePath(Application.ExeName) + 'backup_' + FormatDateTime('yyyyMMddHHmmss', Now) + '.sql';
  Cmd := Format('mysqldump -h localhost -u root -p123456 im_db > "%s"', [BackupPath]);
  WinExec(PChar(Cmd), SW_HIDE); // 执行备份命令
  // 记录备份到tb_backup_log
end;

总结

本系统实现了C/S 架构的局域网即时通讯核心功能,包括文字聊天、文件传输、组织架构管理、本地存储等。对于 Delphi 新手,需重点学习:
  • Indy 组件(TIdTCPServer/TIdTCPClient)的使用。
  • FireDAC 连接数据库(MySQL/SQLite)。
  • 多线程编程(服务器端处理客户端连接、客户端接收消息)。
  • JSON 消息格式的解析与构造。
可根据需求扩展功能(如表情发送、语音聊天、远程桌面),或用 DevExpress 组件美化界面。
本文由

Doubao-Seed-1.6-thinking 生成

发表回复