黑马程序员-自学笔记-聊天程序(基于Socket,Thread)

聊天程序(基于Socket,Thread)

---------------------- ASP.Net+Android+IOS开发.Net培训、期待与您交流! ----------------------

一、基础准备:委托(Delegate)

1、简单回顾委托

1.1、什么是委托?(安全的函数指针)

通俗:就是一个能存放符合某种格式(方法签名)的方法的指针的清单。

 

1.2、委托创建方式

         准备一个方法:stringHelloU(string uName){//方法代码}

          声明 委 托:delegate 返回值类型 委托类型名(参数)

                 例子:delegate stringDGSayHi(string uName);

         创建委托对象:DGSayHidgSay=new DGSayHi(HelloU);

或者:DGSayHidgSay = HelloU;

追 加 方 法:dgSay +=HelloU2;      //Delegate.Combine(,);

删 除 方 法:dgSay -= HelloU2;      // Delegate.Remove(,);

 

1.3、委托调用方式

       dgSay(“binggo”);    // dgSay.Invoke(“binggo”);

 

2、委托作为参数

 2.1、声明使用委托做参数的方法:

         voidIntenationalSayHi(string name, DGSayHi dgSh);

         {

//业务代码

                   dgSh(uName);

                   //业务代码

         }

 

 2.2、调用执行

         传委托对象:

                   IntenationalSayHi(“binggo”,dgSay);

         直接传有和委托相同方法签名的方法:

                   IntenationalSayHi(“binggo”,HelloU);

                   IntenationalSayHi(“binggo”,new DGSayHi(HelloU));

 

3、委托的异步调用

         BeginInvoke  异步调用

EndInvoke   获取异步调用的(方法)返回值

IAsyncResult  异步操作的状态

注意:委托的异步调用只对单播委托

 

二、基础准备:简单文件操作(IO)

1、FileSteam文件流

         FileSteam对象表示在磁盘或网络路径上指向文件的流。

         使用FileSteam类对文件系统上的文件进行读取、写入、打开或关闭操作。

         FileSteam对输入输出进行缓冲,从而提高性能。

         为什么不用File.ReadAllText()?好处之一就是:对于大文件来说,FileSteam可以对文件采取分段读取,即每次只读取一部分到内存。

 

2、字符串和字节数组转换

         字节数组转换成字符串:String

          System.Text.Encoding.UTF8.GetString(byte[])

         字符串转换成字节数组:byte[]

          System.Text.Encoding.UTF8.GetBytes(string)

 

三、基础准备:多线程(Thread)

1、为什么要用多线程

         让计算机“同时”做多件事情,节约时间。

         多线程可以让一个程序“同时”处理多个事情。

         后台运行程序,提高程序的运行效率,也不会使主界面出现无响应的情况。

 

2、.NET中如何实现多线程(线程同步)

         产生一个线程的4个步骤:

                   1)编写产生线程所要执行的方法

                   2)引用System.Threading命名空间

                   3)实例化Thread类,并传入一个指向线程所要运行方法的委托。(这个时候这个线程已经产生,但是还没有运行。)

                   4)调用Thread实例的Start方法,标记该线程可以被CPU执行了,但具体时间由CPU决定。

         线程肯定也是要执行一段代码的。所以要产生一个线程,必须先为该线程写一个方法,这个方法中的代码中的代码就是该线程运行所要执行的代码。(找个人来做一件事情)

         线程启动时,通过委托调用该方法。(委托的好处)线程启动时,调用传过来的委托,委托会执行相应的方法,实现线程执行方法。

 

3、进程与线程

         一个进程至少有一个线程。

         同一个进程中的多个线程之间可以“并发”执行。

 

4、前台线程和后台线程

         前台线程:只有所有的前台线程都关闭才能完成程序关闭。

         后台线程:只要所有前台线程结束,后台线程自动结束。

 

5、Thread类的一些重要成员

         Start()启动线程

         Abort()终止线程

         Thread.Sleep(1)静态方法,可以使当前线程停止一段时间运行

         Name线程名

         Thread.CurrentThread获得当前线程引用

 

四、基础准备:网络编程(Socket)

1、TCP/IP

         应用层(Application):应用层是个很广泛的概念,有一些基本相同的系统级TCP/IP应用以及应用协议,也有许多的企业商业应用和互联网应用。

         传输层(Transport):传输层包括UDP和TCP,UDP几乎不对报文进行检查,而TCP提供传输保证。

         网络层(Network):网络层协议由一系列协议组成,包括ICMP、IGMP、RIP、OSPF、IP(V4,V6)等。

         链路层(Link):又称为物理数据网络接口层,负责报文传输。

 

2、两个人在两个房子里打电话

         人通过 [ 电话 ] 可以通信。

         程序通过 [ Socket] 来通信。

         套接字 就是 程序间的 电话机。

 

3、Socket相关概念

         Socket的英文原义是“孔”或“插座”。作为进程通信机制,取后一种意思。通常也称作“套接字”,用于描述IP地址和端口,是一个通信链的句柄。(其实就是两个程序通信用的。)

         Socket非常类似于电话插座。以一个电话网为例。电话的通信双方相当于相互通信的2个程序,电话号码就是IP 地址。任何用户在通话之前,首先要占有一部电话机,相当于申请一个Socket;同时要知道对方的号码,相当于对方有一个固定的Socket。然后向对方拨号呼叫,相当于发出连接请求。对方假如在场并空闲,拿起电话话筒,双方就可以正式通话,相当于连接成功。双方通话的过程,是一方向电话机发出信号和对方从电话机接收信号的过程,相当于Socket发送数据和从Socket接收数据。通话结束后,一方挂起电话机相当于关闭Socket,撤消连接。

 

4、Socket相关概念[端口]

         在Internet上有很多这样的主机,这些主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务(应用程序)。

         例如:http使用80端口  ftp使用21端口  smtp使用25端口

         Socket常使用的有两种类型:

                   1)流式Socket(STREAM):是一种面向连接的Socket,针对于面向连接的TCP服务应用,安全,但效率低。

                   2)数据报式Socket(DATAGRAM):是一种无连接的Socket,对应于无连接的UDP服务应用,不安全(丢失,顺序混乱,在接收端要分析重排及要求重发),但效率高。

 

5、Socket一般应用模式(服务器和客户端)


 1)服务端WelcomeingSocket 开始监听端口(负责监听客户端连接信息)

 2)客户端ClientSocket连接服务端指定端口(负责接收和发送服务端消息)

 3)服务端WelcomeingSocket监听到客户端连接,创建Connection Socket。(负责和客户端通信)

         服务端的Socket(至少需要两个)

                   一个负责接收客户端连接(但不负责与客户端通信)。

                   每成功接收到一个客户端的连接便在服务端产生一个对应的负责通信的Socket。

                            在接收到客户端连接时创建。

                            为每个连接成功的客户端请求在服务端都创建一个对应的Socket(负责和客户端通信)。

         客户端的Socket

                   必须指定要连接的服务端地址和端口

                   通过创建一个Socket对象来初始化一个到服务端的TCP连接。

 

6、Socket的通讯过程

         服务器端:

                   申请一个Socket

                   绑定到一个IP地址和一个端口上

                   开始侦听,等待接受连接

         客户端:

                   申请一个Socket

                   连接服务器(指明IP地址和端口号)

         服务器端接到连接请求后,产生一个新的Socket(端口大于1024)与客户端建立连接并进行通讯,原监听Socket继续监听。

 

7、Socket的构造函数

         连接通过构造函数完成。

         publicSocket(AddressFamily addressFamily,SocketType socketType,ProtocolTypeprotocolType)

                   AddressFamily成员指定Socket用来解析地址的寻址方案。如,InterNetwork指示当Socket使用一个IP版本4地下连接。

                   SocketType定义要打开的Socket的类型

                   Socket类使用ProtocolType枚举向WindowsSockets API通知所请求的协议

如:Socket mySocket = new Socket (AddressFamily. InterNetwork,SocketType.Stream,ProtocolType.Tcp);

 

8、Socket使用注意事项:

         至少要定义一个要连接的远程主机的IP和端口号。

         端口号必须在1到65535之间,最好在1024以后。

         要连接的远程主机必须正在监听指定端口,出就是说你无法随意连接远程主机。如:

                  IPAddress addr=IPAddress.Parse(“172.20.2.8”)

                   IPEndPointendp=new IpEndPoint(addr,10001);

                   服务端先绑定:serverWelcomeSocket.Bind(endp)

                   客户端再连接:clientSocket.Connect(endp)

         一个Socket一次只能连接一台主机。

         Socket关闭后无法再次使用。

         每个Socket对象只能一台远程主机连接。如果你想连接到多台远程主机,你必须创建多个Socket对象。

 

9、Socket方法

         相关类:

                   IPAddress类:包含了一个IP地址

                   IPEndPoint类:包含一对IP地址和端口号

         方法:

                   Socket():创建一个Socket

                   Bind():绑定一个本地IP和端口号(IPEndPint)

                   Listen():让Socket侦听传入的连接尝试,并指定侦听队列容量

                   Connect():初始化与另一个Socket的连接

                   Accept():接收连接并返回一个新的Socket

                   Send():输出数据到Socket

                   Receive():从Socket中读取数据

                   Close():关闭Socket(销毁连接)

         Socket通信基本流程图:

        

 

10、扩展

         实现传送文件

         如何判断接收数据是文件还是文字?

         设计“协议”:

                   把要传递的字节数组前面都加上一个字节作为标识。0:表示文字   1:表示文件

                   即:文字 0+文字(字节数组表示);文件1+文件的二进制信息。

 

 

附:部分程序代码

namespace Server
{
    public partial class Server : Form
    {
        //存储通信用的Socket字典
        Dictionary<string, Socket> dic = new Dictionary<string, Socket>();


        public Server()
        {
            InitializeComponent();
            //关闭跨线程检查
            Control.CheckForIllegalCrossThreadCalls = false;
        }


        //创建监听用的Socket
        private void btnStart_Click(object sender, EventArgs e)
        {
            //ip地址
            IPAddress ip = IPAddress.Parse(txtIP.Text);
            //IP地址和端口号
            IPEndPoint point = new IPEndPoint(ip, int.Parse(txtPort.Text));
            //监听用的Socket
            Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);


            //通常每个套接字(协议/网络地址/端口)只允许使用一次
            try
            {
                //绑定IP和端口
                socket.Bind(point);
            }
            catch(Exception ex)
            {
                ShowMsg(ex.Message);
                return;
            }
            //监听端口
            socket.Listen(10);   //队列长度为10


            ShowMsg("开始监听!!!");


            //监听端口,如果有客户端连接,创建通信用的Socket
            //Socket conSocket = socket.Accept();     //此时会阻塞窗体的运行
            //用委托解决
            Thread th = new Thread(Listen);
            //设为后台线程
            th.IsBackground = true;
            //启动线程
            th.Start(socket);


        }


       
        //限制连接客户端的个数
        int count = 0;
        void Listen(object o)
        {
            Socket socket = o as Socket;


            //可以不停的接收客户端的连接
            while (count<100)
            {
                count++;
                //监听端口,如果有客户端连接,创建通信用的Socket
                Socket conSocket = socket.Accept();
                //显示连接成功的客户端的IP地址和端口号
                string ipport = conSocket.RemoteEndPoint.ToString();
                ShowMsg("\r\n客户端"+ipport+"  连接成功!");


                cboUsers.Items.Add(ipport);
                dic.Add(ipport, conSocket);


                //本机的IP地址和端口号  conSocket.LocalEndPoint


                Thread th = new Thread(RecMsg);
                th.IsBackground = true;
                th.Start(conSocket);
            }
        }


        //接收消息
        void RecMsg(object o)
        {
            //通信用的Socket
            Socket conSocket = o as Socket;
            //循环接收消息
            while (true)
            {
                //接收数据
                byte[] buffer = new byte[1024 * 1024];
                //接收数据,把数据放到buffer中
                //num是实际接收到的字节个数
                int num=0;
                try
                {
                    num = conSocket.Receive(buffer);
                    //当客户端关闭,会不停的发送空(0)字节
                    if (num == 0)
                    {
                        ShowMsg("  ->" + conSocket.RemoteEndPoint.ToString() + "  离线!");
                        conSocket.Shutdown(SocketShutdown.Receive);
                        conSocket.Close();
                        break;
                    }
                }
                catch(Exception ex)   //客户端异常断开Socket时
                {
                    ShowMsg(ex.Message);
                    break;
                }


                //把实际有效的字节转换成字符串
                string str = Encoding.UTF8.GetString(buffer, 0, num);
                ShowMsg("  ->"+conSocket.RemoteEndPoint.ToString()+":\r\n     "+str);
            }
        }




        //在txtLog文本框中显示消息
        void ShowMsg(string msg)
        {
            txtLog.AppendText(msg + "\r\n");
        }


        //发送消息
        private void btnSender_Click(object sender, EventArgs e)
        {
            //获取下拉框中选择的IP和端口
            string key = cboUsers.Text;
            if (!string.IsNullOrEmpty(key))
            {
                byte[] buffer = Encoding.UTF8.GetBytes(txtMsg.Text);
                try
                {
                    //根据Key获得Socket
                    dic[key].Send(buffer);
                }
                catch (Exception ex)
                {
                    ShowMsg(ex.Message);
                }
            }
            else
            {
                ShowMsg("请选择客户端");
            }                
           
        }


        //选择文件
        private void btnSelect_Click(object sender, EventArgs e)
        {
            OpenFileDialog ofd = new OpenFileDialog();
            ofd.Filter = "图像文件|*.jpg";
            if (ofd.ShowDialog()==DialogResult.OK)
            {
                txtPath.Text = ofd.FileName;
            }
        }


        //发送文件
        private void btnSenFil_Click(object sender, EventArgs e)
        {
            string key=cboUsers.Text;
            //把文件读到字节数组中
            using (FileStream fs=new FileStream(txtPath.Text,FileMode.Open))
            {
                byte[] buffer = new byte[fs.Length];
                fs.Read(buffer, 0, buffer.Length);
                
                //添加“协议”
                List<byte> list = new List<byte>();
                list.Add(1);  //1  表示文件
                list.AddRange(buffer);


                //发送
                dic[key].Send(list.ToArray());
            }
        }


    }
}

------------------------------

namespace Client
{
    public partial class Client : Form
    {
        public Client()
        {
            InitializeComponent();
            Control.CheckForIllegalCrossThreadCalls = false;
        }


        Socket socket;
        private void btnConn_Click(object sender, EventArgs e)
        {
            //客户端连接服务器的IP地址
            IPAddress ip = IPAddress.Parse(txtIP.Text);
            //IP地址和端口号
            IPEndPoint point = new IPEndPoint(ip, int.Parse(txtPort.Text));
            //创建连接使用的Socket
            socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);


            //连接聊天服务器
            //由于目标计算机积极拒绝,无法连接。
            try
            {
                socket.Connect(point);
                ShowMsg("连接成功");


                //连接成功后接收消息
                Thread th = new Thread(RecMsg);
                th.IsBackground = true;
                th.Start();


            }
            catch (Exception ex)
            {
                ShowMsg(ex.Message);
            }
        }


        //在txtLog文本框中显示消息
        void ShowMsg(string msg)
        {
            txtLog.AppendText(msg + "\r\n");
        }


        void RecMsg()
        {
            
            while (true)
            {
                byte[] buffer = new byte[1024 * 1024];
                try
                {
                    int num = socket.Receive(buffer);
                    int first = buffer[0]; //读取协议位 0 文字 1文件
                    if (first == 0)
                    {                        
                        string str = Encoding.UTF8.GetString(buffer, 1, num-1);
                        ShowMsg(str);
                    }
                    else if (first==1)
                    {
                        SaveFileDialog sfd = new SaveFileDialog();
                        sfd.Filter = "图像文件|*.jpg";
                        if (sfd.ShowDialog(this)==System.Windows.Forms.DialogResult.OK)
                        {
                            using (FileStream fs=new FileStream(sfd.FileName,FileMode.Create))
                            {
                                fs.Write(buffer, 1, num - 1);
                            }
                        }
                        
                    }
                }
                catch (Exception ex)
                {
                    ShowMsg(ex.Message);
                    break;
                }
            }
        }


        private void btnSender_Click(object sender, EventArgs e)
        {
            if (socket != null)
            {
                byte[] buffer = Encoding.UTF8.GetBytes(txtMsg.Text);
                socket.Send(buffer);
            }
            else
            {
                ShowMsg("请先连接服务器!");
            }
        }
    }
}

 ---------------------- ASP.Net+Android+IOS开发.Net培训、期待与您交流! ----------------------

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值