聊天程序(基于Socket、Thread)

一笑奈何 提交于 2020-04-06 19:34:00

聊天程序简述


1、目的:主要是为了阐述Socket,以及应用多线程,本文侧重Socket相关网路编程的阐述。如果您对多线程不了解,大家可以看下我的上一篇博文浅解多线程 。

2、功能:此聊天程序功能实现了服务端跟多个客户端之间的聊天,可以群发消息,选择ip发消息,客户端向服务端发送文件。 (例子为WinForm应用程序)


Socket,端口,Tcp,UDP。 概念


 1、Socket还被称作“套接字”,应用程序通常通过套接字向网络发送请求或者应答网络请求。根据连接启动的方式以及本地套接字要连接的目标,套接字之间的连接过程可以分为三个步骤:服务器监听,客户端请求,连接确认。

2、端口:可以认为是计算机与外界通讯交流的出口。

3Tcp TCP是一种面向连接(连接导向)的、可靠的、基于字节流的运输层通信协议。UDP是另一个重要的传输协议。

4UDP:用户数据报协议,是一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。


理解Socket,端口,Tcp,UDP


1、ip跟端口的作用:例如,你用QQ跟好友聊天,首先QQ要知根据好友所在电脑的IP地址发送信息,ip地址能确定好友的所在的电脑,但是不知道好友电脑上的QQ应用程序是哪一个,这就需要QQ提供一个端口号来确定你发过来的信息是QQ接受的数据。这样就简单的阐述了Ip跟端口的作用。

2、Tcp,Udp作用以及差异:首先要说的是,这是两种网路协议,他们的差别就是TCP协议中包含了专门的传递保证机制,当数据接收方收到发送方传来的信息时,会自动向发送方发出确认消息;发送方只有在接收到该确认消息之后才继续传送其它信息,否则将一直等待直到收到确认信息为止。与TCP不同,UDP协议并不提供数据传送的保证机制。如果在从发送方到接收方的传递过程中出现数据报的丢失,协议本身并不能做出任何检测或提示。我们.net程序员一般的应用程序用的都是Tcp协议。但是Tcp协议的执行速度,效率不及Udp快。看别人的博客感觉图解这两个协议,显得更直观点。上图:



3Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。出自同一篇博客的图。

4、到这里如果你对Socket,还不是很清楚透彻,那么在接下来的聊天程序代码中,我还会一点点的阐述。


 创建服务端监听功能———聊天程序(SocketThread)


服务端监听服务是创建一个Socket等待接收客户端的信息。这个需要绑定服务端的Ip、端口号,以便于客户端发送请求的时候找准确服务端聊天程序的具体位置。此外这个Socket还需要设置监听序列的大小,告知应用程序一次性最多处理客户端发来信息的多少。然后创建一个接收客户端通信的Socket,等待客户段发来的信息。

 Socket sck = null;

        //点击开启服务端监听

        private void btn_StarServer_Click(object sender, EventArgs e)

        {

            //创建一个Socket实例

            //第一个参数表示使用ipv4

            //第二个参数表示发送的是数据流

            //第三个参数表示使用的协议是Tcp协议

            sck = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            //获取ip地址

            IPAddress ip = IPAddress.Parse(txt_ip.Text);

            //创建一个网络通信节点,这个通信节点包含了ip地址,跟端口号。

            //这里的端口我们设置为1029,这里设置大于1024,为什么自己查一下端口号范围使用说明。

            IPEndPoint endPoint = new IPEndPoint(ip, int.Parse(txt_port.Text));

            //Socket绑定网路通信节点

            sck.Bind(endPoint);

            //设置监听队列

            sck.Listen(10);

            ShowMsg("开启监听!");

 

            //创建一个接收客户端通信的Socket

            Socket accSck = sck.Accept();

            //如果监听到客户端有链接,则运行到下一部,提示,链接成功!

            ShowMsg("链接成功!");

        }

 

 //消息框里面数据

        void ShowMsg(string str)

        {

            string Ystr="";

            if (txt_AccMsg.Text != "")

            {

                Ystr = txt_AccMsg.Text + "\r\n";

            }

            txt_AccMsg.Text = Ystr+str;

        }

 

问题1代码中的Socket accSck = sck.Accept();这个Socket是让上一个绑定服务端ip端口号的Socket一直处于等待接受客户端发送信息的状态,所以一直占用应用程序一直默认开启的Ui线程,致使点击开启服务监听后,界面无响应。

解决办法:使用多线程,我们在这里写一个自己的线程让这里的监听服务,写在自己的线程里面。修改代码如下:

  Socket sck = null;

        Thread thread = null;

        //点击开启服务端监听

        private void btn_StarServer_Click(object sender, EventArgs e)

        {

            //创建一个Socket实例

            //第一个参数表示使用ipv4

            //第二个参数表示发送的是数据流

            //第三个参数表示使用的协议是Tcp协议

            sck = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            //获取ip地址

            IPAddress ip = IPAddress.Parse(txt_ip.Text);

            //创建一个网络通信节点,这个通信节点包含了ip地址,跟端口号。

            //这里的端口我们设置为1029,这里设置大于1024,为什么自己查一下端口号范围使用说明。

            IPEndPoint endPoint = new IPEndPoint(ip, int.Parse(txt_port.Text));

            //Socket绑定网路通信节点

            sck.Bind(endPoint);

            //设置监听队列

            sck.Listen(10);

            ShowMsg("开启监听!");

 

            //开启一个线程,放入Socket服务监听,上一篇博文中没有介绍这样的线程实例化方法。这里特别说下这样是可以的。

            Thread thread = new Thread(JtSocket);

            //设置为后台线程

            thread.IsBackground = true;

            thread.Start();

        }

 

        //Socket服务监听函数

        void JtSocket()

        {           

            //创建一个接收客户端通信的Socket

            Socket accSck = sck.Accept();

            //如果监听到客户端有链接,则运行到下一部,提示,链接成功!

            ShowMsg("链接成功!");

        }

问题2:代码中sck.Listen(10);设置监听序列,这里设置为10是不是,服务端只能处理10个客户段的请求呢。

答:不是的这里设置的是一次性只能处理10个,如果还有更多就在后面排队,等待这10个处理完成,接下来在处理排着对的信息。

开启服务监听看一下我们的聊天界面:

然后我们再做一个客户端,链接到服务端。


创建客户端链接服务端的Socket———聊天程序(SocketThread)


如果链接服务端的聊天程序则需要知道服务端的Ip地址,端口号。

Socket clientSocket = null;

        Thread thread = null;

        //通过IP地址与端口号与服务端建立链接     

        private void btn_ConServer_Click(object sender, EventArgs e)

        {

            clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            //这里的ip地址,端口号都是服务端绑定的相关数据。

            IPAddress ip = IPAddress.Parse(txt_Clientip.Text);

            IPEndPoint endpoint = new IPEndPoint(ip, Convert.ToInt32(txt_Clientport.Text));

            clientSocket.Connect(endpoint);//链接有端口号与IP地址确定服务端.

        }

然后点击连接服务,查看我们的聊天界面。(首先先打开服务端应用程序,点击开启监听,然后打开客户端应用程序,点击链接服务)

链接成功后,下一步,我们就开始我们的聊天信息接收发送了。


服务端向客户端发送信息,客户端接受信息———聊天程序(SocketThread)


1、这里我们发送消息是通过Tcp协议以 字节数组的类型形式发送,所以在发送之前我们需要把要发送,接收的数据做一个转换为字节数组的类型。

2、客户端通过创建的链接服务端的SocketReceive方法接收消息,服务端通过创建的接受客户端信息的SocketSend方法发送消息。

服务端代码:

 Socket sck = null;

        Thread thread = null;

        //点击开启服务端监听

        private void btn_StarServer_Click(object sender, EventArgs e)

        {

            //创建一个Socket实例

            //第一个参数表示使用ipv4

            //第二个参数表示发送的是数据流

            //第三个参数表示使用的协议是Tcp协议

            sck = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            //获取ip地址

            IPAddress ip = IPAddress.Parse(txt_ip.Text);

            //创建一个网络通信节点,这个通信节点包含了ip地址,跟端口号。

            //这里的端口我们设置为1029,这里设置大于1024,为什么自己查一下端口号范围使用说明。

            IPEndPoint endPoint = new IPEndPoint(ip, int.Parse(txt_port.Text));

            //Socket绑定网路通信节点

            sck.Bind(endPoint);

            //设置监听队列

            sck.Listen(10);

            ShowMsg("开启监听!");

 

            //开启一个线程,放入Socket服务监听,上一篇博文中没有介绍这样的线程实例化方法。这里特别说下这样是可以的。

            Thread thread = new Thread(JtSocket);

            //设置为后台线程

            thread.IsBackground = true;

            thread.Start();

        }

 

        Socket accSck = null;

        //Socket服务监听函数

        void JtSocket()

        {

            while (true)//注意该循环,服务端要持续监听,要不然一个客户端链接过后就无法链接第二个客户端了。

            {

                //创建一个接收客户端通信的Socket

                accSck = sck.Accept();

                //如果监听到客户端有链接,则运行到下一部,提示,链接成功!

                ShowMsg("链接成功!");

            }

        }

 

        //消息框里面数据

        void ShowMsg(string str)

        {

            string Ystr="";

            if (txt_AccMsg.Text != "")

            {

                Ystr = txt_AccMsg.Text + "\r\n";

            }

            txt_AccMsg.Text = Ystr+str;

        }

 

        //向客户端发送数据

        private void btn_SendSingleMsg_Click(object sender, EventArgs e)

        {

            string SendMsg = txt_SendMsg.Text;

            if (SendMsg != "")

            {

                byte[] buffer = System.Text.Encoding.UTF8.GetBytes(SendMsg); //将要发送的数据,生成字节数组。

                accSck.Send(buffer);

                ShowMsg("向客户端发送了:" + SendMsg);

            }

        }

客户端代码:

  public Form1()

        {

            InitializeComponent();

            TextBox.CheckForIllegalCrossThreadCalls = false;

        }

        Socket clientSocket = null;

        Thread thread = null;

        //通过IP地址与端口号与服务端建立链接     

        private void btn_ConServer_Click(object sender, EventArgs e)

        {

            clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            //这里的ip地址,端口号都是服务端绑定的相关数据。

            IPAddress ip = IPAddress.Parse(txt_Clientip.Text);

            IPEndPoint endpoint = new IPEndPoint(ip, Convert.ToInt32(txt_Clientport.Text));

            clientSocket.Connect(endpoint);//链接有端口号与IP地址确定服务端.

 

            //客户端在接受服务端发送过来的数据是通过Socket 中的Receive方法,

            //该方法会阻断线程,所以我们自己为该方法创建了一个线程

            thread = new Thread(ReceMsg);           

            thread.IsBackground = true;//设置后台线程

            thread.Start();

        }

        //接收服务端数据

        public void ReceMsg()

        {

            while (true)

            {

                byte[] buffer = new byte[1024 * 1024 * 2];

                clientSocket.Receive(buffer);//接收服务端发送过来的数据

                string ReceiveMsg = System.Text.Encoding.UTF8.GetString(buffer);//把接收到的字节数组转成字符串显示在文本框中。

                ShowMsg("接收到数据:" + ReceiveMsg);

            }

        }

 

        //消息框里面数据

        void ShowMsg(string str)

        {

            string Ystr = "";

            if (txt_ClientMsg.Text != "")

            {

                Ystr = txt_ClientMsg.Text + "\r\n";

            }

            txt_ClientMsg.Text = Ystr + str;

        }

启动服务端应用程序,点击启动服务监听,启动客户端应用程序,点击连接服务,然后在消息框内输入消息,点击发送。运行效果如下。


 

 接下来做客户端向服务端发送消息:


 客户端向服务端发送信息(文件,字符串),客户端接受信息———聊天程序(SocketThread)


1、这里我们发送不仅只有字符串还有文件。他们都是一字节数组的类型发送出去,区别字符串和文件的思想是:把字节数组的第一个值设置为01,用来区分。

2、这里发送的文件接受的时候,重命名,还要为他写上后缀名。没有深入写。

3、这里客户端连接服务端的成功后,把客户端的ip端口号,写入list列表中,同时也存入Dictionary<string, Socket> socketDir集合中,便于服务端与多个客户端连接时,选择发送信息。同时也避免了,不知道发送给哪个客户端数据。

客户端代码:

using System;

using System.Collections.Generic;

using System.ComponentModel;

using System.Data;

using System.Drawing;

using System.Linq;

using System.Text;

using System.Windows.Forms;

using System.Net.Sockets;

using System.Net;

using System.Threading;

using System.IO;

 

namespace CharClient

{

    public partial class Form1 : Form

    {

        public Form1()

        {

            InitializeComponent();

            TextBox.CheckForIllegalCrossThreadCalls = false;

        }

        Socket clientSocket = null;

        Thread thread = null;

        //通过IP地址与端口号与服务端建立链接     

        private void btn_ConServer_Click(object sender, EventArgs e)

        {

            clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            //这里的ip地址,端口号都是服务端绑定的相关数据。

            IPAddress ip = IPAddress.Parse(txt_Clientip.Text);

            IPEndPoint endpoint = new IPEndPoint(ip, Convert.ToInt32(txt_Clientport.Text));

            clientSocket.Connect(endpoint);//链接有端口号与IP地址确定服务端.

 

            //客户端在接受服务端发送过来的数据是通过Socket 中的Receive方法,

            //该方法会阻断线程,所以我们自己为该方法创建了一个线程

            thread = new Thread(ReceMsg);           

            thread.IsBackground = true;//设置后台线程

            thread.Start();

        }

        //接收服务端数据

        public void ReceMsg()

        {

            while (true)

            {

                byte[] buffer = new byte[1024 * 1024 * 2];

                clientSocket.Receive(buffer);//接收服务端发送过来的数据

                string ReceiveMsg = System.Text.Encoding.UTF8.GetString(buffer);//把接收到的字节数组转成字符串显示在文本框中。

                ShowMsg("接收到数据:" + ReceiveMsg);

            }

        }     

        //消息框里面数据

        void ShowMsg(string str)

        {

            string Ystr = "";

            if (txt_ClientMsg.Text != "")

            {

                Ystr = txt_ClientMsg.Text + "\r\n";

            }

            txt_ClientMsg.Text = Ystr + str;

        }

        //向服务端发送消息

        private void btn_ClientSendSingleMsg_Click(object sender, EventArgs e)

        {

            string txtMsg = txt_ClientSendMsg.Text;

            byte[] buffer = System.Text.Encoding.UTF8.GetBytes(txtMsg);

            byte[] newbuffer = new byte[buffer.Length + 1];//定义一个新数组

            newbuffer[0] = 0;//设置标识,表示发送的是字符串

            Buffer.BlockCopy(buffer, 0, newbuffer, 1, buffer.Length);//源数组中的数据拷贝到新数组中

            clientSocket.Send(newbuffer);//发送新数组中的数据

        }

 

        //向服务端发送文件

        private void btn_ClientSendfile_Click(object sender, EventArgs e)

        {

            using (FileStream fs = new FileStream(txt_ClientFile.Text, FileMode.Open))

            {

                byte[] buffer = new byte[1024 * 1024 * 2];

                int readLength = fs.Read(buffer, 0, buffer.Length);

 

                byte[] newbuffer = new byte[readLength + 1];//定义一个新的数组,然后将原有数组中的数据拷贝该数组中。

                newbuffer[0] = 1;//将第一单元设置为1,表示传送的是文件.

 

                //将数据有一个数组拷贝到另一个数组.

                //第一参数:表示源数组

                //第二个:表示从源数组中的哪个位置开始拷贝

                //第三个:表示目标数组。

                //第四个:表示从目标数组的哪个位置开始填充.

                //五:表示:拷贝多少数据

                Buffer.BlockCopy(buffer, 0, newbuffer, 1, readLength);

                clientSocket.Send(newbuffer);

            }

        }

        //打开文件夹,选择要发送的文件

        private void Btn_see_Click(object sender, EventArgs e)

        {

            OpenFileDialog openfile = new OpenFileDialog();

            if (openfile.ShowDialog() == DialogResult.OK)

            {

                txt_ClientFile.Text = openfile.FileName;

            }

        }

 

    }

}

服务端代码:

using System;

using System.Collections.Generic;

using System.ComponentModel;

using System.Data;

using System.Drawing;

using System.Linq;

using System.Text;

using System.Windows.Forms;

using System.Net.Sockets;

using System.Net;

using System.Threading;

using System.IO;

 

namespace ChatServer

{

    public partial class Form1 : Form

    {

        public Form1()

        {

            InitializeComponent();

            TextBox.CheckForIllegalCrossThreadCalls = false;

        }

 

        Socket sck = null;

        Thread thread = null;

        //点击开启服务端监听

        private void btn_StarServer_Click(object sender, EventArgs e)

        {

            //创建一个Socket实例

            //第一个参数表示使用ipv4

            //第二个参数表示发送的是数据流

            //第三个参数表示使用的协议是Tcp协议

            sck = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            //获取ip地址

            IPAddress ip = IPAddress.Parse(txt_ip.Text);

            //创建一个网络通信节点,这个通信节点包含了ip地址,跟端口号。

            //这里的端口我们设置为1029,这里设置大于1024,为什么自己查一下端口号范围使用说明。

            IPEndPoint endpoint = new IPEndPoint(ip, Convert.ToInt32(txt_port.Text));//创建一个网络通信节点,该节点中包含了IP地址和端口号.

            //Socket绑定网路通信节点

            sck.Bind(endpoint);

            //设置监听队列

            sck.Listen(10);

            ShowMsg("开启监听!");

 

            //开启一个线程,放入Socket服务监听,上一篇博文中没有介绍这样的线程实例化方法。这里特别说下这样是可以的。

            Thread thread = new Thread(ConnectAccept);

            //设置为后台线程

            thread.IsBackground = true;

            thread.Start();

        }

 

        //消息框里面数据

        void ShowMsg(string str)

        {

            string Ystr="";

            if (txt_AccMsg.Text != "")

            {

                Ystr = txt_AccMsg.Text + "\r\n";

            }

            txt_AccMsg.Text = Ystr+str;

        }

 

        //向客户端发送数据

        private void btn_SendSingleMsg_Click(object sender, EventArgs e)

        {

            string sendMsg = this.txt_SendMsg.Text;//获取要发送到客户端的文本

            byte[] buffer = System.Text.Encoding.UTF8.GetBytes(sendMsg);//生成字节数组

            if (!string.IsNullOrEmpty(this.lsb_Ips.Text))

            {

                string ipendpoint = this.lsb_Ips.Text;//在服务端,选择与客户端进行通信的IP地址与端口号

                socketDir[ipendpoint].Send(buffer);//向客户端发送数据

 

                ShowMsg("向客户端发送了:" + sendMsg);

            }

            else

            {

                MessageBox.Show("请选择与哪个客户端进行通信");

            }

        }

 

        //  Socket newSoket = null;//.:不能将与客户端进行通信的Socket定义成全局的.

        Dictionary<string, Socket> socketDir = new Dictionary<string, Socket>();//将每一个与客户端进行通信的Socket放到该集合中.

        public void ConnectAccept()

        {

            while (true)//注意该循环,服务端要持续监听

            {

                Socket newSoket = sck.Accept();//接收客户端发过来的数据,并且创建了一个新的Socket实例.

                socketDir.Add(newSoket.RemoteEndPoint.ToString(), newSoket);//将负责与客户端进行通信的Socket实例添加到集合中。

                lsb_Ips.Items.Add(newSoket.RemoteEndPoint.ToString());

                ShowMsg("客户端链接成功!");

 

 

                ParameterizedThreadStart par = new

                 ParameterizedThreadStart(RecevieMsg);

                Thread thread = new Thread(par);//由于服务端接收客户端发送过来的数据是通过Recevie方法,该方法会阻断线程,所以我们重新定义一个针对该方法的线程.

                // thread.SetApartmentState(ApartmentState.STA);

                thread.IsBackground = true;

                thread.Start(newSoket);//注意:不要忘记传递socket实例

            }

        }

        //该方法负责接收从客户端发送过来的数据

        public void RecevieMsg(object socket)

        {

            Socket newSocket = socket as Socket;//转成对应的Socket类型

            while (true)

            {

                byte[] buffer = new byte[1024 * 1024 * 2];

                int receiveLength = -1;

                try  //由于Socket中的Receive方法容易抛出异常,所以我们在这里要捕获异常。

                {

                    receiveLength = newSocket.Receive(buffer);//接收从客户端发送过来的数据

                }

                catch (SocketException ex)//注意:在捕获异常时,先确定具体的异常类型。

                {

                    ShowMsg("出现了异常:" + ex.Message);

                    socketDir.Remove(newSocket.RemoteEndPoint.ToString());//如果出现了异常,将该Socket实例从集合中移除

                    lsb_Ips.Items.Remove(newSocket.RemoteEndPoint.ToString());

                    break;//出现异常以后,终止整个循环的执行

                }

                catch (Exception ex)

                {

                    ShowMsg("出现了异常:" + ex.Message);

                    break;

                }

                if (buffer[0] == 0)//表示字符串

                {

                    string str = System.Text.Encoding.UTF8.GetString(buffer, 1, receiveLength - 1);//注意,是从下标为1的开始转成字符串,为0的是标识。

                    ShowMsg(str);

                }

                else if (buffer[0] == 1)//表示文件

                {

                    SaveFileDialog savafile = new SaveFileDialog();

                    if (savafile.ShowDialog() == DialogResult.OK)

                    {

                        using (FileStream fs = new FileStream(savafile.FileName, FileMode.Create))

                        {

                            fs.Write(buffer, 1, receiveLength - 1);//将文件写到磁盘上,从1开始到receiveLength-1

                            ShowMsg("文件写成功!");

                        }

                    }

                }

            }

        }

 

    }

}

启动服务端应用程序,点击启动服务监听,可以同时启动多个客户端应用程序,都要先点击连接服务,然后在消息框内输入消息,也可以选取文件,点击发送。运行效果如下。



 总结:剩余一个群发,我没写上去,相信你如果看明白了上面我所写的的话,这个群发,就so easy了。再次友情提醒一下,如果你不懂多线程,我的上一篇博客就是对他的浅解 

 


标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!