Systems.Net.Sockets之下的Socket类在.Net Framework里是显得比较怪异的一个类,因为它其实就是Win32 时代Winsock的托管代码版本。它的编程理念与现如今是有点不匹配的,它是不能直接拿来作为我们通讯组件的技术基底的,我们根据系统的要求先要对它作一定的技术封装,以求减少在组件层暴露太多的技术细节。
很遗憾,以前并没有用Winsock API实现过系统的经历,只是在Delphi、VB下用控件的形式做过一些简单的应用,那些都是上不了台面的东西。所以这里所涉及的并不适用那种严格上的、高性能的网络通讯,有些地方不一定对,有些地方不够严谨,有些地方会有缺陷。
对于不太熟悉的领域,在实现之前总是在网上先搜索一下是否有相关的可参考的实现(即使是熟悉的领域当然也不妨,只是相对来讲搜索结果对你来说价值会打点折扣)。最后发现CodeProject上的一篇关于异步Socket通讯的文章跟我的要求比较的接近,我就在此基础之上进行了自己的工作,其实最终的骨架都是人家的,我自己只改了一些细枝末节的东西。我在这篇文章里主要讲述一些自己的东西为主
我们先回顾一下之前写过的几篇,发现我们在系统的“离线运行”这个地方费了不少笔墨,上篇还专门讲了数据同步的功能设计。在我们的Socket通讯这个地方同样避免不了要考虑网络异常情况,而且为了管控端对于各签到终端的有效控制,我们必须要对于管控端和签到终端之间的“不可见”及时的发现。在系统实现中这个“不可见”的及时发现着实化了我不少的时间。
第一个是如何判断当前的Socket连接是可用的,在Socket类下有一个属性Connected,按字面理解它是可以用来完成我们的任务的,但SKD上明明白白地写着即使这个属性返回true,也无法保证连接是可用的。那这个属性是有何用呢,按我的认识及简单地测试,发现如果一切都是正常运行的,连接是正常开启,正常关闭的,那这个属性是有用的,但如果程序有异常,网络有异常,那这个属性它根本不可知。当前Socket是否处于连接状态我就没有利用Connected这个属性,而是直接用Poll方法来确定,过程如下:
public bool IsConnect
{
get
{
return (workSock == null ? false : workSock.Poll(500,SelectMode.SelectWrite));
}
}
注:属性在500微秒之内执行,这个数值不知是否合适,其他所有用到Connected属性的地方我都改用了Poll来判断,只是后面那个Timeout的数值根据不同的场合作了一些微调,目前系统用下来比较正常。
碰到的第二个问题是机器名的解析,按照比较好的设计,在连接远程主机的时候,机器名或IP地址二种方式都是应该可以的。因为Dns.Resolve这个方法我发现它既可以用IP地址也可以用机器名,由此刚开始时在创建IPEndPoint时我用的new IPEndPoint(Dns.Resolve(host).AddressList[0],port)这样的方式,按道理说不会什么问题。但我测试后发现,这个方法如果送的是机器名一切都按想象的进行,如果送的是IP地址,那在Dns.Resolve这一步非常慢,这可能也跟网络环境有关。为上杜绝这种情况,对于IPEndPoint的创建,就分二种情况不同处理,如果是机器名还是原来的写法,如果是IP地址的话,那就用IPAddress.Parse方法。具体过程如下:
IPEndPoint ipe;
UriHostNameType uriType = Uri.CheckHostName(host);
if (uriType == UriHostNameType.IPv4 || uriType == UriHostNameType.IPv6)
ipe = new IPEndPoint(IPAddress.Parse(host),port);
else
ipe =new IPEndPoint(Dns.Resolve(host).AddressList[0],port);
因为要考虑各种情况,问题是层出不穷,接下来出现状况的是在连接远程主机的过程中。如果当前远程主机是不可用的(网络不通、主机没开启或是连接配置有误机器不存在等),那Connect这个方法是会一直阻塞到抛出异常为止的,很要命的是从开始执行方法到抛出异常这段时间显得比较的长,我无法接受,即使把Connect改用异步方法也不解决问题,因为EndConnect还是照样阻在那里的。最后我把它改成这样:
workSock = new Socket(ipe.AddressFamily,SocketType.Stream,ProtocolType.Tcp);
workSock.Blocking = false;
try
{
workSock.Connect(ipe);
}
catch(SocketException e)
{
if (e.ErrorCode == 10035)
{
if (workSock.Poll(5000,SelectMode.SelectWrite))
{
workSock.Blocking = true;
if (OnConnect != null)
OnConnect(this,null);
ThreadPool.QueueUserWorkItem(new WaitCallback(ReceiveThreadEntryPoint));
workSock.SetSocketOption(SocketOptionLevel.Socket,SocketOptionName.NoDelay,1);
// ThreadPool.QueueUserWorkItem(new WaitCallback(SendThreadEntryPoint));
return true;
}
}
}
workSock.Close();
workSock = null;
return false;
注:10035:这是Winsock错误代码
1 0 0 3 5—W S A E W O U L D B L O C K
资源暂时不可用。对非锁定套接字来说,如果请求操作不能立即执行的话,通常会返回这个错误。比如说,在一个非暂停套接字上调用 c o n n e c t,就会返回这个错误。因为连接请求不能立即执行。
在非阻塞模式下,那个异常会比阻塞模式下抛出来快,但抛出异常不一定就是无法连接的,后面再从异常错误代码与Poll方法联合来判断连接是否可以用了。
上述写法总觉得怪怪的,不知正规如何解决,现在这个方法测试下来还没出问题,一直用到现在。
注意到上面注释了一行代码,那是压一个异步发送的线程到线程池用的,不过我考虑到系统中发送方面要求不是很高,用同步完全可以接受,为了减少复杂性,我就没让那个线程跑起来,直接用同步方法发送了。至于异步接收,基本就是用人家的骨架,只是小改了一个地方。在回调方法里
private void AsynchReadCallback(System.IAsyncResult ar)
{
SocketStateObject so = (SocketStateObject)ar.AsyncState;
Socket s = so.WSock;
try
{
if (s == null || !s.Poll(100,SelectMode.SelectWrite)) return;
int read = s.EndReceive(ar);
if (read > 0)
{
string msg = Encoding.ASCII.GetString(so.buffer, 0, read);
if (OnReceive != null)
{
OnReceive(this,new EventArgs.ComReceiveEventArgs(msg));
}
s.BeginReceive(so.buffer, 0, SocketStateObject.BUFFER_SIZE, 0, new AsyncCallback(AsynchReadCallback), so);
}
else
{
Close(true);
}
}
catch(SocketException)
{
Close(true);
}
catch(Exception e)
{
}
}
我加了二个Close(true)方法来作一些清理工作。第一个在远程主机关闭连接时发生,当远程主机关闭连接时EndReceive方法会返回0。第二个在远程主机发生异常而没有正常关闭连接或是网络异常时发生。
那在Close(true)里会发生什么那,看下面:
public void Close(bool passivity)
{
if (!IsConnect) return;
StopEvent.Set();
try
{
workSock.Shutdown(SocketShutdown.Both);
workSock.Close();
workSock = null;
}
catch(Exception)
{
}
if (passivity)
{
if (OnClose != null)
OnClose(this,null);
}
}
它其实就是停止相关线程(StopEvent就是线程信号),关闭Socket连接并释放资源,如果是被动关闭的话(即passivity为true,意指不是我自己客户端发起的关闭动作),那要向外触发事件,可以让组件层或UI层做一些事情。
经过我上述零零碎碎的改造,现在这个Socket封装类可以用在我的客户端数据通讯组件里的。