.NET 4.0网络开发入门之旅--
与Socket的第一次“约会”
注:
这是一个针对 网络开发领域初学者 的系列文章,可作为《.NET 4.0 面向对象编程漫谈 》一书的扩充阅读,写作过程中我假设读者可以对照阅读此书的相关章节,不再浪费笔墨重复介绍相关的内容。
对于其他类型的读者,除非您已经有相应的.NET 技术背景与一定的开发经验,否则,阅读中可能会遇到困难。
我希望这系列文章能让读者领略到网络开发的魅力!
另外,这些文章均为本人原创,请读者尊重作者的劳动,我允许大家出于知识共享的目的自由转载这些文章及相关示例,但未经本人许可,请不要用于商业盈利目的。
本文如有错误,敬请回贴指正。
谢谢大家!
金旭亮
=================================================
点击以下链接阅读本系列前面的文章:
《 开篇语—— 无网不胜》
《 IP知多少》
《我在“网” 中央 》
=================================================
在前面的文章中,我们已经介绍了使用.NET平台开发网络应用程序诸如IP地址、网络接口之类的背景知识,本文将介绍.NET网络应用程序的主角--Socket。如果把Socket比喻为一位“美女”,那么有关她的“爱情故事”实在太多,而本系列后继的文章,就围绕着这位“美女”所展开。
1 Socket美女的“家庭背景”
Socket,中文译为“套接字”,最早在UNIX中引入并得到广泛应用,后来微软在设计Windows时引入了UNIX中的这个概念和相应的设计理念,并针对Windows的特性略作调整,形成了Windows平台上的Socket,简称为“WinSock”,并为开发者提供了一整套的API,称为“Windows WinSock Win32 API ”。
WinSock经历了两个版本,Windows Sockets 2是目前用得最多的版本(参看 http://en.wikipedia.org/wiki/Winsock ),微软似乎从来没有宣布要开发WinSock 3,也许“永远也不会有”了。
图 1所示为.NET平台下网络应用程序的层次架构:
图 1
WinSock在底层使用一个运行于操作系统核心的系统驱动(Windows Sockets Knernel-mode Driver)tcpip.SYS,由它们负责管理网络连接和缓冲管理。
还有另一个驱动Afd.sys(Ancillary Function Driver for WinSock)则用于支持基于 window socket的应用程序,比如ftp、telnet等,被称为“ Windows NT 套接字驱动程序 ”。
早期的Windows开发者,需要使用C/C++去调用WinSock,比如MFC就提供了一个“CSocket”类封装底层的Socket。
.NET也提供了一组类来封装WinSock Win32 API,这些类集中于System.Net这一命名空间中,其中的核心类型就是Socket。
Socket类是对WinSock API一个很浅的封装,拥有不少方法直接对应于WinSock中的C/C++函数,比如Poll、Select、IOControl等。
Socket有一个Handle属性,它引用位于操作系统核心的Socket核心对象。
提示:
有关系统核心对象(Kernel Object)的通俗解释,请参看《.NET 4.0面向对象编程漫谈 》中的15.1.2节《操作系统的进程管理》
Socket提供了众多的属性,还提供了SetSocketOption方法来设置各种选项,对.NET网络应用程序的数据通讯进行“微调”。
Socket的功能出奇地强大,在.NET平台上,它支持以下四种典型的编程模式:
(1) 居于阻塞模式的Socket编程(单线程或多线程的),每个线程处理一个客户端连接
(2)“非阻塞”模式的Socket编程,这是早期UNIX为提升网络应用程序性能而采用的编程模式,出于兼容和方便移植原有程序的目的而保留,建议新开发的.NET网络程序不要再使用。
(3) 使用IAsyncResult的异步编程模式:Socket类提供有一堆的“BeginXXX/EndXXX”方法实现异步Socket编程,使用线程池中的线程完成工作,性能较好。
(4) 使用EAP的异步编程模式:Socket类提供了“另一堆”以“Async”结尾的方法,在底层使用Windows操作系统的Completion Port(完成端口)和Overlapped I/O mechanism(重叠输入/输出机制),据说可以提供“最高”的性能。
在后面的文章中,将逐步地展开介绍这些编程模式。
提示:
强烈建议读者仔细阅读《.NET 4.0面向对象编程漫谈 》中的第10章《异步编程模式》,以提前掌握.NET异步编程的基础知识与基本技能,否则,后面的文章可以不用看了。
了解了Socket这位“美女”的“家庭背景”之后,在与她进行第一次“约会”之前,我们不妨弄清楚一个问题:
现在我们还有必要掌握Sokcet编程技术吗?
2 Socket是否已人老珠黄?
基于Socket开发网络应用程序已经有很多年的历史了,现在的新技术层出不穷,在.NET平台之上,WCF大有“一统江湖”的势头,Socket是否真的“人老珠黄”?
请看图 2所示的多层“松花蛋”:
图 2
图 2说明,WCF与WinSocket等底层技术之间实际上是一种“包含”关系,每一层都在下一层所提供服务的基础上,又扩充了新的功能,越外层的应用程序,可以使用的功能往往越多,开发效率往往也会更高。
WCF在WinSocket的基础之上扩充了大量的功能,使用它可以很高效地开发网络应用程序,尤其非常适合于开发基于SOA的分布式软件系统,但这并不是说它可以完全把Socket打入冷宫。在不少场合,抛弃WCF那庞大的框架,直接使用Socket更合适:
(1)需要实现自己的通讯协议的场合(比如你要架设一个网络游戏服务器)
(2)你开发的系统需要实现“一问一答”的“交互式”运行模式
(3)你希望能全面控制你的网络应用程序的“每个方面”,不想花时间去理解WCF那个复杂无比的内部架构
(4)你的网络应用程序应用背景非常单一与明确,比如就解决一个问题:定期将分布于多台计算机上的数据文件上传“汇总”到一台中心服务器上。
(5)……
如果需要基于各种标准协议(比如WS-*等)开发SOA的分布式软件系统,再使用Socket就不合适了,那会大大地增加开发的工作量和难度,WCF更适合于解决这个问题。
在实际开发中,我们还可以混用WCF和Socket。比如我们可以基于WCF开发P2P的应用程序,使用NetPeerTcpBinding在P2P节点间“广播消息”,然后,在两个P2P节点之间直接使用Socket“私下”里传送一个“秘密”文件。
是可谓“运用之妙,存乎一心 ”。
好了,下面就介绍使用Socket开发的最基础知识吧。
3 第一个Socket应用程序
一般我们都将网络应用中用于提供“服务”的一方称为“服务端应用程序(Server)”,另一方访问这些服务的称为“客户端应用程序(Client)”。Server端和Client端的Socket用法是不一样的。
3.1 服务端应用程序
开发网络程序的第一步,是创建Socket对象,以下是示例代码:
Socket newsock = new Socket(
AddressFamily.InterNetwork, //使用IPv4
SocketType.Stream, //使用可靠的双向数据流,不保存信息边界
ProtocolType.Tcp //使用TCP协议
);
紧接着,需要将Socket对象“绑定(Bind) ”到一个“终结点(IPEndPoint的实例)”。
IPEndPoint ipep = new IPEndPoint(IP地址,打开端口); //绑定
newsock.Bind(ipep);
提示:
前面的《IP知多少 》一文中介绍过IPEndPoint。WCF中也定义了“终结点 ”,它代表一个WCF服务的访问点。
“绑定(Bind) ”这个术语非常值得关注,简单地说,“绑定”就是将原先可能不相关的两个事物“关联”起来,打个可能不太恰当的比喻,“绑定”就是相爱的两个人最终决定结婚,并领了结婚证。
“绑定”的身影在.NET平台中频频出现,比如“数据绑定(DataBind)”,就是使用控件将数据源中的数据展示在应用程序的界面上,并且将用户对数据的修改和查询等传给数据源。
在Socket应用程序中,“绑定”的作用是让某个Socket对象关联上特定的网络接口(Network Interface)。一台网络主机可能安装有多个网络接口,“绑定”之后,Socket对象将可以在指定那个网络接口(Network Interface )上监听。如果不需要指定特定的网络接口,也不在意使用的端口,那么,可以创建一个使用IPAddress.Any,端口为0的IPEndPoint,Socket绑定这一IPEndPoint之后,操作系统会决定最终使用哪个网络接口,并且在“[1024,5000]”之间的选择一个未用端口分配给此Socket。
注意:
WCF中也有“绑定”,但WCF中的“绑定”的含义要丰富得多,它其实是一组特殊的对象,它的主要功能是创建用于实现WCF应用程序间相互通讯的“信道栈”,WCF基类库中提供了一堆的“绑定”,特定的绑定使用特定的通讯协议和技术,比如NetTcpBinding采用TCP协议,NetMsmqBinding则使用了微软消息队列。
Socket对象绑定网络接口之后,就可以监听并等待客户端连接了:
newsock.Listen(10); //开始监听
Socket client = newsock.Accept(); //等待客户端连接
所谓“监听 (Listen) ”,其实是告诉操作系统:“我关心本机某个网络接口上的数据包,当有数据包到达,并且端口号和我所规定的一致,请通知我”。
Socket.Listen方法的参数有着特殊的含义。此处暂时按下,留待后文分解。
Socket.Accept方法等待客户端发来的连接请求数据包,默认情况下,这一方法是“同步”方法,线程将在此处阻塞等待,直到有客户发来连接请求。
当客户端发来连接请求时,Accept方法返回一个Socket对象,这个对象代表双方已建立了一条数据通讯的链路,可以相互传送数据了。这时,原先的Socket将得到“解放”,可以继续监听。
注意:
负责监听的Socket不负责发送与接收数据,而Accept方法返回的Socket可以用于接收和发送数据,但不能用于接收新的连接,同时,其RemoteEndPoint方法可以获取远程客户端的IP地址和使用的端口
以下代码调用刚得到的Socket对象的Receive方法接收客户端发来的数据:
byte[] data = new byte[1024];
int r ecv = client.Receive(data);
Socket.Receive方法也是一个“阻塞”的同步方法,它将收到的数据保存到一个字节数组中,这个字节数组通常称为“数据缓冲区”。
提示:
数据缓冲区在Socket编程中非常重要,读者会发现,在开发中你时时刻刻都得关注它,一不小心,它就给你捣乱。
Receive方法的返回值代表接收的数据字节数。以下代码使用这一返回值了解客户端到底发来了什么消息:
Console.WriteLine(Encoding.UTF8 .GetString(data, 0, recv ));
上面这句代码中有几点需要特别注意:
(1)一定要使用recv来“定界”客户端传来的数据。
(2)我们假设客户端发送过来的消息是一个字符串,这里使用UTF8进行解码。很明显,这要求客户端与服务端必须事先达成一致,使用同样的编码和解码方式。这种需要在事先进行协商的“东西”,就是“通讯协议 ”。不同的网络应用会使用不同的通讯协议,比如互联网普遍使用HTTP,这是一个业界标准,而我们也可以定义自己的通讯协议,比如QQ就有自己的通讯协议。
提示:
我在《 漫谈.NET开发中的字符串编码 》一文中介绍了字符串编码的基础知识。
数据接收完毕,服务端就可以断开客户的连接:
client.Shutdown(SocketShutdown.Both); //通知OS,不再接收与发送数据
client.Close(); //关闭Socket
完成数据传送任务之后,注意应该及时地关闭Socket。这通常分为两步:
(1)调用Shutdown方法通知TCP/IP协议栈发送所有未发送的数据,或停止接收数据
(2)调用Close方法关闭套接字。
Socket本身对应着一个核心对象,它有一个句柄(Handle)供操作系统内核进行管理。因此,它不再有用时必须及时地被关闭,否则,有可能会造成严重的问题。
提示:
操作系统能管理的句柄数是有限的,而网络应用服务端程序通常会运行很长的时间,如果不及时地关闭不用的Socket,将导致它所占用的句柄不能及时回收,有可能导致服务器Down掉。
Socket本身实现了IDisposable接口,所以也可以使用using关键字实现“自动释放”:
using (newsock)
{
……
} //自动关闭newsock
3.2 客户端应用程序
客户端应用程序与服务端大同小异:
首先创建好一个Socket对象,然后再调用其Connect方法创建到服务端的连接,如果之前Socket没有使用Bind方法指定一个端口,Connect方法会自动选择一个未用的端口:
Socket server = new Socket( AddressFamily.InterNetwork,
SocketType.Stream, ProtocolType.Tcp );
server.Connect(服务端的IP终结点);
如果Connect方法没有抛出异常,则表示成功连接服务器,现在,就可以使用Socket对象的Send方法发送数据,数据同样保存于一个数据缓冲区(其实就是一个byte[])中:
server.Send(Encoding.UTF8 .GetBytes(要发送的消息));
注意这里选择的字符串编码方式必须要与服务端一致,否则,将导致服务端无法正确地解码出字符串。
数据发送完毕,关闭套接字就行了。
3.3 处理网络应用程序中的异常
Socket对象的Connect、Send、Receive等方法都有可能出错,这时,.NET基类库将抛出一个SocketException,它实际上封装的是底层WinSock出错信息。
每一个SocketException对象都有一个对应的错误号,其含义是由底层的WinSock定的。比如错误号为10048的SocketException其含义是:地址已被使用。发生这一异常的原因通常是你尝试把两个Socket对象绑定到同一个IPEndPoint。
以下是Socket网络应用程序中的典型代码框架:
Socket remote=new Socket(……);
try
{
//……
remote.Connect(iep); //iep为远程主机的终结点
//……
remote.Send(……);
//……
}
catch (SocketException e)
{
Console.WriteLine("无法连接远程主机 {0} ,原因:{1},
NativeErrorCode:{2},SocketErrorCode:{3}", iep.Address,
e.Message, e.NativeErrorCode, e.SocketErrorCode);
}
finally
{
server.Close();
}
示例项目IntroduceSocket展示了本文所介绍的知识(图 3)。
图 3
到此,我们与“Socket美女”的“第一次约会”到此结束。您对她的第一印象如何?
点击下载本文示例
==============================================================================
下一篇文章,将介绍Socket美女的“追求者”队伍,以及如何开发“一问一答”的网络应用程序。