C#网络编程(三)

本文探讨了在没有Stream.Read方法的情况下,如何在客户端和服务端保持连接状态下检测断开,包括使用Connected属性、Poll函数、UDP广播和组播技术,以及处理多线程同步和异常检测的问题。
摘要由CSDN通过智能技术生成

检查客户端连接状态

在前一篇文章中,是采用Stream.Read方法来判断客户端是否与服务端失去连接的。那么如果没有Stream.Read方法,客户端和服务端
连接上了,然后双方都处于等待中,也就是执行到了Console.ReadLine语句上。这时候突然关闭了客户端,该怎样去判断呢。
这时候服务端不是会产生异常的,因为你没进行读取数据Stream.Read操作。不能用try catch方式。
如果同时有好几个客户端与服务端连接呢,服务端怎么实现当一与客户端失去连接,就给出提示呢。
用一个循环语句,判断客户端的Connected属性?这个也是不行的,因为我不进行收发数据。仅仅只是保持连接。
看一下MSDN关于Connected属性的解释就明白了,如下:
<<<<<<<<<<<<<<<<<<<<<<<<<<<Connected 属性获取截止到最后一次 I/O 操作时的 Client 套接字的连接状态。如果该属性返回 false,
则表明 Client 要么从未连接,要么已断开连接。
由于 Connected 属性仅反映截止到最近的操作时的连接状态,因此您应该尝试发送或接收一则消息以确定当前状态。
当该消息发送失败后,此属性将不再返回 true。注意此行为是设计使然。您无法可靠地测试连接状态,
因为在测试与发送/接收之间,连接可能已丢失。您的代码应该假定套接字已连接,并妥善处理失败的传输。

另(SOCKET):Connected属性的值反映最近操作时的连接状态。如果您需要确定连接的当前状态,请进行非阻止、零字节的 Send 调用。如果该调用成功返回或引发 WAEWOULDBLOCK 错误代码 (10035),则该套接字仍然处于连接状态;否则,该套接字不再处于连接状态。

示例:

try
{
    byte [] tmp =newbyte[1];

    client.Blocking =false;
    client.Send(tmp, 0, 0);
    Console.WriteLine("Connected!");
}
catch(SocketException e)
{
   // 10035 == WSAEWOULDBLOCK
   if(e.NativeErrorCode.Equals(10035))
        Console.WriteLine("Still Connected, but the Send would block");
   else
    {
        Console.WriteLine("Disconnected: error code {0}!", e.NativeErrorCode);
    }
}

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>以上来自MSDN>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
所以Connected无法得到正确的连接状态。
那应该怎么办呢,可以使用Poll函数,它有两个参数,第一个参数是microSeconds,这个参数表示函数等待的时间,单位为微秒。
如果为负数,则表明一直等待,直到有一个结果,如客户端关闭。第二个参数是mode,指定要检查的模式。是一个枚举类型。
知道了Poll,我们再来实现上面所要求的功能。看例子:
服务端代码:
   public class Server
    {
        //创建存储客户端的动态数组
        public static System.Collections.ArrayList cliArryList = new System.Collections.ArrayList();
        //这个方法被执行,就表明有一个客户端要连接了。
        public static void AcpClientCallback(IAsyncResult ar)
        {
            TcpListener listener = (TcpListener)ar.AsyncState;
            //调用对应的方法EndAcceptTcpClient,获得连接的客户端TcpClient
            TcpClient client = listener.EndAcceptTcpClient(ar);
            //把客户端添加到动态数组里
            cliArryList.Add(client);
            //输出客户端信息
            Console.WriteLine("一个客户端连接上了,客户端信息:{0}", client.Client.RemoteEndPoint);
            //再接收一个客户端连接
            AsyncCallback callback = new AsyncCallback(AcpClientCallback);
            listener.BeginAcceptTcpClient(callback, listener);
          
        }
        public static void Main()
        {
            //绑定IP,监听端口
            IPAddress ip = new IPAddress(new byte[] { 127, 0, 0, 1 });
            TcpListener listener = new TcpListener(ip, 9372);
            listener.Start();
            //委托方法
            AsyncCallback callback = new AsyncCallback(AcpClientCallback);
            //接收客户端连接,异步
            listener.BeginAcceptTcpClient(callback, listener);
            //循环输出一些信息
       
            while (true)
            {
                for(int i=0;i<cliArryList.Count;i++)
                {
                    TcpClient client=(TcpClient)cliArryList[i];
                    if (client.Client.Poll(100, SelectMode.SelectRead))
                    {
                        Console.WriteLine("与一个客户端失去连接:{0}", client.Client.RemoteEndPoint);
                        //删除掉这个元素
                        cliArryList.RemoveAt(i);
                        i--; 
                    }

                }
              
                System.Threading.Thread.Sleep(300);
            }
        }

    }

客户端代码:
  class Program
    {
        static void Main(string[] args)
        {
            TcpClient[] client = new TcpClient[3];
            //创建三个客户端与服务端连接
            for (int i = 0; i < 3; i++)
            {
                client[i] = new TcpClient();
                client[i].Connect("localhost", 9372);
            }
            //关闭三个客户端
            Random ram = new Random();
            for (int i = 0; i < 3; i++)
            {
                //生成范围在1000至3000的随机数
                int msec=ram.Next(1000, 3000);
                //睡眠
                System.Threading.Thread.Sleep(msec);
                //关闭连接
                client[i].Close();
            }

        }
    }

当然上面那个例子并不完善,只适用于特定的情况下。真正应用到实际中来,是不行的,因为有些问题没处理,比如动态数组里的元素添加和访问,是在两个线程中进行的,这就牵扯到线程同步的问题了。
像上面的循环语句,在里面读取动态数组的元素,如果这时候,另外一个线程正好给这个动态数组添加了一个元素。
那么这个动态数组里面的一些数据就会改变,比如长度。
那么循环语句又进行了删除元素操作,这些都是很容易起冲突,产生异常的。
所以我们就得线程同步,规定在执行循环语句的时候,另一个线程是不能执行添加元素操作的,直到循环语句结束。
关于怎样使线程同步,请参考如流,新一代智能工作平台
还有一个就是,我直接以poll函数的返回值为真来判断客户端连接失败的。没有进行进一步的判断。
因为poll返回真,也有可能是当前客户端有可供读取的数据。(SelectMode.SelectRead模式下)
如果进一步的判断,那就读取一下客户端的数据(使用Read方法)。返回0或产生异常那就是与客户端失去连接了。

另:也可以用多线程和定时器来检查所有客户端的连接状态。

异步读取异常
我们也可以用异步读取异常来判断,一个客户端是否关闭,如下:
服务端代码:
    class Server
    {
        public static void ReadCallback(IAsyncResult ar)
        {
            TcpClient client = (TcpClient)ar.AsyncState;
            try
            {
                int res = client.GetStream().EndRead(ar);
                //如果客户端正常关闭了,也抛出异常
                if (res == 0) throw new Exception("客户端正常关闭,为0");
            }
            catch (Exception e)
            {
                Console.WriteLine("与一个客户端失去连接了:{0}", client.Client.RemoteEndPoint);
                Console.WriteLine("错误信息:{0}", e.Message);
            }

        }
        static void Main(string[] args)
        {
            //绑定IP地址,监听端口
            IPAddress ip = IPAddress.Parse("127.0.0.1");
            TcpListener listener = new TcpListener(ip, 9372);
            listener.Start();
            //等待客户端连接,获取数据流
            TcpClient remoteClient = listener.AcceptTcpClient();
            Console.WriteLine("与一个客户端连接了:{0}", remoteClient.Client.RemoteEndPoint);
            NetworkStream Stream = remoteClient.GetStream();
            //异步读取数据
            byte[] buffer = new byte[800];
            Stream.BeginRead(buffer, 0, 800, ReadCallback, remoteClient);
            Console.ReadLine();
        }
    }

客户端代码:
  class Client
    {
        public static void Main(string[] args)
        {
            //连接服务端
            TcpClient client = new TcpClient();
            client.Connect("127.0.0.1", 9372);
            Console.ReadLine();
        }
    }

另外如果客户端非正常关闭了,往客户端写数据也会产生异常。也可以通过这个异常来判断连接状态。

用UDP协议进行广播

需要复习UDP协议的请参考:如流,新一代智能工作平台

广播只能在子网中,或者说只能在局域网内,我这里就直接把局域网和子网划上等号了,事实上局域网可以划分为多个子网。我这里就假设不划分。

在局域网进行广播,跟平常的UDP编程步骤是没什么两样的,只是关键在于广播地址。

在进行UDP广播编程之前,先来了解一下子网IP地址的范围。也就是哪些IP地址是属于同一个局域网的。

如果子网掩码是255.255.255.0,而你的局域网IP地址是192.168.3.78那么子网的范围是192.168.3.0至192.168.3.255。

如果子网掩码255.255.0.0那么范围就是192.168.0.0至192.168.255.255。

最常见也就是上面两类,有的子网掩码也不一定是255和0的组合,如果是不同的话,具体是怎么算的子网范围,可以参考其它资料。

这个也不是必需深入了解的。

其实有一种算法,可以确定两个IP地址是否属于同一个子网,只要把它们和子网掩码进行按位与运算,如果得到的结果相同,那就是在同一个子网中。

知道了子网范围,那么也就是知道你的广播在哪些计算机上有用,哪些计算机会收到你的广播。

前面说过了,关键点在于广播地址,如何确定子网中的广播地址,子网中最大的一个IP地址,就是这个子网广播地址,比如上面举例中第一个子网的广播地址是192.168.1.255,第二个是192.168.255.255。

要进行广播的话,把发送的目的IP地址改变成广播地址就行,然后绑定端口号一致。其它跟UDP协议普通编程没什么两样。

另外找子网广播地址的事,系统也帮你做了。可以直接使用IPAddress.Broadcast代替,这个就表示子网的广播地址。

看例子:(只要在同一个子网中运行这个程序,就可以组成一个简单的局域聊天系统。所有运行这个程序的人都能接到数据,并发送数据给其他人)

    class Program
    {
        public static UdpClient client;
        static void Main(String[] args)
        {
            //绑定端口号
            IPEndPoint iep = new IPEndPoint(IPAddress.Broadcast, 9372);
            client = new UdpClient(9372);
            //允许使用广播地址发送接收数据。
            client.EnableBroadcast = true;
           //创建一个线程来接收数据
            System.Threading.Thread thread = new System.Threading.Thread(RecvData);
            thread.Start();
            //发送广播数据
            while (true)
            {
                //等待输入字符串,再发送
                String msg = Console.ReadLine();
                Byte[] buffer = Encoding.Unicode.GetBytes(msg);
                client.Send(buffer, buffer.Length, iep);
            }
        }
        public static void RecvData()
        {
            //接收一个发送端的信息
            IPEndPoint remoteEP = null;
            while (true)
            {
                //等待发送端发送数据过来
                byte[] buffer = client.Receive(ref remoteEP);
                String msg = Encoding.Unicode.GetString(buffer);
                //输出信息
                Console.WriteLine("从{0}上发来信息:{1}", remoteEP, msg);
            }
        }

    }

UDP组播

广播的话会给子网内每一台机子发送数据,有的并没有运广播的程序,也会被广播到,只要在同一个子网内。这样就影响了效率,有没有什么方法,只给运行相应程序的计算机进行广播呢,当然有,那就是组播。组播的优势明显比广播多,组播可以不受子网的限制,也就是它

可以全网“广播”。当然只限于加入组播的计算机。

像广播一样,有个广播地址,那么组播也有个组播地址,不过这个组播地址不是唯一的,它的范围是224.0.0.0至239.255.255.255

因为组播只是针对加入进来的计算机广播,那么进行组播前必须加入一个组播组,UdpClient类里的JoinMulticastGroup就可以加入一个

组播组。

 看例子:

    class Program
    {
        public static UdpClient client;
        public static IPEndPoint iep;
        static void Main(String[] args)
        {
            //绑定端口号,225.226.229.228是一个组播地址
            IPAddress ip=IPAddress.Parse("225.226.229.228");
            iep = new IPEndPoint(ip, 9372);
            client = new UdpClient(9372);
            //允许发送接收广播数据。
            client.EnableBroadcast = true;
           //创建一个线程来接收数据
            System.Threading.Thread thread = new System.Threading.Thread(RecvData);
            thread.Start();
            //发送组播数据
            while (true)
            {
                //等待输入字符串,再发送
                String msg = Console.ReadLine();
                Byte[] buffer = Encoding.Unicode.GetBytes(msg);
                //iep对应的是组播地址
                client.Send(buffer, buffer.Length, iep);
            }
         
        }
        public static void RecvData()
        {
        
            //加入组播组,并发送组播进行通知,50是允许50次路由转发,TIL。
            client.JoinMulticastGroup(IPAddress.Parse("225.226.229.228"), 50);
            //获得本机IP地址
            IPAddress []ip=Dns.GetHostAddresses(Dns.GetHostName());
            String ntMsg = String.Format("{0}加入了组播组!!!",ip[0]);
            Byte[] ntBuffer = Encoding.Unicode.GetBytes(ntMsg);
            client.Send(ntBuffer, ntBuffer.Length, iep);
            //接收一个发送端的信息
            IPEndPoint remoteEP = null;
            while (true)
            {
                //等待发送端发送数据过来
                byte[] buffer = client.Receive(ref remoteEP);
                String msg = Encoding.Unicode.GetString(buffer);
                //输出信息
                Console.WriteLine("从{0}上发来信息:{1}", remoteEP, msg);
            }
        }

    }

另外可以把加入组播组中的计算机(iep),保存在一个ArrayList数组里,然后每隔一段时间,调用client.Send发送数据给各个端,如果失败了。则这个端已经退出组播组,这是检测一个端非正常退出组的一种方法。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Bczheng1

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

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

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

打赏作者

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

抵扣说明:

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

余额充值