检查客户端连接状态
在前一篇文章中,是采用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发送数据给各个端,如果失败了。则这个端已经退出组播组,这是检测一个端非正常退出组的一种方法。