黑马程序员----简单的多线程聊天框的编写

来源:互联网 发布:国家大数据项目 编辑:程序博客网 时间:2024/06/04 20:11

---------------------- <a href="http://edu.csdn.net"target="blank">ASP.Net+Android+IOS开发</a>、<a href="http://edu.csdn.net"target="blank">.Net培训</a>、期待与您交流! ----------------------

    今天认真观看了黑马班基础视频中的C#中的多线程及简单的聊天框基础视频.然后根据老师的写法还有自己认为一些地方可以更加完善的以及加深对程序和基础知识的理解,对程序做了自己的想法,并且有相应的改动.然后自己动趁热打铁写了一遍,并且加上相应的注释,以便于日后相关方面的基础知识可以参考,也和前面的SQL等一样的风格,也方便自己复习.

<1>服务端可以自己搜索所有网卡的IP4地址,然后让用户选择绑定哪个地址和端口绑定监听.

<2>服务端和用户端可以互相发送文字或者文件.

<3>服务端可以同时连接多客户端,并且互相不影响通信.

<4>对一些网络通信异常做相应的处理.

<5>完善界面.

项目文件下载:http://pan.baidu.com/s/1jGlyM7G

Sever端:

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;//socket及IPAddress命名空间.using System.Text;using System.Threading.Tasks;using System.Windows.Forms;using System.Threading;using System.IO;//线程的命名空间namespace CSocketServer{    public partial class Form1 : Form    {        public Form1()        {            InitializeComponent();            //关闭跨线程操作检查.            TextBox.CheckForIllegalCrossThreadCalls = false;            //定义一个方法,在程序初始化的时候,得到本机的所有IP4地址.包括多网卡情况,并将这些地址放在listbox中.            TakeLocalIp();            //初始化将发送文字的发送按钮设置为不可用,在连接以后释放.            btnSend.Enabled = false;        }        //获得本机的IP4地址.        void TakeLocalIp()        {            IPHostEntry ipe = Dns.GetHostEntry(Dns.GetHostName());            //遍历获得的所有IP地址.            foreach (IPAddress ip in ipe.AddressList)            {                //如果是IP4地址就将地址加到listbox中.                if (ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)                    listOnLine.Items.Add(ip.ToString());            }            //如果没有获得任何IP4地址,则提示并返回.            if (listOnLine.Items.Count <= 0)            {                ShowMsg("获取本机IP失败.");                return;            }            //选中第一个.            listOnLine.SelectedIndex = 0;            //输出消息提示用户.初始化完毕.            ShowMsg("请选择一个IP地址,然后点击启动服务.");        }        //定义一个线程,用来监听用户的连接        Thread threadAcc = null;        //定义一个线程,用来接受客户端发送的消息.        Thread threadRev = null;        //定义用来绑定端口的socket        Socket socketServer = null;        //定义一个Dictionary以终结点的ip端口为key以连接的socket为值.        Dictionary<string, Socket> dictSocket = new Dictionary<string, Socket>();        private void button1_Click(object sender, EventArgs e)        {            IPAddress ipLocal;            //如果当前没有选中一个IP.提示并返回.            if (listOnLine.Text == null || listOnLine.Text == "")            {                ShowMsg("没有选中任何IP.");                return;            }            //如果有选中,则将选中的IP地址转换为IPAddress.因为上面是IPAddress通过系统转过来的,所以不做格式检查.            ipLocal = IPAddress.Parse(listOnLine.Text);            int ServerPort = 0;            //将输入的端口号尝试转换成一个int.如果转换失败,或者转换完成后,端口号<=0,则提示并返回.            if (!int.TryParse(txtPort.Text.Trim(), out ServerPort) || ServerPort <= 0)            {                ShowMsg("端口号输入有误!请重新输入.");                return;            }            //将本机启动监听的IP打印出来.            ShowMsg("本机IP:" + ipLocal.ToString());            //建立用来绑定的终结点.            IPEndPoint endPoint = new IPEndPoint(ipLocal, ServerPort);            //创建用来绑定的socket,为IP4,流式,TCP的套接字.            socketServer = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);            //绑定终结点.            socketServer.Bind(endPoint);            ShowMsg("绑定端口成功..");            //队列数为10            socketServer.Listen(10);            ShowMsg("监听启动成功..");            //新建线程用来监听连接进来的用户.            //创建多线程使用new Thread创建即可.            //IsBackground将线程设置为前台线程或者后台线程.            //所有的前台线程关闭以后,程序才算退出.只要所有的前台线程退出,后台线程自动退出.            //线程中是一个委托,没有返回值.可以有一个参数,参数类型为Object            threadAcc = new Thread(Acc);            threadAcc.IsBackground = true;            //调用Thread的Start方法启动线程.            //Start()方法重载了有一个Object参数的方法.所有的方法都可以通过C#多态的特点,转换为object对象然后传送过去.            //比如List<> Dictionary<>这样的泛型集合.            threadAcc.Start();            ShowMsg("服务端启动成功!");        }        //用来监听连接进来的用户,每成功连接进来的用户,将用户的IP和端口号存在listbox中,然后用这个作为key,然后将连接的sokcet存到Dictionary中.        void Acc()        {            //先将listbox清空.            listOnLine.Items.Clear();            //建立一个用来循环的socket.            Socket socketConnTemp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);            while (true)            {                //获得连接的socket.                socketConnTemp = socketServer.Accept();                //添加到dictionary中.                dictSocket.Add(socketConnTemp.RemoteEndPoint.ToString(), socketConnTemp);                //然后将这个终结点转换成string放到list中.                listOnLine.Items.Add(socketConnTemp.RemoteEndPoint.ToString());                //提示用户.                ShowMsg("连接成功:" + socketConnTemp.RemoteEndPoint.ToString());                //新建一个线程,每次新建的名字都叫threadRev,并且抛弃管理这个线程,当这个线程中Rev因异常终端,则这个线程自动停止执行.                //视频中用一个将远程连接的终结点的信息转化为string和thread来存储到Dictionary<string,thread>这样来保存这个线程                //这样方便管理,但是我是在看了视频之前自己做的,所以暂时不做修改.                 threadRev = new Thread(Rev);                threadRev.IsBackground = true;                //并将每次连接过后的套接字传送过去.                threadRev.Start((Object)socketConnTemp);                //有用户连接以后将发送按钮释放.                btnSend.Enabled = true;            }        }        //新建的用于接收消息的套接字处理方法.        void Rev(object socketRev)        {            //拿到套接字,新建一个线程.            Socket forRevClient = (Socket)socketRev;            byte[] bytesRev = new byte[1024 * 1024];            int numRev = 0;            //当发生异常时候退出.我不知道有没有别的异常,我碰到的主要是因为先关服务端或者客户端然后造成连接中断的异常.            //杨中科老师说程序中间尽量不要使用try..catch..,但是目前不知道怎么做.先用着.            try            {                while (true)                {                    numRev = forRevClient.Receive(bytesRev);                    //如果接受的首位是0,那么就为文字.然后从第二位开始读取,将读取出来的文字打印出来.                    //这里在文字的长度上比较容易出错.                    if (bytesRev[0]!=1)                    {                        ShowMsg("接收到" + forRevClient.RemoteEndPoint.ToString() + "发送的消息共" + numRev.ToString() + "个字节:");                        ShowMsg(System.Text.Encoding.UTF8.GetString(bytesRev, 1, numRev-1));                        //如果第一位不是1,表示不是一个文件,那么当做文字显示以后,直接进入下一次循环.                        continue;                    }                    //提示用户接受到文件,包括字节数.其实更应该将文件的名字一同传过来.可以在前面做个flag这样子.这样的话显示的更好.                    //调用另存为对话框,然后ShowDialog后面要带参数,否则显示在后台线程中,看不到.                    ShowMsg("接受到文件,共" + numRev.ToString() + "字节..");                    SaveFileDialog dlg = new SaveFileDialog();                    if (dlg.ShowDialog(this) == DialogResult.OK)                    {                        ShowMsg("开始处理.");                        //将一个byte数组转换成FileStream流,然后把这个流写成一个文件                        //打开的另存为对话框没有新建线程,其实是会阻塞当前线程.不做修改                        //使用using关键词,使用完即时释放流.比较占内存.                        using (FileStream fs = new FileStream(dlg.FileName, FileMode.Create))                        {                            ShowMsg("写入文件.");                            //从第二位开始写,然后将接受到的byte数组减一位,存储为文件.                            fs.Write(bytesRev, 1, bytesRev.Length - 1);                        }                        ShowMsg("文件存储到:" + dlg.FileName);                    }                    else//如果没有点击OK,则默认放弃存储.                    {                        ShowMsg("放弃存储文件....");                        continue;                    }                }            }            catch (Exception exp)            {                //得到显示异常信息                ShowMsg(forRevClient.RemoteEndPoint.ToString() + "消息:");                ShowMsg(exp.Message);            }        }        void ShowMsg(string Msg)        {            txtShow.Text += (Msg + "\r\n");        }        //点击发送按钮        private void btnSend_Click(object sender, EventArgs e)        {            //获得发送文本框中的值            string strSend = txtSend.Text.Trim();            //如果文本框为空,则取消发送,提示用户并返回.            if (strSend == null || strSend == "")            {                ShowMsg("发送数据为空!");                return;            }            //如果用户没有选择发送对象,则提示用户,并返回.            if (listOnLine.Text == "")            {                ShowMsg("请选择一个在线客户端!");                return;            }            //定义一个byte数组,将发送的文字转换成数组并赋值给这个定义的数组.            byte[] bytesend = System.Text.Encoding.UTF8.GetBytes(strSend);            //发送这个数组,并取得发送的字数.            try            {                int numSend = dictSocket[listOnLine.Text].Send(bytesend);                //将发送的情况打印出来.                ShowMsg("共发送" + numSend.ToString() + "个字节到" + listOnLine.Text + ":");                ShowMsg(strSend);                //清空发送框                txtSend.Text = null;            }            catch (Exception exp)            {                //在服务器端,如果这个线程报错,则将这个线程所使用的那个socket从Dictionary中拿掉,并从表中拿掉                //其实应该写一个方法封装将dictionary刷新到list中最好.然后在别的地方直接刷新就可以了.更容易懂,代码简洁,符合封装的思想.                ShowMsg(exp.Message);                ShowMsg("连接出错,删除连接:"+listOnLine.Text);                dictSocket.Remove(listOnLine.Text);                listOnLine.Items.Remove(listOnLine.Text);            }        }    }}

Client端:

using System;using System.Collections.Generic;using System.ComponentModel;using System.Data;using System.Drawing;using System.Linq;using System.Text;using System.Threading.Tasks;using System.Windows.Forms;using System.Net;using System.Threading;using System.Net.Sockets;using System.IO;namespace CSocketClient{    public partial class Form1 : Form    {        public Form1()        {            InitializeComponent();            //强制关闭跨线程操作检查.            TextBox.CheckForIllegalCrossThreadCalls = false;            //初始化发送按钮不可用,只有当连接以后,按键才可用            btnsend.Enabled = false;        }        //定义一个新线程,用来监听接收消息        Thread threadRev = null;        //定义一个全局套接字,用来和服务端连接的        Socket socketConn=null;        //点击连接服务器按键        private void btnConnection_Click(object sender, EventArgs e)        {            //获取IP和端口两个文本框内的值            string strAddress = txtServerIp.Text.Trim();            string strPoint = txtServerport.Text.Trim();            //定义即将需要连接的主机的IP地址            IPAddress  ipServerAddr ;            //尝试将地址文本转换成IPAddress            if (!IPAddress.TryParse(strAddress,out ipServerAddr))            {                //如果转换失败,提示并返回.                showMsg("地址不正确,请重新输入.");                return;            }            //定义即将连接服务器的端口号            int serverPort;            //尝试将端口文本转换为数字,如果失败,提示并返回.            if (!int.TryParse(strPoint, out serverPort))            {                showMsg("端口格式不正确,请重新输入");                return;            }            //如果IPAddress和端口号转换完成,则开始创建终结点.            IPEndPoint endPoint = new IPEndPoint(ipServerAddr, serverPort);            //new一个将要用来和主机进行连接的套接字.            socketConn = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);            //尝试连接主机,如果连接失败,则打印失败信息,并返回.            try            {                socketConn.Connect(endPoint);            }            catch(Exception exp)            {                showMsg(exp.Message);                return;            }                        showMsg("连接服务器成功");            //创建新线程来获取服务器发送的消息.            threadRev = new Thread(Rev);            //设置为后台线程,程序关闭后线程关闭.            threadRev.IsBackground = true;            //启动线程            threadRev.Start();            btnsend.Enabled = true;        }        void Rev()        {            //初始化一个1MB的接受缓存.            byte[] bytesRev=new byte[1024*1024];            //定义一个整数来存储接受的字符数量.            int numRev;            //使用try catch,如果连接中断,让程序的错误打印到msg中.            try            {                //不停的循环去获取服务端发送的消息                while (true)                {                    //获取消息,并且返回接收到的数量.                    numRev = socketConn.Receive(bytesRev);                    showMsg("收到服务端" + socketConn.RemoteEndPoint.ToString() + "共"+numRev.ToString()+"个字节:");                    //将获取到的byte数组转换成string,并且打印出来.                    showMsg(System.Text.Encoding.UTF8.GetString(bytesRev, 0, numRev));                }            }            catch (Exception exp)            {                //打印异常信息.                showMsg(exp.Message);            }        }        //在ricktextbox中输出消息的方法.        void showMsg(string Msg)        {            txtShow.Text+=(Msg+"\r\n");        }        //发送字符给服务器端        private void btnsend_Click(object sender, EventArgs e)        {            //获取发送框中的值,判断是否为空,为空则提示用户并退出.            string strSend=txtSend.Text.Trim();            if(strSend==null||strSend=="")            {                showMsg("发送消息为空!请重新输入!");                return;            }            //同服务端一样,创建一个byte数组用来传送数据,并在客户端也打印发送信息.            byte[] bytesSend=new byte[strSend.Length+1];            System.Text.Encoding.UTF8.GetBytes(strSend.ToArray(),0,strSend.Length,bytesSend,1);            bytesSend[0] = 0;            int numSend=socketConn.Send(bytesSend);            showMsg("发送给"+socketConn.RemoteEndPoint.ToString()+" 共"+numSend.ToString()+"个字节:");            showMsg(strSend);        }        //点击浏览,打开一个打开文件对话框,然后将文件的路径写到前面的textbox中.        private void btnBrowser_Click(object sender, EventArgs e)        {            OpenFileDialog dlg=new OpenFileDialog();            if (dlg.ShowDialog() == DialogResult.OK)            {                txtFilePath.Text = dlg.FileName;            }        }        //点击发送文件按钮        private void btnSendFile_Click(object sender, EventArgs e)        {            //取得文件路径,并判断是否为空,如果为空,则提醒并返回.            string strFilePath = txtFilePath.Text;            if (strFilePath == "" || strFilePath == null)            {                showMsg("请选择一个文件!");                return;            }            //新建一个5MB的缓存字节组.            //使用using关键词,即时释放使用完的内存.            byte[] bytesSendFile=new byte[1024*1024*5];            using (FileStream fs = new FileStream(strFilePath, FileMode.Open))            {                //将数组第一位设置为1,标识是个文件,在发送文字的时候,同样将第一位标识为0,这样在接收的时候可以正常判断.                int numReadToBytes = fs.Read(bytesSendFile, 1, bytesSendFile.Length - 1);                bytesSendFile[0] = 1;                int sendSize = socketConn.Send(bytesSendFile, numReadToBytes + 1, SocketFlags.None);                showMsg("发送文件成功,共发送" + sendSize.ToString() + "字节.");            }        }    }}



--------------------- <a href="http://edu.csdn.net"target="blank">ASP.Net+Android+IOS开发</a>、<a href="http://edu.csdn.net"target="blank">.Net培训</a>、期待与您交流! ----------------------
Server端:

--------------------- <a href="http://edu.csdn.net"target="blank">ASP.Net+Android+IOS开发</a>、<a href="http://edu.csdn.net"target="blank">.Net培训</a>、期待与您交流! ----------------------

0 0