网络程序设计第1章.docx
《网络程序设计第1章.docx》由会员分享,可在线阅读,更多相关《网络程序设计第1章.docx(13页珍藏版)》请在冰豆网上搜索。
![网络程序设计第1章.docx](https://file1.bdocx.com/fileroot1/2023-1/29/39b8f0b9-3602-4bbd-9213-ef5e175c5c95/39b8f0b9-3602-4bbd-9213-ef5e175c5c951.gif)
网络程序设计第1章
第1章绪论
本章介绍计算机网络程序设计的一些基本内容、概念和方法。
1-1概述
随着计算机网络的飞速发展和日益普及,网络应用越来越多。
业界对计算机网络程序设计的需求也相应增多。
因此,计算机网络程序设计作为一种重要的知识技能越来越受到人们的重视。
计算机网络程序设计就是利用网络应用编程接口编写网络应用程序,实现网络应用进程间的信息交互功能。
一般来说,应用进程间的通信可以分为两种:
同一系统上的应用进程间的通信和不同系统上的应用进程间的通信。
同一系统上的应用进程间的通信又称为进程间通信,即IPC。
而不同系统上的进程间的通信,必须通过网络编程接口访问网络协议提供的服务来实现。
IPC是进程间通信(InterprocessCommunication)的简称。
传统上该术语描述的是运行在某个操作系统上的不同进程间消息传递(MessagePassing)的不同方式。
IPC的主要形式有:
消息传递(管道、FIFO、消息队列),同步(互斥、条件变量、读写锁、文件与记录锁、信号灯),共享存储区(匿名共享存储区、有名共享存储区),远程过程调用(RPC)等。
本书主要讨论的是为实现不同系统上的进程间的通信而进行的网络应用编程的原理、接口和方法。
事实上,同一系统上的不同应用进程间的通信也可以通过网络编程接口来实现,只是性能上会有些差别。
很多网络应用编程接口,如插口(Socket,也有文献译为套接口或套接字,本书统一采用“插口”)API,对同一系统中的网络通信进行了优化(如通过IPC)。
考虑到分布式应用的可移植性等问题,使用网络编程接口比使用IPC机制要好。
网络通信离不开网络协议,网络编程接口访问网络协议所提供的服务。
不同的网络协议可能提供不同的服务访问接口,同一网络应用编程接口可能提供访问不同网络协议的接口。
如著名的网络应用编程接口——插口API,支持对很多协议的访问,如TCP,UDP,rawIP,数据链路层协议,Unix域协议等。
DECnet网络应用编程接口则与插口完全不同,因为其协议的命名规则、地址格式、协议服务与TCP/IP有很大区别。
然而,在一些系统中,插口API也支持DECnet协议。
Unix域协议并不是一个实际的协议族,它只是在同一台主机上进行客户-服务器通信时,使用与不同主机上的客户和服务器间通信时采用的相同API的一种方法。
要学好网络编程,对于操作系统、网络协议、网络编程模式和方法要有比较好的理解,因为网络应用编程与它们是密不可分的。
1-2网络服务
本书主要讨论基于TCP/IP协议栈的网络编程,即网络应用程序访问TCP/IP协议提供的服务来实现进程间的通信,并且主要是针对IPv4协议而不是IPv6协议。
网络协议是分层的,如OSI/RM分七层,TCP/IP分四层。
网络应用编程的目的是如何利用各协议层所提供的功能实现用户的应用,而不是要实现各协议层的功能。
操作系统一般都把各协议层的功能作为系统内核的一部分来实现,应用程序只需调用系统提供的应用编程接口即可。
需要说明的是,因为协议的层次性,通过应用编程接口访问某一协议层提供的服务,事实上还需要间接用到其下各层提供的服务。
因为大多数应用程序访问运输层服务和数据链路层服务,因此本节主要介绍运输层服务和数据链路层服务。
1-2-1运输层服务
从通信和信息处理的角度看,运输层属于面向通信部分的最高层。
然而,从网络功能或用户功能来划分,则运输层又属于用户功能中的最低层。
运输层的任务是根据下面通信子网的特性最佳地利用网络资源,并以可靠和经济的方式为两端主机上的进程之间透明地传送报文。
正是因为运输层的这种特殊地位,大多数网络应用均要使用运输层提供的服务。
因此,大部分网络应用程序设计都是针对运输层协议进行的。
TCP/IP协议栈中的两个最主要的运输层协议是TCP和UDP。
其中,TCP提供可靠的、有序的、端到端的数据传输服务,而UDP则提供的是不可靠的、不保证有序到达的、端到端的数据传输服务。
有关TCP/IP的文献非常之多,读者可以从中了解更多细节。
1-2-2数据链路层服务
数据链路层负责在两个相邻结点间的链路上,无差错地传送以帧为单位的数据。
每一帧包括一定数量的数据和一些必需的控制信息。
数据链路层要负责建立、维护和释放数据链路的连接。
在传送数据时,若接收结点检测到数据有差错,就要通知发方重发这一帧,直到这一帧正确到达接收结点为止。
目前,大多数的操作系统都提供了数据链路层访问接口,它使得应用程序可以实现以下功能:
(1)监视数据链路层上所收到的分组,这使得我们可以在普通计算机系统上通过tcpdump程序来监视系统,而无需特殊的硬件设备。
(2)作为普通应用程序而不是内核的一部分运行某些程序。
例如,大多数Unix系统的RARP服务器是普通的应用进程,它们从数据链路读取RARP请求(RARP不是IP数据报),并把应答写回数据链路。
异步转移模式(ATM)提供的也是数据链路层服务。
在很多支持ATM的系统中(如IBM的AIX操作系统),也提供了类似插口API的访问ATM服务的应用编程接口。
1-3网络应用编程接口ok
网络通信是进程间通信的一种非常重要的手段。
网络通信是在协议的控制下进行的,网络编程的目的是访问网络协议提供的服务。
访问协议提供的服务就是通过网络应用编程接口(API)来实现的。
本节主要介绍一些流行的网络应用编程接口。
1-3-1Berkeley插口API
插口API,有时称为“Berkeley插口API”,因为它最开始出现在BerkeleyUnix中。
Berkeley插口API是在1983年随4.2BSD操作系统引入的,刚开始只支持TCP/IP协议族和Unix域协议。
今天的插口还支持很多其他协议,如OSI/RM中的序列协议和DECnet协议等。
插口API是最重要的网络编程接口,特别是在Unix系统中,几乎所有的网络应用程序都是利用插口API来实现的。
本书的重点就是讨论基于插口API的网络应用编程。
1-3-2TLI
TLI是AT&T开发的运输层接口。
X/Open组织于1998年发布了一个TLI的修正版XTI(theX/OpenTransportInterface,XTI/Open传输接口)。
XTI只是对TLI作了少量修改,它是TLI的一个超集。
在上世纪80年代中期,Posix.1出现之前,AT&TUnix与BerkeleyUnix之间存在着巨大差异。
在当时的网络界,OSI/RM协议的研究非常流行,并盛传OSI/RM协议将取代TCP/IP。
在这样的背景下,AT&T于1986年在SystemV版本3.0(SVR3)中首次推出称为TLI(运输层接口)的不同于插口API的编程接口。
TLI与插口API编程有很多相似之处,不过TLI是按照OSI/RM传输服务定义建模的。
同插口API相比,使用TLI作为网络应用编程接口的程序设计人员要少得多。
另外,TLI编程方法与插口编程方法比较类似。
因此,本书不准备介绍基于TLI的网络应用编程。
1-3-3WindowsSockets
WindowsSockets(简称为WinSock)API是Windows下得到广泛应用的、开放的、支持多种协议的网络编程接口。
从1991年的1.0版到1997年的2.2.1版,经过不断完善并在Intel,Microsoft,Sun,SGI,Informix,Novell等公司的全力支持下,已成为Windows环境下事实上的网络编程接口标准。
WinSock以Berkeley插口API为范例定义了一套MicrosoftWindows下网络编程接口。
它不仅包含了人们所熟悉的BerkeleySocket风格的库函数,也包含了一组针对Windows的扩展库函数,以使程序员能充分地利用Windows消息驱动机制进行编程。
WindowsSockets规范的目的在于提供给应用程序开发者一套简单的API,并让各家网络软件供应商共同遵守。
此外,在一个特定版本Windows的基础上,WindowsSockets也定义了一个二进制接口(ABI),以此来保证使用WindowsSocketsAPI的应用程序能够在任何网络软件供应商的符合WindowsSockets协议的实现上工作。
因此,这份规范定义了应用程序开发者能够使用,并且网络软件供应商能够实现的一套库函数调用和相关语义。
由于Windows操作系统被广泛用于PC机,因此本书也将基于WindowsSockets的网络应用编程作为一个重要内容加以介绍。
1-3-4可视化编程环境下的网络控件
随着MicrosoftWindows下可视化编程工具的快速发展,如VisualBasic,VisualC++,PowerBuilder,Delphi,C++Builder等,在这些可视化编程环境中的网络编程也变得越来越容易。
在可视化编程环境中,大部分网络编程是通过网络控件来进行的。
它屏蔽了网络编程细节,这同时也限制了网络编程的灵活性。
这些可视化网络控件也是基于WindowsSockets接口的,是比WindowsSockets更高一级的网络应用编程接口。
本书也将简要介绍这方面的内容。
1-3-5其他网络编程接口
远程过程调用(RPC:
RemoteProcedureCall)指的是由本地系统上的进程激活远程系统上的进程,由远程过程完成某项任务后将结果返回给本地进程。
RPC在形式上与普通的本地过程调用类似,但其本质上的差异在于调用者与被调用者处在不同的主机系统上,因而需要解决数据表示、参数传递、调用语义、异常处理和安全等一系列问题。
RPC最早由施乐(Xerox)公司提出,并于1981推出了第一个RPC产品——CourierRPC,该产品作为XNS的网络系统的一部分。
后来,其他公司也推出了多种RPC的实现,如Sun公司的RPC,Apollo公司的ApolloRPC等。
今天,RPC成了大多数Unix操作系统上的标准配置。
RPC是比插口API更高级的网络编程接口。
RPC程序设计包含两个层次:
一个是高层RPC程序设计,另一个是低层RPC程序设计。
对用户而言,高层RPC程序设计更少地涉及对运输层的控制以及鉴别机制的选择;而低层RPC程序设计则要由用户显式地进行运输层的控制和选择鉴别方式。
高层RPC编程接口只有三个库函数,且只能使用UDP作为传输协议。
低层RPC编程接口的库函数就多得多,且可以选择TCP还是UDP作为传输协议,但需要程序员比较了解低层协议和RPC更多的细节内容。
某些系统中有一些帮助进行低层RPC程序设计的工具,如SunRPC中的rpcgen工具就可以帮助RPC程序员生成大部分的RPC程序,使程序员把主要精力放在客户方和服务器方的远程主程序设计上。
1-4网络编程模式
网络编程模式一般采用客户-服务器模式(Client-Server)。
客户-服务器模式描述的是进程之间的服务和被服务的关系。
在这种模式下,大多数网络应用系统由两部分组成:
客户(Client)和服务器(Server)。
客户是主叫方,服务器是被叫方。
客户与服务器的通信关系一旦建立,通信就可以是双向的,客户和服务器都可以发送和接收信息。
在实际网络应用中,有很多客户服务器的例子,如Web浏览器(客户)和Web服务器之间的信息交换;FTP客户与FTP服务器之间的文件传输;Telnet客户通过远程主机上的Telnet服务器提供远程登录手段。
一般情况下,客户与服务器之间的关系是多对多的关系,即一个客户可以与多个服务器通信,一个服务器可以同时与多个客户保持联系。
根据实现方式的不同,服务器又可分为多种类型:
串行服务器(单进程串行地服务客户请求),并发服务器(服务器为每一个客户创建一个进程或线程来服务客户的请求)。
并发服务器又可分为:
预先创建服务子进程并发服务器,预先创建服务线程并发服务器,按需创建服务子进程并发服务器,按需创建服务线程并发服务器等。
还可以根据并发服务器中接受连接请求的方式对其作进一步区分。
我们将在第6章中详细介绍服务器设计方法。
客户与服务器之间的通信涉及到网络通信协议。
如,Web浏览器客户与Web服务器之间的通信使用了TCP协议,而TCP又使用了下层的IP协议,IP协议则使用下层的数据链路层协议,再往下则是物理层协议。
因此,客户与服务器之间的通信可以这样来描述:
在发送端,从上层往下层走,上层调用下层提供的服务,每一层次的协议的实现又为上一层提供服务;在接收端,从下层往上层走,执行相反的过程。
1-5网络编程要考虑的问题
计算机网络程序设计因为涉及不同平台之间的信息交互,因此与单机上的程序设计有很大的差别。
了解网络程序设计要注意的一些问题,有助于网络编程人员设计、编写高质量的网络应用程序。
1-5-1并发环境下的网络编程
单进程应用与多进程或多线程应用程序的编程有着很大的区别。
在多进程或多线程应用程序中,涉及到资源共享、进程或线程间的同步,因而要复杂得多。
在多进程或多线程应用中,使用的系统调用或函数必须是可重入的。
哪些系统调用或系统函数是可重入的,这在不同系统中是不同的。
一般的系统都会对此有详细的说明。
对于那些不可重入的调用或函数,系统如果不提供多线程安全的版本,则应用编程人员需要避免使用或自己编写相应的函数。
在多线程应用中,对调用或函数的使用有很多限制。
如,在Solaris2.5中,在Solaris线程中访问定界数据会导致总线错误。
在Solaris操作系统中,在一个线程中关闭另一个线程正在使用的网络端口会导致应用程序“死掉”,而这种情况在DigitalUnix(后称为Tru64Unix)中则不会出现。
因此,在多进程或多线程应用中,需要仔细考虑这些限制。
1-5-2异构环境下的网络编程
网络通信常常是在多个平台之间进行,因此网络应用程序必须考虑不同平台之间的异构性,特别是不同平台上的数据格式的差异。
这些差异主要表现在以下几个方面:
(1)字节顺序。
不同的平台以不同的方式存放一个二进制数。
最常见的有两种格式:
大数在前的(big-endian)字节顺序和小数在前的(little-endian)字节顺序。
大数在前的字节顺序是指将一个多字节数的高序字节存储在内存的起始地址;而小数在前的字节顺序则相反,将低序字节存储在内存的起始地址。
在操作系统中,IBMAIX,SunOS,HPUnix,Solaris采用大数在前的字节顺序;而DigitalUnix,Linux,BSDi,SystemV4,DOS,Windows9x/2000/NT则采用的是小数在前的字节顺序。
同一数值在具有不同字节顺序的平台上的表示刚好相反。
因此,作为网络编程人员,必须清楚各种字节顺序间的区别,并采用相应的措施来解决因这种差别所带来的问题。
网络应用程序中的通信数据主要包括两部分:
网络分组中的首部和数据部分,首部与协议有关,数据部分与应用有关。
不管是网络分组中的协议首部,还是网络分组中的数据部分都要考虑这个问题。
协议首部将影响协议的正确操作,而分组中的数据则影响网络应用程序的功能。
(2)字的长度。
不同的实现对于相同的数据类型可能有不同的表示长度。
如,64位操作系统与32位操作系统中,类型longint的长度是不一样的。
在DigitalUnix中的longint的长度为8字节,而在Solaris中则为4字节。
对short,int或long等数据类型的大小也没有确定的规定。
(3)字节定界问题。
不同的平台上给结构(struct)或联合(union)打包的方式也是不同的,这取决于所有数据类型的位数及机器的定界限制。
一般情况下,操作系统在分配内存时,数据结构以4字节定界。
例如,在很多系统中,默认情况下,结构struct{chara;intb}的长度为8而不是5。
只有在1字节定界的情况下其长度才为5。
有几种方法可以解决上述问题。
对于具有相同字节顺序的平台,通信双方均以单字节定界;对于具有不同字节顺序的平台间的通信,显式地定义格式(位数、字节顺序类型),远程过程调用(RPC)采用外部数据表示XDR(ExternalDataRepresentation),实际上就是用这种方法来解决这一问题;分布式计算平台:
报文传递接口(MPI)也采取了类似的方法;另一种方法是,将需要发送的信息的结构在发送前变换成一种统一的格式(转换成一个字符数组),到达接收方后再执行相反的过程。
对于数据结构中有比特变量的情况,处理起来更加复杂。
因此,在实际网络编程中,尽量不要使用比特变量。
在很多网络协议的设计中,常常需要填充一些无用的字节以满足四字节定界,从而简化协议的实现。
1-5-3阻塞与非阻塞通信
可以将通信分为两种模式:
阻塞和非阻塞。
在网络编程时,选择通信模式也是一件很重要的事情。
对于不同的协议,阻塞通信和非阻塞通信有不同的表现。
以插口为例,在阻塞模式下,利用TCP协议发送一个报文时,如果低层协议没有可用空间来存放用户数据,则应用进程将阻塞等待直到协议有可用的空间。
而在非阻塞模式下,调用将直接返回而不需等待。
在应用进程调用接收函数接收报文时,如果是在阻塞模式下,若没有到达的数据,则调用将一直阻塞直到有数据到达或出错;而在非阻塞模式下,将直接返回而不需等待。
对于UDP协议而言,因为UDP没有发送缓存,所有UDP协议即使在阻塞模式下也不会发生阻塞。
对于面向连接的协议,在连接建立阶段,阻塞与非阻塞也表现不一。
在阻塞模式下,如果没有连接请求到达,则等待连接调用将阻塞直到有连接请求到达;但在非阻塞模式下,如果没有连接请求到达,等待连接调用将直接返回。
在连接建立阶段,不管是阻塞模式还是非阻塞模式,发起连接请求的一方总是会使调用它的进程阻塞,阻塞间隔最少等于到达服务器的一次往返时间。
不同操作系统下,在非阻塞模式下,不能完成的I/O操作返回的错误代码也是不一样的。
例如,系统V返回EAGAIN错误,而源自Berkeley的实现返回EWOULDBLOCK错误。
更混乱的是,Posix.1指定使用EAGAIN而Posix.1g指定使用EWOULDBLOCK。
幸运的是,大多数当前使用的系统(包括SVR4和4.3BSD)将这两个错误代码定义为相同的值。
通信模式对应用程序的设计方法也有直接的影响。
在非阻塞模式下,应用程序不断地轮询查看是否有数据到达或有连接请求到达。
这种轮询方式比其他的技术耗费更多的CPU时间,因此要尽量避免使用,可以采用多路复用技术(调用select或poll函数)来解决这一问题。
而在阻塞模式下,不存在这一问题。
但是阻塞模式的缺点是进程或线程在执行I/O操作时将被阻塞而不能执行其他的工作,因此在单进程或单线程应用中不能使用这种模式。
在多线程应用中比较适合采用阻塞模式,一个线程被阻塞不影响其他线程的工作。
另外,通信模式的选择与操作系统的类型也有关系。
例如,在非占先的Windows操作系统中,应用程序应尽量不要使用阻塞模式工作。
1-5-4服务类型的选择
从通信的角度看,网络协议栈中的各层所提供的服务可以分为两大类:
面向连接(connection-oriented)服务与无连接(connectionless)服务。
(1)面向连接服务。
所谓连接,就是两个对等实体为进行数据通信而进行的一种结合。
面向连接服务要求:
在数据交换之前,必须先建立连接;当数据交换结束后,则应终止这个连接。
一般来说,面向连接服务过程分为三个阶段:
连接建立、数据传输和连接释放。
在传送数据时是按序传送的。
这点和电路交换的许多特性很相似,因此面向连接服务又称为“虚电路服务”。
面向连接服务比较适合于在一段时间间隔内要向同一目的地发送许多报文的情况。
对于发送很短的零星报文,连接建立和释放所带来的开销就显得过大了。
在TCP/IP协议栈中,TCP协议提供面向连接的服务。
(2)无连接服务。
无连接服务指的是两个实体之间的通信不需要先建立好一条连接,其所需的下层资源在数据传输时动态地进行分配。
无连接服务的另一个特征就是它不需要通信的两个实体同时是活跃的。
只有发送端的实体正在进行发送时,它才必须是活跃的。
而接收端的实体只有在进行接收操作时才必须是活跃的。
无连接服务的优点是灵活方便和效率高;但它不能防止报文的丢失、重复或失序,而将这些问题交由应用程序根据需要自行解决。
无连接服务特别适合于传送少量零星的报文。
无连接服务又可分为以下三种类型:
■数据报(datagram)。
它的特点是不需要接收端做任何响应,因而是一种不可靠的服务。
数据报常被描述为“尽最大努力交付”(besteffortdelivery)。
在TCP/IP协议栈中,UDP协议提供数据报服务。
■证实交付(confirmeddelivery)。
它又被称为可靠的数据报。
这种服务对每一个报文产生一个证实给发方,不过这个证实不是来自接收端的用户而是来自提供服务的协议层。
这种证实只能保证报文已经发送给远端的目的站了,但并不能保证目的站用户已收到这个报文。
■请求应答(request-reply)。
这种类型的数据报是收端用户每收到一个报文,就向发端用户发送一个应答报文。
与“证实交付”的主要区别在于应答是由接收端的用户而不是提供服务的协议层发出的。
事务处理(transaction)中的“一问一答”方式的短报文,以及数据库中的查询,都很适合使用这种类型的服务。
(3)两种服务的比较。
表1-1列出了面向连接服务与无连接服务的主要区别。
这些区别为网络应用编程选择何种类型的服务时提供了依据。
表1-1面向连接服务与无连接服务之间的比较
服务类型
项目
面向连接服务
无连接服务
端到端的连接
必须有
不需要
目的站地址
仅在连接建立阶段使用
每个分组都要有目的站的完整地址
路由选择
在连接建立时确定
每个分组独立选择路由
分组的顺序
总是按发送顺序到达目的站
到达目的站的顺序可能与发送顺序不同
端端排序
由通信子网负责
均由主机负责
端端的差错处理和端端的流量控制
均由通信子网负责
均由主机负责
灵活性
差
好
可靠性
好
差
效率
低
高
适用场合
适合有许多连续报文且要求可靠、有序地数据发送
适合少量零星报文,且可靠性要求不高的数据传送
在网络应用编程时,必须选择使用何种类型的网络服务。
这两种服务各有优缺点,具体选择何种服务需要根据应用的特点来确定。
另外,在无连接服务的基础上增加一些差错处理、拥塞控制等措施后也能完成面向连接的服务。
1-5-5差错处理
任何应用程序均需要进行差错处理,即检查函数返回的调用结果的正确性并作出相应的处理。
网络应用编程接口提供差错处理的基本设施,更高一级的差错处理需要应用程序来实现。
在Unix系统中,有一个全局变量errno,每当一个Unix函数(不仅仅是网络应用编程接口中的函数)中出现差错时,errno被置成一个指示错误类型的正数,而函数本身则通常返回-1。
errno的值只在函数发生错误时设置。
如果函数正确返回,则errno的值就无定义。
在Unix系统中,所有的错误代码都是常数值,在头文件sys/errno.h中定义。
把错误代码保存在全局变量errno中不适合于在一个进程中共享所有全局变量的多线程。
因此,在多线程环境中,每个线程必须有自己的errno变量。
通常情况下,提供一个局限于线程的errno变量的隐式请求是自动处理的,不过需要告诉编译器所编译的程序是可重入的。
这一工作通常通过给编译器指定编译选项-D_REENTRANT来实现。
该编译选项将errno.h头文件中的errno宏扩展为一个函数,由它访问errno变量的局限于某个线程的副本。
在有些Unix系统中,如DigitalUnix,还有一个与错误处理有关的全局变量sys_errlist[],它是一个二