• 1. C#网络编程方法概述
  • 2. 学习目标了解TCP/IP结构及其基本概念。 掌握.NET网络编程基础知识。 掌握套接字编程的基本原理。 掌握C#中的多线程编程方法。
  • 3. 本章内容5.1 TCP/IP概述 5.2 .NET网络编程基础 5.3 套接字编程 5.4 多线程编程 5.5 基于多线程的编程实例
  • 4. 5.1 TCP/IP概述 1.OSI参考模型 在计算机网络产生之初,每个计算机厂商都有一套自己的网络体系结构的概念,它们之间互不兼容。为此,国际标准化组织(ISO)在1979年成立了一个分委员会来专门研究一种用于开放系统互连的体系结构(Open Systems Interconnection,OSI)。其中“开放”这个词表示:只要遵循OSI标准,一个系统可以和位于世界上任何地方的、也遵循OSI标准的其他任何系统进行连接。这个分委员会提出了开放系统互连,即OSI参考模型,它定义了连接不同类型计算机的标准框架。 OSI参考模型将计算机网络通信定义为一个七层框架模型,如图5.1所示。这七层分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。图5.1 OSI参考模型5.1.1 OSI参考模型与TCP/IP模型
  • 5. 5.1 TCP/IP概述 各层的主要功能及其相应的数据单位如下: (1)物理层(Physical Layer) 要传递信息就要利用一些物理媒体,如双绞线、同轴电缆等,但具体的物理媒体并不在OSI的七层之内,有人把物理媒体当作第0层,物理层的任务就是为它的上一层提供一个物理连接,以及它们的机械、电气、功能和过程特性。如规定使用电缆和接头的类型,传送信号的电压等。在这一层,数据还没有被组织,仅作为原始的位流或电气电压处理,单位是比特。 (2)数据链路层(Data Link Layer) 数据链路层负责在两个相邻结点间的线路上,无差错地传送以帧为单位的数据。每一帧包括一定数量的数据和一些必要的控制信息。和物理层相似,数据链路层负责建立、维持和释放数据链路的连接。在传送数据时,如果接收点检测到所传数据中有差错,就要通知发送方重发这一帧。 (3)网络层(Network Layer) 在计算机网络中进行通信的两个计算机之间可能会经过很多个数据链路,也可能还要经过很多通信子网。网络层的任务就是选择合适的网间路由和交换结点,以确保数据及时传送。网络层将数据链路层提供的帧组成数据包,包中封装有网络层包头,其中含有逻辑地址信息(源站点和目的站点地址的网络地址)。5.1.1 OSI参考模型与TCP/IP模型
  • 6. 5.1 TCP/IP概述 (4)传输层(Transport Layer) 传输层的任务是根据通信子网的特性来最佳地利用网络资源,并以可靠和经济的方式,为两个端系统(也就是源站和目的站)的会话层之间,提供建立、维护和取消传输连接的功能,并负责可靠地传输数据。在这一层,信息的传送单位是报文。 (5)会话层(Session Layer) 会话层也称为会晤层或对话层,在会话层及以上的高层次中,数据传送的单位不再另外命名,都统称为报文。会话层不参与具体的传输,它提供包括访问验证和会话管理在内的建立和维护应用之间通信的机制。如服务器验证用户登录便是由会话层完成的。 (6)表示层(Presentation Layer) 表示层主要解决用户信息的语法表示问题。它将欲交换的数据从适合于某一用户的抽象语法,转换为适合于OSI系统内部使用的传送语法。即提供格式化的表示和转换数据服务。数据的压缩和解压缩,加密和解密等工作都由表示层负责。5.1.1 OSI参考模型与TCP/IP模型
  • 7. 5.1 TCP/IP概述 (7)应用层(Application Layer) 应用层确定进程之间通信的性质以满足用户需要以及提供网络与用户应用软件之间的接口服务。 当然,OSI参考模型只是一个框架,它的每一层并不执行某种功能。在这个OSI七层模型中,每一层都为其上一层提供服务,并为其上一层提供一个访问接口或界面。不同主机之间的相同层次称为对等层。如主机A中的表示层和主机B中的表示层互为对等层,主机A中的会话层和主机B中的会话层互为对等层等。对等层之间互相通信需要遵守通信协议,主要通过软件来实现。每一种具体的协议一般都定义了OSI模型中的各个层次具体实现的技术要求,主机正是利用这些协议来接收和发送数据的。 5.1.1 OSI参考模型与TCP/IP模型
  • 8. 5.1 TCP/IP概述 2.TCP/IP模型 OSI参考模型的提出是为了解决不同厂商、不同结构的网络产品之间互连时遇到的不兼容性问题。但是该模型的复杂性阻碍了其在计算机网络领域的实际应用。与此相反,由技术人员自己开发的传输控制协议/网际协议(Transfer Control Protocol/Internet Protocol,TCP/IP)协议栈模型都获得了更为广泛的应用,成为因特网的基础。实际上,TCP/IP协议也是目前因特网范围内运行的唯一一种协议。TCP/IP模型和OSI参考模型的对比示意图,如图5.2所示。 TCP/IP模型是美国国防部高级研究计划局计算机网(Advanced Research Projects Agency Network,ARPANET)和其后继因特网使用的参考模型。ARPANET是由美国国防部(U.S.Department of Defense,DoD)赞助的研究网络。最初,它只连接了美国境内的四所大学。但在随后的几年中,它通过租用的电话线连接了数百所大学和政府部门。最终ARPANET发展成为全球规模最大的互连网络—因特网。 5.1.1 OSI参考模型与TCP/IP模型
  • 9. 5.1 TCP/IP概述 从名字上看,TCP/IP包括两个协议,即传输控制协议(Transfer Control Protocol,TCP)和网际协议(Internet Protocol,IP),但实际上TCP/IP是一系列协议的代名词,它包括上百个各种功能的协议,如:地址解析协议(ARP)、Internet控制消息协议(ICMP)、文件传输协议等,而TCP协议和IP协议只是保证数据完整传输的两个重要协议。通常讲TCP/IP,但实际上指的是因特网协议系列,而不仅仅是TCP和IP两个协议,所以也常称为TCP/IP协议族。该协议族分为四个层次:链路层、网络层、传输层和应用层。其各层所包含的主要协议如图5.3所示,具体各层所负责的功能如下。5.1.1 OSI参考模型与TCP/IP模型图5.2 OSI参考模型与TCP/IP模型对比 图5.3 TCP/IP协议族
  • 10. 5.1 TCP/IP概述 (1)链路层 链路层是TCP/IP协议族的最低层,有时也被称作数据链路层或网络接口层,通常包括操作系统中的设备驱动程序和计算机中对应的网络接口卡,它们一起处理与电缆(或其他任何传输媒体)的物理接口。该层负责接收IP数据报并通过网络发送到网络传输媒体上,或者从网络上接收物理帧,抽出IP数据报交给IP层。实际上,TCP/IP模型并没有真正描述这一层的实现,只是要求能够提供给其上层(网络层)一个访问接口,以便在其上传递IP分组。由于这一层次未被定义,所以其具体的实现方法也随着网络类型的不同而不同。 (2)网络层 网络层是整个TCP/IP协议栈的核心,有时也被称为互联网层或IP层。该层的主要功能是把分组发往目标网络或主机。同时,为了尽快发送分组,可能需要沿不同的路径同时进行分组传递。因此,分组到达的顺序和发送的顺序可能不同,这就需要上层对分组进行排序。网络层除了完成上述功能外,还完成将不同类型的网络(异构网)进行互连的功能。除此之外,网络层还需要完成拥塞控制的功能。 在TCP/IP协议族中,网络层协议包括IP协议(网际协议)、ICMP协议(因特网控制报文协议)和IGMP协议(因特网组管理协议)。5.1.1 OSI参考模型与TCP/IP模型
  • 11. 5.1 TCP/IP概述 (3)传输层 传输层主要为两台主机上的应用程序提供端到端的数据通信,它分为两个协议:TCP(传输控制协议)和UDP(用户数据报协议)。TCP提供有质量保证的端到端的数据传输。若传输层使用TCP协议,则该层负责数据的分组、质量控制和超时重发等,对于应用层来说,就可以忽略这些工作。UDP则只负责简单地把数据报从一端发送到另一端。若传输层使用UDP协议,则数据是否到达、是否按时到达、是否损坏都必须由应用层来控制。这两种协议各有用途,前者可用于面向连接的应用,后者则在及时性服务中有着重要的用途,如网络多媒体通信等。 (4)应用层 应用层负责处理实际的应用程序细节,主要包括超文本传输协议(HTTP)、简单网络管理协议(SNMP)、文件传输协议(FTP)、简单邮件传输协议(SMTP)、域名系统(DNS)、远程登录协议(Telnet)等。其中,有些应用层协议是基于TCP来实现的,例如FTP、HTTP等,有些则是基于UDP来实现的,如SNMP等。5.1.1 OSI参考模型与TCP/IP模型
  • 12. 5.1 TCP/IP概述 3.TCP/IP工作原理 由上述OSI参考模型可知,在因特网上源主机的协议层与目的主机的同层通过下层提供的服务实现对话。TCP/IP协议族模型也是按照这一原则来工作的。它们之间的对话实际上是在源主机上从上到下传递然后穿越网络到达目的主机后再从下到上到达相应层。 下面以图5.4中的客户机A(信源)和服务器B(信宿)之间采用应用层协议HTTP协议提交数据请求为例,说明TCP/IP的工作原理。5.1.1 OSI参考模型与TCP/IP模型
  • 13. 5.1 TCP/IP概述 图5.4中的逻辑传输线路表明了数据传输的方向,以及信源和信宿,实际传输线路则表明了请求数据的真实传输链路。请求数据从信源传输到目的信宿的过程可描述如下: (1)在信源上,利用应用层协议(HTTP)将需传输的请求数据流传送给信源上的传输层(TCP)。 (2)信源上的传输层将应用层的请求数据流截成若干分组,并加上TCP首部形成TCP段,送交信源上的网络层(IP)。 (3)信源的网络层给TCP段加上包括源、目的主机IP地址的IP首部,生成一个IP数据报,并将IP数据报送交信源的链路层。 (4)信源的链路层在其MAC帧的数据部分装上IP数据报,再加上源、目的主机的MAC地址和MAC帧头,并根据其目的MAC地址,将MAC帧发往信宿或中间路由器,例如路由器R。 (5)路由器是一个具有多个接口的网络互连设备,可以把数据从一个网络转发到另一个网络。当数据传输到路由器后,路由器将根据数据包中的目的地址进行传输路径的选择,并根据所选择的传输路径进行数据传输。通常,路由器只处理链路层和网络层的数据。在本实例中,路由器接收客户机A发送过来的IP数据报,并将该数据报转发给服务器B。5.1.1 OSI参考模型与TCP/IP模型
  • 14. 5.1 TCP/IP概述 (6)当数据传输到信宿,链路层将MAC帧的帧头去掉,并将IP数据报送交信宿的网络层。 (7)信宿的网络层检查IP数据报首部,假如首部中校验和与计算结果不一致,则丢弃该IP数据报;若校验和与计算结果一致,则去掉IP首部,将TCP段送交信宿的传输层。 (8)信宿的传输层检查顺序号,判定是否是正确的TCP分组,然后检查TCP首部数据。若正确,则向信源发确认信息;若不正确或丢包,则向信源要求重发信息。 (9)信宿的传输层去掉TCP首部,将排好顺序的分组组成应用数据流送给信宿上相应的应用程序。这样信宿接收到的来自信源的字节流,就像是直接接收到来自信源的字节流一样。 5.1.1 OSI参考模型与TCP/IP模型
  • 15. 5.1 TCP/IP概述 5.1.2 TCP/IP基本概念 1.IP地址 IP 地址是进行TCP/IP协议通信的基础,IP地址是对连接在因特网中的设备进行唯一性标识的设备编码,与日常生活中寄信时所用的信箱号类似,以便设备之间能根据IP地址来识别。在因特网中,根据TCP/IP协议规定,在IPv4中,IP地址由32位二进制数组成,其地址空间是0~232-1。为了便于记忆,将这32位二进制数分成四段,每段8位,中间用小数点隔开,将每八位二进制数转换成一位十进制数,这样就形成了点分十进制的表示方法。例如:192.168.0.181。一个简单的IP地址的格式为:IP地址 = 网络地址 + 主机地址,包含了网络地址和主机地址两部分重要的信息。由于IPv4定义的有限地址空间将被耗尽,地址空间的不足必将影响因特网的进一步发展。所以在最新出台的IPv6中IP地址升至128位。 IP地址共分五类:A类、B类、C类、D类和E类。其中A类、B类和C类为基本类;D类用于多播传送;E类属于保留类,暂未使用。它们的格式如下所示,其中“*”代表网络号位数。
  • 16. 5.1 TCP/IP概述 5.1.2 TCP/IP基本概念 A类地址的最高位必须是“0”,其第一个字节为网络地址,后三个字节为主机地址。因此A类地址可拥有126个网络地址数,其中每个网络最多可以包含的主机数目为224-2(主机地址全1和全0都属于特殊地址),即有16777214台主机。因此,A类地址适用于超大规模的网络。 B类地址的最高两位必须是“10”,前两个字节为网络地址,后两个字节为主机地址。B类IP地址中网络地址长度为14位,有16384个网络,其中每个网络最多可以包含的主机数目为216−2,即有65534台主机。因此,B类地址适用于中等规模的网络。
  • 17. 5.1 TCP/IP概述 5.1.2 TCP/IP基本概念 C类地址的最高三位必须是“110”,前三个字节为网络地址,最后一个字节为主机地址。因此,C类地址的网络数目为221,即有2907152个网络,其中每个网络可以包含的主机数目为28−2,即有254台主机。因此,C类地址适用于小规模的局域网络。 D类地址与前三类地址不同,它是一种特殊的IP地址类,应用于多播通信,因此也被称为多播地址。地址前面有4个引导位“1110”,其余的28位表示多播地址,因此其地址范围为:224.0.0.0~239.255.255.255。D类地址只能作为目的地址,不能作为源地址。 E类地址是一般不用的实验性地址,前面包含4个引导位“1111”,因此其地址范围为:240.0.0.0~255.255.255.255。
  • 18. 5.1 TCP/IP概述 5.1.2 TCP/IP基本概念 除上述几类地址外,还有几个特殊的地址。 网络地址:IP地址中主机地址为0的地址表示网络地址。这类地址不指派给任何主机,它只保留用来定义某个网络的地址。例如,某主机的IP地址为175.22.10.48,它是一个B类地址,则该主机所在网络的地址为175.22.0.0。 广播地址:在A、B、C三类地址中,主机号全为“1”的地址为广播地址。这类地址用来同时向指定网络的所有主机发送信息。例如,如果某台主机使用175.22.255.255为目标地址发送数据报时,则网络地址为175.22.0.0的网络中的所有主机都能收到该数据报。 回送地址:在IP地址中,首字节数值为“127”的地址是一个保留地址,称为回送地址。如:127.0.0.1即为一个回送地址。该类地址用于网络测试或本机进程间通信。发送到这种地址的数据报不输出到线路上,而是立即被返回,又当作输入数据报在本机内部进行处理。例如,常用的“ping”命令,就是发送一个将回送地址作为目的地址的数据报,以测试IP软件能否接受和处理数据报。
  • 19. 5.1 TCP/IP概述 5.1.2 TCP/IP基本概念 2.子网与掩码 如上所述,IP地址最初采用的是网络地址和主机地址两级结构,然而在实际组网过程中,常常会出现使用C类地址时,主机编址空间不够,而使用A类或B类地址时,又会造成大量IP地址浪费的现象。为此,IP地址现在多采用三级结构,即IP地址=网络地址+子网地址+主机地址。把每个网络的主机地址空间根据需要再进一步划分成若干个子网,则原来两级地址结构中的主机地址又细分为子网地址和主机地址,子网地址位数根据子网的实际规模来确定。具体三级结构地址的确定需要借助子网掩码来实现。 子网掩码是一个32位地址掩码,对应于网络地址和子网地址的地址掩码位设置为“1”,而对应于主机地址的地址掩码位设置为“0”。子网掩码用于屏蔽IP地址的一部分以区别网络标识和主机标识,并说明该IP地址是在局域网上,还是在远程网上。 确定子网掩码的过程也就是划分子网的过程,通常划分步骤如下: (1)确定网络地址,划出网络标识和主机标识。 例如:申请到的网络号为“202.195.a.b”,该网络地址为C类IP地址,网络标识为“202.195”,主机标识为“a.b”。 (2)根据需求确认子网个数。 在确认子网个数时应当考虑将来的扩展情况。例如:现在需要12个子网,将来可能需要16个子网,则至少需要用第三个字节的前四位来确定子网掩码,而后四位仍然用于主机地址。所以将前四位都置为“1”,后四位全置为“0”,即第三个字节为“11110000”。
  • 20. 5.1 TCP/IP概述 5.1.2 TCP/IP基本概念 (3)得出子网掩码。 对应于网络地址和子网地址的地址掩码位设置为“1”,而对应于主机地址的地址掩码位设置为“0”。则子网掩码的二进制形式为:“11111111.11111111.11110000.00000000”,即为 “255.255.240.0”。 3.端口号 按照TCP/IP模型的描述,应用层所有的应用进程(应用程序)都可以通过传输层再传送到IP层,传输层从IP层收到数据后必须交付给指明的应用进程,因此必须给应用层的每一个应用程序赋予一个非常明确的标志。由于在因特网上使用的计算机的操作系统种类很多,不同的系统会使用不同的进程标识符,因此无法采用计算机中的进程标识符来作为标志,必须采用统一的方法对TCP/IP体录的应用进程进行标志。为了标识通信实体中进行通信的进程,TCP/IP协议提出了协议端口(protocol port,简称端口)的概念。 端口是一种抽象的软件结构(包括一些数据结构和I/O缓冲区)。应用程序通过系统调用与某端口绑定(binding)后,传输层传给该端口的数据都被相应的进程所接收,相应进程发给传输层的数据也通过该端口输出。类似于文件描述符,每个端口都拥有一个叫端口号的整数描述符,用来区别不同的端口。TCP/IP协议使用一个16位的整数来标识一个端口,它的范围是0~65535。由于TCP协议和UDP协议是两个完全独立的软件模块,因此各自的端口号也相互独立。如TCP有一个255号端口,UDP也可以有一个255号端口,两者并不冲突。
  • 21. 5.1 TCP/IP概述 5.1.2 TCP/IP基本概念 端口号的分配通常有以下两种方法: (1)全局分配 这是一种集中分配方式,由一个公认权威的机构根据用户需要进行统一分配,并将结果公布于众。 (2)本地分配 本地分配又称动态连接,即进程需要访问传输层服务时,向本地操作系统提出申请,操作系统返回本地唯一的端口号,进程再通过合适的系统调用,将自己和端口连接起来。 TCP/IP端口号的分配综合了以上两种方式,将端口号分为两部分,少量的作为保留端口,以全局方式分配给服务进程。每一个标准服务器都拥有一个全局公认的端口,即使在不同的机器上,其端口号也相同。剩余的为自由端口,以本地方式进行分配。TCP和UDP规定,小于256的端口才能作为保留端口。具体讲,TCP/IP端口号分为如下两类。 (1)服务器端使用的端口号。 服务器端的端口号又分为两类,最重要的一类叫公认端口号(well-known port number)或系统端口号,从0~1023,它们紧密绑定于一些服务。通常这些端口的通信明确表明了某种服务的协议。例如,对于每个TCP/IP实现来说,FTP服务器的TCP端口号都是21,每个Telnet服务器的TCP端口号都是23,每个TFTP(简单文件传送协议)服务器的UDP端口号都是69,HTTP通信的端口号实际上总是80端口等。
  • 22. 5.1 TCP/IP概述 5.1.2 TCP/IP基本概念 另一类叫注册端口号(Registered Ports),从1024~49151。它们松散地绑定于一些服务。也就是说有许多服务绑定于这些端口,这些端口同样用于许多其他目的。例如,许多系统处理动态端口从1024左右开始。使用这类端口号必须在IANA按照规定的手续登记,以防重复。 (2)客户端使用的端口号。 这类端口通常又称为动态和/或私有端口(Dynamic and/or Private Ports),从49152~65535。理论上,不应为服务分配这些端口。这类端口号是留给客户进程选择暂时使用的。当服务器进程收到客户进程的报文时,就知道了客户进程所使用的端口号,因而可以把数据发送给客户进程。通信结束后,刚才已经使用过的客户端口号就不复存在。这个端口号就可以供其他客户进程以后使用。实际上,机器通常从1024开始分配动态端口。 4.地址解析 地址解析(Address Resolution)就是将计算机中的协议地址翻译成物理地址(或称MAC地址,即媒体映射地址)。  地址解析技术可分为如下3种。 (1)表查询(Table-Lookup)。该方法适用于广域网,通过建立映射数组(协议地址↔物理地址)的方法解决。当需要进行地址解析时,由软件通过查询找到物理地址。
  • 23. 5.1 TCP/IP概述 5.1.2 TCP/IP基本概念 (2)相近形式计算(Closed-Form Computation)。该方法适用于可以自行配置的网络,IP地址和物理地址相互对应。通常分配给计算机的协议地址是根据其物理地址经过仔细挑选的,使得计算机的物理地址可以由它的协议地址经过基本的逻辑和算术运算计算出来。 例如:   202.195.50.1→ XXXl   202.195.50.2→ XXX2 可通过这种算法得到物理地址:物理地址 = 协议地址 & 0xFF。 (3)信息交换(Message Exchange)。该方式适用于LAN,是基于分布式的处理方式,即主机发送一个解析请求,以广播的形式发出,并等待网络内各个主机的响应。   TCP/IP协议包含了地址解析协议(Address Resolution Protocol,ARP)。ARP标准定义了两种基本信息类型:请求与响应。当一台主机要求转换一个IP地址时,它广播一个含有该IP地址的ARP请求,如果该请求与一台机器的IP地址匹配,则该机器发出一个含有所需物理地址的响应。响应是直接发给广播该请求的机器的。 在使用ARP的计算机上都保留了一个高速缓存,用于存放最近获得的IP地址到物理地址的绑定,在发送分组时,计算机先到缓存中寻找所需的绑定,如没有,则发出一个ARP请求。接收方在处理ARP分组之前,先更新它们缓存中发送方的IP地址到物理地址的绑定信息,再进行响应或抛弃。
  • 24. 5.1 TCP/IP概述 5.1.2 TCP/IP基本概念 5.域名系统 在Internet上,既可以使用主机名标识一台主机,也可以使用IP地址来标识。但是在TCP/IP中,点分十进制的IP地址记起来总是不如名字那么方便,人们更愿意使用便于记忆的主机名标识符,所以,就采用了域名系统(Domain Name System,DNS)来管理名字和IP地址的对应关系。一个系统的全域名由主机名、域名和扩展名三部分组成,各部分间使用“.”分隔,例如www.sina.com.cn。在TCP/IP应用中,域名系统是一个分布的数据库,由它来提供IP地址和主机名之间的映射信息,可以通过在程序中调用标准库函数来编程实现域名与IP地址之间的相互转换,这一转换过程称为“域名解析”。通过从域名地址到IP地址的映射,使得在日常的网络应用中,可以使用域名这种便于记忆的地址表示形式。所有的网络应用程序理论上都应该具有内嵌的域名解析机制。
  • 25. 5.1 TCP/IP概述 5.1.2 TCP/IP基本概念 域名解析的流程由以下几步构成: (1)客户机提出域名解析请求,并将该请求发送给本地的域名服务器。 (2)当本地的域名服务器收到请求后,就先查询本地的缓存,如果有该记录项,则本地的域名服务器就直接把查询的结果返回。 (3)如果本地的缓存中没有该记录,则本地域名服务器就直接把请求发给根域名服务器,然后根域名服务器再返回给本地域名服务器一个所查询域(根的子域)的主域名服务器的地址。 (4)本地服务器再向上一步返回的域名服务器发送请求,然后接受请求的服务器查询自己的缓存,如果没有该记录,则返回相关的下级域名服务器的地址。 (5)重复第四步,直到找到正确的记录。 (6)本地域名服务器把返回的结果保存到缓存,以备下一次使用,同时还将结果返回给客户机。
  • 26. 5.1 TCP/IP概述 5.1.2 TCP/IP基本概念 当主机通过网络向其他设备传输数据时,首先要对数据进行打包,这一打包的过程就称为数据封装。在TCP/IP模型中,为了实现通信并交换信息,每一层都有各自的协议数据单元(Protocol Data Units,PDU),通过封装使每个PDU附加到数据上。每个PDU都有其特定的名称,如:链路层-数据帧,网络层-数据包,传输层-数据段。这种PDU信息只能由接收方设备中的对等层读取,在读取之后,报头就被剥离,然后把数据交给上一层。数据的封装过程如图5.5所示。解封装则是数据封装的逆过程,当目的主机收到一个以太网数据帧时,数据就开始从协议栈中由底向上升,同时去掉各层协议加上的报文首部。每层协议都要去检查报文首部中的协议标识,以确定接收数据的上层协议。这个过程称为解封装。
  • 27. 5.2 .NET网络编程基础 5.2.1 .NET中的网络组件 C#和C++的差异之一,就是它本身没有类库,C#所使用的类库是.Net框架中的类库—.Net Framework SDK。因此了解并掌握.Net框架为网络编程提供的类库是学习C#网络编程的前提。.Net框架为网络开发提供了两个顶层命名空间:System.Net和System.Web,同时它们又包含多个子命名空间,C#就是通过这些命名空间中封装的类和方法实现网络通信编程、Web应用编程以及Web Service编程的。具体命名空间及其所含类的功能概述如表5.1 所示。
  • 28. 5.2 .NET网络编程基础 5.2.1 .NET中的网络组件 命 名 空 间功 能 概 述System.Net为当前网络上流行的多种协议提供一个统一、简单的编程接口。其中WebRequest和WebResponse类形成了“可插入协议”的基础,利用这种网络服务的实现,可以开发在使用Internet资源时不必考虑所用协议具体细节的应用程序System.Net.Cache定义类型和枚举,这些类型和枚举用于为使用WebRequest和HttpWebRequest类获取的资源定义缓存策略System.Net.Configuration所含类提供以编程方式访问和更新System.Net命名空间的配置设置的功能System.Net.Mail用于将电子邮件发送到简单邮件传输协议(SMTP)服务器进行传送的类System.Net.Mime包含用于表示多用途Internet邮件交换(MIME)标头的类型。这些类型与System.Net.Mail命名空间中的类型一起使用,用于在使用SmtpClient类发送电子邮件时指定Content-Type、Content-Disposition和Content-transfer-Encoding标头System.Net.NetworkInformation提供对网络流量数据、网络地址信息和本地计算机的地址更改通知的访问。该命名空间还包含实现Ping实用工具的类。您可以使用Ping和相关的类检查是否可通过网络访问某台计算机System.Net.Security为网络流在主机间的传输提供了安全控制System.Net.Sockets为需要严格控制网络访问的开发人员提供Windows套接字(Winsock)接口的托管实现System.Web包含启用浏览器/服务器通信的类和接口。这些命名空间类用于管理到客户端的HTTP输出(HttpResponse)和读取HTTP请求(HttpRequest)。附加的类则提供了一些功能,用于服务器端的实用程序以及进程、cookie管理、文件传输、异常信息和输出缓存控制System.Web.UI包含创建Web窗体页的类,包括Page类和用于创建Web用户界面的其他标准类System.Web.UI.HtmlControls包含创建ASP.NET Web服务器控件的类。当添加到Web窗体时,这些控件将呈现浏览器特定的HTML和脚本,用以创建与设备无关的Web用户界面System.Web.Mobile包含生成ASP.NET移动Web应用程序所需的核心功能,包括身份验证和错误处理System.Web.UI.MobileControls包含一组ASP.NET服务器控件,这些控件可以针对不同的移动设备呈现应用程序System.Web.Services包含使您能够生成和使用XML Web services的类,这些服务是驻留在Web服务器中的可编程实体,并通过标准Internet协议公开
  • 29. 5.2 .NET网络编程基础 5.2.1 .NET中的网络组件 类 名功 能 概 述DNS提供简单域名解析功能DnsPermission控制对网络DNS服务器的访问EndPoint用于标识网络地址FileWebRequest为WebRequest类提供了一个文件系统实现FileWebResponse为WebResponse类提供了一个文件系统实现HttpVersion定义了由HttpWebRequest和HttpWebResponse类支持的HTTP版本号HttpWebRequest为WebRequest类提供了特定于HTTP的实现HttpWebReponse为WebResponse类提供了特定于HTTP的实现IPAddress提供了IP地址IPEndPoint以IP地址和端口号的形式代表一个网络终端IPHostEntry为Internet主机地址信息提供了容器类ProtocolViolationException当使用网络协议时出现错误,则将抛出由该类所代表的异常SocketAddress代表一个套接字地址SocketPermission控制在传输地址上生成或接收连接的权限SocketPermissionAttribute允许将SocketPermission的安全动作,施用于使用声明安全性的代码WebClient为客户与Internet资源间的数据发送和接收提供了通用方法WebException当通过可插入协议访问网络时出现错误,则将抛出由该类代表的异常WebProxy包含WebRequest类的HTTP代理WebRequest代表一个到URI的请求WebResponse代表来自URI的响应
  • 30. 5.2 .NET网络编程基础 5.2.1 .NET中的网络组件 类 名功 能 概 述LingerOption包含套接字延迟时间的信息,即当数据仍在发送时,套接字应在关闭后保持的时间MulticastOption包含了IP多点传送数据包的选项值NetworkStream为网络访问提供了基础数据流Socket实现了Berkeley套接字接口SocketException当出现套接字错误时,将抛出由该类所代表的异常TCPClient为TCP网络服务提供了客户连接TCPListener用以监听TCP客户连接UDPClient用于提供UDP网络服务
  • 31. 5.2 .NET网络编程基础 5.2.1 .NET中的网络组件 类 名功 能 概 述HttpApplication定义了ASP.NET应用程序中所有应用程序对象的通用方法、属性和事件HttpApplicationState允许ASP.NET应用程序中的多个会话和请求共享全局信息HttpBrowserCapabilities允许服务器收集客户端浏览器的性能信息HttpContext封装了所有关于HTTP请求的特定信息HttpException提供了生成HTTP异常的手段HttpFileCollection为由用户上传的文件提供访问和组织手段HttpParseException为生成HTTP解析异常提供了手段HttpPostedFile提供了访问由客户上传的文件的方式HttpRequest允许ASP.NET读取在Web请求中由客户发送的HTTP值HttpResponse封装了来自一个ASP.NET操作的HTTP响应信息HttpUtility为处理Web请求时的URL编码和解码提供了方法ProcessInfo提供了当前运行的进程信息
  • 32. 5.2 .NET网络编程基础 5.2.2 网络编程中的常用类 1.IP地址类 与IP地址相关的类有IPAddress类、IPHostEntry类、IPEndPoint类等。IPAddress类是一个描述IP地址的类,主要用来存储IP地址。IPAddress类的属性和方法如下表所示。属性、方法名说 明Any只读属性,提供一个IP地址,标识服务器应该监听所有网络接口上的客户活动Broadcast只读属性,提供IP广播地址,等价于255.255.255.255Loopback只读属性,提供IP回送地址,等价于127.0.0.1None只读属性,提供一个IP地址,标识不应使用网络接口Address获取或设置一个IP地址AddressFamily指定IP地址的地址族Equals( )比较两个IP地址GetHashCode( )获取IP地址哈希值HostToNetworkOrder( )将主机字节顺序值转换为网络字节顺序值Parse( )将IP地址字符串转换为IP地址实例
  • 33. 5.2 .NET网络编程基础 5.2.2 网络编程中的常用类 IPHostEntry类是为Internet主机地址信息提供容器的类,它将DNS主机名与一个别名数组和匹配的IP地址数组相关。通常IPHostEntry类作为Dns类的辅助类使用。该类有如下几个属性。 Aliases属性:获取或设置与主机相关的别名清单。 AddressList属性:获取或设置与主机相关的IP地址。其值为IPAddress类型的数组,其中包含的IP地址用于解析Aliases属性中的主机名。 HostName属性:获取或设置主机的DNS名。包含服务器的基础主机名,如果服务器的DNS项定义了附加别名,则可通过Aliases属性使用它们。 IPEndPoint类以IP地址和端口号的形式代表一个网络终端。该类中包含应用程序连接到主机服务时需要的主机和端口信息,通过组合主机的IP地址和端口号构成服务的一个连接点。IPEndPoint类的属性和方法如下表所示。属性、方法名说 明Address获取或设置EndPoint的IP地址AddressFamily获取IP地址族Port获取或设置EndPoint的TCP端口号MaxPort用于指定可被赋予Port属性的最大值MinPort用于指定可被赋予Port属性的最小值Create( )调用Creat( )方法,以根据套接字地址创建EndPointSerialize( )调用Serialize( )方法,以将EndPoint信息序列化到一个SocketAddress实例中
  • 34. 5.2 .NET网络编程基础 5.2.2 网络编程中的常用类 2.域名解析类 Dns类是一个静态类,它提供了有关域名解析的操作。它将从网络主机域名系统中获取IP地址和主机名、WWW域名的对应关系。它返回一个IPHostEntry对象以保存结果。如果返回值是多个信息,IPHostEntry将返回主机的多个地址和别名。Dns类的方法如下表所示。属性、方法名说 明BeginGetHostByName( )开始由主机名获得IPHostEntry信息,异步操作BeginGetHostEntry( )开始由IP地址或主机名获得IPHostEntry信息,异步操作BeginResolve( )开始请求域名解析,由WWW名获得IPHostEntry信息,异步操作EndGetHostByName( )终止对DNS信息的异步请求(与BeginGetHostByName( )对应)EndGetHostEntry( )终止对DNS信息的异步请求(与BeginGetHostEntry( )对应)EndResolve( )终止对DNS信息解析的异步请求GetHostByAddress( )根据指定IP地址创建一个IPHostEntry实例GetHostByName( )根据主机名获取一个IPHostEntry实例GetHostEntry( )根据IP地址或主机名获取一个IPHostEntry实例GetHostName( )获取本地计算机的主机名Resolve( )将DNS主机名或IP字符串转换为IPHostEntry实例
  • 35. 5.2 .NET网络编程基础 5.2.2 网络编程中的常用类 DnsPermission类控制对网络DNS服务器的访问。默认情况下,所有本地和Internet域中的应用程序都能访问DNS服务,并且对Internet应用程序无DNS许可。DnsPermission类的方法如下表所示。属性、方法名说 明Copy( )创建当前实例的拷贝FromXml( )根据XML编码重构DnsPermission实例Intersect( )创建当前DnsPermission实例与指定DnsPermission实例的交集IsSubsetOf( )确定当前DnsPermission实例是否为指定DnsPermission实例的子集IsUnrestricted( )检查对象的许可状态ToXml( )使用当前的DnsPermission实例及其状态创建XML编码Union( )创建当前DnsPermission实例与指定DnsPermission实例的并集
  • 36. 5.2 .NET网络编程基础 5.2.2 网络编程中的常用类 3.类使用实例 以上介绍的IP地址类和域名解析类是网络编程中常用的基础类。下面用一个获取主机名和IP地址的实例来说明上述类的使用方法。 using System; using System.Collections.Generic; using System.Text; using System.Net; namespace IPDnsTest { class Program { static void Main(string[] args) { string strHostName; //获取本地计算机名称 strHostName = Dns.GetHostName( ); Console.WriteLine("本地计算机名:" + strHostName); //由本地计算机名称获取本机IP地址 IPHostEntry ipEntry = Dns.GetHostEntry(strHostName); IPAddress[] addr = ipEntry.AddressList; //显示本机IP地址 for (int i = 0; i < addr.Length; i++) { Console.WriteLine("IP地址[{0}]:{1}", i, addr[i].ToString( )); Console.WriteLine("地址类型[{0}]:{1}",i,addr[i].AddressFamily.ToString( )); } Console.ReadKey( ); } } }
  • 37. 5.3 套接字编程 5.3.1 套接字简介 套接字(Socket)的概念首先是由BSD UNIX提出的。当时在UNIX编程中,引入了文件描述符(file descriptor)的概念。一个文件描述符提供了到一个文件对象的编程接口。因为UNIX操作系统中几乎所有的对象都定义成文件,文件描述符可以被用来在UNIX系统中收发数据,这些数据可以包含很多对象。因为不需要考虑所操作的文件(或设备)的类型,因此在UNIX中,用一个套接字表示一个网络文件描述符,编程就显得简便的多。在此基础上加利福尼亚大学Berkeley学院为UNIX开发了网络通信编程接口。但是最初它只能运行在UNIX操作系统,不支持DOS和Windows操作系统。随着Windows操作系统的日益推广,20世纪90年代初,微软和第三方厂商共同制定了一套标准,即Windows Sockets规范,简称WinSock。 Windows Sockets以UNIX中流行的Socket接口为范例定义了一套Microsoft Windows网络编程接口。Windows Sockets规范旨在提供给应用程序开发人员一套简单的API,并让各家网络软件供应商共同遵守。Windows Sockets 1.1和Berkeley Sockets都是基于TCP/IP协议的;Windows Sockets 2从Windows Sockets 1.1发展而来,与协议无关并向下兼容,可以使用任何底层传输协议提供的通信能力,来为上层应用程序完成网络数据通信,而不用关心底层网络链路通信的情况,真正实现了底层网络通信对应用程序的透明。
  • 38. 5.3 套接字编程 5.3.1 套接字简介 Windows Sockets规范定义并记录了如何使用API与Internet协议族(IPS,通常是指TCP/IP)连接,尤其要指出的是,所有的Windows Sockets实现都支持流套接接口和数据报套接接口,应用程序调用Windows Sockets的API实现相互之间的通信。Windows Sockets又利用下层的网络通信协议功能和操作系统调用实现实际的通信工作。它们之间的关系如下图所示。 套接字是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。套接字存在于通信域中,Windows Sockets只支持一个通信域:网际域(AF-INET),这个域被使用网际协议族通信的进程所使用。 套接字有两种不同的类型:流套接字和数据报套接字。
  • 39. 5.3 套接字编程 5.3.1 套接字简介 TCP/IP的Socket则提供3种类型的套接字。 1.流式套接字(SOCK_STREAM) 提供面向连接、可靠的数据传输服务,数据无差错、无重复的发送,且按发送顺序接收。内设流量控制,避免数据流超限;数据被看作是字节流,无长度限制。文件传输协议(FTP)即使用流式套接字。 2.数据报式套接字(SOCK_DGRAM) 提供无连接服务。数据包以独立包形式发送,不提供无差错保证,数据可能丢失或重复,并且接收顺序混乱。网络文件系统(NFS)使用数据报式套接字。 3.原始套接字(SOCK_RAW) 该接口允许对较低层协议,如IP、ICMP直接访问。常用于检验新的协议实现或访问现有服务中配置的新设备。
  • 40. 5.3 套接字编程 5.3.2 套接字编程原理 1.C/S编程模式 在TCP/IP网络中软硬件资源、运算能力和信息通常都是不均等的,为了能够对这些资源进行共享,需要一种机制在希望通信的进程间建立联系,为二者的数据交换提供服务,这种机制即为通信进程间的作用模式。通常,通信的两个进程间相互作用的主要模式是客户机/服务器模式(client/server)。在一个具有多台计算机的网络中,那些在其上运行的应用程序是为了请求另一台计算机上的服务(如访问数据库)的计算机称为客户端(Client),而处理这些服务请求(例如对数据库进行检索,将结果返回)的计算机称为服务器(Server)。客户机/服务器模式就是客户机向服务器提出请求,服务器接收到请求后,提供相应服务的一种作用模式。 客户机/服务器模式工作时要求有一套为客户机和服务器所公认的协议来保证服务能被提供(或接受)。根据不同的情况,协议可以是非对称的也可以是对称的。在对称的协议中,每一方都有可能扮演主从角色;在非对称协议中,一方被不可改变的认为是主机,而另一方则是从机。 服务器软件既包括遵循OSI或其他网络结构的网络软件,又包括由该服务器提供给网络上的应用程序或服务软件。在服务器上执行的计算通常被称为后端处理。服务器端程序通常在一个众所周知的地址监听对服务的请求。也就是说,服务进程一直处于休眠状态,直到一个客户对这个服务的地址提出了连接请求。在这个时刻,服务程序被“唤醒”并且为客户提供服务。因此服务器方一般都是先启动的。
  • 41. 5.3 套接字编程 5.3.2 套接字编程原理 服务器端程序执行步骤如下。 (1)打开一个通信通道并告知本地主机,它愿意在某一地址和端口上接收客户请求。 (2)等待客户请求到达该端口。 (3)接收到重复服务请求,处理该请求并发送应答信号。接收到并发服务请求,激活一个新的进程(或线程)来处理这个客户请求。新进程(或线程)处理此客户请求,并不需要对其他请求作出应答。服务完成后,关闭此新进程与客户的通信链路,并终止。 (4)返回第二步,等待另一客户请求。 (5)关闭服务器。 与服务器端相对应,客户机执行的计算通常被称为前端处理。客户机端软件一般由网络接口软件、支持用户需求的应用程序以及实现某些网络功能的实用程序(如电子邮件等)组成。应用程序软件执行具体的任务,如字处理,电子表格和数据库查询等。实用程序软件通常执行几乎所有网络用户都要求的标准任务。网络接口软件提供各种数据传输服务,其执行步骤如下。 (1)打开一个通信通道,并连接到服务器所在主机的特定端口。 (2)向服务器发服务请求报文,等待并接收应答;继续提出请求。 (3)请求结束后关闭通信通道并终止。
  • 42. 5.3 套接字编程 5.3.2 套接字编程原理 2.Socket编程的通信方式 在利用Socket进行编程时要先了解以下几个概念,同步(Synchronous)、异步(Asynchronous)、阻塞(Block)和非阻塞(Unblock)。其中,同步、异步是属于通信模式的概念,而阻塞、非阻塞则属于套接字模式的概念。 (1)同步方式 通信的同步,指客户端在发送请求后,必须在服务端有回应后才能发送下一个请求。所以这个时候的所有请求将会在服务端得到同步。 (2)异步方式 通信的异步,指客户端在发送请求后,不必等待服务端的回应就可以发送下一个请求,这样对于所有的请求动作来说将会在服务端得到异步,这条请求的链路就像是一个请求队列,所有的动作在这里不会得到同步。 (3)阻塞方式 阻塞套接字是指执行此套接字的网络调用时,所调用的函数只有在得到结果之后才会返回,在调用结果返回之前,当前线程会被挂起,即此套接字一直阻塞在网络调用上。比如调用StreamReader类的ReadLine( )方法读取网络缓冲区的数据,如果调用的时候没有数据到达,那么此ReadLine( )方法将一直挂在调用上,直到读到一些数据,此函数才返回。
  • 43. 5.3 套接字编程 5.3.2 套接字编程原理 (4)非阻塞方式 非阻塞和阻塞的概念相对应,非阻塞套接字是指在执行此套接字的网络调用时,即使不能立刻得到结果,该函数也不会阻塞当前线程,而会立刻返回。对于非阻塞套接字,同样调用StreamReader类的ReadLine( )方法读取网络缓冲区的数据,不管是否读到数据都立即返回,而不会一直挂在此函数调用上。 对象是否处于阻塞模式和函数是不是阻塞调用有很强的相关性,但是并不是一一对应的。阻塞对象上可以有非阻塞的调用方式,可以通过一定的API去轮询状态,在适当的时候调用阻塞函数,就可以避免阻塞。而对于非阻塞对象,调用特殊的函数也可以进入阻塞调用。函数select就是这样的一个例子。 在Windows网络通信软件开发中,最常用的方法就是异步非阻塞套接字。客户端/服务器结构的软件采用的方式就是异步非阻塞模式。在利用C#进行网络编程时,由于.NET Framework SDK对阻塞和非阻塞的工作机制进行了封装,因此不需要深入了解同步、异步、阻塞、非阻塞的原理。 3.套接字工作原理 套接字可以像Stream流一样被视为一个数据通道,这个通道架设在客户端应用程序和服务器端程序之间,数据的读取(接收)和写入(发送)均针对这个通道来进行。因此要通过网络进行通信,就至少需要一对套接字,其中一个运行于客户端,称之为客户端套接字(ClientSocket),另一个运行于服务器端,称之为服务器端套接字(ServerSocket)。当创建了这两个套接字对象之后,将这两个套接字连接起来就可以实现数据传送了。
  • 44. 5.3 套接字编程 5.3.2 套接字编程原理 根据连接启动的方式以及本地套接字要连接的目标,套接字之间的连接过程可以分为三个步骤:服务器监听,客户端请求,连接确认。 (1)服务器监听 服务器监听时服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态。 (2)客户端请求 客户端请求是指由客户端的套接字发出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后再向服务器端套接字提出连接请求。 (3)连接确认 连接确认是指当服务器端套接字监听到(或接收到)客户端套接字的连接请求时,它就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,连接就建立好了。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。 在TCP/IP网络中,IP网络交互分为两大类:面向连接的交互和无连接的交互。下面给出这两种交互的套接字编程流程。分别如图5.7和图5.8所示。
  • 45. 5.3 套接字编程 5.3.2 套接字编程原理 图5.7 面向连接的套接字编程流程
  • 46. 5.3 套接字编程 5.3.2 套接字编程原理 图5.8 面向无连接的套接字编程流程
  • 47. 5.3 套接字编程 5.3.3 .NET中的Socket类 针对Socket编程,.NET框架的System.NET.Sockets命名空间为需要严密控制网络访问的开发人员提供了WinSock接口的托管实现。其中Socket类是WinSock32 API提供的套接字服务的托管代码版本,为实现网络编程提供了大量的方法。在大多数情况下,Socket类方法只是将数据封送到它们的本机Win32副本中并处理任何必要的安全检查。 Socket类用于实现Berkeley套接字接口。 1.Socket类的构造函数 Socket类的构造函数原型如下: public Socket( AddressFamily addressFamily, SocketType socketType, ProtocolType protocolType ); 构造函数使用3个参数来定义创建的Socket实例。AddressFamily用来指定网络类型;SocketType用来指定套接字类型(即数据连接方式);ProtocolType用来指定网络协议。3个参数均是在命名空间System.Net.Sockets中定义的枚举类型。但它们并不能任意组合,不当的组合反而会导致无效套接字。如对于常规的IP通信网络,AddressFamily只能使用AddressFamily.InterNetwork,此时可用的SocketType、ProtocolType组合如表5.9所示。
  • 48. 5.3 套接字编程 5.3.3 .NET中的Socket类 表5.9 IP套接字定义组合 SocketType值ProtocolType值描 述StreamTcp面向连接套接字DgramUdp无连接套接字RawIcmp网际消息控制协议套接字RawRaw基础传输协议套接字表5.10 Socket类的公共属性 属 性 名描 述AddressFamily获取Socket的地址族Available获取已经从网络接收且可供读取的数据量Blocking获取或设置一个值,该值指示Socket是否处于阻塞模式Connected获取一个值,该值指示Socket是否已连接到远程主机Handle获取Socket的操作系统句柄LocalEndPoint获取本地终结点EndPointRemoteEndPoint获取远程终结点EndPointProtocolType获取Socket的协议类型SocketType获取Socket的类型
  • 49. 5.3 套接字编程 5.3.3 .NET中的Socket类 2.Socket类的常用方法 (1)Bind(EndPoint address) 在服务器端,当一个套接字被创建后,需要将它绑定到系统的一个特定地址。可以使用Bind( )方法来完成,其参数为一个IPEndPoint实例(包含IP地址和端口信息)。 (2)Listen(int con_num) 服务器端的套接字完成了与地址的绑定后,就使用Listen( )方法监听客户发送的连接请求。其参数con_num为一整型值,该值表示服务器可以接受的最大连接数目。超过这个数目的连接都会被拒绝。con_num数值的设定会影响到服务器的运行,因为每个接受的连接都要使用TCP缓冲区,如果连接的数目过大,收发数据的缓存将减少。 (3)Accept( ) 在服务器进入监听状态时,如有从客户端发来的连接请求,服务器将使用Accept( )方法来接受连接请求。Accept( )返回一个新的套接字,该套接字包含所建立的连接的信息并负责处理本连接的所有通信。而服务器刚开始创建的套接字仍然负责监听,并在需要时调用Accept( )接受新的连接请求。 (4)Send( ) 当服务器接受了来自客户端的连接请求后,服务器和客户端双方就可以利用Send( )方法来发送数据。Send( )有四种重载方法,如表5.11所示。
  • 50. 5.3 套接字编程 5.3.3 .NET中的Socket类 表5.11 Send( )、Receive( )重载方法 方 法说 明Send(byte[]data)将数据发送到连接的SocketSend(byte[]data,SocketFlang sf)使用指定的SocketFlags将数据发送到连接的SocketSend(byte[]data,int size,SocketFlags sf)使用指定的SocketFlags,将指定字节数的数据发送到已连接的SocketSend(byte[]data,int offset,int size,SocketFlags sf)使用指定的SocketFlags,将指定字节数的数据发送到已连接的Socket(从指定的偏移量开始)Receive(byte[] data)从绑定的套接字接收数据,将数据存入接收缓冲区Receive(byte[] data, SocketFlags sf)使用指定的SocketFlags,从绑定的套接字接收数据,将数据存入接收缓冲区Receive(byte[]data,int size,SocketFlags sf)使用指定的SocketFlags,从绑定的套接字接收指定字节数的数据,并将数据存入接收缓冲区Receive(byte[]data,int offset,int size,SocketFlags sf)使用指定的SocketFlags,从绑定的套接字接收指定字节数的数据,并将数据存入接收缓冲区的指定偏移量位置
  • 51. 5.3 套接字编程 5.3.3 .NET中的Socket类 (5)Receive( ) 当服务器接受了来自客户端的连接请求后,服务器和客户端双方就可以利用Receive( )方法来接受数据。Receive( )有四种重载方法如表5.11所示。 (6)Connect(EndPoint remoteEP) 同服务器端一样,客户端的套接字建立后也必须与一个地址绑定。在客户端使用Connect( )方法实现绑定,remoteEP参数为所要连接的服务器端的IPEndPoint实例。调用Connect( )方法后,它将一直阻塞到连接建立,如果连接不成功,将返回一个异常。 (7)Shutdown(SocketShutdown how) 当客户端和服务器端的通信结束时,必须关闭相应的套接字实例。可以使用Shutdown( )方法来禁止该套接字上的发送和接收,Shutdown( )方法有一个枚举类型的参数,如SocketShutdown.Send表示禁用发送套接字,SocketShutdown.Receive表示禁用接收套接字,SocketShutdown.Both表示禁用发送和接收的套接字。图5.9 获取套接字属性运行结果 (8)Close( ) 禁止套接字上的发送和接收之后,使用Close( )方法关闭套接字连接并释放所有相关资源。这样套接字会在系统内部缓冲区处理完毕后关闭套接字并释放资源。 下面用一个简单的实例来说明创建套接字以及获取该套接字属性的使用方法,程序实现如代码实例5.2所示。程序运行结果如图5.9所示。
  • 52. 5.3 套接字编程 5.3.3 .NET中的Socket类 using System; using System.Collections.Generic; using System.Text; using System.Net; using System.Net.Sockets; namespace TestSocket { class Program { static void Main(string[] args) { //创建IPEndPoint实例 IPAddress ipa = IPAddress.Parse("127.0.0.1"); IPEndPoint ipep = new IPEndPoint(ipa,8080); //创建Socket实例 Socket test_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); Console.WriteLine("AddressFamily: {0}",test_socket.AddressFamily); Console.WriteLine("SocketType: {0}",test_socket.SocketType); Console.WriteLine("ProtocolType: {0}",test_socket.ProtocolType); Console.WriteLine("Blocking: {0}",test_socket.Blocking); //修改Socket实例的属性 test_socket.Blocking = false; Console.WriteLine("new Blocking: {0}",test_socket.Blocking); Console.WriteLine("Connected: {0}",test_socket.Connected); //调用Bind( )方法,使Socket与一个本地终结点相关联 test_socket.Bind(ipep); IPEndPoint sock_iep = (IPEndPoint)test_socket.LocalEndPoint; Console.WriteLine("Local EndPoint: {0}",sock_iep.ToString( )); //关闭Socket test_socket.Close( ); Console.ReadKey( ); } } }
  • 53. 5.4 多线程编程 5.4.1 进程与线程 1.进程 程序是为完成特定任务、用某种语言编写的一组指令的集合,是一段静态的代码。而进程通常被定义为一个正在运行的程序的实例,是系统进行调度和资源分配的一个独立单位。进程使用系统中的运行资源,而程序不能申请系统资源,不能被系统调度,也不能作为独立运行的单位,因此,它不占用系统的运行资源。 进程由两个部分组成。 (1)操作系统用来管理进程的内核对象(Kernel object)。内核对象是系统的一种资源,系统对象一旦产生,任何应用程序都可以开启并使用该对象。系统给予内核对象一个计数值(usage count)作为管理之用。 (2)操作系统用来管理地址的空间。它包含所有可执行模块或DLL模块的代码和数据。它还包含动态内存分配的空间,如线程堆栈和堆栈分配空间。 进程可以简单地分为系统进程(包括一般Windows程序和服务进程)和用户进程。简单地说,凡是用于完成操作系统的各种功能的进程就是系统进程,它们就是处于运行状态下的操作系统本身;而用户进程就是由用户启动的进程。进程和程序所不同的是,程序是静止的,而进程是动态的。
  • 54. 5.4 多线程编程 5.4.1 进程与线程 2.线程 线程与进程相似,是一段完成某个特定功能的代码,是程序中的一个执行流。 线程也由两个部分组成。 (1)操作系统用来管理线程的内核对象。内核对象也是系统用来存放线程统计信息的地方。 (2)另一个是线程的堆栈。它用于维护线程在执行代码时需要的所有函数的参数和局部变量。 线程总是在某个进程环境中创建,而且它的整个生命期都是在该进程中生存的。这意味着线程是在它的进程地址空间中执行代码的,并且在地址的进程空间中对数据进行操作。 典型的Win32应用具有两种不同类型的线程:用户界面线程(user-interface thread)和工作线程(worker thread)。用户界面线程与一个或多个窗口相关联。这些线程已有自己的消息循环以及能对用户的输入作出输入响应。工作线程用于后台处理没有相关联的窗口,通常也没有消息循环。一个应用程序有多个用户界面线程和多个工作线程。工作线程比较简单,它会去后台完成一些数据处理工作。用户可以把一些不需要用户处理的事情交给此类线程去完成,任其自生自灭。这种线程对处理后台计算、后台打印很有用。 使用工作线程在后台工作是很方便的。它可以运行数据处理或进行等待。如果让它运行某种事件的发生,它也不会强迫用户和它一起等待。
  • 55. 5.4 多线程编程 5.4.1 进程与线程 3.线程与进程的比较 一个进程就是一个执行中的程序。每一个进程都有自己独立的一块内存空间和一组系统资源。在进程概念中,每一个进程的内部数据和状态都是完全独立的。多进程是指在操作系统中能同时运行多个任务程序。 线程是比进程更小的执行单位。一个进程在其执行过程中,可以产生多个线程。每个线程是进程内部一个单一的执行流。多线程则指的是在单个程序中可以同时运行多个不同的线程,执行不同的任务。多线程意味着一个程序的多行语句看上去像在同一时间内同时运行。 (1)进程的特点是允许计算机同时运行两个或更多的程序。 (2)在基于线程的多任务处理环境中,线程是最小的处理单位。 (3)多个进程的内部数据和状态都是完全独立的,而多线程共享一块内存空间和一组系统资源,有可能互相影响。 (4)线程本身的数据通常只有寄存器数据,以及一个程序执行时使用的堆栈,所以线程的切换比进程的切换负担要小。
  • 56. 5.4 多线程编程 5.4.1 进程与线程 4.线程的工作方式 AppDomain是一个物理进程到逻辑进程的动态表示。在AppDomain有一个或多个线程在执行。线程是操作系统分配处理时间的最基本单元。每个AppDomain用单一的线程来启动,但可以用它的任何一个线程启动其他的线程。 每个线程都在维护异常句柄,调度优先权和系统在它即将调度时用来保存线程context的数据结构集合。每个线程context包括线程执行时的机器寄存器组和堆栈,堆栈包含该线程的进程的地址空间。 强占式多任务的操作系统从进程里引起多线程的并发执行。在一台多处理器的计算机上,操作系统能够像多个线程一样在多个处理器上并发执行。 多任务操作系统将可用的处理时间分配给那些需要处理的进程和线程。强占式多任务操作系统将时间片分配给它执行的线程。当当前线程的执行时间到了,它就会挂起。这时允许其他线程开始调入执行。系统在线程中来回切换时,它将保存强占式线程的context,并将队列里的下一个线程恢复以前的context。 时间片的长度由操作系统和处理器决定。因为时间片比较小,多个线程似乎在同时执行。这就像在多个处理器的系统上执行。然而,在一个应用程序中使用多线程必须注意,因为如果线程太多,系统性能将大大下降。线程处理在设计应用程序时还要考虑资源要求和潜在的冲突。为了避免冲突,必须对共享资源进行同步或控制对共享资源的访问。
  • 57. 5.4 多线程编程 5.4.1 进程与线程 5.线程的优点 要提高对用户的响应速度并且处理所需数据以便几乎同时完成工作,使用多个线程是一种最为强大的技术。 对应用程序设计人员来说,线程的好处是能够使用多个线程在应用程序中同时运行。例如,一个进程有和用户(如键盘和鼠标)交互的用户界面线程和工作线程,当用户界面线程等待用户输入时,工作线程去完成其他的任务。如果给用户界面线程更高的优先级,程序会给用户更多的响应。在没有用户输入时,工作线程可以更加有效地利用处理器。 有多个线程的进程能够用线程管理互斥的任务,比如提供用户一个界面和完成后台计算。创建一个多线程的进程能够方便地让程序并发执行几个类似的或相同的任务。 例如,单个应用程序域可以使用多个线程来完成以下任务: (1)通过网络、Web服务器与数据库进行通信。 (2)执行占用大量时间的操作。 (3)区分具有不同优先级的任务。例如,高优先级线程管理时间关键的任务,低优先级线程执行其他任务。 (4)使用户界面在将时间分配给后台任务时仍能快速做出响应。
  • 58. 5.4 多线程编程 5.4.2 C#中多线程的开发 在.NET中编写的程序将被自动分配一个线程。.NET的运行时环境的主线程由Main ( )方法来启动应用程序,而且.NET的编译语言有自动的垃圾收集功能,这个垃圾收集发生在另外一个线程里面,所有的这些都是在后台发生的,我们只是感觉到默认情况下,只有一个线程来完成所有的程序任务。但是更多的情况下,我们必须根据自己的需要,添加更多的线程让程序更好的协调工作。在.NET基础类库的System.Threading命名空间中,提供了大量的类和接口支持多线程程序设计所需要实现的功能,包括线程的创建、启动、停止以及多线程同步等。下面分别介绍常用的线程操作和同步技术。 1.线程操作 System.Threading.Thread类是创建并控制线程,设置其优先级并获取其状态最为常用的类。该类以对象的方式封装了特定应用程序域中给定的程序执行路径,类中提供许多线程操作的常用方法。下面以Thread类为例介绍具体的线程操作方法。Thread类的常用属性和方法分别如表5.12和表5.13所示。
  • 59. 5.4 多线程编程 5.4.2 C#中多线程的开发 表5.12 Thread类的常用属性 属 性 名描 述CurrentThread只读属性,获取当前正在运行的线程IsAlive判断线程是否处于活动状态IsBackground获取或设置一个值,该值指示某个线程是否为后台线程IsThreadPoolThread获取一个值,该值指示线程是否属于托管线程池Name获取或设置线程的名称PriorityThreadPriority枚举类型,代表线程的优先级,如:Normal、AboveNormal、BelowNormal、Highest、LowestThreadStateThreadState枚举类型,代表当前线程的状态,如:Unstarted、Running、WaitSleepJoin、Stopped、AbortRequested、Suspended、Aborted等表5.13 Thread类的常用方法 方 法 名描 述GetDomain( )返回当前线程所在的应用程序域GetDomainID( )返回当前线程所在的应用程序域的IDStart( )启动线程的执行Suspend( )挂起线程,或者如果线程已挂起,则不起作用Resume( )继续已挂起的线程Interrupt( )终止处于Wait或者Sleep或者Join线程状态的线程Join( )阻塞调用线程,直到某个线程终止时为止Sleep( )将当前线程阻塞指定的毫秒数Abort( )终止一个线程的运行。如果线程已经终止,则不能通过Thread.Start( )来启动线程
  • 60. 5.4 多线程编程 5.4.2 C#中多线程的开发 线程的控制操作一般包括以下几个方面。 (1)创建线程 创建一个线程就是实例化一个Thread类的对象,Thread类的构造函数带有一个ThreadStart类型的参数,这是一个委派用于传递线程的入口方法,创建ThreadStart对象时需要一个静态方法或实例方法作为参数。示例如下: using System; using System.Collections.Generic; using System.Text; using System.Threading; namespace FirstThread { class Program { static void Main(string[] args) { Thread t1 = new Thread(new ThreadStart(Thread1)); //创建线程 t1.Start( );//启动线程 } public static void Thread1( ) { Console.WriteLine("This is a Thread test!"); } } }
  • 61. 5.4 多线程编程 5.4.2 C#中多线程的开发 (2)启动线程 启动线程很简单,只需要调用Thread类的Start方法,如上例所示。 (3)休眠线程 线程的休眠是让当前的线程进入一定时间的休眠状态,时间一到线程将继续执行。通过Thread类的Sleep方法来实现线程的休眠。Thread类中有两个重载的Sleep方法,一个带有int类型的参数,用于指定休眠的毫秒(ms)数,另一个带有TimeSpan类型的参数,指定休眠的时间段。示例如下: Thread.Sleep(1000);//线程休眠1000毫秒 TimeSpan WaitTime = new TimeSpan(0,0,0,0,1000); Thread.Sleep(WaitTime);//线程休眠按天 小时 分钟 秒 毫秒计算 (4)挂起线程 线程的挂起是暂停线程,如果不再启动线程,它将永远保持暂停状态。只有当前运行的线程才可以被挂起,对已经挂起的线程实施挂起没有作用,因此在使用Supend方法前,一般要先检查该线程是否正在运行。通常是查询Thread的ThreadState属性值。示例如下: if (t1.ThreadState == ThreadState.Running)//判断线程是否正在运行 t1.Suspend( );
  • 62. 5.4 多线程编程 5.4.2 C#中多线程的开发 (5)继续线程 已经挂起的线程可以使用Thread类的Resume方法继续运行。如果没有被挂起的线程使用该操作将不起作用,所以使用Resume方法前,一般也先判断线程是否已经被挂起。示例如下: if (t1.ThreadState == ThreadState.Suspended)//判断线程是否已被挂起 t1.Resume( ); (6)终止线程 在终止线程之前,一般先判断线程的IsAlive属性,确认该线程是否处于活动状态,处于活动状态的线程才可以使用Thread类的Abort方法进行终止。示例如下: if (t1.IsAlive)//判断线程是否处于活动状态 t1.Abort( ); 2.线程同步 在包含多个线程的应用程序中,线程间有时会共享存储空间,当两个或多个线程同时访问同一共享资源时,必然会出现冲突问题。如一个线程可能尝试从一个文件中读取数据,而另一个线程则尝试在同一个文件中修改数据。在这种情况下,数据可能变得不一致。针对这种问题,通常需要让一个线程彻底完成其任务后,再运行下一个线程;或者要求一个线程对共享资源访问完全结束后,再让另一个线程访问该资源,必须保证一个共享资源一次只能被一个线程使用。实现此目的的过程称为线程同步。
  • 63. 5.4 多线程编程 5.4.2 C#中多线程的开发 在C#.NET中提供了多种实现线程同步的方法。如加锁(Lock)、监视器(Monitor)、互斥体(Mutex)等。 (1)加锁(Lock) 实现多线程同步的最直接办法就是加锁,就像服装店的试衣间一样,当一个顾客进去试衣时把试衣间门锁上,其他顾客必须等他出来后才能进去试衣。C#语言的lock语句就可以实现这个功能。它可以把一段代码定义为互斥段,在一个时刻内只允许一个线程进入执行,而其他线程必须等待。 其基本格式如下: lock(expression)statement_block 其中expression代表要加锁的对象,必须是引用类型。一般地,如果要保护一个类的实例成员,可以使用this;如果要保护一个静态成员,或者要保护的内容位于一个静态方法中,可以使用类名, 格式为:lock(typeof(类名)){ }。 statement_block:代表共享资源,在一个时刻内只能被一个线程执行。 (2)监视器(Monitor) Monitor的功能和lock有些相似,但是它比lock功能更灵活、更强大。Monitor相当于服装店试衣间的开门人,他管着试衣间的钥匙,而线程好比是要使用试衣间的顾客,他要进入试衣间之前,必须先从看门人手上获取钥匙,试衣出来以后,需要把钥匙还给看门人,看门人可以把它交给下一个正在等待进入试衣间的顾客。在这个过程中,顾客会出现3种状态,分别对应于多线程程序中线程的状态,如表5.14所示。
  • 64. 5.4 多线程编程 5.4.2 C#中多线程的开发 顾 客 状 态线 程 状 态已经获得钥匙的顾客正在使用共享资源的线程准备获取钥匙的顾客位于就绪队列中的线程排队等待的顾客位于等待队列中的线程 在.NET平台下,命名空间System.Threading中的Monitor类封装了像试衣间看门人那样监视共享资源的功能。由于Monitor类是一个静态的类,不能使用它来定义对象,它的所有方法都是静态的。Monitor类通过使用Enter方法向单个线程授予获取锁定对象的钥匙来控制对对象的访问,该钥匙提供限制访问代码块(通常称为临界区,由Monitor类的Enter方法标记临界区的开头,Exit方法标记临界区的结尾)的功能。当一个线程拥有对象的钥匙时,其他任何线程都不能获取该钥匙。Monitor类的常用方法如下表所示。方 法 名描 述Enter( )获取锁定对象的钥匙,同时标记临界区的开头。如果此时正有另外一个线程掌管这把钥匙,该方法将进入阻塞状态,直到锁定的对象被打开,如果同一线程正掌管钥匙,再次调用Enter方法将不会阻塞TryEnter( )和Enter方法类似,不同的是,当对象正被其他线程使用时,该方法不会自动阻塞,而是返回一个false值表明失败Exit( )结束对一个对象的监视,即交出锁定对象的钥匙,同时标记受锁定对象保护的临界区的结尾Pulse( )该方法通知位于等待队列中的下一个线程进入就绪队列,一旦调用该方法的线程释放钥匙后,就绪队列的线程就可以获取钥匙PulseAll( )和Pulse方法相似,通知等待队列中的所有线程转移到就绪队列中Wait( )释放对象的锁,并阻止当前线程直到它重新获取该锁
  • 65. 5.4 多线程编程 5.4.2 C#中多线程的开发 (3)互斥体(Mutex) 互斥体是通过只向一个线程授予对共享资源的独占访问权。如果一个线程获取了互斥体,则要获取该互斥体的第二个线程将被挂起,直到第一个线程释放该互斥体。在命名空间System.Threading中的Mutex类代表了互斥体,Mutex类继承于WaitHandle类,该类代表了所有的同步对象。Mutex类用WaitOne方法来请求互斥体的所有权,用ReleaseMutex方法来释放互斥体的所有权。这两个方法以及Mutex类的构造函数都有多种重载形式,具体如下表所示。函 数 原 型描 述public Mutex( )调用此构造函数相当于调用构造函数public Mutex(false),将互斥体的初始所属权指定为false,即调用线程不拥有互斥体所有权public Mutex(bool initiallyOwned)如果initiallyOwned为false,则使用情况同上,如果为true,则表示调用线程拥有互斥体的初始所有权public Mutex(bool initiallyOwned,string name)第一个参数使用情况同上。第二个参数表示用字符串name作为该互斥体的名称来初始化Mutex类的新实例,如果name值为空引用,则Mutex是未命名的public Mutex(bool initiallyOwned,string name,out bool createdNew)前两个参数同上。第三个参数是用来在该方法返回时指示调用线程是否被授予了互斥体的初始所有权,如果调用线程被授予了互斥体的初始所有权,则方法返回时createdNew为true,否则为falsepublic Mutex(bool initiallyOwned,string name,out bool createdNew,MutexSecurity mutexSecurity)前三个参数同上。第四个参数用于指示该已命名的互斥体的访问控制安全性public virtual bool WaitOne( )用于请求互斥体的所有权public virtual bool WaitOne(TimeSpan timeout,bool exitContex)第一个参数用于指定等待时间,如果超过等待时间线程还没有获得互斥体,WaitOne方法将返回false。第二个参数用来指定在开始等待之前,是否先退出调用上下文所在的同步域(如果处于同步上下文中),exitContext为true,则等待之前先退出上下文的同步域,否则为falsepublic virtual bool WaitOne(int millisecondsTimeout,bool exitContex)public void ReleaseMutex( )用于释放已经拥有的互斥体
  • 66. 5.5 基于多线程的编程实例 有时,当某一个线程进入同步方法后,共享变量并不满足它所需要的状态,该线程需要等待其他线程将共享变量改为它所需要的状态后才能往下执行。由于此时其他线程无法进入临界区,所以就需要该线程放弃监视器,并返回到排队状态等待其他线程交回监视器。“生产者和消费者”问题就是这一类典型的问题,设计程序时必须解决:生产者比消费者快时,消费者会漏掉一些数据没有取到的问题;消费者比生产者快时,消费者又存在取相同数据的问题。对于这一问题,通常通过设置一个中间类来解决。该类负责对共享变量的读写,读写共享变量的方法需要使用同步控制技术。同时,两个线程之间还需要一个信号变量,以此来通知对方“我操作完了,该你了。”。通过该信号变量来表明线程所需要的共享变量是否已经满足要求,若不满足还需等待。 下面用一个模拟吃苹果的实例,说明C#中多线程的实现方法。要求开发一个程序实现如下情况:一个家庭有三个孩子,爸爸妈妈不断削苹果往盘子里面放,老大、老二、老三不断从盘子里面取苹果吃。盘子的大小有限,最多只能放5个苹果,并且爸妈不能同时往盘子里面放苹果,妈妈具有优先权。三个孩子取苹果时,盘子不能为空,三人不能同时取,老三优先权最高,老大最低。老大吃的最快,取的频率最高,老二次之。 程序设计了四个类:EatAppleSmp类、Productor类、Consumer类和Dish类。其中Dish类是中间类,包含了共享数据区和放苹果、取苹果的方法,这两个方法利用lock语句实现了同步控制。程序实现如代码实例5.3所示。运行结果如图5.10所示。
  • 67. 5.5 基于多线程的编程实例 using System; using System.Collections.Generic; using System.Text; using System.Threading; namespace ThreadSample { class EatAppleSmp { public EatAppleSmp( ) { Thread th_mother, th_father, th_young, th_middle, th_old; Dish dish = new Dish(this, 30); Productor mother = new Productor("妈妈", dish);//建立线程 Productor father = new Productor("爸爸", dish); Consumer old = new Consumer("老大", dish, 1000); Consumer middle = new Consumer("老二", dish, 1200); Consumer young = new Consumer("老三", dish, 1500); th_mother = new Thread(new ThreadStart(mother.run)); th_father = new Thread(new ThreadStart(father.run)); th_old = new Thread(new ThreadStart(old.run)); th_middle = new Thread(new ThreadStart(middle.run)); th_young = new Thread(new ThreadStart(young.run)); th_mother.Priority = ThreadPriority.Highest;//设置优先级 th_father.Priority = ThreadPriority.Normal; th_old.Priority = ThreadPriority.Lowest; th_middle.Priority = ThreadPriority.Normal; th_young.Priority = ThreadPriority.Highest; th_mother.Start( ); th_father.Start( ); th_old.Start( ); th_middle.Start( ); th_young.Start( ); }
  • 68. 5.5 基于多线程的编程实例 static void Main(string[] args) { EatAppleSmp mainstart = new EatAppleSmp( ); } } class Dish { int f = 5;//盘子最多只能放5个苹果 EatAppleSmp oEAP; int EnabledNum;//可放苹果总数 int n = 0; public Dish(EatAppleSmp oEAP, int EnabledNum) { this.oEAP = oEAP; this.EnabledNum = EnabledNum; } public void put(string name) { lock (this)//同步控制放苹果 { while (f == 0)//苹果已满,线程等待 { try { System.Console.WriteLine(name + "正在等待放入苹果"); Monitor.Wait(this); } catch (ThreadInterruptedException) { } } f = f - 1;//削完一个苹果放一次 n = n + 1; System.Console.WriteLine(name + "放1个苹果"); Monitor.PulseAll(this); if (n > EnabledNum) Thread.CurrentThread.Abort( ); } }
  • 69. 5.5 基于多线程的编程实例 public void get(string name) { lock (this)//同步控制取苹果 { while (f == 5) { try { System.Console.WriteLine(name + "等待取苹果"); Monitor.Wait(this); } catch (ThreadInterruptedException) { } } f = f + 1; System.Console.WriteLine(name + "取苹果吃..."); Monitor.PulseAll(this); } } } class Productor { private Dish dish; private string name; public Productor(string name, Dish dish) { this.name = name; this.dish = dish; }
  • 70. 5.5 基于多线程的编程实例 public void run( ) { while (true) { dish.put(name); try { Thread.Sleep(600);//削苹果时间 } catch (ThreadInterruptedException) { } } } } class Consumer { private string name; private Dish dish; private int timelong; public Consumer(string name, Dish dish, int timelong) { this.name = name; this.dish = dish; this.timelong = timelong; } public void run( ) { while(true) { dish.get(name); try { Thread.Sleep(timelong);//吃苹果时间 } catch(ThreadInterruptedException){} } } } }