第三章、C#简单界面在线聊天室&C#一对多聊天(使用TCP转发实现的在线聊天室,文章末尾附免费项目资源)

C#网络通信系列学习笔记



前言

在上一章中,我们实现了对socket的简单封装,从而实现让控制台能够一对一聊天,在这一篇文章中,我将会使用界面制作一个简易的在线聊天室
该聊天室服务端与多个客户端建立tcp连接,客户端向服务端发送数据由服务端再转发给其他客户端从而实现多人聊天
项目资源将放在文章末尾免费分享,需要自取。


提示:以下是本篇文章正文内容,下面案例可供参考

一、客户端

1、构建界面

如图所示
在这里插入图片描述
做的比较简单,只有一个显示的文本框用来显示其他人的消息,一个发送框用于编辑发送内容,一个发送按钮。
显示的文本框设置为只读,不允许修改。

2、引用类文件

注意命名空间与项目一致,否则识别不到类

由于这一次逻辑变得复杂,我便将客户端和服务端的类分开写了

using System.Text;
using System.Net.Sockets;
using System.Net;
using System.Threading;

namespace ConsoleTest
{
    class SocketTest
    {
        private Socket MySocket;
        //开启一个线程循环执行接收方法来实时接收数据
        private Thread ReceiveThread;
        public delegate void MyEvent(string ReceiveMsg);//定义一个委托,用来将接收数据传出去
        public event MyEvent HaveYourMsg;

        //用于客户端连接服务端
        public bool Connect(IPAddress iP, int port)
        {
            try
            {
                MySocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                IPEndPoint endPoint = new IPEndPoint(iP, port);
                MySocket.Connect(endPoint);
                return true;
            }
            catch
            {
                return false;
            }
        }

        public bool Send(string str)
        {
            try
            {
                byte[] arrsendmsg = Encoding.UTF8.GetBytes(str);
                if (MySocket.Send(arrsendmsg) != arrsendmsg.Length)
                    return false;
                return true;
            }
            catch
            {
                return false;
            }
        }

        public void recmsg()
        {
            while (true)
            {

                try
                {
                    byte[] arrserverrecmsg = new byte[1024];
                    int length = MySocket.Receive(arrserverrecmsg);
                    if (length > 0)
                    {
                        HaveYourMsg.Invoke(Encoding.UTF8.GetString(arrserverrecmsg, 0, length));
                    }
                }
                catch
                {
                    HaveYourMsg.Invoke("警告:您的连接已断开!!!");
                    return;
                }
                Thread.Sleep(200);
            }
        }

        public void StartReceive()
        {
            ReceiveThread = new Thread(recmsg);
            ReceiveThread.IsBackground = true;
            ReceiveThread.Start();
        }

        public void ColseSocket()
        {
            //如果接收线程正在执行那么先关闭线程在关闭socket
            if (ReceiveThread.ThreadState == ThreadState.Running)
                ReceiveThread.Abort();
            MySocket.Close();
        }
    }
}

相对于上一篇,这一次类的主要变化是添加了委托,由于线程所执行的函数是不能直接返回的,直接操控控件又会有较高的耦合度,因此添加了委托用来将信息传递给窗体

3、添加界面逻辑

using System;
using System.Windows.Forms;
using ConsoleTest;
using System.Net;

namespace 聊天室
{
    public partial class Form1 : Form
    {
        private delegate void Deal();
        public Form1()
        {
            InitializeComponent();
        }

        SocketTest socketTest;
        private void Form1_Load(object sender, EventArgs e)
        {
            socketTest = new SocketTest();
            if (!socketTest.Connect(IPAddress.Parse("127.0.0.1"), 20000))
            {
                MessageBox.Show("连接服务端失败,请重试!");
            }
            //绑定事件接收函数,当有消息时会触发该函数
            socketTest.HaveYourMsg += new SocketTest.MyEvent(HaveMyMsg);
            socketTest.StartReceive();
        }

        private void Button1_Click(object sender, EventArgs e)
        {
            if (textBox2.Text.Length <= 0)
            {
                MessageBox.Show("不允许发送空内容");
                return;
            }
            if (!socketTest.Send(textBox2.Text))
            {
                MessageBox.Show("发送失败了");
                return;
            }
            textBox2.Clear();
        }
        private void HaveMyMsg(string Msg)
        {
            //使用内联委托处理多线程调用窗体控件的问题
            this.Invoke(new Deal(() =>
            {
                textBox1.Text += Msg + "\r\n";
            }));
        }
    }
}

比较短,客户端就直接贴全部代码了,首先窗体加载完成时与服务端建立连接,连接不成功就退出程序。

二、服务端

1.构建界面

在这里插入图片描述
一如既往简单的页面,左侧上下各一个textbox。与客户端的差别只有右侧多了个checklistbox,这个控件主要是为了指定客户端客户端单独发消息的,勾选哪个客户端就给哪个客户端发信息,当然只是服务器发的信息,客户端发的信息还是全局转发。

2.添加服务端类

using System.Text;
using System.Net.Sockets;
using System.Net;
using System.Threading;
using System.Collections.Generic;
using System;
using System.Linq;

namespace ConsoleTest
{
    class Client
    {//自定义一个类,方便统一管理客户端的资源
        public Client(Socket ClientSocket, Thread ClientThread)
        {
            this.ClientSocket = ClientSocket;
            this.ClientThread = ClientThread;
        }
        public Socket ClientSocket { get; set; }
        public Thread ClientThread { get; set; }
    }

    class SocketTest
    {
        public delegate void MyEvent(string ReceiveMsg);//定义一个委托,用来将接收数据传出去
        public event MyEvent HaveYourMsg;
        public delegate void ClientsChangedEvent(List<string> Clients);//当字典变化时将数据传递给窗体,用于实时显示在线客户端
        public event ClientsChangedEvent ClientsChanged;
        private Dictionary<string, Client> DicSocket = new Dictionary<string, Client>();//字典,用来存储客户端socket对象

        //服务端监听
        public bool Listen(IPAddress ip, int port, int MaxClientNum)
        {
            try
            {
                Socket socketwatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                IPEndPoint endPoint = new IPEndPoint(ip, port);
                socketwatch.Bind(endPoint);
                socketwatch.Listen(MaxClientNum);//开始监听,参数表示最大允许几台主机连接我
                new Thread(new ThreadStart(() =>
                {
                    while (true)
                    {
                        Socket ClientSocket = socketwatch.Accept();//如果有客户端连接,就会返回这个客户端的sockeet对象
                        Thread ClientThread = new Thread(new ParameterizedThreadStart(recmsg));
                        ClientThread.IsBackground = true;//设置为后台线程
                        ClientThread.Start(ClientSocket);//把获取到的客户端Socket作为参数传到线程中
                        //把当前客户端的资源放到字典中管理
                        DicSocket.Add(ClientSocket.RemoteEndPoint.ToString(), new Client(ClientSocket, ClientThread));
                        ClientsChanged.Invoke(DicSocket.Keys.ToList());
                    }
                })).Start();
                return true;
            }
            catch
            {
                return false;
            }
        }
        //向客户端发消息
        public void Send(List<string> SendTo, string str)
        {
            try
            {
                byte[] arrsendmsg = Encoding.UTF8.GetBytes(str);
                foreach (string client in SendTo)
                {
                    if (DicSocket[client].ClientSocket.Send(arrsendmsg) != arrsendmsg.Length)
                        HaveYourMsg.Invoke("提示信息:向" + client + "发送失败!!!");
                }
            }
            catch (Exception ex)
            {
                HaveYourMsg.Invoke("警告:发送遇到错误," + ex.Message);
            }
        }

        public void recmsg(object ClientSocket)
        {
            //给线程传参的一种方式,不可以直接传别的类型
            Socket ThisClientSocket = ClientSocket as Socket;
            while (true)
            {
                try
                {
                    byte[] arrserverrecmsg = new byte[1024];
                    int length = ThisClientSocket.Receive(arrserverrecmsg);
                    if (length <= 0)
                        continue;
                    string ReceiveStr = Encoding.UTF8.GetString(arrserverrecmsg, 0, length);
                    //将收到的字符串通过委托事件传给窗体,用于窗体显示
                    HaveYourMsg.Invoke(ThisClientSocket.RemoteEndPoint.ToString() + ":" + ReceiveStr);
                    //直接转发给所有客户端
                    Send(DicSocket.Keys.ToList(), ThisClientSocket.RemoteEndPoint.ToString() + ":" + ReceiveStr);
                }
                catch
                {
                    HaveYourMsg.Invoke("提示信息:" + ThisClientSocket.RemoteEndPoint.ToString() + "的连接已断开!!!");
                    //断开后将字典中维护的客户端移除掉
                    DicSocket.Remove(ThisClientSocket.RemoteEndPoint.ToString());
                    ClientsChanged.Invoke(DicSocket.Keys.ToList());
                    return;
                }
                Thread.Sleep(200);
            }
        }

        public List<string> GetClients()
        {
            return DicSocket.Keys.ToList();
        }

        public void Colse(List<string> CloseWho)
        {
            foreach (string client in CloseWho)
            {
                if (DicSocket[client].ClientThread.ThreadState == ThreadState.Running)
                    DicSocket[client].ClientThread.Abort();
                //如果接收线程正在执行那么先关闭线程在关闭socket
                DicSocket[client].ClientSocket.Close();
            }
        }
    }
}

3.添加界面逻辑

(1)窗体加载完成后开始监听并绑定相应事件

在这里插入图片描述

(2)发送按钮事件

        private void Button1_Click(object sender, EventArgs e)
        {
            if (checkedListBox1.CheckedItems.Count <= 0)
            {
                MessageBox.Show("未选择客户端");
                return;
            }
            if (textBox2.Text.Length <= 0)
            {
                MessageBox.Show("不能发送空内容");
                return;
            }
            List<string> SelectClients = new List<string>();
            foreach (System.Collections.IEnumerator ThisClient in checkedListBox1.CheckedItems)
            {
                SelectClients.Add(ThisClient.Current.ToString());
            }
            socketTest.Send(SelectClients, textBox2.Text);
        }


三、测试一下

两个项目都编译完成后,先运行服务端
在这里插入图片描述
然后找到客户端的bin目录文件夹,直接打开生成的exe
在这里插入图片描述
可以多打开几个客户端试验一下,像这样
在这里插入图片描述

总结

这样,一个简单的多人聊天室就完成了,比起上一篇文章,这一篇文章应用了容器、委托、内联线程、lambda表达式等技术。如果有写错的地方,还请多多指导。
下面放上项目连接,可以免费下载,仅做学习交流使用。

基于C#socket通信的tcp多人聊天室
使用VS2019直接打开就可以

  • 10
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

jumul

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值