模拟聊天室


在学习了多线程和套接字之后,跟着老师把聊天室程序完整的做了下来,以下是我的总结:

1.       首先:为大家展示一下这个聊天程序的基本样子,如图:


2.       先来看服务器端,在启动服务按钮下完成设置网络通信的IP和端口号,在这里我们就用本地电脑地址127.0.0.1,以后如果联网,将IP地址改为外网IPV4地址即可,端口号可以任意设置。

public Server()
        {
            InitializeComponent();
            TextBox.CheckForIllegalCrossThreadCalls = false;//关闭对文本框跨线程检查
        }
        Thread thread = null;//负责监听客户端连接请求的线程
        Socket socketWatch = null;//负责服务器的监听功能
        private void btnBeginListen_Click(object sender, EventArgs e)
        {
            //获得文本框中的IP地址
            IPAddress address = IPAddress.Parse(txtIP.Text.Trim());
            //创建包含IP和端口号的网络节点对象
            IPEndPoint endpoint = new IPEndPoint(address, int.Parse(txtProt.Text.Trim()));
            //创建服务器负责监听的套接字,参数(使用IP4寻址协议,使用流式连接,使用TCP协议传出数据)
            socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            //将负责监听的套接字绑定到唯一的Ip和端口号上
            socketWatch.Bind(endpoint);
            //设置监听队列长度
            socketWatch.Listen(10);
            //创建线程解决Accept()带来的长时间占用当前线程
            Thread thread = new Thread(WatchConnection);
            //设置后台线程
            thread.IsBackground = true;
            //启动线程
            thread.Start();
            ShowMsg("服务器启动监听成功!");
        }
        void ShowMsg(string msg)
        {
            txtMsg.AppendText(msg + "\n");
        }


3.       这个时候因为没有任何客户端与之通信,可以使用微软提供的命令提示符的方式对其进行下一步操作:打开cmd,在里面输入IP和端口号,如127.0.0.1空格800,这样就可以测试是否可以连接成功

        

//保存了服务器端所有负责和客户端通信的套接字
        Dictionary<string, Socket> dict = new Dictionary<string, Socket>();
        //保存了服务器端所有页面调用通信套接字receive方法的线程
        Dictionary<string, Thread> dictThread = new Dictionary<string, Thread>();
        // 监听客户端请求的方法
        void WatchConnection()
        {
            while (true)//持续不断地监听新的客户端的连接请求
            {
                //创建一个新的套接字,开始监听客户端的连接请求,注意:Accept()会阻断当前线程
                Socket socketConnection = socketWatch.Accept();
                //像列表控件中添加客户端Ip端口字符串,作为客户端唯一标志
               lbOnline.Items.Add(socketConnection.RemoteEndPoint.ToString());
                //将与客户端通信的套接字对象socketConnection添加到键值集合中,并以客户端IP端口作为键
               dict.Add(socketConnection.RemoteEndPoint.ToString(), socketConnection);
                //创建一个新的线程,即通信线程
                ParameterizedThreadStart pts = new ParameterizedThreadStart(ReciveMsg);
                Thread thr = new Thread(pts);
                thr.IsBackground = true;
                thr.SetApartmentState(ApartmentState.STA);//创建并进入一个单线程单元
                thr.Start(socketConnection);
               dictThread.Add(socketConnection.RemoteEndPoint.ToString(), thr);
                //socketConnection.RemoteEndPoint中保存的是当前连接客户端的IP和端口
                ShowMsg("客户端连接成功!" +socketConnection.RemoteEndPoint.ToString());
            }
        }

4.       接下来是服务器向客服端发送数据和接收数据,接收的数据要进行特殊处理,判断数据的是消息还是文件。

//发送消息到客户端
        private void btnsend_Click(object sender, EventArgs e)
        {
            if (string.IsNullOrEmpty(lbOnline.Text))
            {
                MessageBox.Show("请选择要发送的对象!");
            }
            else
            {
                string str = txtsend.Text.Trim();
                //将 要发送的字符串转成utf8对应的字节数组
                byte[] arrMsg = System.Text.Encoding.UTF8.GetBytes(str);
                //获得列表中选中的Key
                string strClientKey =lbOnline.SelectedItem.ToString();
                //通过Key,找到字典集合中对应的某个客户端通信的套接字的send方法,发送给对方
                try
                {
                   dict[strClientKey].Send(arrMsg);
                    ShowMsg("发送了:" + str);
                }
                catch (SocketException ex)
                {
                    ShowMsg("发送数据时异常:" + ex);
                }
                catch (Exception ex)
                {
                    ShowMsg("发送数据时异常:" + ex);
                }
            }
        }
//接收客户端消息
void ReciveMsg(object socketClientPara)
        {
            Socket socketClient = socketClientPara as Socket;
            while (true)
            {
                //定义一个2M的接收缓存区
                byte[] arrMsg = new byte[1024 * 1024 * 2];
                //将接收的数据存入arrMsg数组,并返回真正接收到的数据长度
                int length = -1;
                try
                {
                    length =socketClient.Receive(arrMsg);
                }
                catch (SocketException ex)
                {
                    ShowMsg("异常:" + ex);
                    //从通信套接字删除被中断连接的套接字对象
                   dict.Remove(socketClient.RemoteEndPoint.ToString());
                    //从通信线程中删除被中断连接的套接字对象
                   dictThread.Remove(socketClient.RemoteEndPoint.ToString());
                    //从列表中移除被中断的Ip和端口号
                   lbOnline.Items.Remove(socketClient.RemoteEndPoint.ToString());
                    break;
                }
                catch (Exception ex)
                {
                    ShowMsg("异常:" + ex);
                    break;
                }
                if (arrMsg[0] == 0)//判断接收的数据文本第一个字符是0,代表文字;1,代表文件
                {
                    //此时是将数组所有元素装成字符串,而真正接收到的只有服务端发过来的字符
                    string str = System.Text.Encoding.UTF8.GetString(arrMsg, 1,length - 1);
                    ShowMsg("接收到:" + str);
                }
                else if (arrMsg[0] == 1)
                {
                    //在win7中,一个线程调用SaveFileDialog()方法时,要把该线程设置为thr.SetApartmentState(ApartmentState.STA);
                    SaveFileDialog sfd = new SaveFileDialog();//文件保存对象
                    if (sfd.ShowDialog() ==System.Windows.Forms.DialogResult.OK)             {//用户选择好路径后
                        string fileSave = sfd.FileName;//文件的保存路径
                        using (FileStream fs = new FileStream(fileSave, FileMode.Create))                        {//新建一个文件,保存文件流
                            fs.Write(arrMsg, 1,length - 1);//去掉标识符
                            ShowMsg("文件保存成功" + fileSave);
                        }
                    }
                }
            }
        }


5.       使用群发消息时,我们可以这样做

        

private void btnSendToAll_Click(object sender, EventArgs e)
        {
            string str = txtsend.Text.Trim();
            byte[] arrMsg = System.Text.Encoding.UTF8.GetBytes(str);
            foreach (Socket s in dict.Values)
            {
                s.Send(arrMsg);
           }
            ShowMsg("群发完毕!");
        }

6.       接下来看客户端如何进行操作的,服务器打开后,客户端需要与服务器的IP和端口一致才可以进行通信,这部分和服务器端代码很类似,首先是判断从文本中后的socket通信地址,然后与服务器连接即可。

       

 public Cilent()
        {
            InitializeComponent();
            TextBox.CheckForIllegalCrossThreadCalls = false;
        }
        Socket socketConnect = null;//负责通信的套接字
        private void btnConnect_Click(object sender, EventArgs e)
        {
            IPAddress address = IPAddress.Parse(txtIP.Text.Trim());
            IPEndPoint endPoint = new IPEndPoint(address, int.Parse(txtProt.Text.Trim()));
            socketConnect = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            try
            {
               socketConnect.Connect(endPoint);//建立与远程主机连接
            }
            catch (Exception ex)
            {
                ShowMsg("异常:" + ex);
            }
            Thread thread = new Thread(ReciveMsg);
            thread.IsBackground = true;
            thread.Start();
        }

7.       剩下就是客户端向服务端发送数据和从服务端接收数据。在这里我们将这些数据先转换成二进制流,便于进行TCP数据传输,然后再转换成原来的格式。

//客户端接收数据
void ReciveMsg()
        {
            while (true)
            {
                //定义一个2M的接收缓存区
                byte[] arrMsg = new byte[1024 * 1024 * 2];
                //将接收的数据存入arrMsg数组,并返回真正接收到的数据长度
                int length = -1;
                try
                {
                    length =socketConnect.Receive(arrMsg);
                }
                catch (SocketException ex)
                {
                    ShowMsg("异常:" + ex);
                    break;
                }
                catch (Exception ex)
                {
                    ShowMsg("异常:" + ex);
                    break;
                }
                //此时是将数组所有元素装成字符串,而真正接收到的只有服务端发过来的字符
                string str = System.Text.Encoding.UTF8.GetString(arrMsg, 0,length);
                ShowMsg("接收到:" + str);
            }
        }
        void ShowMsg(string msg)
        {
            txtMsg.AppendText(msg + "\n");
        }
        //向服务器端发送数据
        private void btnsend_Click(object sender, EventArgs e)
        {
            string str = txtsend.Text.Trim();
            if (!string.IsNullOrEmpty(str))
            {
                //将 要发送的字符串转成utf8对应的字节数组
                byte[] arrMsg = System.Text.Encoding.UTF8.GetBytes(str);
                //通过Key,找到字典集合中对应的某个客户端通信的套接字的send方法,发送给对方
                byte[] arrMsgSend = new byte[arrMsg.Length + 1];
                arrMsgSend[0] = 0;
                Buffer.BlockCopy(arrMsg, 0, arrMsgSend, 1,arrMsg.Length);
                socketConnect.Send(arrMsgSend);
                ShowMsg("发送了:" + str);
            }
        }


8.       为了扩展聊天室功能,为它添加了文件传输功能,这里主要是对文件选择和文件处理进行介绍。

       

 //选择发送的文件
        private void btnCheckFile_Click(object sender, EventArgs e)
        {
            OpenFileDialog ofd = new OpenFileDialog();
            if (ofd.ShowDialog() ==System.Windows.Forms.DialogResult.OK)
            {
                txtFile.Text = ofd.FileName;
            }
        }
        //向服务器发送文件
        private void btnSendFile_Click(object sender, EventArgs e)
        {
            //用文件流打开用户选择的文件
            using (FileStream fs = new FileStream(txtFile.Text, FileMode.Open))
            {
                byte[] arrMsg = new byte[1024 * 1024 * 2];
                //将文件数据存入arrMsg数组,并返回真正接收到的数据长度
                int length = fs.Read(arrMsg, 0,arrMsg.Length);
                //新建一个比原来数组长度加1的数组
                byte[] arrFileSend = new byte[length + 1];
                //将数组的第一个值设为标识符,1代表是文件类型
                arrFileSend[0] = 1;
                //数组复制1
                //for (int i = 0; i < length; i++)
                //{
                //   arrFileSend[i + 1] = arrMsg[i];
                //}
                //数组复制2
                //arrMsg.CopyTo(arrFileSend, length);
                //数组复制3,将arrMsg数组里的元素,从第0个开始拷贝,拷贝到arrFileSend数组里,从第1个位置开始存放,一共拷贝length个长度
                Buffer.BlockCopy(arrMsg, 0, arrFileSend, 1,length);
                //发送包含了标识符的新数组到服务器
                socketConnect.Send(arrFileSend);
            }
        }

9.       经过这个小项目,使我在委托,线程,套接字,文件处理等方面都有了新的认识,也对现有的知识进行了巩固,感谢传智老师认真详细的见解。



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值