注册 登录  
 加关注
查看详情
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

农村人

 
 
 

日志

 
 

引用 计算机网络组成Linux环境C语言设计  

2010-04-09 18:48:52|  分类: 默认分类 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |

 

引用

远方的星星计算机网络组成Linux环境C语言设计

计算机网络是通过通信线路互相连接的计算机的集合,它是由计算机及外围设备、数据通信和中断设备等构成的一个群体。TCP/IP协议是Internet上使用的协议,而Internet是世界上最大的计算机网络。国际标准化组织ISO对网络标准提出了OSI参考模型,该模型进一步规范了计算机网络的设计并解决了TCP/IP协议没有涉及的底层实现问题。Linux系统的一个主要特点是它的网络功能非常强大。随着网络的日益普及,基于网络的应用也将越来越多。本章将讲解计算机网络的基本概念,以及基础的网络编程方法。

18.1  计算机网络组成

在学习网络编程前,首先需要了解的是计算机网络的组成,只有这样才能知道如何设计程序在其间进行通信,以及网络编程所面对的问题。物理层面上,计算机网络由计算机设备、网络连接设备、传输介质这3个部分组成;逻辑层面上,计算机网络由网络协议、网络应用软件、数据这3个部分组成。计算机网络根据其组成的形式又可分为多种结构,有的结构适用于某种环境,但更多情况是将多种网络结构复合使用组成实际的网络。为了规范不同的计算机和计算机网络进行通信,通常用网络模型来描述需要解决问题的层次,并以网络模型为基础编制出了多种网络传输协议。

18.1.1  网络结构

大多数的计算机网络是局域网,整个网络位于一幢建筑物或一个房间内。局域网用于在多台计算机之间共享资源。例如,连接两台计算机和一台打印机的局域网允许任何一台计算机访问打印机,如图18.1所示。

计算机网络组成Linux环境C语言设计 - 远方的星星 - 这里因为你而精彩!

图18.1  简单的局域网

根据局域网的组成形式,可以将局域网分为星型网络、环状网络和总线网络3种基本网络结构。计算机都连在一个中心站点上,那么该网络即是星型网络。星型网络像车轮的轮辐,所以星型网络的中心通常被称为集线器或交换机。典型的集线器或交换机包括了这样一种电子装置,它从发送计算机接收数据并把数据传输到合适的目的地,如图18.2所示。

计算机网络组成Linux环境C语言设计 - 远方的星星 - 这里因为你而精彩!

图18.2  星型网络

环状网络将计算机连接成一个封闭的圆环,一根电缆连接第一台计算机与第二台计算机,另一根电缆连接第二台计算机与第三台,依次类推,直到一根电缆连接最后一台计算机与第一台计算机,如图18.3所示。

计算机网络组成Linux环境C语言设计 - 远方的星星 - 这里因为你而精彩!

图18.3  环状网络

总线网络通常有一根连接计算机的长电缆,任何连接在总线上的计算机都能通过总线发送信号,并且所有计算机也都能接收信号。由于所有连接在电缆上的计算机都能检测到电子信号,因此任何计算机都能向其他计算机发送数据,如图18.4所示。

计算机网络组成Linux环境C语言设计 - 远方的星星 - 这里因为你而精彩!

图18.4  总线网络

每种网络结构都有优点与缺点。环状网络使计算机容易协调使用以及容易检测网络是否正确运行。然而,如果其中两根电缆断掉,整个环状网络都要失效。星型网络能保护网络不受某一根电缆损坏的影响,因为每根电缆只连接一台机器。总线网络所需的布线比星型网络少,但是有和环状网络一样的缺点。所以,对于某一个小的区间来说,网络的实现可能是以上任何一种,但对于大型网络来说,通常是由这3种网络组成的复合结构。

18.1.2  OSI 参考模型

国际标准化组织开发了开放式系统互联参考模型,以促进计算机系统的开放互联。开放式互联特点是支持不同系统环境互联。该模型为计算机间开放式通信所需要定义的功能层次建立了全球标准。该模型的层次依次为:

  ● 物理层:物理层并非是指网络硬件或传输媒介,它只存在与抽象结构中,是负责数据流传输的最底层功能模块。物理层从第二层数据链路层(DDL)接收数据帧,然后以串行方式发送数据帧,每次只发送一个字节。另外,它也负责接收数据流,然后组合成数据帧传送给数据链路层。

  ● 数据链路层:数据链路层的作用是将数据流打包成数据帧,然后将数据帧交给物理层进行传递。也从物理层接收数据帧,并通过循环校验来检测数据传输的可靠性。

  ● 网络层:网络层用于设备间建立路由,处理数据帧中的地址信息。但是,网络层不检验数据的完整性,而是交由数据链路层完成。

  ● 传输层:传输层是以数据包和网段为对象的数据处理层,它是高度抽象化的数据链路层服务。传输层对数据的完整性负责,如果某一数据包丢失,它将要求对方重新发送该数据包。

  ● 会话层:会话层用于建立两个网络终端间的联系,与传输层关系极为密切,用于决定通信的模式是单工还是双工,以及基本的握手协议。

  ● 表示层:表示层用于处理不同计算机的数据编码方式,负责对数据编码进行转换。不同计算机的数据编码系统可能有差别,例如IBM和APPLE系统之间的差别。

  ● 应用层:应用层不包括任何应用,只是为OSI参考模型提供接口。通常,网络协议被应用程序调用的是应用层。

为了更清晰地展现OSI参考模型每一层的功能,以及两个网络终端以OSI参考模型进行通信的原理,可以用垂直方向图表示该模型,如图18.5所示。OSI参考模型在两个网络终端中层层对应,因此每一层都具备输入和输出的功能。

18.1.3  TCP/IP参考模型

OSI参考模型并非实际应用中的标准,而只是一种抽象化表示方法。目前真正被广泛使用的是TCP/IP参考模型,它是以OSI参考模型作为基础设计的。Internet的高速发展使TCP/IP参考模型被所有计算机所使用。

TCP/IP协议是一个协议集,其核心为TCP协议与IP协议。TCP/IP参考模型也是一个开放模型,能适用互联网等各种网络的需要,它具有如下4个特点。

计算机网络组成Linux环境C语言设计 - 远方的星星 - 这里因为你而精彩!

图18.5  OSI模型垂直方向的结构层次

  ● TCP/IP是一种标准化的高级协议,同时提供了多种网络服务协议。

  ● 完善的网络地址分配方法,网络中每个点都具备独立的地址。

  ● 非专利技术,与操作系统及硬件结构无关。

  ● 与网络硬件无关,适合于各种网络结构。

TCP/IP参考模型有4个层次。其中应用层与OSI中的应用层对应,传输层与OSI中的传输层对应,网络层与OSI中的网络层对应,物理链路层与OSI中的物理层和数据链路层对应。TCP/IP中没有OSI中的表示层和会话层,如图18.6所示。4个层次分别是:

计算机网络组成Linux环境C语言设计 - 远方的星星 - 这里因为你而精彩!

图18.6  TCP/IP模型与OSI模型比较

1.应用层

应用层是TCP/IP参考模型的最高层,它向用户提供一些常用应用程序,如电子邮件等。应用层包括了所有的高层协议,并且总是不断有新的协议加入。应用层协议主要有:网络终端协议TELNET,用于实现互联网中的远程登录功能;文件传输协议FTP,用于实现互联网中交互式文件传输功能;简单电子邮件协议SMTP,实现互联网中电子邮件发送功能;域名服务DNS,用于实现网络设备名字到IP地址映射的网络服务;网络文件系统NFS,用于网络中不同主机间的文件系统共享。

2.传输层

传输层也称为TCP层,主要功能是负责应用进程之间的端-端通信。传输层定义了两种协议:传输控制协议TCP与用户数据包协议UDP。TCP协议是一种可靠的面向连接的协议,主要功能是保证信息无差错地传输到目的主机。UDP协议是一种不可靠的无连接协议,它与TCP协议不同的是它不进行分组顺序的检查和差错控制,而是把这些工作交给上一级应用层完成。

3.网络层

网络层又称为IP层,负责处理互联网中计算机之间的通信,向传输层提供统一的数据包。它的主要功能有以下3个方面:处理来自传输层的分组发送请求;处理接收的数据包;处理互联的路径。

4.物理链路层

物理链路层主要功能是接收IP层的IP数据包,通过网络向外发送,或接收处理从网络上来的物理帧,抽出IP数据包,向IP层发送。该层是主机与网络的实际连接层。

18.2  TCP/IP协议

TCP/IP协议(Transmission Control Protocol/Internet Protocol)是随着Internet而发展的网络协议,目前应用最为广泛。Internet最初是因为美国国防需要而建立的,用于保证美国政府的计算机网络间能够互通,并保证遭受核打击时不至于瘫痪。TCP/IP很好地解决了不同网络互访问性和网路的健全性,领导着Internet发展。几乎所有的操作系统都支持TCP/IP协议,Linux系统更是将TCP/IP协议作为重要标准,成为了世界上最流行的网络服务器操作系统。

18.2.1  IP协议与Internet

计算机网络技术在近50年的发展路程中,产生过多种不同的网络结构和通信协议,很多至今还在使用。让不同网络可相互访问的结局方案有两种:第一种是选择一种组网络结构为标准,使所有网络都按照同一方法来组建。这种方案显然没有可行性,因为不但网络重建的费用太高,而且没有一种网络结构能满足所有应用。因此,第二种方法被提出,该方法要求设计一种协议,能够让所有网络结构都能支持。TCP/IP协议由此诞生,解决网络互通问题的是IP协议。

IP协议又称为网际协议,对应于TCP/IP参考模型的网络层,是Internet中最重要的协议。IP协议规定数据包由数据包正文与报头两部分组成。数据包正文是要传递的数据,没有格式要求。报头包括发送主机的网络地址、接收主机的网络地址、数据包的报头校验和、数据包的长度等信息。

IP协议的主要功能有数据包传输、数据包路由选择和拥塞控制。数据包采用“无连接”方式传递,即两台主机在通信之前不需要建立连接。网络主机间使用统一的IP数据包,这样能保持不同物理网络间能够传递和识别数据。如果目的地为同一网段的计算机,那么数据包将被直接传输过去。如果两台主机处于Internet上的不同子网中,IP协议将通过路由器获得主机间的传输路径,通过交换机或服务器接力的方式,将数据包传递过去。

路由器是网络中选择路径的专用计算机,它以图算法为核心,负责找到两点之间最短的距离。同时也会考虑网络的拥堵状况,找到实际最快的传输路径。一些比较大的数据被拆分为数据包后,很可能是以不同的路径传递到目的地。

18.2.2  IP互联网协议地址

所有Internet上的计算机都必须有一个Internet上唯一的编号作为其在Internet的标识,这个编号称为IP地址。每个数据包中包含有发送方的IP地址和接收方的IP地址。IP地址是一个32位二进制数,即4个字节,为方便起见,通常将其表示为w.x.y.z的形式。其中,w、x、y、z分别为一个0至255的十进制整数,对应二进制表示法中的一个字节。这样的表示叫做点分十进制表示。

IP地址的取得方式,简单地说是大的组织先向Internet的NIC(Network Information Center)申请若干IP地址,然后将其向下级组织分配,下级组织再向更下一级的组织分配IP地址。各子网的网络管理员将取得的IP地址指定给子网中的各台计算机。IP地址分为         3类。

1.A类地址

A类IP地址的最高位为0,其前8位为网络地址,是在申请地址时由管理机构设定的,后24位为主机地址,可以由网络管理员分配给本机构子网的各主机。A类地址的第一个十进制整数的值在1至126之间。一个A类地址最多可容纳224(约1600万)台主机,最多可有127个A类地址。当然这是纯从数学上讲的,事实上不可能达到,因为一个网络中有些地址另有特殊用途,不能分配给具体的主机和网络。下面在B类、C类地址中的数字也是同样的。

2.B类地址

B类IP地址的前16位为网络地址,后16位为主机地址,且第一位为1,第二位为0。B类地址的第一个十进制整数的值在128至191之间。一个B类网络最多可容纳216即65536台主机,最多可有214个B类地址。

3.C类地址

C类IP地址的前24位为主机地址,最后8位为主机地址,且第一位、第二位为1,第三位为0。C类地址的第一个整数值在192至223之间。一个C类网络最多可容纳28即256台主机,共有221个C类地址。

有几个特殊的IP地址,第一个是回送地址,该地址用于网络测试或本机进程间通信,十进制形式为127.0.0.1。第二个是广播地址,用于呼叫整个网络内的计算机,子网中最后一个地址即被用作广播地址,例如16.255.255.255用于A类网络16.0.0.0中所有计算机的呼叫。第三个是子网地址,用于识别子网,子网中第一个地址即是子网地址,例如192.168.0.0。

18.2.3  TCP协议

原始的互联网使用的传输介质为电话线,计算机通过调制解调器将数值信号转为模拟信号,然后使用电流载波。因为电话线的噪声极大,很容易造成误码,因此TCP协议具有完善的循环校验机制。TCP是重要的传输层协议,必须保证数据传递的完整性。另外,数据包报文中有计算机端口号信息,可以用来区别同一计算机上不同应用程序的数据。

数据包是很小的数据单位,而通过网络传递的连续数据往往是数据包长度的很多倍。因此,数据包报文中还有一个顺序编号,使接收的计算机能够更具编号重新按顺序还原数据。TCP协议的另一个重要功能就是把大的数据切成较小的数据包,或者将接收到的数据包按顺序还原为原始数据。如果发现某一个数据包丢失了,TCP协议会向源计算机发送请求,要求重新传递丢失的数据包。这种处理能力,被称之为全双工。

TCP协议最小的处理单位为字节,因此TCP是面向字节的顺序协议。数据包内的每个字节都会被分配一个顺序编号,以及为了验证数据真实性的奇偶校验位。虽然这种做法传递了过多的冗余数据,但根本原因是由早期网络极为不可靠造成的。

为可靠的完成数据传输任务,TCP将报文或数据分成可管理的长度并加上TCP头,并定义一些主要的字段,如图18.7所示。

计算机网络组成Linux环境C语言设计 - 远方的星星 - 这里因为你而精彩!

图18.7  TCP报文结构

TCP报文中的字段定义如下。

  ● 源端口:源计算机指定的端口编号。

  ● 目的地端口:接收计算机的端口编号。

  ● 顺序号:分配给TCP包的编号。

  ● 应答号:接收计算机向源计算机发送的编号。

  ● 偏移位:指出TCP 头的长度(即TCP头中的32位字的数)。它表明数据开始和TCP头结束。对于正常的20 字节的头,这个字段设置成0101。

  ● 保留位:为将来使用而保留。必须设置为0。

  ● 控制位:用作个别控制位,如表18.1所示。

  ● 窗口号:窗口字段也称接收窗口大小,表示在TCP连接上准备由主机接收的8位字节的数目。

  ● 校验位:一个差错检验数,用于确定被接收的数据包文在传输期间是否被讹误。包括TCP头和所有数据。

  ● 紧急指针:它指出了紧接紧急数据的字节的顺序编号。

  ● 可选项:长度变量,它考虑到TCP使用的各种选项:选项表的结束、无操作、最大分段长度。

表18.1  TCP报头控制位指令

指    令

说    明

URG

紧急指示字段

ACK

如果设置,该包包含确认

PSH

启用推入功能

RST

恢复连接。用于一个功能不接收连接请求时

SYN

用于建立同步序号

FIN

数据不再从连接的发送点进入,结束总报文

TCP提供的主要服务有:

  ● 建立、维持和终结两个进程之间的连接。

  ● 可靠的包传递(经过确认过程)。

  ● 编序包(可靠的数据传送)。

  ● 控制差错的机制。

  ● 通过使用端口,允许在个别的源和目的地主机内部实现和不同进程多重连接的       能力。

  ● 使用全双工操作的数据交换。

18.2.4  UDP协议

UDP又称用户数据包文协议,也是TCP/IP的传输层协议,它是无连接的、不可靠的传输服务。当接收数据时它不向发送方提供确认信息,它不提供输入包的顺序。如果出现丢失包或重复包的情况,也不会向发送方发出差错报文,与IP协议非常类似。UDP的主要作用是分配和管理端口编号,以正确无误的识别运行在网络站点上的个别应用程序。由于它执行功能时具有低的开销,因而执行速度比TCP快。它多半用于不需要可靠传输的应用程序,例如网络管理域、域名服务器等。UDP协议的报文结构如图18.8所示。

任何与UDP相配合作为传输层服务的应用程序必须提供确认和顺序系统,以确保数据包是以发送时的顺序到达。也就是说,使用UDP的应用程序必须提供这类服务。传输层具有独特的、与所有其他层不相关的帧头。UDP报头及其数据被封装在IP报头内,由IP协议将这个数据包文发送到数据链路层,依次下去,数据链路层又使用它的帧头包装这个报文,最后将数据送到物理层实际传输。

当数据包被接时,数据链路层将把地址解释为它自己的,剥去它的帧头,将包传递给IP层。IP层将根据IP报头上的正确IP地址接受包。剥去它的报头,最后将数据包交给UDP软件,UDP软件接受包必须按UDP报头上的端口编号进行译码。

计算机网络组成Linux环境C语言设计 - 远方的星星 - 这里因为你而精彩!

图18.8  UDP报文结构

18.3  Socket套接字

Socket套接字由远景研究规划局(Advanced Research Projects Agency, ARPA)资助加里福尼亚大学伯克利分校的一个研究组研发。其目的是将TCP/IP协议相关软件移植到UNIX类系统中。设计者开发了一个接口,以便应用程序能简单地调用该接口通信。这个接口不断完善,最终形成了Socket套接字。Linux系统采用了Socket套接字,因此,Socket接口就被广泛使用,到现在已经成为事实上的标准。与套接字相关的函数被包含在头文件sys/socket.h中。

18.3.1  Socket套接字简介

Socket的英文原意是“插座”,作为类UNIX系统的进程通信机制,它如同插座一样方便的帮助计算机接入互联网通信。

任何用户在通信之前,首先要先申请一个Socket号,Socket号相当于自己的电话号码。同时要知道对方的电话号码,相当于对方有一个Socket。然后向对方拨号呼叫,相当于发出连接请求(假如对方不在同一区内,还要拨对方区号,相当于给出网络地址)。对方假如在场并空闲(相当于通信的另一主机开机且可以接受连接请求),拿起电话话筒,双方就可以正式通话,相当于连接成功。双方通话的过程,是向电话机发出信号和从电话机接受信号的过程,相当于Socket发送数据和从Socket接受数据。通话结束后,一方挂起电话机,相当于关闭Socket,撤销连接。

由此可见,Socket的通信机制与电话交换机制非常相似。Socket实质上提供了进程通信的端点。进程通信之前,双方首先必须各自创建一个端点,否则是没有办法建立联系并相互通信的。每一个Socket都用一个半相关描述。

{协议,本地地址,本地端口}。

一个完整的Socket则用一个相关描述:

{协议,本地地址,本地端口,远程地址,远程端口}。

每一个Socket有一个本地的唯一Socket号,由操作系统分配。套接字有3种类型:流式套接字(SOCK_STREAM)、数据包套接字(SOCK_DGRAM)和原始套接字。流式套接字可以提供可靠的、面向连接的通信流。如果通过流式套接字发送了顺序的数据:1、2。那么数据到达远程时候的顺序也是1、2。流式套接字可用于Telnet远程连接、WWW服务等需要使数据顺序传递的应用,它使用TCP协议保证数据传输的可靠性。流式套接字的工作原理如图18.9所示,我们将网络中的两台主机分别作为服务器和客户机看待。

数据包套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证可靠性。数据包套接字使用者数据包协议UDP,数据只是简单地传送到对方。数据包套接字的工作原理如图18.10所示。

计算机网络组成Linux环境C语言设计 - 远方的星星 - 这里因为你而精彩! 

图18.9  流式套接字的工作原理

   计算机网络组成Linux环境C语言设计 - 远方的星星 - 这里因为你而精彩!

                                  图18.10  数据套接字的工作原理

原始套接字允许对低层协议如IP或ICMP直接访问,主要用于新的网络协议实现的测试等。原始套接字主要用于一些协议的开发,可以进行比较底层的操作。它功能强大,但是没有上面介绍的两种套接字使用方便,一般的程序也涉及不到原始套接字。

18.3.2  创建套接字

套接字是通过标准的UNIX文件描述符和其他的程序通信的一个方法。套接字在使用前必须先被建立,建立套接字的系统调用为socket(),它的一般形式是:

int socket(int domain, int type, int protocol);

创建出来的套接字是一条通信线路的一个端点,domain参数负责指定地址族,type参数负责指定与这个套接字一起使用的通信类型,而protocol参数负责制定所使用的协议。domain参数的取值范围如表18.2所示。

表18.2  domain参数的取值范围

参    数

说    明

AF_UNIX

UNIX内部(文件系统套接字)

AF_INET

ARPA因特网协议(UNIX网络套接字)

AF_ISO

ISO标准协议

AF_NS

施乐网络系统协议

AF_IPX

NOVELL IPX协议

AF_APPLETALK

Appletalk DDS

最常用的套接字域是AF_UNIX和AF_INET,前者用于通过UNIX文件系统实现的本地套接字,后者用于UNIX网络套接字。AF_INET套接字可以用在穿过包括Internet在内的各种TCP/IP网络而进行通信的应用程序中。

套接字参数type指定了与新套接字对应的通信特性。它的取值范围为枚举常量SOCK_STREAM和SOCK_DGRAM。SOCK_STREAM是一个有序的、可靠的、基于连接的双向字节流。对于一个AF_INET域的套接字来说,如果在恋歌流式套接字的两端之间建立的是一个TCP连接,连接时默认值即为该特性。SOCK_DGRAM是一个数据图服务,可以用来发送最大长度是一个固定值的消息,但消息是否会被送达或者消息的先后次序是否会在网络传输中被重新安排并没有保证。对于AF_INET域的套接字来说,这种类型的通信是由UDP提供的。

通信所用的协议通常是由套接字的类型和套接字的域来决定,如果还有其他的协议可以选择,那么就在protocol参数里设置。protocol参数默认值为0,表示使用默认的协议。

socket系统调用返回的是一个描述符,它与文件描述符非常相似。当这个套接字和通信线路另一端的套接字连接好以后,就可以进行数据的传输和接收操作了。

18.3.3  套接字地址

每个套接字域都有独特的地址格式。对于一个AF_UNIX套接字来说,它的地址是由一个包含在sys/un.h头文件里的sockaddr_un结构描述的。该结构的定义为:

struct sockaddr_un {

   sa_family_t sun_family;      // AF_UNIX

   char sun_path[];             // 路径

};

因为不同类型的地址都需要传递到对套接字进程处理的系统调用里去,所以定义各种地址格式时使用的结构也都很相似,每个结构的开始都是一个定义地址类型(即套接字域)的数据项。sun_family_t是由X/Open技术规范定义的,在Linux系统上,它被声明为一个short类型。sun_path给出的路径长度是有限制的,Linux规定其最长不能超过108个字符。因为地址结构在长度方面是不固定的,所以许多套接字调用都要用到或输出一个用来复制特定地址结构的长度值。

AF_INET域里的套接字地址是由一个定义在netinet/in.h头文件里的sockaddr_in结构确定的。该结构的定义为:

struct sockaddr_in {

   short int sin_family;             // AF_INET

   unsigned short int sin_port;     // 端口号

   struct in_addr sin_addr;     // Internet地址

};

其中Internet地址是netinet/in.h头文件中定义的另一个结构体,该结构体的定义为:

struct in_addr {

   unsigned long int s_addr;

};

一个AF_INET套接字完全可以由它的域、IP地址和端口号确定下来。从应用程序的角度看,各种套接字的行为就像是文件描述符,用一个独一无二的整数就可以把它们表示出来。

18.3.4  套接字的名字

要使socket()调用创建的套接字能够被其他进程使用,程序就必须给该套接字起个名字。AF_UNIX套接字会关联到一个文件系统的路径名上去,AF_INET套接字将关联到一个IP端口号上去。为套接字命名可使用bind()系统调用,它的一般形式如下:

int bind(int socket, const struct sockaddr *address, size_t address_len);

bind()系统调用的作用是把参数address中给出的地址赋值给与文件描述符socket相关联的未命名套接字。地址结构的长度是通过address_len参数传递的。地址的长度和类型取决于地址族。bind()调用需要用一个与之对应的地址结构指针指向真正的地址类型。该调用成功时将返回0,否则返回–1,并将errno变量设置为表18.3中的值。

表18.3  bind()系统调用返回的错误代码

代    码

说    明

EBADF

文件描述符无效

ENOTSOCK

该文件描述符代表的不是一个套接字

EINVAL

该文件描述符是一个已命名套接字

EADDRNOTAVAIL

地址不可用

EADDRINUSE

该地址已经绑定一个套接字

AF_UNIX套接字对应的错误代码比上表要多出两个,分别是EACCESS,表示权限不足,不能创建文件系统中使用的名字;ENOTDIR/ENAMETOOLONG,表示路径错误或路径名太长。

18.3.5  创建套接字队列

为了能够在套接字上接受接入的连接,服务器程序必须创建一个队列来保存到达的请求。创建队列可使用系统调用listen()完成,它的一般形式为:

int listen(int socket, int backlog);

Linux系统可能会对队列里能够容纳的排队连接的最大个数有限制。在这个最大值的范围内,listen()将把队列长度设置为backlog个连接。在套接字上排队的接入连接个数最多不能超过这个数字,再往后的连接将被拒绝,用户的连接请求将会失败。这是listen()提供的一个机制,在服务器程序紧张地处理着上一个客户的时候,后来的连接将被放到队列里排队等号。backlog常用的值是5。

listen()函数成功时会返回0,否则返回–1,它的错误代码包括EBADF、EINVAL和ENOTSOCK,含义同bind()系统调用的错误代码相同。

18.3.6  接受连接

服务器上的应用程序创建好命名套接字之后,就可以通过accept()系统调用来等待客户端程序建立对该套接字的连接了。accept()的一般形式是:

int accept(int socket, struct sockaddr *address, size_t *address_len);

accept()系统调用会等到有客户程序试图连接到由socket参数指定的套接字时才返回。该客户就是套接字队列里排在第一位的连接。accept()函数将创建出一个新的套接字来与该客户进行通信,返回的是与之对应的文件描述符。新套接字的类型与服务器监听套接字的类型相同。

套接字必须是被bind()调用命名过的,并且还要有一个由listen()调用分配的连接队列。客户的地址将被放在address指向的sockaddr结构里。如果不关心客户的地址,可以在这里使用一个空指针。

参数address_len给出了客户结构的长度。如果客户地址的长度超过了这个值,就会被截短。在调用accept()之前,必须把address_len设置为预期的地址长度。当这个调用返回时,address_len将被设置为客户的地址结构的实际长度。

如果套接字队列里没有排队等候的连接,accept将阻塞程序,直到有客户建立连接为止。这个行为可以用O_NONBLOCK标志改变,方法是对这个套接字文件描述符调用fcntl()函数。代码如下:

int flags = fcntl(socket, F_GETFL, 0);

fcntl(socket, F_SETFL, O_NONBLOCK|flags);

如果有排队等候的客户连接,accept()函数将返回一个新的套接字文件描述符,否则它将返回–1。其错误原因除类似于bind()调用和listen()调用中的情况之外,还有一个EWOULDBLOCK,如果前面指定了O_NONBLOCK标志,但队列里没有排队的连接,就会出现这个错误。如果进程阻塞在accept()调用里的时候执行被中断了,就会出现EINTR错误。

18.3.7  请求连接

当客户想要连接到服务器的时候,它会尝试在一个未命名套接字和服务器的监听套接字之间建立一个连接。它们用connect()系统调用来完成这一工作,它的一般形式是:

int connect(int socket, const struct sockaddr *address, size_t address_len);

参数socket指定的套接字将连接到参数address指定的服务器套接字上去,服务器套接字的长度由参数address_len指定。套接字必须是通过socket调用获得的一个有效的文件描述符。如果操作成功,函数返回0,否则返回–1。该函数产生的错误代码如表18.4所示。

表18.4  connect()系统调用返回的错误代码

代    码

说    明

EBADF

文件描述符无效

EALREADY

套接字上已经有了一个正在使用的连接

ETIMEDOUT

连接超时

ECONNREFUSED

连接请求被服务器拒绝

如果连接不能立刻建立起来,connect()会阻塞一段不确定的倒计时时间,这段倒计时时间结束后,这次连接就会失败。如果connect()调用是被一个信号中断的,而这个信号又得到了处理,connect还是会失败,但这次连接尝试是成功的,它会以异步方式继续尝试。

类似于accept()调用,connect()的阻塞特性可以用设置该文件描述符的O_NONBLOCK标志的办法来改变。在这种情况下,如果连接不能立刻建立起来,connect()会失败并把errno变量设置为EINPROGRESS,而连接将以异步方式继续尝试。

异步连接的处理是比较困难的,而我们可以在套接字文件描述符上用一个select()调用来表明该套接字已经处于写就绪状态。

18.3.8  关闭连接

系统调用close()函数可以结束服务器和客户上的套接字连接,就像对底层文件描述符进行操作一样。要想关闭套接字,就必须把服务器和客户两头都关掉才行。对服务器来说,应该在read()返回0时进行该操作,但如果套接字是一个面向连接的类型并且设置了SOCK_LINGER选项,close()调用会在该套接字尚有未传输数据时阻塞。

18.3.9  套接字通信

本节将设计两个例子演示套接字通信的过程,其中一个为服务器程序,另一个为客户程序。

1.服务器程序

服务器程序的代码如下:

#include <sys/types.h>

#include <sys/socket.h>                         // 包含套接字函数库

#include <stdio.h>

#include <netinet/in.h>                         // 包含AF_INET相关结构

#include <arpa/inet.h>                          // 包含AF_INET相关操作的函数

#include <unistd.h>

int main()

{

   int server_sockfd, client_sockfd; // 用于保存服务器和客户套接字标识符

   int server_len, client_len;          // 用于保存服务器和客户消息长度

   struct sockaddr_in server_address;   // 定义服务器套接字地址

   struct sockaddr_in client_address;   // 定义客户套接字地址

   server_sockfd = socket(AF_INET, SOCK_STREAM, 0); // 定义套接字类型

   server_address.sin_family = AF_INET; // 定义套接字地址中的域

   server_address.sin_addr.s_addr = inet_addr("127.0.0.1");

                                        // 定义套接字地址

   server_address.sin_port = 9734;      // 定义套接字端口

   server_len = sizeof(server_address);

   bind(server_sockfd, (struct sockaddr *) &server_address, server_len); // 定义套接字名字

   listen(server_sockfd, 5);         // 创建套接字队列

   while (1) {

      char ch;

      printf("服务器等待消息\n");

      client_len = sizeof(client_address);

      client_sockfd = accept(server_sockfd,          // 接收连接

            (struct sockaddr *) &client_address,

            (socklen_t *__restrict) &client_len);

      read(client_sockfd, &ch, 1);                   // 读取客户消息

      ch++;

      write(client_sockfd, &ch, 1);                  // 向客户传送消息

      close(client_sockfd);                          // 关闭连接

   }

}

该程序监听本地的9734端口,程序运行后等待客户通过该端口连接,从客户传送的消息里读取一个字符,然后对该字符进行加1操作后,再传送给客户,并关闭该连接。

2.客户程序

客户程序的源代码如下:

#include <sys/types.h>

#include <sys/socket.h>                     // 包含套接字函数库

#include <stdio.h>

#include <netinet/in.h>                     // 包含AF_INET相关结构

#include <arpa/inet.h>                      // 包含AF_INET相关操作的函数

#include <unistd.h>

int main() {

   int sockfd;                              // 用于保存客户套接字标识符

   int len;                                 // 用于保存客户消息长度

   struct sockaddr_in address;              // 定义客户套接字地址

   int result;

   char ch = 'A';                           // 定义要传送的消息

   sockfd = socket(AF_INET,SOCK_STREAM, 0); // 定义套接字类型

   address.sin_family = AF_INET;         // 定义套接字地址中的域

   address.sin_addr.s_addr = inet_addr("127.0.0.1"); // 定义套接字地址

   address.sin_port = 9734;                             // 定义套接字端口

   len = sizeof(address);

   result = connect(sockfd, (struct sockaddr *) &address, len); 

                                                        // 请求连接

   if (result == -1) {

      perror("连接失败");

      return 1;

   }

   write(sockfd, &ch, 1);                               // 向服务器传送消息

   read(sockfd, &ch, 1);                             // 从服务器接收消息

   printf("来自服务器的消息是%c\n", ch);

   close(sockfd);                                       // 关闭连接

   return 0;

}

客户端程序向本地的9734端口请求连接,如果连接成功即发送一个字符A作为消息。然后从服务器传送的消息中读取一个字符,并将该字符输出,退出程序。

将这两个程序分别编译,然后打开两个终端,第一个终端运行服务器程序,这时会出现提示符“服务器等待消息”。第二个终端运行客户程序,客户程序会将字符A作为消息传送给服务器程序,然后服务器程序对该字符进行加1处理,传送回客户程序。客户程序的输出是“来自服务器的消息是B”。这样两个程序就完成了连接和通信。结束客户端程序可使用组合键Ctrl + C。

18.4  网 络 通 信

在18.3节的例子中,实现了在同一台计算机中通过套接字通信的方法。如果是在网络中,需要使客户端连接的地址为一个有效的IP地址,这样就能在两台计算机之间通信。除了IP地址以外,计算机名也可以用来代表一台网络中的计算机,例如在浏览器中使用的域名就是Internet中由DNS服务所提供的网络地址机制。

18.4.1  查询主机名称

查询主机名称是通过访问主机数据库实现的,服务器数据库接口函数在头文件netdb.h中定义。与此相关的函数有sethostbyaddr()和gethostbyname()两个,它们的一般形式如下:

struct hostent *gethostbyaddr(const void *addr, size_t len, int type);

struct hostent *gethostbyname(const char *name);

函数的返回值是指向hostent结构的指针,该结构用于保存主机名称等信息,hostent结构的定义如下:

struct hostent {

   char *h_name;                     // 主机名

   char **h_aliases;                 // 别名列表

   int h_addrtype;                  // 地址类型

   int h_length;                 // 地址的字节长度

   char **h_addr_list             // 地址列表

};

gethostbyaddr()是通过IP地址查询主机信息,gethostbyname()是通过主机名查询主机信息。如果在主机数据库中没有查到相关主机或地址的项,这些函数会返回一个空指针。

与服务及其关联的端口号有关的信息可以通过getservbyname()函数和getservbyport()函数查询,它们的一般形式如下:

struct servent *getservbyname(const char *name, const char *proto);

struct servent *getservbyport(int port, const char *proto);

其中,proto参数指定了用来连接到该项服务的协议,SOCK_STREAM类型的TCP连接对应的是tcp,UDP连接对应的是udp。函数的返回值是servent结构指针,该结构的定义如下:

struct servent {

   char *s_name;             // 服务名

   char **s_aliases;         // 服务别名列表

   int s_port;              // 端口号

   char *s_proto;            // 协议类型

};

如果需要将地址信息转换为四分十进制法表示,可使用inet_ntoa()函数来完成。该函数被包含在头文件“arpa/inet.h”中,它的一般形式是:

char *inet_ntoa(struct in_addr in);

如果执行成功,它将返回一个指向四分十进制法表示地址的字符串的指针,否则返回–1。查询当前主机的主机名的函数是gethostname(),该函数的一般形式是:

int gethostname(char *name, int namelength);

如果执行成功,*name参数所指向的内存空间将被写入主机名,namelength参数限定了*name参数所指向内存空间的长度。如果主机名太长,会被截短到namelength限定的长度。函数执行成功时返回0,否则返回–1。下面用一个示例说明查询主机名称操作的方法:

#include <sys/socket.h>                     // 包含套接字相关函数

#include <netinet/in.h>                     // 包含AF_INET相关结构

#include <netdb.h>                          // 包含读取主机信息的相关函数

#include <stdio.h>

#include <unistd.h>

int main(int argc, char *argv[])

{

   char *host;                               // 用于保存主机名

   int sockfd;                              // 用于保存套接字标识符

   int len, result;

   struct sockaddr_in address;              // 定义套接字地址

   struct hostent *hostinfo;             // 定义主机信息结构

   struct servent *servinfo;             // 定义服务信息结构

   char buffer[128];

   if (argc == 1)

      host = "localhost";                    // 如果没有指定主机名,则置为本机

   else

      host = argv[1];

   hostinfo = gethostbyname(host);          // 获得主机信息

   if (!hostinfo) {

      fprintf(stderr, "找不到主机: %s\n", host);

      return 1;

   }

   servinfo = getservbyname("daytime", "tcp");      // 获得服务信息

   if (!servinfo) {

      fprintf(stderr, "无daytime服务\n");

      return 1;

   }

   printf("daytime服务端口是:%d\n", ntohs(servinfo -> s_port)); 

                                                    // 输出端口信息

   sockfd = socket(AF_INET, SOCK_STREAM, 0);     // 建立套接字

   address.sin_family = AF_INET;                 // 定义套接字地址中的域

   address.sin_port = servinfo -> s_port;           // 定义套接字端口

   address.sin_addr = *(struct in_addr *) *hostinfo -> h_addr_list;

                                                    // 定义套接字地址

   len = sizeof(address);

   result = connect(sockfd, (struct sockaddr *) &address, len);

                                                    // 请求连接

   if (result == -1) {

      perror("获得数据出错");

      return 1;

   }

   result = read(sockfd, buffer, sizeof(buffer));   // 接收数据

   buffer[result] = '\0';

   printf("读取%d字节:%s", result, buffer);         // 输出数据

   close(sockfd);                                   // 关闭连接

   return 0;

}

运行程序时,将一个UNIX服务器地址作为该程序的运行参数。daytime服务的端口号是通过网络数据库函数getserverbyname()确定的,这个函数返回的是关于网络服务方面的资料,它们和主机资料差不多。程序会先尝试连接指定主机信息数据库里的地址,如果成功就读取daytime服务返回的信息,该信息是一个表示UNIX时间和日期的字符串。如果测试平台是Linux桌面操作系统,修改“/etc/xinetd.d/daytime”文件,将此文件中两个disable的值由yes改为no,再重启计算机即可运行daytime服务。

18.4.2  Internet守护进程

提供多项网络服务的Linux系统通常是以超级服务器的方式来运行的,由Internet守护进程inetd同时监听着许多端口地址上的连接。当有客户连接到某项服务时,inetd程序就会运行相应的服务器程序。这使服务器程序不必一直运行着;它们可以在必要时由inetd启动执行。下面是inetd配置文件“/etc/inetd.conf”中的一个片段,这个文件的作用是决定需要运行哪些个服务器。

#

# <service_name> <sock_type> <proto> <flags> <user> <server_path> <args>

#

# Echo, discard, daytime, and chargen are used primarily for testing.

#

daytime stream tcp nowait root internal

daytime dgram udp wait root internal

#

# These are standard services.

#

ftp stream tcp nowait root /usr/sbin/tcpd /usr/sbin/wu.ftpd

telnet stream tcp nowait root /usr/sbin/tcpd /usr/sbin/in.telnetd

#

# End of inetd.conf.

连接daytime服务的操作实际是由inetd本身负责处理的,SOCK_STREAM(tcp)套接字和SOCK_DGRAM(udp)套接字都能使用这项服务。文件传输服务ftp只能通过SOCK_STREAM套接字提供,并且是由一个外部程序提供的。通过编辑该文件并将服务与某一程序相联系,就可以改变通过inetd提供的服务。

18.5  小    结

本章介绍了计算机网络的相关概念,以及使用套接字进行网络间通信的方法。套接字通信的方法适用于设计较底层的通信协议设计,其他的各种网络协议如FTP、HTTP等都是在套接字的基础上建立的。如果需要进行网络间多点的连接,可使用多进程或多线程的编程方法,在每个进程或线程中建立一个套接字连接,这样就能保证多个客户同时连接到服务器。全面、系统、深入探讨Linux环境C程序设计的核心技术与思想。高屋建瓴,采用较高难度的复杂案例展示开源软件设计的思想。《本文章内容是抄自IBM》

  评论这张
 
阅读(34)| 评论(0)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2018