linux C语言编程保垒:
1,文件编程:
2,标准I/O库;
3,进程操作;
4,线程操作;
5,网络编程;
6,数据库编程;
5,网络编程
TCP协议
原始的互联网使用的传输介质为电话线,计算机通过调制解调器将数值信号转为模拟信号,然后使用电流载波。因为电话线的噪声极大,很容易造成误码,因此TCP协议具有完善的循环校验机制。TCP是重要的传输层协议,必须保证数据传递的完整性。另外,数据包报文中有计算机端口号信息,可以用来区别同一计算机上不同应用程序的数据。
数据包是很小的数据单位,而通过网络传递的连续数据往往是数据包长度的很多倍。因此,数据包报文中还有一个顺序编号,使接收的计算机能够更具编号重新按顺序还原数据。TCP协议的另一个重要功能就是把大的数据切成较小的数据包,或者将接收到的数据包按顺序还原为原始数据。如果发现某一个数据包丢失了,TCP协议会向源计算机发送请求,要求重新传递丢失的数据包。这种处理能力,被称之为全双工。
TCP协议最小的处理单位为字节,因此TCP是面向字节的顺序协议。数据包内的每个字节都会被分配一个顺序编号,以及为了验证数据真实性的奇偶校验位。虽然这种做法传递了过多的冗余数据,但根本原因是由早期网络极为不可靠造成的。
为可靠的完成数据传输任务,TCP将报文或数据分成可管理的长度并加上TCP头,并定义一些主要的字段,如图18.7所示。
![]() |
(点击查看大图)图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提供的主要服务有:
建立、维持和终结两个进程之间的连接。
可靠的包传递(经过确认过程)。
编序包(可靠的数据传送)。
控制差错的机制。
通过使用端口,允许在个别的源和目的地主机内部实现和不同进程多重连接的能力。
使用全双工操作的数据交换。
UDP协议
UDP又称用户数据包文协议,也是TCP/IP的传输层协议,它是无连接的、不可靠的传输服务。当接收数据时它不向发送方提供确认信息,它不提供输入包的顺序。如果出现丢失包或重复包的情况,也不会向发送方发出差错报文,与IP协议非常类似。UDP的主要作用是分配和管理端口编号,以正确无误的识别运行在网络站点上的个别应用程序。由于它执行功能时具有低的开销,因而执行速度比TCP快。它多半用于不需要可靠传输的应用程序,例如网络管理域、域名服务器等。UDP协议的报文结构如图18.8所示。
任何与UDP相配合作为传输层服务的应用程序必须提供确认和顺序系统,以确保数据包是以发送时的顺序到达。也就是说,使用UDP的应用程序必须提供这类服务。传输层具有独特的、与所有其他层不相关的帧头。UDP报头及其数据被封装在IP报头内,由IP协议将这个数据包文发送到数据链路层,依次下去,数据链路层又使用它的帧头包装这个报文,最后将数据送到物理层实际传输。
当数据包被接时,数据链路层将把地址解释为它自己的,剥去它的帧头,将包传递给IP层。IP层将根据IP报头上的正确IP地址接受包。剥去它的报头,最后将数据包交给UDP软件,UDP软件接受包必须按UDP报头上的端口编号进行译码。
![]() |
(点击查看大图)图18.8 UDP报文结构 |
Socket套接字
Socket套接字由远景研究规划局(Advanced Research Projects Agency, ARPA)资助加里福尼亚大学伯克利分校的一个研究组研发。其目的是将TCP/IP协议相关软件移植到UNIX类系统中。设计者开发了一个接口,以便应用程序能简单地调用该接口通信。这个接口不断完善,最终形成了Socket套接字。Linux系统采用了Socket套接字,因此,Socket接口就被广泛使用,到现在已经成为事实上的标准。与套接字相关的函数被包含在头文件sys/socket.h中。
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所示。
![]() |
(点击查看大图)图18.9 流式套接字的工作原理 |
![]() |
(点击查看大图)图18.10 数据套接字的工作原理 |
创建套接字
套接字是通过标准的UNIX文件描述符和其他的程序通信的一个方法。套接字在使用前必须先被建立,建立套接字的系统调用为socket(),它的一般形式是:
创建出来的套接字是一条通信线路的一个端点,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系统调用返回的是一个描述符,它与文件描述符非常相似。当这个套接字和通信线路另一端的套接字连接好以后,就可以进行数据的传输和接收操作了。
套接字地址
每个套接字域都有独特的地址格式。对于一个AF_UNIX套接字来说,它的地址是由一个包含在sys/un.h头文件里的sockaddr_un结构描述的。该结构的定义为:
因为不同类型的地址都需要传递到对套接字进程处理的系统调用里去,所以定义各种地址格式时使用的结构也都很相似,每个结构的开始都是一个定义地址类型(即套接字域)的数据项。sun_family_t是由X/Open技术规范定义的,在Linux系统上,它被声明为一个short类型。sun_path给出的路径长度是有限制的,Linux规定其最长不能超过108个字符。因为地址结构在长度方面是不固定的,所以许多套接字调用都要用到或输出一个用来复制特定地址结构的长度值。
AF_INET域里的套接字地址是由一个定义在netinet/in.h头文件里的sockaddr_in结构确定的。该结构的定义为:
其中Internet地址是netinet/in.h头文件中定义的另一个结构体,该结构体的定义为:
套接字的名字
要使socket()调用创建的套接字能够被其他进程使用,程序就必须给该套接字起个名字。AF_UNIX套接字会关联到一个文件系统的路径名上去,AF_INET套接字将关联到一个IP端口号上去。为套接字命名可使用bind()系统调用,它的一般形式如下:
bind()系统调用的作用是把参数address中给出的地址赋值给与文件描述符socket相关联的未命名套接字。地址结构的长度是通过address_len参数传递的。地址的长度和类型取决于地址族。bind()调用需要用一个与之对应的地址结构指针指向真正的地址类型。该调用成功时将返回0,否则返回-1,并将errno变量设置为表18.3中的值。
表18.3 bind()系统调用返回的错误代码
代 码 |
说 明 |
EBADF |
文件描述符无效 |
ENOTSOCK |
该文件描述符代表 的不是一个套接字 |
EINVAL |
该文件描述符是一 个已命名套接字 |
EADDRNOTAVAIL |
地址不可用 |
EADDRINUSE |
该地址已经绑定一个套接字 |
AF_UNIX套接字对应的错误代码比上表要多出两个,分别是EACCESS,表示权限不足,不能创建文件系统中使用的名字;ENOTDIR/ENAMETOOLONG,表示路径错误或路径名太长。
创建套接字队列
为了能够在套接字上接受接入的连接,服务器程序必须创建一个队列来保存到达的请求。创建队列可使用系统调用listen()完成,它的一般形式为:
Linux系统可能会对队列里能够容纳的排队连接的最大个数有限制。在这个最大值的范围内,listen()将把队列长度设置为backlog个连接。在套接字上排队的接入连接个数最多不能超过这个数字,再往后的连接将被拒绝,用户的连接请求将会失败。这是listen()提供的一个机制,在服务器程序紧张地处理着上一个客户的时候,后来的连接将被放到队列里排队等号。backlog常用的值是5。
listen()函数成功时会返回0,否则返回-1,它的错误代码包括EBADF、EINVAL和ENOTSOCK,含义同bind()系统调用的错误代码相同。
接受连接
服务器上的应用程序创建好命名套接字之后,就可以通过accept()系统调用来等待客户端程序建立对该套接字的连接了。accept()的一般形式是:
accept()系统调用会等到有客户程序试图连接到由socket参数指定的套接字时才返回。该客户就是套接字队列里排在第一位的连接。accept()函数将创建出一个新的套接字来与该客户进行通信,返回的是与之对应的文件描述符。新套接字的类型与服务器监听套接字的类型相同。
套接字必须是被bind()调用命名过的,并且还要有一个由listen()调用分配的连接队列。客户的地址将被放在address指向的sockaddr结构里。如果不关心客户的地址,可以在这里使用一个空指针。
参数address_len给出了客户结构的长度。如果客户地址的长度超过了这个值,就会被截短。在调用accept()之前,必须把address_len设置为预期的地址长度。当这个调用返回时,address_len将被设置为客户的地址结构的实际长度。
如果套接字队列里没有排队等候的连接,accept将阻塞程序,直到有客户建立连接为止。这个行为可以用O_NONBLOCK标志改变,方法是对这个套接字文件描述符调用fcntl()函数。代码如下:
请求连接
当客户想要连接到服务器的时候,它会尝试在一个未命名套接字和服务器的监听套接字之间建立一个连接。它们用connect()系统调用来完成这一工作,它的一般形式是:
参数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()调用来表明该套接字已经处于写就绪状态。
关闭连接
系统调用close()函数可以结束服务器和客户上的套接字连接,就像对底层文件描述符进行操作一样。要想关闭套接字,就必须把服务器和客户两头都关掉才行。对服务器来说,应该在read()返回0时进行该操作,但如果套接字是一个面向连接的类型并且设置了SOCK_LINGER选项,close()调用会在该套接字尚有未传输数据时阻塞。
套接字通信
本节将设计两个例子演示套接字通信的过程,其中一个为服务器程序,另一个为客户程序。
1.服务器程序
服务器程序的代码如下:
该程序监听本地的9734端口,程序运行后等待客户通过该端口连接,从客户传送的消息里读取一个字符,然后对该字符进行加1操作后,再传送给客户,并关闭该连接。
2.客户程序
客户程序的源代码如下:
客户端程序向本地的9734端口请求连接,如果连接成功即发送一个字符A作为消息。然后从服务器传送的消息中读取一个字符,并将该字符输出,退出程序。
将这两个程序分别编译,然后打开两个终端,第一个终端运行服务器程序,这时会出现提示符"服务器等待消息"。第二个终端运行客户程序,客户程序会将字符A作为消息传送给服务器程序,然后服务器程序对该字符进行加1处理,传送回客户程序。客户程序的输出是"来自服务器的消息是B"。这样两个程序就完成了连接和通信。结束客户端程序可使用组合键Ctrl + C。
网络通信
在上节的例子中,实现了在同一台计算机中通过套接字通信的方法。如果是在网络中,需要使客户端连接的地址为一个有效的IP地址,这样就能在两台计算机之间通信。除了IP地址以外,计算机名也可以用来代表一台网络中的计算机,例如在浏览器中使用的域名就是Internet中由DNS服务所提供的网络地址机制。
查询主机名称
查询主机名称是通过访问主机数据库实现的,服务器数据库接口函数在头文件netdb.h中定义。与此相关的函数有sethostbyaddr()和gethostbyname()两个,它们的一般形式如下:
函数的返回值是指向hostent结构的指针,该结构用于保存主机名称等信息,hostent结构的定义如下:
gethostbyaddr()是通过IP地址查询主机信息,gethostbyname()是通过主机名查询主机信息。如果在主机数据库中没有查到相关主机或地址的项,这些函数会返回一个空指针。
与服务及其关联的端口号有关的信息可以通过getservbyname()函数和getservbyport()函数查询,它们的一般形式如下:
其中,proto参数指定了用来连接到该项服务的协议,SOCK_STREAM类型的TCP连接对应的是tcp,UDP连接对应的是udp。函数的返回值是servent结构指针,该结构的定义如下:
如果需要将地址信息转换为四分十进制法表示,可使用inet_ntoa()函数来完成。该函数被包含在头文件"arpa/inet.h"中,它的一般形式是:
如果执行成功,它将返回一个指向四分十进制法表示地址的字符串的指针,否则返回-1。查询当前主机的主机名的函数是gethostname(),该函数的一般形式是:
如果执行成功,*name参数所指向的内存空间将被写入主机名,namelength参数限定了*name参数所指向内存空间的长度。如果主机名太长,会被截短到namelength限定的长度。函数执行成功时返回0,否则返回-1。下面用一个示例说明查询主机名称操作的方法:
运行程序时,将一个UNIX服务器地址作为该程序的运行参数。daytime服务的端口号是通过网络数据库函数getserverbyname()确定的,这个函数返回的是关于网络服务方面的资料,它们和主机资料差不多。程序会先尝试连接指定主机信息数据库里的地址,如果成功就读取daytime服务返回的信息,该信息是一个表示UNIX时间和日期的字符串。如果测试平台是Linux桌面操作系统,修改"/etc/xinetd.d/daytime"文件,将此文件中两个disable的值由yes改为no,再重启计算机即可运行daytime服务。
Internet守护进程
提供多项网络服务的Linux系统通常是以超级服务器的方式来运行的,由Internet守护进程inetd同时监听着许多端口地址上的连接。当有客户连接到某项服务时,inetd程序就会运行相应的服务器程序。这使服务器程序不必一直运行着;它们可以在必要时由inetd启动执行。下面是inetd配置文件"/etc/inetd.conf"中的一个片段,这个文件的作用是决定需要运行哪些个服务器。
连接daytime服务的操作实际是由inetd本身负责处理的,SOCK_STREAM(tcp)套接字和SOCK_DGRAM(udp)套接字都能使用这项服务。文件传输服务ftp只能通过SOCK_STREAM套接字提供,并且是由一个外部程序提供的。通过编辑该文件并将服务与某一程序相联系,就可以改变通过inetd提供的服务。