《通信网络程序设计》课件第4章_第1页
《通信网络程序设计》课件第4章_第2页
《通信网络程序设计》课件第4章_第3页
《通信网络程序设计》课件第4章_第4页
《通信网络程序设计》课件第4章_第5页
已阅读5页,还剩147页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

第4章NetBIOS网络编程4.1概述4.2NetBIOS应用服务与实现4.3数据报通信程序设计4.4会话通信程序设计小结

NetBIOS是1983年IBM为PC-Network开发的一套网络标准,由于NetBIOSAPI具有在面向连接或非连接的通信过程中,应用程序都能通过它访问传输层联网协议;再加上NetBIOS协议短小精悍,非常适用于实时性要求较高的小型LAN网络环境;以及其良好的网络通信性能,因而NetBIOS一经推出就成为LAN接入服务的标准接口协议。虽然NetBIOS不是一种可路由的协议,数据包无法跨网段传输,已渐渐成为网络通信的次要协议,但其编程思想明晰简要,且与后续的编程协议基本一致,因此依然活跃在当前的网络通信中。基于上述原因,网络编程的初学者则有必要学习NetBIOS协议。

NetBIOS是早期的网络通信协议,起初NetBIOS驻留在LANA型网络适配器(adapter)的扩展BIOSROM中,随后IBM开发了NETBIOS.COM、令牌环NETBEUI以及IBMLAN支持程序,以适应不断发展的网络通信需求。微软在IBMNetBIOS的基础上进行了更深入的开发,并将NetBIOS相继应用到其陆续推出的Windows系列产品中,对NetBIOS的广泛使用起到了极大的推动作用。4.1概述可以这样说,微软早期的客户机/服务器网络系统就是基于NetBIOS的网络,WindowsNT操作系统中大量的内部联网工作也都是利用NetBIOS来完成的。除此之外,微软还为许多其他协议提供了标准的NetBIOS界面,如TCP/IP(NetBIOS设置见图4-1)、NetBEUI和NWLink,使NetBIOS的应用更加方便。

目前,NetBIOS最广泛的应用之一就是对NetBIOS用户扩展接口(NetBIOSExtendUserInterface,NetBEUI)协议的使用,在WindowsXP中对NetBEUI协议的添加如图4-2所示。NetBEUI可以看做是NetBIOS协议的延伸及改良,具有体积小、效率高以及速度快等特点,运行在标准802.2数据链协议层上。NetBEUI规范了在NetBIOS中未标准化的传输帧,并加入了额外功能。NetBEUI协议同样是一种非路由协议(路由器会将它收到的非路由协议数据包统统丢弃),主要用于本地局域网中,一般不能与其他网络的计算机进行沟通,这一点不同于TCP/IP、IPX/SPX协议。

随着PC-Network被令牌环和以太网取代,以及NetBIOS不具备路由功能等原因,在广域网、城域网被广泛应用的今天,NetBIOS已经逐渐退居配角。但是,由于NetBIOS被适配到了各种其他协议上,如IPX/SPX和TCP/IP,目前仍然有很多软件的开发在使用NetBIOSAPI。同时,鉴于NetBIOSAPI的高速、编程接口与协议无关等优点,还有相当部分网络应用需要用到NetBIOS,如浏览网上邻居和共享文件等,所以NetBIOS依然活跃于网络编程设计工作中。

图4-1在Windows2000/XP中设置NetBIOS

图4-2在Windows2000/XP中添加NetBEUI协议

4.2.1NetBIOS应用服务

NetBIOS程序的网络通信功能主要是通过名字服务、数据报服务、会话服务及一般命令服务这四种NetBIOS应用服务来实现的。在整个NetBIOS程序的工作过程中:首先,网络通信程序通过名字的标识区别于其他程序,并将名字注册到LANA中,获得合法的网络身份;4.2NetBIOS应用服务与实现然后,网络通信程序可以使用NetBIOS的数据报或会话服务与驻留在相同或不同主机中的其他应用程序进行通信。通信过程中,用户还可以使用一般命令服务对适配器进行管理和监测。最后,在通信结束后,通信程序需要删除已注册的名字并释放所占用的网络资源。

1.名字服务

在1.3节中,曾讨论过网络中的应用在建立通信前必须标识自己,即进行编号。NetBIOS采用名字来对一个进程进行编号。

每个NetBIOSLAN适配器在其所处的网络中用一个或多个网络名字来标识自己,以区别于其他的适配器。网络通信程序用名字开始和结束会话,这样就允许LAN应用程序将信息正确地传送到某个指定的适配器上的某个特定应用程序上,并且还可以在发送信息中标记发送该信息的来源。在多个程序配置一台单独的机器时,每个程序都有独特的NetBIOS名称。每台支持应用的PC通过用户定义或内部方法而获得NetBIOS站名。一个NetBIOS名称包含16个字节(前15个字节由用户指定,第16个字节通常为保留值)。名称中每个字符都是有效的,并且对大小写敏感。名字的第一个字符不能是二进制的0或*

号。一个应用程序可使用多个名字,多个应用程序也可以使用同一个给定的名字。一台使用NetBIOS的PC在网络上能完全工作起来之前,PC必须先登记NetBIOS名称,即获得注册使用该名字的权利。登记NetBIOS名称的过程如下:

(1)客户端A在所有地方广播它自己和它的NetBIOS名字声明包6到10次,确保其他网络成员收到信息。

(2)如果有客户端B已用声明包中的NetBIOS名,客户端B则发布它自己的广播,包括它正在使用的名字,以请求登录的客户端A停止登记企图。客户端A收到客户端B的广播后,重新选取名字并返回上一步。

(3)如果无其他客户端反对登记,请求登录的客户端A完成NetBIOS名称登记过程。

一个名字在成功注册后,除了第一个注册名字以外,以后均可用NetBIOS的名字管理命令来撤销。重新启动系统或关闭工作站电源将会擦掉NetBIOS的命名表(见后面介绍)。NetBIOS名字根据功能的不同可以分为唯一名、组名、永久节点名、符号名。

(1)唯一名。唯一名是标识与网络上的单个用户或计算机相关联的某个资源的唯一名称,通常用来向计算机上的特定进程进行网络通信。如果一个应用程序将一个名字注册为唯一名,那么在同一网络上的其他应用程序就不能再用这个名字注册。如果这个唯一名又被其他应用程序作为一个唯一名或组名在注册,那么使用该唯一名的适配器会发出冲突信号,致使正在进行的注册工作遭到拒绝。如果一个应用程序注册一个唯一名成功,那么它在该网上拥有该名字的独占权。

(2)组名。组名是标识与网络上的一组用户或计算机相关联的某个资源的名称,通常用来同时向多台计算机发送信息。如果一个应用程序试图将一个名字注册成一个组名,那么这个名字必须是未被其他应用程序注册为唯一名字的,否则注册就会失败。如果这个名字已被注册成为唯一名,那么必定会有一个应用程序发出冲突信号,使得要进行的注册遭到拒绝。否则,该应用程序可以在网上非独占地使用这个名字。可以允许其他应用程序用同样的名字作为组名重新注册,但不能作为唯一名来注册。在将信息同时送往一组工作站时,可利用组名进行信息传送。

(3)永久节点名。每块IBMLAN适配器都有与它们对应的一个唯一的6字节编号,这个编号固化在适配器的ROM中,保证每块IBMLAN适配器都能与其他的适配器区别开,这个编号称为永久节点名。永久节点名作为一个适配器发送的所有信息的LAN地址标记,可作为所有发送信息的目的地址。这个48位的值构成了决定哪些信息要被丢弃、哪些信息要被接收的电子信息过滤器。

(4)符号名。使用永久节点名来传送信息容易发生错误,因为永久节点名的数字太多;同时当工作站更换适配器时,原先的程序就需要重写,这些都造成了不必要的麻烦。因此可以采用相对较短的、与该工作站有关的自然名字将LAN适配器地址进行拟人化标记。这种假名称为符号名,并且它可以作为一个唯一名或组名在NetBIOS命名表中注册。符号名可以被注册和重新注册。

(5)命名表及命名编号。在名字注册成功后,适配器的NetBIOS将这个名字放到一个本地管理的内部表中,即所谓的NetBIOS命名表(nametable),再将名字与一个字节的值一起向LAN应用报告,注册取得成功。这一个字节的值是一个无符号整数,称为名字编号(namenumber),这个编号将被用于各种与该名字有关的NetBIOS命令中。NetBIOS以255为模,循环递增地分配名字编号,0和255为保留值,因此名字编号的顺序为1,2,3,…,254,2,3,…,254,依次类推。NetBIOS命名表是RAM中一个暂时性的表格,每次系统重启或适配器复位后,这个命名表都要重构。因为每个适配器都有自己专用的命名表,所以LAN上的NetBIOS的命名标识问题完全是自动解决的,不需要命名管理中心。如果一个NetBIOS模块支持一个工作站上的多个LAN适配器,那么适配器有自己独立的NetBIOS命名表。在Windows操作系统下该命名表可以使用“nbtstat”命令进行查询。

名字服务分别使用AddName、AddGroupName、DeleteName三个命令在程序中实现相关功能,命令的含义详见附录2。

2.数据报服务

数据报是一种短信息,它的大小可由NetBIOS的执行程序改变且不保证信息交付,即数据报不管信息是否安全到达,NetBIOS也不提供接收数据。数据报可以将数据发送到特定的地点或组中所有的成员,也可广播到整个局域网。数据报通信有广播式数据报和普通数据报两种。与其他数据服务相比,NetBIOS数据报是无连接、非可靠的一种通信方式。

1)广播式数据报(广播型)

广播式数据报是用NetBIOS的发送广播数据报命令所发送的。任何适配器(包括发送适配器),如果事前发送过NetBIOS接收广播数据报命令,它就可以接收到这个广播数据报。广播数据报可能引起进程间数据收发的混乱,应当谨慎使用。

2)普通数据报(定向型)

普通数据报是用NetBIOS的发送数据报命令发送的。与NetBIOS发送广播数据报命令不同的是,应用程序要用发送数据报名指出接收的NetBIOS的名字。普通数据报可被传送给使用唯一名的一个适配器,或传送给共享同一组名的一组适配器,包括发送适配器在内的任何适配器,都可接收到数据报。

数据报通信的主要优点是它所消耗的工作资源比会话通信少,省去了建立连接所需的开销。但这种类型的服务不提供任何保障。除非依赖自服务器传来的响应,否则会因目标接收机退出网络、掉电、数据丢失等多种原因导致数据传输失败,不会提供任何错误提示。数据报服务分别使用SendDatagram、ReceiveDatagram、SendBroadcast、ReceiveBroadcast等四个命令来在程序中实现相关功能,命令的含义详见附录2。

3.会话服务

NetBIOS会话连接是在两个应用程序之间建立一个可靠的虚电路,应用程序可以驻留在同一个工作站(本地会话)或不同的工作站(远程会话)中,每一个应用程序构成会话或一边。实际上,NetBIOS会话服务提供给用户程序一种面向连接、可靠的、完全双重的信息服务。NetBIOS会话的建立需要双方预定的合作,会话过程需要一个是客户端程序,一个是服务器端程序。

会话实际是一种双向的通信数据流,通信的每一方都可向另一方发送消息。面向连接的服务可担保在两个端点之间,任何数据都能准确无误地传递。在这种服务中,服务器通常将自己注册到一个已知的名字下。客户机会搜寻这个名字,以便建立与服务器的通信。NetBIOS服务器进程会针对想通过它建立通信的每一个LANA编号,将自己的名字加入与其对应的名字表。而对于其他机器上的客户来说,就可将一个服务名解析成机器名,然后要求同服务器进程建立连接。显然,为建立这种虚拟回路,必须采取一些适当的步骤,通常会话过程包括建立会话、接收命令、发送命令、结束会话四个步骤。

1)建立会话

当应用程序发出NetBIOS监听命令以访问NetBIOS名字表中的某个名字时,会话就产生了。监听命令也可指出一个远程名,申请对话的应用程序把它看做会话伙伴。第二个应用程序接着发出一条NetBIOS调用命令,去访问NetBIOS名字表中的名字,而这个名字正是第一个应用所期望伙伴的名字。该调用命令还访问它在NetBIOS名字表中的第一个应用程序的名字,两个名字完全相匹配就会满足两个应用程序建立会话的标准,所进行的监听及调用命令也随之完成。

2)接收命令

会话建立以后,在会话过程中双方可以使用NetBIOS的send和receive命令操作传输数据。如果用一个名字产生几个会话,则应用程序可发出NetBIOS“指定数据任意接收”命令,该命令就提供与这个指定名有关的任何会话数据。更一般的情况是,应用程序可以发出“任意名的任意接收”命令,提供适配器所建立的任何会话中的数据。

3)发送命令

应用程序发出NetBIOSsend命令以把数据发送给其他应用程序。该命令允许应用程序发送的信息范围是0~64

KB;数据必须处于连续的内存中。应用程序也可以通过NetBIOS链路发送命令,该命令允许数据驻留在两个区域的缓冲区中。

4)结束会话

在会话最后,会话的一边或两边发出NetBIOSHangup命令就可终止会话,各程序将执行挂起命令。在以后发出这样的命令时,其他应用程序也会得到会话结束的通知。应用程序也可发出NetBIOS会话状态命令以指出会话的状态。

会话通信的优点体现在能够保证通信具有极高的可靠性,而且数据包的收发顺序也能保证正确无误。但是,会话是一种“以消息为基础”的服务,它的可靠性建立在建立会话、维护会话及适配器之间数据包应答的开销上。需引起注意的是:NetBIOS假定局域网足够快,能够传输需要的数据,因而没有被会话服务的实际流控制。会话服务分别使用Call、Listen、Send、SendReceive、ReceiveAny、HangUp、ChainSend、SendNoAck、ChainSendNoAck、Sessionstatus等10个命令来在程序中实现相关功能,命令的含义详见附录2。

4.一般命令服务

一般命令可以对适配器进行管理,包括复位、适配器的状态显示、查找名字、跟踪、取消并断开链路等。通过这些命令,可以收集到大量有用的NetBIOS信息,从而了解网络通信程序的运行状况,并有针对地管理适配器。

会话服务分别使用Reset、Astatus、FindName、Trace、Cancel、LanaEnum、Lanstatusalert、Unlink等8个命令来在程序中实现相关功能,命令的含义详见附录2。4.2.2NCB/MCB

前面所述的24个命令是实现四种服务的基础。这些命令中的任何一个实现都要通过填写NCB/MCB,并将填写结果通过中断提交给系统才能执行,也就是说,应用程序是通过使用网络控制块(NCB)实现对NetBIOS的访问的。NCB实际上是一个数据结构,在令牌环中也称为信息控制块(MCB)。通常,应用程序调用NetBIOS网络服务功能时,需要完成以下三个步骤:

(1)构造一个NCB,这个NCB包含应用程序想要进行的NetBIOS操作描述;

(2)把NCB地址写入ES:BX寄存器,作为指向该NCB的远程地址指针;

(3)执行5CH中断,执行时,NetBIOS自动进入ES:BX寄存器指定的地址,读取NCB得知一切有关该操作的信息。

Win32环境下,在VC++编译器进行编程过程中,除第一步以外,后面的两步是通过调用Netbios()这个API函数一次性完成的。

1.NCB/MCB域结构

NCB的结构在nb30.h文件中定义。NCB有64个字符,分为14个域(或称为字段)和一个14字节的保留域。下面是NCB结构的定义:

typedefstruct_NCB{

UCHAR ncb_command; /*命令码*/

UCHAR ncb_retcode; /*返回码*/

UCHAR ncb_lsn; /*本地会话编号*/

UCHAR ncb_num; /*名字编号*/

PUCHAR ncb_buffer; /*缓冲区地址*/

WORD ncb_length; /*缓冲区长度*/

UCHAR ncb_callname[NCBNAMSZ]; /*调用(远程)名*/

UCHAR ncb_name[NCBNAMSZ]; /*本地名*/

UCHAR ncb_rto; /*接收超时*/

UCHAR ncb_sto; /*发送超时*/

void (CALLBACK*ncb_post)(struct_NCB*); /*POST例程地址*/

UCHAR ncb_lana_num; /*LANA编号*/

UCHAR ncb_cmd_cplt; /*命令结束标志*/

UCHAR ncb_reserve[10]; /*保留*/

HANDLE ncb_event; /*Win32事件句柄*/

}NCB,*PNCB;

NCB各域的解释如下:

1)命令域

每一个发往NetBIOS的NCB都代表一项要执行的动作,具体执行哪项动作,由命令字段的取值决定(参见附录2)。NetBIOS命令的使用方式有两种,即等待和非等待(或称为同步与异步),详见“命令的完成”部分的介绍。

2)返回码域

命令提交给NetBIOS驱动程序后,该命令的成功与否即可在该字段中反映出来。若返回码字段的值为00h,则表示命令成功。对于异步NetBIOS命令,NetBIOS将立即在返回码字段中返回值FFh,表明该命令已经排队,即将执行。命令执行完毕后,该字段将置成最终的返回码。

3)本地会话编号域

同远程应用程序处理建立了会话后,NetBIOS驱动程序将相应地设置该字段(局部会话号)。在随后的通信中,若想同远程处理进行通信,本地处理只需在NCB结构中指明局部会话号,不再需要在本地会话编号字段中指定完整的远程处理逻辑名。

单就一个适配器而言,工作站上的每一个处理一次至多能进行254个会话,只要指定相关的局部会话号,就能达到会话的目的。系统保留值为0和255,不将它们作为局部会话号使用。

4)名字编号域

工作站上的每一个处理最多可向名表中加进254个逻辑名。成功地将一个名字加进LANA的私有名表后,NetBIOS将置名字编号字段值为该名在名表中的索引值(索引值称为名号),在以后同远程处理进行的非连接式通信中,可使用这个名号。名号0和255为系统保留,而适配器物理地址总在名表中的第1项。

5)缓冲区地址域

该字段的值是要发送的数据缓冲区的地址,或者要在其中存放接收到的数据的缓冲区的地址。

6)缓冲区长度

该字段指定的是由缓冲区地址字段指定的缓冲区的长度。接收到一块数据时,NetBIOS将相应地设置该字段。

7)调用(远程)名域

这是一个由应用程序设置的16字节域,其值是远程处理的逻辑名。应用程序设置一个连接或向远程处理发送一个数据表包时,将相应设置该字段。在远程驱动程序连接正期待着接收连接呼叫的本地处理时,NetBIOS将填写该字段。因此,接收呼叫的处理能够找出远程呼叫方的名。第一个字节为“*”,代表任意远程名。8)本地名域

这是由应用程序设置的16字节字段(所有的字节均有用),其值是本地处理的逻辑名,应用程序设置一个连接或向远程处理发送一个数据表包时,将相应设置该字段。该字段的第一个字节不能是二进制的0或

*

号;另外,IBM保留了头三个字节,所以头三个字节不能是IBM;最后,第16个字节不能是00h到1Fh之间的值。在局域网管理器环境下,最后一个字符(即第16个字节)有特殊的含义。

9)接收超时域

当期望从一个或数个远程处理接收到一个数据表包时,应用程序可在接收超时(接收时间限制)字段中指定等待的最大时间数值(以1/2秒为间隔单位)。若超过了指定时间仍未接收到包,则NetBIOS驱动程序将在接收超时字段中返回错误。若接收超时字段值为00h,则表示阻止执行,直到本地处理接收到一个数据包。

10)发送超时域

发送超时(发送时间限制)字段类似于接收超时字段,但它指定的是等待NetBIOS发送命令(Send)完成的时间。若超过了指定时间,则将返回错误。若发送超时字段值为00h,则表示不为发送操作指定时间限制。此时,命令将阻止执行,直到要么成功地发送了一个数据包,要么NetBIOS层停止了重试。

11)

POST例程地址域

在提交异步命令时,应用程序可以设置该字段。在MS-DOS中,应用程序将后处理例程的地址填在该字段中。所谓后处理例程,即命令执行完毕后NetBIOS驱动程序将要调用的例程。

12)

LANA编号域

一台工作站上可能有不止一个LAN卡或网络协议(或传输驱动程序),所以NCB中的LANA编号字段指明了应用程序想使用哪一个网络适配器上的哪个网络协议。该字段称为LAN适配器号或LANA号,LANA编号在0~9之间。

在像MicrosoftLANManager这样的网络软件环境中,可以同时装入多个传输驱动程序(例如TCP/IP、NetBIOS或XNS),其中每一个驱动程序都提供了一个NetBIOS接口。同时一个工作站可能有不止一个LAN适配器卡,这种情况下,LANA编号字段指定的是某一特定对,即应用程序想使用的传输驱动程序和LAN卡组合。例如:某工作站安装了两块网卡,以及三种具有NetBIOS能力的传输协议,如NetBT/NBT(TCP/IP)、Nbf(NetBEUI)和NwlnkNb(IPX/SPX),那么该工作站就有6个LANA编号可能,如以下对应关系所示:TCP/IP—网卡1,NetBEUI—网卡1,IPX/SPX—网卡1;TCP/IP—网卡2,NetBEUI—网

卡2,IPX/SPX—网卡2。这6个组合系统会赋予6个不同的LANA编号。

需说明的是,LANA编号并不是按顺序累加的,而是由系统随机分配的。

注意:只有基于相同NetBIOS协议的程序才能相互通信。例如:服务器上的NetBIOS应用程序A工作在LANA编号2上,在服务器上LANA编号2正好对应于TCP/IP协议。假设客户机上的某应用程序B想要与A建立连接,恰好B也工作在LANA编号2上,然而客户机上LANA编号2正好对应于NetBEUI协议,这就会导致问题:尽管两者安装的TCP/IP和NetBEUI都提供NetBIOS功能,但是协议不同导致了两个应用程序之间无法通信。因而在程序设计时,为了保证一台PC上的NetBIOS应用程序具有一定的鲁棒性,一方面要安装尽可能多的提供NetBIOS服务的协议(最好是TCP/IP、NetBEUI、IPX/SPX三种都安装);另一方面应用程序应当对机器中每块网络适配器上的每种提供NetBIOS服务的协议,即每个LANA编号尝试通信。基于此,在设计程序时,服务器应用程序应对每个LANA编号上的客户机连接进行监听;客户机上的应用程序需要通过本机安装的每个LANA编号尝试连接。

13)命令结束标志域

NetBIOS驱动程序利用该字段来表明异步命令已完成。起先,当应用程序提交一条异步命令时,NetBIOS将置该字段值为FFh;待命令执行完毕后,再将最终值填入该字段。也就是说,提交了一条非等待命令后,应用程序可以监视(轮询)该字段的取值以了解命令是否完毕,直到其值不再是FFh为止。

14)保留域

NCB的保留域为14字符长的保留域,NetBIOS可能用它来返回扩充的错误信息。另外,NetBIOS在处理请求的过程中,用它来作暂存区。应用程序不应使用NCB的保留域,因为一旦它遭到破坏,NetBIOS的行为将是不可预测的。

15)事件句柄域

在提交异步命令时,应用程序可以设置该字段。该域用于存放后处理例程的句柄。

注意:当命令域的设置使用ASYNCH(异步)标志时,该域的设置与POST例程地址域的设置互斥,即其中必须有一个域的设置为0,而另一个为非0值。

2.命令的调用

填充NCB后,就可以调用Netbios()函数进行NetBIOS命令的调用了。Netbios()函数是将ES:BX寄存器对指向NCB地址,调用INT5Ch中断请求来实现命令的调用的,另一个调用NetBIOS请求的方法是使用INT2Ah请求。后一种方法将AX寄存器值赋为0400h或0401h,同样,ES:BX寄存器对应作为Ncb的远程地址指针。用INT2Ah调用NetBIOS中断时,若AX寄存器值赋为0400h,则表明在命令失败时不会自动重试所调用的命令。若AX寄存器值赋为0401h,则表明命令失败后会重试该命令。调用INT5Ch中断请求方法的程序代码如下所示:

#defineNetbiosInt5c ((unsignedchar)0x5C)

#defineNetbiosInt21FunctionCode ((unsignedchar)0x2A)

voidNetbios(structNCB*NcbPtrNear)

{

unionREGSInRegs,OutRegs;

structNCBfar*NcbPtrFar =(structNCBfar*)NcbPtrNear;

segread(&SegRegs);

SegRegs.es=FP_SEG(NcbPtrFar);

InRegs.x.bx=FP_OFF(NcbPtrFar);

int86x(NetbiosInt5c,&InRegs,&OutRegs,&SegRegs);

}

通过执行5Ch中断,NetBIOS自动进入ES:BX寄存器指定的地址,读取NCB并得知一切有关该操作的信息,最后通知硬件完成该操作。

3.命令的完成

当一个应用程序向NetBIOS提交NCB后,NetBIOS给发出请求的应用程序提供了一个返回码,它通过对返回码的观察就可以了解NetBIOS命令的完成情况。提供返回码的方式取决于命令使用等待(同步)方式或是不等待(异步)方式选项,即由命令域的高位为二进制1或0来确定,可以通过在命令域中命令与ASYNCH标志进行“或”操作来实现。ASYNCH的值实际上为80h,称为高比特设置符(highbitset)。

1)等待选项

如果命令域的高位为二进制数0,则命令使用等待方式。如果命令使用等待选项,适配器直到完成命令后,才将控制权还给应用程序。当命令执行完后,最后的返回码放在AL寄存器,返回码域及命令结束标志域(Ncb返回码的含义见附录2NetBIOS命令返回值参考)。控制权返回紧随在NetBIOS请求中断后的那条指令。

2)不等待选项

如果命令域的高位为二进制数1,则命令使用不等待方式。如果命令使用不等待选项,NetBIOS提交两个返回码。初始扫描NCB后,立即将返回码放在AL寄存器,控制权返回紧跟在NetBIOS请求后面的那条指令。如果立即返回码不是00h,则适配器不能成功执行该命令。如果立即返回码是FFh,适配器请求排入队列,等待最终完成。当适配器完成命令后,提供最后的返回码。不等待选项时有POST域为0和不为0两种情况,下面分别进行讨论:

(1)有POST的不等待选项。如果POST域不是0,NetBIOS在AL寄存器和返回码域中都放置了最后返回码。然后,NetBIOS保存寄存器的内容,将ES:BX寄存器对指向已完成的NCB,将标志压入堆栈,屏蔽可屏蔽的中断并执行对POST例程的远程调用。

在应用的POST例程中,最后的返回码可以从AL寄存器或返回码域中获得,POST例程应使用IRET指令返回到NetBIOS。POST例程不必保存或恢复寄存器内容,POST例程中能调用NetBIOS请求。然而,因为PCDOS不可重入,而某个DOS调用请求可能已被激活的POST例程所中断,所以在POST例程中不能调用PCDOS请求。

(2)无POST的不等待选项。如果POST域是0,表明没定义POST例程。最后返回码放在命令结束标志域及返回码域。应用程序绝不应以循环查询返回码域来理解命令是否完成,但应监视命令结束标志域的值,以了解什么时候0FFh发生改变。

NetBIOS常用的命令域操作代码如表4-1所示(包括同步和异步方式)。表4-1NetBIOS命令操作代码表4.2.3NetBIOS编程基础

在了解了NCB的结构和NetBIOS命令的执行过程后,就可以进行基于NetBIOS的网络编程了。在NetBIOS程序开发时,一般都需要对适配器进行初始化、复位、添加应用程序名,在程序完成后还需要进行适配器资源的释放,这些命令包括LanaEnum、ResetAll、Addname、Cancel、Hangup、DelName等,它们都是进行NetBIOS编程的基础,下面逐一介绍有关这些命令的程序代码。

1.探测LANA资源

LanaEnum是几乎所有NetBIOS应用都会用到的一个基本函数。我们知道,一台主机中可能不止安装一块网卡、一种具有NetBIOS功能的通信协议,为了了解可以使用的LANA资源,通过调用该函数,可探测系统上可用的所有LANA编号。在填写NCB时,首先对该NCB结构清零,以防止内存中原来的数据导致当NetBIOS命令结束时NetBIOS错误地进入其他内存区域。然后,为这个清零后的NCB块填写参数,在命令域填入NCBENUM命令索引,同时为返回值指定接收缓冲区(格式为LANA_ENUM,LANA_ENUM如下文所示)并设置缓冲区长度,这样就完成了LanaEnum命令的NCB块的填写。最后我们只要调用NetBIOS函数(详见4.2.2节)来执行该命令即可。如果该命令调用成功,将返回NRC_GOODRET值;如果返回值不为NRC_GOODRET,函数将返回错误代码,对错误代码的解释见附录2。

intLanaEnum(LANA_ENUM*lenum)

{

NCBncb;

ZeroMemory(&ncb,sizeof(NCB));

ncb.ncb_command =NCBENUM;

ncb.ncb_buffer =(PUCHAR)(lenum);

ncb.ncb_length =sizeof(LANA_ENUM);

if(Netbios(&ncb)!=NRC_GOODRET)

{

printf("NetbiosNCBENUMERROR:%d\n",ncb.ncb_retcode);

returnncb.ncb_retcode;

}

returnNRC_GOODRET;

}

LANA_ENUM的格式:

Typedefstruct_LANA_ENUM

{

UCHARlength;

UCHARlana[MAX_LANA];

}LANA_ENUM;

并不是在对NetBIOS的每次调用中都需要用到NCB结构内的全部成员,因而在调用一个NetBIOS命令时并不一定要填写每一个NCB域;此外,NCB中的一些域具有输出参数的功能,命令执行后的返回值将填充到这些域中,详见附录2中的NetBIOS参考命令。还应注意的是,在填写NCB结构成员之前,必须对这个NCB结构清零,以消除残留参数的影响,当然这也是一个良好的编程习惯。

2.复位LANA

ResetAll是几乎所有NetBIOS应用都会用到的一个基本函数。一旦拥有一个LANA-ENUM结构,并有来自LanaEnum的LANA编号,就需要对每个LANA编号进行复位,一方面这是一个好的编程习惯,另一方面有些操作系统,如WindowsNT,要求使用前必须对每一个LANA号进行复位。复位的方法很简单,在命令域填入NCBRESET命令索引、本地会话编号置1、依次输入LANA号即可,所有的资源都会释放。当本地会话编号为0时,资源会释放且会分配新的资源,新资源的分配按照ncb_callname域的设置进行分配。ncb.ncb_callname[0]指定同时进行的最大会话数量;ncb.ncb_callname[1]指定每个LANA增加的最大NetBIOS名字数量;ncb.ncb_callname[2]为TRUE时,一个客户机可以将机器名作为自己的NetBIOS进程名。

intResetAll(LANA_ENUM*lenum,UCHARucMaxSession,UCHARunMaxName,BOOLbFirstname)

{

NCBncb;

ZeroMemory(&ncb,sizeof(NCB));

ncb.ncb_command =NCBRESET;

ncb.ncb_callname[0] =ucMaxSession;

ncb.ncb_callname[1] =unMaxName;

ncb.ncb_callname[2] =(UCHAR)bFirstname;

for(inti=0;i<lenum->length;i++)

{

ncb.ncb_lana_num =lenum->lana[i];

if(Netbios(&ncb)!=NRC_GOODRET)

{

printf("NetbiosNCBRESET[%d]ERROR:%d\n",ncb.ncb_lana_num,ncb.ncb_retcode);

returnncb.ncb_retcode;

}

}

returnNRC_GOODRET;

}

3.添加程序名

名字是应用程序标识自己的重要参数,只有把名字成功加入到名字表中,应用程序才具有合法的网络身份;同时通过名字区分,网络通信程序才能够与想要与之建立联系的其他程序建立会话,或把数据报发送到指定的地方。添加程序名的命令很简单,在命令域填入NCBADDNAME命令索引,在LANA编号域填入LANA号,在本地名域填写程序名。命令完成后会在ncb.ncb_num中返回名字编号,名字编号是网络信息传递命令所必需的参数,十分重要。

intAddName(intlana,char*name,UCHAR*num)

{

NCBncb;

ZeroMemory(&ncb,sizeof(NCB));

ncb.ncb_command =NCBADDNAME;

ncb.ncb_lana_num =lana;

strncpy((char*)ncb.ncb_name,name,strlen(name));

if(Netbios(&ncb)!=NRC_GOODRET)

{

printf("NetbiosNCBADDNAME[lana=%d;name=%s]ERROR:%d\n",lana,name,ncb.ncb_retcode);

returnncb.ncb_retcode;

}

*num =ncb.ncb_num;

returnNRC_GOODRET;

}

4.获取适配器状态

该程序命令用于取得本地或远程(由ncb_callname指向的应用程序所运行的位置决定)适配器的状态,在命令完成后将适配器状态信息填充到一个ADAPTER_STATUS结构中(该结构的介绍略)。

intAstatus(ADAPTER_STATUS*astat,intlana,char*name)

{

NCBncb;

ZeroMemory(&ncb,sizeof(NCB));

ncb.ncb_command =NCBASTAT;

ncb.ncb_buffer =(PUCHAR)astat;

ncb.ncb_length =sizeof(ADAPTER_STATUS);

memset(&ncb.ncb_callname,'',NCBNAMSZ);

strncpy((char*)&ncb.ncb_callname,name,strlen(name));

ncb.ncb_lana_num =lana;

if(Netbios(&ncb)!=NRC_GOODRET)

{

Printf("NetbiosNCBASTATERROR:%d\n",ncb.ncb_retcode);

returnncb.ncb_retcode;

}

returnNRC_GOODRET;

}

除了上述基本的NetBIOS命令外,AddGroupName、LanStalert等命令也非常有用,它们的程序设计方法类似,读者可以根据附录2中的说明自行设计。常用的NetBIOS命令可以集中定义在NetBIOScmd.h文件中,以方便调用。

为了更好地说明NetBIOS的编程,设计一个用NetBIOS编写获取网络适配器信息的完整程序(对于程序中使用的已介绍过的函数不另行说明,后同)。该程序利用网络基本输入/输出系统NetBIOS创建了一个能获取主机MAC(网络适配器)信息及其他一些信息的文件Astatus.exe。

#include"stdafx.h"

#include<windows.h>

#include<stdio.h>

#include<nb30.h>

typedefstruct_ASTAT

{

ADAPTER_STATUSadapt;

NAME_BUFFERNameBuffer[30];

}ASTAT,*PASTAT;

ASTATAdapter;

intAstatus(ADAPTER_STATUS*astat,intlana,char*name){…}//略

intmain(intargc,char*argv[])

{

NCBncb;

UCHARuRetCode=0;

charname[16];

//resetadapter

ZeroMemory(&ncb,sizeof(NCB));

memset(&ncb,0,sizeof(ncb));

ncb.ncb_command =NCBRESET;

ncb.ncb_lana_num =0;

Netbios(&ncb);

memset(&ncb,0,sizeof(ncb));

//getthestatusoftheadapter

strcpy(name,"*");

if(uRetCode==Astatus((struct_ADAPTER_STATUS*)&Adapter,0,name))

printf("TheEthernetNumberis:%02x_%02x_%02x_%02x_%02x_%02x\n",

Adapter.adapt.adapter_address[0],

Adapter.adapt.adapter_address[1],

Adapter.adapt.adapter_address[2],

Adapter.adapt.adapter_address[3],

Adapter.adapt.adapter_address[4],

Adapter.adapt.adapter_address[5]);

if(Adapter.adapt.adapter_type==0xFF)

printf("TheadapterisTokenRingadapter.\n");

elseif(Adapter.adapt.adapter_type==0xFE)

printf("TheadapterisEthernetadapter.\n");

printf("Thesoftware-releaselevelis%d.%d\n",Adapter.adapt.rev_major,Adapter.adapt.rev_minor);

printf("Thenumberofnamesinthelocalnamestableis%d\n",A_count);

for(inti=0;i<A_count;i++)

printf("%s\n",Adapter.NameBuffer[i].name);

return0;

}首先程序构造一个NCB结构,并对其进行清0操作(为了简单起见仅对LANA0进行处理)。由于本程序的功能是获取适配器信息,没有特指某一个应用程序,因而ncb_callname域的第一个字符设为星号,表示任意程序。接着程序执行Astatus()函数,执行后的结果放入一个ASTAT结构。ASTAT包含两个成员,一个成员是ADAPTER_STATUS结构的adapt,用于收集适配器的状态信息;另一个成员为NAME_BUFFER结构的NameBuffer指针,用于收集适配器上该LANA号0所属的名字表。adapt的成员具有不同的含义:adapter_address指针指向包含适配器物理地址的缓冲区;adapter_type的值代表适配器类型,当值为0xFF时,适配器为令牌环网络适配器;当值为0xFE时,适配器为以太网适配器;rev_major和rev_minor表示适配器版本号;name_count包含适配器LANA0上的名字表中名字的个数,当然这些名字都被收集进NameBuffer所指向的缓冲区。最后上述参量被打印出来,执行结果如图4-3所示。图4-3Astatus.exe的执行结果为了方便上述NetBIOS基本函数复用,将常用的函数写入头文件NetBIOScmd.h,使用语句

#include“NetBIOScmd.h”即可引用。此外,进行NetBIOS程序的编写过程中还需引入静态链接库Netapi32.lib。方法是:在VC6.0编译器的“project→setting→link”的“Object/librarymodules”选项里添加该静态链接库,当然可以在程序代码中直接添加,参阅9.2.3节。

当一个适配器在网络中激活后,宿主机上的应用程序将自己的名字注册到LANA编号上的名字表中,应用程序就可以用NetBIOS与驻留在同一个或不同计算机上的其他应用通信。应用程序之间的网络通信可以使用数据报或会话两种方式。4.3数据报通信程序设计数据报是一条短信息,它的长度随NetBIOS实现方法的不同而不同。数据报不能保证数据的正确传输,也不提供来自接收方的指示,在很多情况下,如目标不存在、未加电或不接收数据报时,发出的数据报就可能不会被任何计算机接收到,但是数据报具有占用网络资源少的优点,适合于发送重要性要求不高的一般信息。4.3.1数据报通信模型

广播型和定向型数据报是数据报通信的两种不同类型,前者不区分接收者的身份,后者必须指定接收者的身份,这两种信息传递方式实现的关键是对NetBIOS数据报发送和接收命令的调用。

广播型数据报完全不区分接收者,可以使用NetBIOS的SendBroadcastDatagram命令来发送数据,接收方可通过调用NetBIOS的ReceiveBroadcasetDatagram命令来接收数据。SendBroadcastDatagram命令给本地网上的每个NetBIOS系统发送信息。当NetBIOS节点收到广播数据后,执行ReceiveBroadcastDatagram命令的每个进程都收到数据。当广播数据被收到时,或没有ReceiveBroadcastDatagram命令在运行,数据将被丢弃。

定向型数据报指定数据报接收者的组名,任何一方都可以调用NetBIOSSendDatagram命令发送数据,接收方可以通过调用NetBIOS的ReceiveDatagram命令来接收发送过来的数据。Send_Datagram命令需要调用者设定目的名。如果目的名是组名,组中每个成员都将收到数据。Receive_Datagram命令的调用者必须确定它接收数据的本地名。除了实际数据外,Receive_Datagram也返回发送者的名称。如果NetBIOS收到数据,但却没有Receive_Datagram命令在等待,数据将被丢弃。

同一个数据报通信程序一般可分为发送部分和接收部分,下面按照两种不同类型分别进行程序介绍。4.3.2广播型数据报程序

广播型数据报程序datagramBC.exe的main首先用LanaEnum收集宿主机中所有可用的LANA编号,然后用ResetAll重设每个LANA,这几乎是所有NetBIOS应用程序都要采用的两个步骤。接着main为变量分配资源,这样初始化工作就基本完成了。

完成初始化工作后,需要将程序注册到LANA中。这里我们希望编写一个既能发送消息又能接收消息的程序,即LAN中收发端程序名相同。为了不产生名字冲突,因此采用NCBADDGRNAME命令的方式注册名字。NCBADDGRNAME命令与NCBADDNAME命令的实现函数类似,只不过是将命令域设为NCBADDGRNAME索引,其他设置均相同。

注意:在本程序中,数据报的接收不受名字的限制,只要执行ReceiveBroadcastDatagram命令的程序都可以接收到本广播式数据报程序发送的消息。

程序利用NCBADDGRNAME命令得到了名字编号dwNum,就可以进行数据报的发送了,发送功能由DatagramSendBC()函数完成。函数在NCB命令域填入NCBDGSENDBC命令索引,在名字编号域填入编号,在缓冲区地址域设置缓冲区buffer指针地址(用来存放需要发送的消息),在缓冲区长度域设置缓冲区的长度buflen,在LANA编号域填入LANA号lana,最后执行Netbios()函数即可。如果函数执行成功,buffer中的消息将通过LANA传递到LAN上,并会返回整数NRC_GOODRET。

当main发出NCBDGSENDBC命令后,程序执行广播式数据报接收命令,接收LAN中由NCBDGSENDBC命令发送的任意消息。广播式数据报接收命令在NCB命令域填入NCBDGRECVBC索引(这里我们将为每个lana投放一个接收命令,因此应设置ASYNCH使接收成为异步命令),名字编号域填入编号,缓冲区地址域设置缓冲区buffer(用来存放收到的消息),缓冲区长度域设置缓冲区的长度buflen,LANA编号域填入LANA号lana。这里我们还需再填入一个事件句柄hEvent,这个句柄是用来进行接收监控的,稍后将作解释。最后执行Netbios()函数。如果函数调用成功,会返回整数NRC_GOODRET。在为每个lana投放一个异步接收命令后,我们并不知道其中哪一个会首先接收到数据报,因此采用一个监控方法来完成等待任务。这个监控方法采用Win32事件作为传信机制,具体是利用处于循环状态的WaitForMultipleObjects()函数监控上述NCBLISTEN命令绑定的事件句柄来完成的。WaitForMultipleObjects()函数的第一个参数是监控对象的个数。第二个参数是指向监控对象句柄数组的指针,这里是上述lenum.length个NCBLISTEN命令的NCB事件域填入的事件hEvent[i]所处的数组指针hEvent。第三个参数是等待标志位,如果为TRUE,表示所有事件都有响应才有返回值;如果为FALSE,表示任何一个事件有响应就有返回值。第四个参数为等待时间,设为INFINITE。通过WaitForMultipleObjects()函数的监控,一旦任何一个NCBDGSENDBC接收到数据报,WaitForMultipleObjects()函数将会返回这个NCBDGSENDBC的序号。接下来,我们根据序号查看这个NCBDGSENDBC的NCB命令结束标志域,如果不为NRC_PENDING接收成功,buffer中将填充从LAN上接收到的数据报。

最后main打印所收听到的消息,以及发送者的NetBIOS名。对于某些程序的NetBIOS名不能打印,可使用FormatNetbiosName()函数进行转化,这样就完成了一个简单的广播式数据报程序设计,如下所示:

#include"stdafx.h"

#include<windows.h>

#include<stdio.h>

#include<nb30.h>

#include"NetBIOScmd.h"

#defineMAX_SESSIONS 254

#defineMAX_NAMES 254

#defineMAX_DATAGRAM_SIZE 512

//mainfunction

intmain(intargc,char*argv[])

{

LANA_ENUM lenum;

char szSender[NCBNAMSZ+1];

char szLocalName[NCBNAMSZ+1] ="datagram";

char *MessageArray =NULL,

num ='';

int i=0,

j=0,

len;

DWORD dwErr;

UCHAR *dwNum =NULL;

NCB *ncb =NULL;

HANDLE *hEvent =NULL;

DWORD dwRet;

if(LanaEnum(&lenum)!=NRC_GOODRET)

return1;

if(ResetAll(&lenum,(UCHAR)MAX_SESSIONS,(UCHAR)MAX_NAMES,false)!

=NRC_GOODRET)

return1;

ncb =(NCB*) GlobalAlloc(GMEM_FIXED|GMEM_ZEROINIT,

sizeof(NCB)*lenum.length);

dwNum =(UCHAR*) GlobalAlloc(GMEM_FIXED|GMEM_ZEROINIT,

sizeof(UCHAR)*lenum.length);

MessageArray =(char*) GlobalAlloc(GMEM_FIXED,MAX_DATAGRAM_SIZE);

hEvent =(HANDLE*) GlobalAlloc(GMEM_FIXED|GMEM_ZEROINIT,

sizeof(HANDLE)*lenum.length);

for(i=0;i<lenum.length;i++)

{

dwErr=AddGroupName((int)lenum.lana[i],szLocalName,&dwNum[i]);

printf("addgroupname(%s)tolana[%d]\n",szLocalName,lenum.lana[i]);

}

strcpy(MessageArray,"testbroadcastmassage");

for(i=0;i<lenum.length;i++)

{

len=strlen(MessageArray);

DatagramSendBC(lenum.lana[i],dwNum[i],MessageArray,len);

}

for(j=0;j<lenum.length;j++)

{

hEvent[j]=CreateEvent(0,TRUE,FALSE,0);

if(DatagramRecvBC(&ncb[j],lenum.lana[j],dwNum[j],MessageArray,

MAX_DATAGRAM_SIZE,hEvent[j])!=NRC_GOODRET)

return1;

}

printf("setuphasbeenfinished,nowbeginstolisten.\n\n\n");

dwRet=WaitForMultipleObjects(lenum.length,hEvent,FALSE,INFINITE);

if(dwRet==WAIT_FAILED)

{

printf("WaitForMultipleObjectsfailed:%d\n",::GetLastError());

return1;

}

for(j=0;j<lenum.length;j++)

{

if(ncb[j].ncb_cmd_cplt==NRC_PENDING)

Cancel(&ncb[j]);

else

{

ncb[j].ncb_buffer[ncb[j].ncb_length]=0;

FormatNetbiosName((char*)ncb[j].ncb_callname,szSender);

printf("message:[%s],\ncomesfrom[%s],\nreceivedfrom:[LANA%d].\n",

MessageArray,szSender,ncb[j].ncb_lana_num);

ResetEvent(hEvent[j]);

}

}

for(i=0;i<lenum.length;i++)

{

DelName(lenum.lana[i],szLocalName);

CloseHandle(hEvent[i]);

}

GlobalFree(MessageArray);

GlobalFree(hEvent);

GlobalFree(ncb);

return0;

}

程序编译完以后,可以将其复制到LAN上的多台机器上并分别运行。当任意一台机器处于接收等待状态时再运行一个datagramBC.exe程序,其他处于等待的程序都会打印消息“testbroadcastmassage”。4.3.3定向型数据报程序

定向型数据报程序把数据报发送到注册为指定的唯一名或组名网络应用程序上。在程序编写上与广播型数据报程序的区别在于:数据报发送和接收分别采用NCBDGSEND命令和NCBDGSRECV命令。这两个命令的实现函数与DatagramSendBC()和DatagramRecvBC()非常相似,如下所示。

DatagramRecv()与DatagramRecvBC()的区别仅是命令索引不同,为NCBDGRECV;而DatagramSend()与DatagramSendBC()的区别除了命令索引不同,为NCBDGSEND外(参见附录1),还必须在调用名域填写接收应用程序的NetBIOS名(可以是唯一名,也可以是组名),数据报的接收将受到这个名字的限制。

intDatagramSend(intlana,intnum,char*buffer,intbuflen,char*name)

{

NCBncb;

ZeroMemory(&ncb,sizeof(NCB));

ncb.ncb_command =NCBDGSEND;

ncb.ncb_lana_num =lana;

ncb.ncb_num =(UCHAR)num;

ncb.ncb_buffer =(PUCHAR)(buffer);

ncb.ncb_length =buflen;

memset(ncb.ncb_name,'',NCBNAMSZ);

strncpy((char*)ncb.ncb_name,name,strlen(name));

if(Netbios(&ncb)!=NRC_GOODRET)

{

printf("NetbiosNCBDGSENDBCERROR:%d\n",ncb.ncb_retcode);

returnncb.ncb_retcode;

}

returnNRC_GOODRET;

}

//receiveadirecteddatagramonthespecifiedLANAnumberfromtheregisteredname

intDatagramRecv(PNCBpncb,intlana,intnum,char*buffer,intbuflen,HANDLEhEvent)

{

ZeroMemory(pncb,sizeof(NCB));

ZeroMemory(buffer,sizeof(MAX_DATAGRAM_SIZE));

pncb->ncb_command =NCBDGRECV|ASYNCH;

pncb->ncb_lana_num =lana;

pncb->ncb_num =num;

pncb->ncb_buffer =(PUCHAR)(buffer);

pncb->ncb_length =buflen;

pncb->ncb_event =hEvent;

if(Netbios(pncb)!=NRC_GOODRET)

{

printf("NetbiosNCBDGRECVBCERROR:%d\n",pncb->ncb_retcode);

returnpncb->ncb_retcode;

}

returnNRC_GOODRET;

}用上述函数替换广播型数据报程序datagramBC.exe中的DatagramRecv()与DatagramRecvBC(),并做相应处理(为程序注册不同的名字),就实现了定向型数据报程序的设计。在运行多个这样的程序时会发现,只有与DatagramSend()设置的

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论