C#简易聊天室(服务器+客户端)

一、整体项目概述

本项目基于 C# WinForms 实现简易聊天室,包含服务器端与客户端两部分,采用 TCP 协议实现网络通信,支持服务器与客户端、客户端与客户端(通过服务器转发)的消息交互,具备客户端连接管理、消息广播/私发、连接断开处理等核心功能。

核心技术点:Socket 网络编程、多线程异步处理、WinForms 控件交互、集合类(Dictionary、List)数据管理、委托与跨线程 UI 更新。

二、服务器端代码详细解析

(一)命名空间与引用

复制代码
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Security;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

解析:引入所需的系统命名空间,涵盖集合操作(Dictionary、List)、网络编程(Socket、IPEndPoint)、多线程(Thread、Task、CancellationTokenSource)、WinForms 控件及基础数据类型处理等,为后续功能实现提供基础支持。

(二)命名空间与窗体类定义

复制代码
namespace 简易聊天室服务器
{
    public partial class Form1 : Form
    {
        // 后续代码均在此类中
    }
}

解析:定义服务器端项目命名空间为"简易聊天室服务器",主窗体类 Form1 继承自 WinForms 的 Form 类,partial 关键字表示该类为分部类,可拆分到多个文件中(此处主要用于窗体设计器生成的代码与自定义代码分离)。

(三)核心字段定义

复制代码
// 定义保存所有客户端的字典:键为客户端终端(EndPoint),值为客户端 Socket 对象
Dictionary<EndPoint, Socket> clients = new Dictionary<EndPoint, Socket>();

// 下拉框数据源:存储客户端信息(终端+Socket),用于 UI 展示
List<Client> clients2 = new List<Client>();

// 服务器端 Socket 对象,负责监听客户端连接
Socket socketServer = null;

// 异步任务取消令牌源,用于控制多线程任务的终止
CancellationTokenSource cts1 = null;

解析:

  • clients 字典:核心数据结构,用于快速查找客户端 Socket,键为客户端的网络终端信息(IP+端口),值为对应的 Socket 对象,便于后续消息发送与连接管理。

  • clients2 列表:辅助 UI 展示,存储自定义 Client 类对象,包含客户端终端与 Socket 信息,作为下拉框(comboBox1)的数据源,实现客户端选择功能。

  • socketServer:服务器端核心 Socket,负责绑定 IP 和端口、监听客户端连接请求,是整个服务器的通信基础。

  • cts1:用于管理异步任务的取消,避免服务器关闭后线程仍在运行,防止资源泄漏。

(四)窗体构造函数

复制代码
public Form1()
{
    InitializeComponent();
}

解析:窗体构造函数,调用 InitializeComponent() 方法初始化窗体上的所有控件(如按钮、文本框、下拉框、富文本框等),该方法由 WinForms 设计器自动生成,无需手动修改。

(五)启动/关闭服务器按钮点击事件(button1_Click)

复制代码
private void button1_Click(object sender, EventArgs e)
{
    //启动服务器
    try
    {
        if (button1.Text == "启动服务器")
        {
            // 打开服务器
            StartServer();

            // 监听客户端是否连入及数据读取
            Accepte();
            button1.Text = "关闭服务器";
        }
        else
        {
            //关闭服务器
            CloseServer();
            button1.Text = "启动服务器";
        }
    }
    catch (Exception ex)
    {
        // 异常处理:此处仅输出异常信息,可根据需求添加弹窗提示
        // MessageBox.Show(ex.Message);
        Console.WriteLine(ex.Message);
    }
}

解析:

  • 通过按钮文本判断当前操作:若为"启动服务器",则调用 StartServer() 初始化服务器,调用 Accepte() 开始监听客户端连接与数据,同时将按钮文本改为"关闭服务器";若为"关闭服务器",则调用 CloseServer() 释放资源,按钮文本恢复为"启动服务器"。

  • 添加 try-catch 块捕获异常,避免程序崩溃,此处将异常信息输出到控制台,可根据需求改为弹窗提示用户。

(六)服务器启动方法(StartServer)

复制代码
/// <summary>
/// 打开服务器的方法
/// </summary>
public void StartServer()
{
    // 初始化服务器 Socket:IPv4地址族、流式套接字(TCP)、TCP 协议
    socketServer = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    
    // 绑定服务器 IP 和端口(从窗体文本框获取输入)
    socketServer.Bind(new IPEndPoint(IPAddress.Parse(txtIP.Text), int.Parse(txtPort.Text)));
    
    // 开始监听客户端连接,最大等待连接数为1000
    socketServer.Listen(1000);
}

解析:

  • 初始化服务器 Socket,指定通信协议为 TCP(流式套接字),适用于稳定的面向连接的通信场景。

  • Bind() 方法将服务器 Socket 绑定到指定的 IP 地址和端口(从窗体的 txtIP 和 txtPort 文本框获取用户输入),确保服务器能在该地址上接收客户端请求。

  • Listen() 方法开始监听客户端连接,参数1000表示服务器最多可同时等待1000个客户端连接请求,超出的请求会被拒绝。

(七)服务器关闭方法(CloseServer)

复制代码
/// <summary>
/// 关闭服务器
/// </summary>
public void CloseServer()
{
    // 关闭服务器前,先断开所有客户端连接并发送断开通知
    if (socketServer != null)
    {
        foreach (var item in clients2)  // 遍历所有客户端
        {
            Socket s1 = item.Socket; // 获取客户端 Socket 对象
            
            // 向客户端发送断开标识"n1",通知客户端服务器即将关闭
            byte[] data = Encoding.UTF8.GetBytes("n1");
            s1.Send(data);
            
            // 断开客户端连接
            s1.Disconnect(false);
        }
        
        // 取消异步任务,释放线程资源
        cts1.Cancel(); 
        // 关闭服务器 Socket
        socketServer.Close();
        socketServer = null;
    }
    
    // 清空客户端集合与 UI 数据源
    clients.Clear();
    clients2.Clear();
    comboBox1.DataSource = null;
    button1.Text = "启动服务器";
}

解析:

  • 关闭服务器前,先遍历所有客户端,向每个客户端发送"n1"标识(约定的断开信号),通知客户端服务器即将关闭,随后断开客户端连接。

  • 调用 cts1.Cancel() 取消异步监听任务,避免线程残留;关闭服务器 Socket 并置为 null,释放系统资源。

  • 清空客户端字典与列表,重置下拉框数据源,恢复按钮初始文本,确保服务器关闭后状态正常。

(八)客户端监听与数据接收方法(Accepte)

该方法核心功能:监听客户端连接请求,接收客户端数据并处理连接断开逻辑,采用异步多线程避免阻塞 UI 线程。以下按逻辑拆分各模块,保留原有代码结构与核心逻辑。

8.1、方法整体框架与初始化

复制代码
/// <summary>
/// 连接客户端 和读取客户端发来的数据
/// </summary>
public void Accepte()
{
    // 1. 初始化异步任务取消令牌源,用于控制监听任务的终止
    cts1 = new CancellationTokenSource();
    
    // 2. 启动异步任务,监听客户端连接(避免阻塞 UI 线程)
    Task.Run(() =>
    {
        // 后续连接监听与数据接收逻辑
    }, cts1.Token);
}

解析:方法入口,核心是初始化取消令牌源,启动异步任务处理客户端连接,防止 UI 线程卡顿。

8.2、客户端连接监听核心循环

复制代码
// 循环监听客户端连接,直到任务被取消
while (!cts1.IsCancellationRequested)
{
    Socket socketClient = null;  // 定义客户端 Socket 对象,存储新连接的客户端
    
    // 接收客户端连接请求
    try
    {
        socketClient = socketServer.Accept();
    }
    catch (Exception)
    {
        // 若接收连接时出错(如服务器关闭),退出循环,终止监听
        break;
    }
    
    // 后续客户端去重、添加及 UI 更新逻辑
}

解析:外层循环持续监听客户端连接,通过 Accept() 方法接收新客户端,异常时退出循环,保证服务器关闭时能正常终止监听。

8.3、客户端去重与添加逻辑

复制代码
// 检查客户端是否已存在(按终端去重),避免重复添加
if (!clients.ContainsKey(socketClient.RemoteEndPoint))
{
    // 将客户端添加到字典:键为客户端终端,值为客户端 Socket
    clients.Add(socketClient.RemoteEndPoint, socketClient);
    
    // 将客户端信息封装为 Client 对象,添加到 UI 数据源列表
    clients2.Add(new Client()
    {
        EndPoint = socketClient.RemoteEndPoint,
        Socket = socketClient
    });
}

解析:通过客户端终端(RemoteEndPoint)判断是否已存在,避免重复添加客户端;同时将客户端信息存入字典(用于快速查找)和列表(用于 UI 展示)。

8.4、跨线程更新 UI(刷新客户端下拉框)

复制代码
// 跨线程更新 UI:刷新下拉框数据源,展示最新客户端列表
Invoke(new Action(() =>
{
    comboBox1.DataSource = null;
    comboBox1.DataSource = clients2;
    comboBox1.DisplayMember = "EndPoint"; // 下拉框显示客户端终端信息
    comboBox1.ValueMember = "Socket";     // 下拉框值绑定为客户端 Socket 对象
}));

解析:WinForms 控件不允许跨线程直接操作,通过 Invoke() 方法将 UI 更新操作委托给 UI 线程,刷新下拉框数据源,同步展示最新客户端列表。

8.5、客户端数据接收异步任务

复制代码
// 为当前客户端启动独立的异步任务,持续接收其发送的数据
cts1 = new CancellationTokenSource();
Task.Run(() =>
{
    // 循环接收客户端数据,直到任务被取消
    while (!cts1.IsCancellationRequested)
    {
        try
        {
            // 初始化缓冲区,接收客户端数据
            byte[] buffer = new byte[socketClient.Available];
            int len = socketClient.Receive(buffer);
            
            // 若接收到有效数据,进行处理
            if (len > 0)
            {
                // 后续数据解析、断开处理及 UI 更新逻辑
            }
        }
        catch (Exception)
        {
            // 接收数据时出错,输出异常信息(可根据需求优化)
            Console.WriteLine();
        }
    }
}, cts1.Token);

解析:为每个客户端启动独立的异步接收任务,避免不同客户端的数据接收相互干扰;通过循环持续接收数据,异常时仅终止当前客户端的接收任务,不影响整体监听。

8.6、数据解析与客户端断开处理

复制代码
// 获取客户端终端信息,用于标识消息来源
EndPoint end = socketClient.RemoteEndPoint;
// 将字节数组转换为 UTF-8 编码的字符串
string message = Encoding.UTF8.GetString(buffer);

// 处理客户端断开请求(约定标识"n1")
if (message == "n1")
{
    // 查找并移除断开的客户端
    Client c1 = clients2.Find(x => x.EndPoint == end);
    clients2.Remove(c1);
    
    // 跨线程更新 UI:刷新下拉框数据源
    if (clients2.Count == 0)
    {
        Invoke(new Action(() =>
        {
            comboBox1.DataSource = null;
        }));
    }
    else
    {
        Invoke(new Action(() =>
        {
            comboBox1.DataSource = null;
            comboBox1.DataSource = clients2;
            comboBox1.DisplayMember = "EndPoint";
            comboBox1.ValueMember = "Socket";
        }));
    }
}

解析:将接收的字节数据转换为字符串,判断是否为约定的断开标识"n1";若是则移除该客户端,并同步刷新 UI 下拉框,保证客户端列表与实际连接状态一致。

8.7、跨线程更新 UI(展示客户端消息)

复制代码
// 跨线程更新 UI:将客户端消息添加到富文本框
Invoke(new Action(() =>
{
    richTextBox1.Text += $"{end}:{message}" + Environment.NewLine;
    // 将光标定位到文本末尾,自动滚动显示最新消息
    richTextBox1.SelectionStart = richTextBox1.TextLength;
    richTextBox1.ScrollToCaret();
}));

解析:将客户端发送的消息格式化后添加到富文本框,通过 SelectionStart 和 ScrollToCaret() 方法实现自动滚动,确保最新消息始终可见,提升用户体验。

解析:

  • 采用异步多线程方式处理客户端连接与数据接收,避免阻塞 UI 线程,保证窗体响应流畅。

  • 外层循环持续监听客户端连接请求,通过 socketServer.Accept() 接收新客户端,将其添加到字典和 UI 数据源,并用 Invoke() 实现跨线程更新下拉框(WinForms 控件不允许跨线程直接操作)。

  • 内层循环为每个客户端启动独立的异步任务,持续接收客户端发送的数据:

    • 通过 socketClient.Available 获取可接收的字节数,初始化缓冲区后调用 Receive() 接收数据。

    • 若接收到"n1"标识,说明客户端请求断开,移除该客户端并刷新 UI 数据源。

    • 将接收到的消息格式化后添加到富文本框,通过 SelectionStart 和 ScrollToCaret() 实现自动滚动显示最新消息。

  • 使用 CancellationTokenSource 控制异步任务的取消,确保服务器关闭时线程能正常终止。

(九)发送消息按钮点击事件(button2_Click)

复制代码
/// <summary>
/// 发送数据
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button2_Click(object sender, EventArgs e)
{
    // 检查服务器是否已启动
    if (socketServer == null)
    {
        MessageBox.Show("请先打开服务器");
        return;
    }
    
    // 检查发送内容是否为空
    if (string.IsNullOrWhiteSpace(textBox1.Text))
    {
        MessageBox.Show("发送的内容不能为空");
        return;
    }
    
    // 广播模式:向所有客户端发送消息
    if (checkBox1.Checked)
    {
        foreach (var item in clients2)
        {
            item.Socket.Send(Encoding.UTF8.GetBytes("服务器说:" + textBox1.Text));
        }
    }
    // 私发模式:向下拉框选中的客户端发送消息
    else
    {
        Socket s1 = (Socket)comboBox1.SelectedValue;
        if (s1 != null && s1.Connected)
        {
            s1.Send(Encoding.UTF8.GetBytes("服务器说:" + textBox1.Text));
        }
    }
}

解析:

  • 发送消息前先进行合法性校验:检查服务器是否启动、发送内容是否为空,避免无效操作。

  • 根据复选框(checkBox1)的选中状态判断发送模式:

    • 广播模式:遍历所有客户端,向每个客户端发送"服务器说:+消息内容"的格式消息。

    • 私发模式:获取下拉框选中的客户端 Socket,向其单独发送消息,确保仅目标客户端能接收。

  • 使用 Encoding.UTF8.GetBytes() 将字符串消息转换为字节数组,因为 Socket 通信仅支持字节数据传输。

(十)窗体关闭事件(Form1_FormClosing)

复制代码
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
    CloseServer();
}

解析:窗体关闭时触发该事件,调用 CloseServer() 方法关闭服务器、释放所有资源,避免程序退出后残留线程或占用系统资源。

(十一)自定义 Client 类

复制代码
public class Client
{
    // 客户端终端对象(包含 IP 和端口)
    public EndPoint EndPoint { get; set; }
    // 客户端 Socket 对象
    public Socket Socket { get; set; }
}

解析:自定义客户端实体类,用于封装客户端的核心信息(终端和 Socket),作为 UI 下拉框的数据源,方便统一管理客户端信息,简化代码逻辑。

三、客户端代码详细解析

(一)命名空间与引用

复制代码
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

解析:与服务器端引用的命名空间基本一致,涵盖网络编程、多线程、WinForms 控件等所需资源,确保客户端能实现与服务器的通信及 UI 交互。

(二)命名空间与窗体类定义

复制代码
namespace 简易聊天室客户端
{
    public partial class Form1 : Form
    {
        // 后续代码均在此类中
    }
}

解析:定义客户端项目命名空间为"简易聊天室客户端",主窗体类 Form1 继承自 Form 类,与服务器端结构一致,实现客户端的 UI 与功能逻辑。

(三)窗体构造函数

复制代码
public Form1()
{
    InitializeComponent();
}

解析:与服务器端构造函数功能一致,初始化窗体上的所有控件,为后续操作提供 UI 基础。

(四)连接/断开服务器按钮点击事件(button1_Click)

复制代码
private void button1_Click(object sender, EventArgs e)
{
    try
    {
        if (button1.Text == "连接服务器")
        {
            // 连接服务器
            ConnetServer();
            // 启动异步任务接收服务器消息
            Accept();
        }
        else
        {
            // 断开与服务器的连接
            DisConnetServer();
        }
    }
    catch (Exception ex)
    {
        // 输出异常信息,可根据需求添加弹窗提示
        Console.WriteLine(ex.Message);
    }
}

解析:通过按钮文本判断操作类型:若为"连接服务器",则调用 ConnetServer() 建立与服务器的连接,调用 Accept() 接收服务器消息;若为"断开服务器",则调用 DisConnetServer() 释放连接资源。添加 try-catch 块捕获异常,保证程序稳定性。

(五)核心字段定义

复制代码
// 客户端 Socket 对象,用于与服务器通信
Socket socketClient;

// 异步任务取消令牌源,控制接收消息任务的终止
CancellationTokenSource cts;

解析:

  • socketClient:客户端核心 Socket,负责与服务器建立连接、发送和接收消息,是客户端通信的基础。

  • cts:用于控制接收服务器消息的异步任务,确保断开连接时线程能正常终止,避免资源泄漏。

(六)连接服务器方法(ConnetServer)

复制代码
/// <summary>
/// 连接服务器
/// </summary>
public void ConnetServer()
{
    // 初始化客户端 Socket:IPv4地址族、流式套接字(TCP)、TCP 协议
    socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    
    // 解析服务器 IP 和端口(从窗体文本框获取输入)
    IPEndPoint endponit = new IPEndPoint(IPAddress.Parse(txtIP.Text), int.Parse(txtPort.Text));
    
    // 与服务器建立连接
    socketClient.Connect(endponit);
    
    // 连接成功后,向服务器发送初始消息
    socketClient.Send(Encoding.UTF8.GetBytes("建立连接成功"));
    
    // 更改按钮文本为"断开服务器"
    button1.Text = "断开服务器";
}

解析:

  • 初始化客户端 Socket,指定与服务器一致的通信协议(TCP),确保双方能正常通信。

  • 从窗体文本框获取服务器的 IP 和端口,封装为 IPEndPoint 对象,调用 Connect() 方法与服务器建立连接。

  • 连接成功后,向服务器发送"建立连接成功"的消息,通知服务器客户端已上线,同时更改按钮文本为"断开服务器"。

(七)接收服务器消息方法(Accept)

复制代码
/// <summary>
/// 接收服务器消息
/// </summary>
public void Accept()
{
    // 初始化异步任务取消令牌源
    cts = new CancellationTokenSource();
    
    // 启动异步任务,持续接收服务器消息
    Task.Run(() =>
    {
        while (!cts.IsCancellationRequested)
        {
            try
            {
                // 初始化缓冲区,接收服务器发送的数据
                byte[] buffer = new byte[socketClient.Available];
                int length = socketClient.Receive(buffer);
                
                if (length > 0)
                {
                    // 将字节数组转换为字符串(UTF-8编码)
                    string message = Encoding.UTF8.GetString(buffer);
                    
                    // 跨线程更新 UI:将消息添加到富文本框
                    Invoke(new Action(() =>
                    {
                        richTextBox1.Text += message + "\r\n";
                        
                        // 若接收到服务器断开标识"n1",则断开与服务器的连接
                        if (message == "n1")
                        {
                            DisConnetServer();
                        }
                    }));
                }
            }
            catch (Exception)
            {
                // 接收消息时出错,输出异常信息
                Console.WriteLine("");
            }
        }
    }, cts.Token);
}

解析:

  • 采用异步多线程方式接收服务器消息,避免阻塞 UI 线程,保证客户端窗体响应流畅。

  • 循环接收服务器数据,通过 socketClient.Available 获取可接收的字节数,初始化缓冲区后调用 Receive() 接收数据,转换为字符串后更新到富文本框。

  • 若接收到服务器发送的"n1"标识,说明服务器已关闭,自动调用 DisConnetServer() 断开客户端连接。

  • 使用 Invoke() 实现跨线程更新 UI,避免 WinForms 控件跨线程操作异常。

(八)断开服务器连接方法(DisConnetServer)

复制代码
/// <summary>
/// 断开服务器连接
/// </summary>
public void DisConnetServer()
{
    if (socketClient != null)
    {
        // 向服务器发送断开标识"n1",通知服务器客户端即将断开
        socketClient.Send(Encoding.UTF8.GetBytes("n1"));
        
        // 断开客户端连接,关闭 Socket
        socketClient.Disconnect(false);
        socketClient.Close();
        
        // 释放资源,取消异步任务
        socketClient = null;
        cts.Cancel();
        
        // 恢复按钮初始文本
        button1.Text = "连接服务器";
    }
}

解析:

  • 断开连接前,向服务器发送"n1"标识,通知服务器客户端即将下线,便于服务器及时更新客户端列表。

  • 调用 Disconnect() 断开连接,Close() 关闭客户端 Socket,释放系统资源;同时取消接收消息的异步任务,避免线程残留。

  • 将 socketClient 置为 null,恢复按钮文本为"连接服务器",确保客户端状态正常,可重新连接服务器。

(九)发送消息按钮点击事件(button2_Click)

复制代码
private void button2_Click(object sender, EventArgs e)
{
    // 检查客户端是否已连接服务器,且发送内容不为空
    if (socketClient != null && !string.IsNullOrEmpty(textBox1.Text))
    {
        // 将消息转换为字节数组,发送给服务器
        socketClient.Send(Encoding.UTF8.GetBytes(textBox1.Text));
    }
}

解析:发送消息前先校验客户端连接状态和消息内容,避免无效发送;将文本框中的消息转换为字节数组后,通过 Socket 发送给服务器,由服务器转发给目标客户端(广播或私发)。

四、核心知识点总结

  • Socket 通信流程:服务器端绑定 IP 和端口→监听连接→接收客户端连接→收发数据;客户端指定服务器 IP 和端口→建立连接→收发数据→断开连接。

  • 多线程处理:使用 Task.Run() 启动异步任务,处理客户端连接和数据接收,避免阻塞 UI 线程;通过 CancellationTokenSource 控制线程终止,防止资源泄漏。

  • 跨线程 UI 更新:WinForms 控件不允许跨线程直接操作,需通过 Control.Invoke() 方法将 UI 更新操作委托给 UI 线程执行。

  • 数据约定:服务器与客户端约定"n1"为断开标识,便于双方及时处理连接断开事件,保证通信稳定性。

  • 集合类使用:通过 Dictionary 快速查找客户端 Socket,通过 List 管理客户端信息并作为 UI 数据源,实现高效的客户端管理。

相关推荐
伯恩bourne1 小时前
Qdrant 快速入门 :数据模型介绍
服务器·数据库·人工智能
dcrenl1 小时前
C# 程序发布时加入发布时间
c#·版本号·发布时间
爱喝水的鱼丶1 小时前
SAP-ABAP: BAdI 开发规范与避坑指南:10 类典型错误与运维方案
运维·性能优化·sap·abap·增强·经验交流·开发交流
CDN3601 小时前
CDN 回源负载均衡实战:解决多源站回源流量不均、源站单点故障,出海业务配置指南
运维·负载均衡
吴声子夜歌1 小时前
编写Shell脚本——启动项目
linux·运维·shell
拦路雨g1 小时前
docker_data.vhdx大文件瘦身
运维·docker·容器
闲云野鹤在人间1 小时前
云计算入门|网络与存储云服务知识梳理
linux·运维·服务器·网络·centos·云计算·php
影寂ldy2 小时前
C# UdpClient 单播 三种通信版本
stm32·单片机·c#
好评1242 小时前
【Linux】五种IO模型与非阻塞IO
linux·服务器·nat