现在的位置: 首页 > 综合 > 正文

与Socket的第一次“约会”

2013年08月29日 ⁄ 综合 ⁄ 共 7092字 ⁄ 字号 评论关闭

.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美女的“追求者”队伍,以及如何开发“一问一答”的网络应用程序。


抱歉!评论已关闭.