1.64k likes | 1.76k Vues
第 2 章 基于 TCP 套接字的编程. 2.1 概述 2.2 套接字和套接字地址 2.3 基本套接字函数 2.4 高级套接字函数 2.5 多路复用 2.6 网络字节传输顺序及主机字节顺序 2.7 DNS 与域名访问 2.8 基于 IP 和域名的通信编程 2.9 基于 TCP 套接字编程示例 习题. 2.1 概述.
E N D
第2章 基于TCP套接字的编程 • 2.1 概述 • 2.2 套接字和套接字地址 • 2.3 基本套接字函数 • 2.4 高级套接字函数 • 2.5 多路复用 • 2.6 网络字节传输顺序及主机字节顺序 • 2.7 DNS与域名访问 • 2.8 基于IP和域名的通信编程 • 2.9 基于TCP套接字编程示例 • 习题
2.1 概述 • 20世纪80年代早期,美国国防高级研究计划局(ARPA,Advanced Research Projects Agency)资助了加利福尼亚大学伯克利分校一个研究组,将TCP/IP 软件移植到UNIX操作系统中,并将结果提供给其他网点。作为项目的一部分,设计者们希望像访问文件一样去访问网络,因此,创建了一个接口,应用进程使用这个接口可以方便的进行通信。为了支持TCP/IP功能增加的新系统调用接口,形成了Berkeley Socket,这个系统被称为Berkeley UNIX或BSD UNIX(TCP/IP首次出现在BSD 4.1版本(release 4.1 of Berkeley Software Distribution))。由于许多计算机厂商都采用了Berkeley UNIX,Socket得到了迅速普及并被广泛使用。
在UNIX系统中,网络应用编程界面有两类:UNIX BSD的套接字(socket)和UNIX System V的TLI。由于Sun公司采用了支持TCP/IP的UNIX BSD操作系统,使TCP/IP的应用有更大的发展,其网络应用编程接口──套接字在网络软件中也被广泛应用,至今已引进到Linux和Windows系统中,成为开发网络应用软件的强有力工具,本章将详细讨论套接字的使用。在UNIX系统中,网络应用编程界面有两类:UNIX BSD的套接字(socket)和UNIX System V的TLI。由于Sun公司采用了支持TCP/IP的UNIX BSD操作系统,使TCP/IP的应用有更大的发展,其网络应用编程接口──套接字在网络软件中也被广泛应用,至今已引进到Linux和Windows系统中,成为开发网络应用软件的强有力工具,本章将详细讨论套接字的使用。 • Linux产生时,UNIX系统的网络功能已经相当成熟了,Linux网络的开发者选择了重新开发网络功能。在Linux网络代码开发的过程中,很多程序员做出了贡献。它提供的套接字有三种类型:流式套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM)及原始套接字(SOCK_RAW)。下面将详细介绍这些套接字的定义与应用。
2.2 套接字和套接字地址 • 2.2.1 套接字 • 套接字是两个通信通道上的端节点。套接字函数可以用来产生通信信道,通过信道两个应用程序间可以传送数据。图2-1显示了利用套接字进行通信的示例。
图2-1 套接字 (a) 套接字作为网络传输端点;(b) 套接字对多种协议的支持
套接字是信道的末端,当应用程序产生一个套接字后,套接字函数就返回所用文件的描述符。在这里,可以把支持虚电路服务的信道看做电话线,套接字就像一个电话。同样,可以把提供数据报服务的信道看做邮局系统,套接字看做信箱,人们可以向邮箱投递信件,信件通过邮局系统到达另一个信箱。应用程序利用套接字发数据报,数据报通过信道传向另一个套接字。在产生信道时,用户可以指定所用的传输提供者。例如,可以用TCP、UDP、XNS作传输提供者。套接字是信道的末端,当应用程序产生一个套接字后,套接字函数就返回所用文件的描述符。在这里,可以把支持虚电路服务的信道看做电话线,套接字就像一个电话。同样,可以把提供数据报服务的信道看做邮局系统,套接字看做信箱,人们可以向邮箱投递信件,信件通过邮局系统到达另一个信箱。应用程序利用套接字发数据报,数据报通过信道传向另一个套接字。在产生信道时,用户可以指定所用的传输提供者。例如,可以用TCP、UDP、XNS作传输提供者。
Linux支持多种类型的套接字,也叫做套接字寻址簇,这是因为每种类型的套接字都有自己的寻址方法。Linux支持以下的套接字类型: Linux支持多种类型的套接字,也叫做套接字寻址簇,这是因为每种类型的套接字都有自己的寻址方法。Linux支持以下的套接字类型: • UNIX UNIX域套接字 • INET Internet地址簇CP/IP协议支持通信 • IPX Novell IPX • APPLETALK Appletalk DDP • X25 X25 • 这些类型的套接字代表各种不同的连接服务。
Linux的BSD 套接字支持下面的几种套接字类型: • (1) 流式(stream)。这种套接字提供了可靠的双向顺序数据流连接。它可以保证数据传输中的完整性、正确性和单一性。INET寻址簇中的TCP协议支持这种类型的套接字。 • (2) 数据报(datagram)。这种类型的套接字也可以像流式套接字一样提供双向的数据传输,但它们不能保证传输的数据一定能够到达目的节点,也无法保证到达数据以正确的顺序到达以及数据的单一性、正确性。UDP协议支持这种类型的套接字。 • (3) 原始(raw)。这种类型的套接字允许进程直接存取下层的协议。 • (4) 可靠递送消息(reliable delivered messages)。这种套接字和数据报套接字一样,只能保证数据的到达。
(5) 顺序数据包(sequenced packets)。这种套接字和流式套接字相同,但是它的数据包的大小是固定的。 • (6) 数据包(packet)。这不是标准的BSD套接字类型,而是Linux中的一种扩展。它允许进程直接存取设备层的数据包。 • 套接字的特点: • (1) 套接字没有与它相连的设备文件。应用程序可以用scoket()产生套接字,指定所用的信道类型。scoket()返回与所用信道末端相适应的文件描述符。 • (2) 只要进程保存文件描述符,套接字就一直存在,直到没有进程打开文件描述符为止,套接字才被撤消。
(3) 可以产生一个套接字,也可以同时产生一对套接字。如果产生一对套接字,则操作系统会自动在它们之间建立信道。如果只产生一个套接字,则用户程序就需要用套接字函数在该套接字与其他套接字间建立信道。 • 因此,socket是一个工具,或者说是一种不可见控件,应用程序可以通过socket函数,来访问底层网络协议。
2.2.2 套接字地址 • 套接字接口利用传送提供者进行工作,不同的传送提供者有不同的地址,套接字接口允许指定任意类型的地址。 • Linux系统的套接字是一个通用的网络编程接口,它支持多种协议,每一种协议使用不同的套接字地址结构。Linux系统定义了一种通用的套接字地址结构,可以保持套接字函数调用参数的一致性。如下所示: • struct sockaddr • { • unsigned short sa_family; /*地址类型,AF_xxx */ • char sa_data[14]; /*协议地址*/ • };
其中: • sa_family:保存协议标识符。 • AF_INET:代表TCP/IP协议簇。 • sa_data:保存具体的协议地址。 • TCP/IP协议簇的套接字地址也可以采用如下结构: • #include< netinet /in.h> • #include<sys/socket.h>
struct in_addr • { • _u32 s_addr; • /* UINT类型*/ • } • struct sockaddr_in • { • short int sin_family; • /*地址类型:AF_XXX */ • unsigned short int sin_port; • /*端口号*/ • struct in_addr sin_addr; • /* Internet 地址*/ • unsigned char sin_zero[8]; • };
(3) sin_port和sin_addr必须保证以网络字节顺序传输。 • (4) IP地址作为参数传送时,注意×××addr.sin_addr与×××addr.sin_addr.s_addr之间的差别。×××addr.sin_addr形式引用的是struct sin_addr结构类型的数据,×××addr.sin_addr.s_addr形式的引用是整数类型的数据。 • (5) 由于<linux/in.h>和<linux/socket.h>是Linux特有的头文件,为了能够保持代码的可移植性,在程序中不要直接包含它们,而与平台无关的<netinet /in.h>、<sys/ socket.h>包含了它们。因此,程序中应该包含这两个头文件。
2.2.3 IP地址的使用 • 在设置sockaddr_in类型的地址时,需要进行字符串形式的IP地址和二进制形式的地址间的转换,有如下一系列的函数可以处理IP地址的转换: • #include <sys/socket.h> • #include <netinet/in.h> • #include <arpa/inet.h> • int inet_aton(const char *cp,struct in_addr *inp); • unsigned long int inet_addr(const char *cp); • char * inet_ntoa(struct in_addr in);
这几个函数将点分十进制数字形式表示的IP地址与32位的网络字节顺序的二进制形式的IP地址进行转换。例如,可以使用 inet_addr()程序把诸如“192.168.5.10”形式的IP地址转化为无符号的整型数C0A8000A。函数inet_addr和inet_aton功能相同,函数inet_addr已过时,编程时应使用函数inet_aton。也可以调用inet_ntoa()把地址转换成数字和句点的形式:这几个函数将点分十进制数字形式表示的IP地址与32位的网络字节顺序的二进制形式的IP地址进行转换。例如,可以使用 inet_addr()程序把诸如“192.168.5.10”形式的IP地址转化为无符号的整型数C0A8000A。函数inet_addr和inet_aton功能相同,函数inet_addr已过时,编程时应使用函数inet_aton。也可以调用inet_ntoa()把地址转换成数字和句点的形式: • printf("%s",inet_ntoa(ina.sin_addr)); • 这将会打印出IP地址。它返回的是一个指向字符串的指针。例如:
char *a1,*a2; • … • a1 = inet_ntoa(ina1.sin_addr); • /*这是192.92.129.1的二进制形式*/ • a2 = inet_ntoa(ina2.sin_addr); • /*这是132.241.5.10的二进制形式*/ • printf("address 1: %s\n",a1); • printf("address 2: %s\n",a2); • 输出如下: • address 1: 192.92.129.1 • address 2: 132.241.5.10
2.3 基本套接字函数 • 在各种网络编程接口中,socket脱颖而出,越来越得到大家的重视,这是因为socket规范是一套开放的、支持多种协议的网络编程接口。经过不断发展和完善,并在Intel、Microsoft、Sun、SGI、Informix、Novell等公司的全力支持下,已成为网络编程的事实上的标准。下面给出套接字函数及其使用方法。 • 1.socket() • 在利用套接字进行网络通信时,进程要做的第一件事就是调用socket(),产生一个套接字,并指明将要使用的通信协议,如TCP、UDP、XNS、SPP等。
#include <sys/types.h> • #include <sys/socket.h> • int socket(int family,int type,int protocol); • socket()返回一文件描述符,从应用的角度讲,该文件描述符是指通信信道的末端。如果调用失败,则返回-1。其中参数定义为: • ● family:表示所用的协议是协议簇中的哪一个。协议簇是有相同地址格式的一组传送提供者。例如,TCP和UDP有同样的地址格式,因此它们属于同一协议簇。family的值可以为:
AF_INET: TCP/IP协议集合。 • AF_UNIX: UNIX域协议簇,在本机的进程间通信时使用。 • AF_ISO: ISO协议簇。 • ● type:表示套接字类型: • sock_STREAM:提供虚电路服务的流套接字。 • sock_DGRAM:提供数据报服务的套接字。 • sock_RAW:原始套接字,只对Internet协议有效,可以用来直接访问IP协议。 • sock_SEQPACKET:有序分组套接字。 • sock_RDM:能可靠交付信息的数据报套接字。
● protocol:表示指定所用协议。对于大多数应用protocol都被设置为0,表示使用默认协议。但是,如果对给定的family及服务类型有多种协议可选,就须指定所用协议。例如,套接字要用TCP协议。因为TCP是TCP/IP协议集合中的一员,要用这个协议簇,应将family设为AF_INET。TCP/IP支持虚电路服务,故应将第二个参数types设为SOCK_STREAM。因为TCP是TCP/IP协议集合中惟一提供虚电路服务的传送提供者,所以protocol可以设为0。socket()调用为: • int fd; • fd=socket(AF_INET,SOCK_STREAM,0);
又例如,用UDP协议,支持数据报服务。因为UDP属于TCP/IP协议簇,是TCP/IP协议集合中仅有的提供数据报服务的传输提供者,所以,protocol设置为0。该socket()调用为:又例如,用UDP协议,支持数据报服务。因为UDP属于TCP/IP协议簇,是TCP/IP协议集合中仅有的提供数据报服务的传输提供者,所以,protocol设置为0。该socket()调用为: • int fd; • fd=socket(AF_INET,SOCK_DGRAM,0); • 例如,产生一个套接字,访问TCP/IP协议集合的低层协议,可将套接字类型设为SOCK_RAW,这样允许访问低层协议,包括IP和ICMP。要让产生的套接字直接访问IP,protcol应设为IPPROTO_RAW。socket()调用为: • int fd • fd=socket(AF_INET,SOCK_RAW,IPPROTO_RAW);
调用socket函数时,socket执行体将建立一个socket,并返回一个指向描述符表入口的socket句柄。实际上创建一个socket意味着为一个socket数据结构分配存储空间,如图2-2所示。调用socket函数时,socket执行体将建立一个socket,并返回一个指向描述符表入口的socket句柄。实际上创建一个socket意味着为一个socket数据结构分配存储空间,如图2-2所示。
表2-1给出了适应AF_INET及AF_UNIX协议簇的type及protocol的属性取值。表2-1给出了适应AF_INET及AF_UNIX协议簇的type及protocol的属性取值。
表2-1 AF_INET 及AF_UNIX协议簇的type及protocol的属性取值
2.socketpair() • socketpair()产生两个套接字,连接这两个套接字,然后返回相应的文件描述符,它也称为UNIX域套接字。其调用格式为: • #include <sgs/types.h> • #include <sys/socket.h> • int socketpair(int family,int type,int protocol,int fd_array[2]); • 函数socketpair()返回两个套接字描述符:socket[0]和socket[1],与管道pipe相似,只是socketpair()返回一对套接字描述符,而不是文件描述符。socketpair()返回的两个套接字描述符是双向的,而管道是单向的。
Family、type、protocol参数含义与socket()函数一样,但是socket()返回一个无连接套接字的文件描述符,socketpair()返回两个连接好的套接字。调用成功返回2,否则返回-1。 • family只能取值AF_UNIX,由于此系统调用仅用于UNIX支配协议,因此只有两种可用形式。一种是: • int rc,fd_array[2]; • rc=socketpair(AF_UNIX,SOCK_STREAM,0,fd_array);
socketpair()用SVR4 面向连接的传输提供者之一产生一个通信信道。另一种是: • int rc,fd_array[2]; • rc = socketpair(AF_UNIX,SOCK_DGRAM,0,fd_array); • 这里,函数socketpair()用无连接的传输提供者产生一个通信信道。 • 下面的函数可以用来生成一个域套接字间的通信信道。 • #include <sys/types.h> • #include <sys/socket.h> • /*如果成功,则返回0,否则返回-1 */ • /*用fd返回两个文件描述符*/
int fd[2]; • { • return(socketpair(AF_UNIX,SOCK_STREAM,0,fd)); • } • 3.bind() • 调用socket函数创建套接字后,存在一个名字空间,但它没有被命名。bind()将套接字地址(包括本地主机地址和本地端口地址)与所创建的套接字句柄联系起来,即将名字赋予套接字,以指定本地址中的{协议,本地地址,本地端口}。其调用格式如下: • #include <sys/types.h> • #include <sys/socket.h> • int=bind(int fd,struct sockaddr *addressp,int addrlen);
其中: • fd:由socket()函数返回的套接字描述符。 • addressp:向协议传送地址的指针,包含有关的地址信息:名称、端口和IP地址。 • addrlen:地址结构的字节数。 • bind()把传送地址与套接字连接在一起,调用成功返回0,否则返回-1。地址的格式取决于与套接字相连的信道。例如,如果套接字的family设置为AF_UNIX,那么地址取sockaddr_un结构地址。 • 服务器和客户机均可以调用函数bind绑定套接字地址,服务器往往绑定公认端口号,提供公共服务。
处理地址绑定的程序片段如下: • bzero(&servaddr,sizeof(servaddr)); • servaddr.sin_family=AF_INET; • /*地址簇*/ • servaddr.sin_port=htons(serverport); • /*端口号*/ • servaddr.sin_addr.s_addr=htonl(INADDR_ANY); • /*使用本机IP地址*/
if (bind(sockfd,(struct sockaddr*)&servaddr,sizeof(struct sockaddr)) < 0) • { • perror("bind to serverport error"); • exit(1) • }
注意,在给网络地址赋值时,程序使用了INADDR_ANY,而不是确定的IP地址。这里INADDR_ANY的含义是任何网络设备接口。对于一台只有一个IP地址的主机,它就对应于它的IP地址。但是有的主机可能有多个网络接口,即多个网卡,并拥有多个IP地址,INADDR_ANY表示来自所有网络接口的相应传输层端口的连接请求,都由这个服务器进行处理。路由器就是典型的例子。注意,在给网络地址赋值时,程序使用了INADDR_ANY,而不是确定的IP地址。这里INADDR_ANY的含义是任何网络设备接口。对于一台只有一个IP地址的主机,它就对应于它的IP地址。但是有的主机可能有多个网络接口,即多个网卡,并拥有多个IP地址,INADDR_ANY表示来自所有网络接口的相应传输层端口的连接请求,都由这个服务器进行处理。路由器就是典型的例子。 • 一般来说,绑定操作具有以下几种组合方式,见表2-2。
(1) 服务器的套接字设定公认端口号和INADDR_ANY地址。服务器的套接字设定INADDR_ANY地址表明它要接收来自任何网络设备接口的客户连接请求,这是服务器常用的地址绑定方式。 • (2) 服务器的套接字设定公认端口号和IP地址。如果服务器的套接字绑定公认端口号和具体IP地址,则表明服务器只接收来自这个IP地址的网络接口卡的客户机的连接请求。当服务器只有一个网络接口设备时,这种设置和(1)没有区别。当服务器有多个网络接口设备时,这种方法可以用来限制服务器的接收范围。
(3) 客户机指定套接字地址中的端口号。当客户机调用connect函数请求TCP连接时,系统会自动为它选择一个未用端口号,并且用本地的IP地址设置套接字地址的相应项。因此,一般来说,客户机不用指定自己套接字地址的端口号。当客户机需要使用特定端口号时,如Linux系统中的rlogin命令,应当调用bind函数绑定一个未用的保留端口号。 • (4) 客户机设置指定IP地址和连接端口号。当客户机绑定指定IP地址和连接端口号时,表示要用指定的网络设备接口和端口号进行通信。
(5) 指定客户机的IP地址。客户机使用指定的网络设备接口进行通信,由系统为其选择一个未用的端口号。这种情况在系统具有多个网络设备接口时使用。 • 4.connect() • 客户进程在用socket()产生套接字后,用connect()将该套接字与服务器套接字相连接。 • #include <sys/types.h> • #include <sys/socket.h> • int connect(int fd,struct sockaddr * addressp,int addrlen);
其中: • fa:是套接字描述符。 • addressp:套接字地址指针。 • addrlen:地址结构的字节数。 • 地址的格式取决于所用的信道。例如,如果信道的domain=AF_INET,那么用sockaddr_in结构保存地址;如果domain=AF_UNIX,那么用sockaddr_un结构保存地址。 • 如果用数据报服务,则connect()在套接字与目的地址间建立简单的联系。系统在所有通过信道发送的数据报上均放上该地址。
通常,在调用connect()前,客户应用程序应绑定套接字地址,提出连接请求的程序片段如下:通常,在调用connect()前,客户应用程序应绑定套接字地址,提出连接请求的程序片段如下: • bzero(&servaddr,sizeof(servaddr)); • servaddr.sin_family=AF_INET; • servaddr.sin_port=htons(serverport); • if (inet_aton("192.168.0.2",&servaddr. sin_addr)<0) • { • perror("inet_aton error"); • exit(1); • }
… • if (connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0) • { • perror("connect error"); • exit(1); • } • ... • 客户机一般不指定自己的端口号,而由系统为其选一个端口号,即自由端口。
5.listen() • 当服务器完成当前请求以前又发生多个服务请求时,例如,假设重复服务器处理完最近服务请求以前,客户程序试图与它相连,或者假设并行服务器在为最近服务请求建立好一个单独进程以前,客户程序请求和服务器建立连接。在这种情况下,服务器可以拒绝到来的服务请求。为了更好地处理这些问题,服务器可以使用listen()函数将所有的服务请求放在一个请求队列中排队,并尽快地处理这些请求。
socket执行体建立一个输入请求队列,将到达的服务请求保存在此队列上,直到处理完它们为止,即listen()函数不仅使socket处于被动的侦听状态,同时告诉socket执行体对此socket处理多个同时的服务请求。面向连接的服务器用该函数指明它愿意接收连接。其调用格式为: socket执行体建立一个输入请求队列,将到达的服务请求保存在此队列上,直到处理完它们为止,即listen()函数不仅使socket处于被动的侦听状态,同时告诉socket执行体对此socket处理多个同时的服务请求。面向连接的服务器用该函数指明它愿意接收连接。其调用格式为: • #include <sys/socket.h> • int listen(int fd,int qlen);
其中: • fd:套接字的文件描述符,套接字只能是SOCK_STREAM,SOCK_SEQPACKET类型。 • qlen:连接请求队列长度。 • 当一个连接请求到达时,被插入请求队列。服务器用accept()函数从队列中移走一个请求并响应这个请求。listen()函数不进入睡眠状态等待请求的到达,它只建立一等待队列,控制便返回应用程序。执行成功便返回0值,否则返回-1。 • 在后面的例程中,将看到listen()函数的使用。
6.accept() • 面向连接的服务器执行了listen()函数后,执行accept()函数等待来自某一客户进程的实际连接请求。然后进入睡眠状态,等待客户机的连接请求。当服务请求到达accept()函数监视的socket时,socket执行体将自动建立一个新的socket,并将此socket和客户进程连接起来。收到服务请求的初始socket仍具有设定的地址,因此,它可以继续接收到达的服务请求。 • 下面是accept()函数的声明: • #include <sys/types.h> • #include <sys/socket.h> • int accept(int fd,sockaddr *addressp,int *addrlen);
参数addressp 用于返回所连接的对等进程的地址,里面存储着远程连接的计算机信息(比如远程计算机的IP地址和端口)。参数addrlen是一个本地的整型数值,由协议执行体设置它的值。一般情况下,调用者把该值设置为缓冲区的大小,该调用也可设置成sockaddr 的结构大小,在系统调用返回时将此值改变成为存放在缓冲区中的实际字节数。
accept()从队列上取出第一个连接请求,建立一个与参数fd相同特性的套接字。参数fd所指套接字类型可以是SOCK_STREAM或SOCK_SEQPACKET。如果采用同步通信模式,则accept()将等待连接请求的到达。在连接请求到达时,accept()产生一个新套接字,并对其行连接,返回新套接字的文件描述符。此刻用所产生的新套接字与客户应用进行通信,而用原来的套接字接收其他连接请求。 accept()从队列上取出第一个连接请求,建立一个与参数fd相同特性的套接字。参数fd所指套接字类型可以是SOCK_STREAM或SOCK_SEQPACKET。如果采用同步通信模式,则accept()将等待连接请求的到达。在连接请求到达时,accept()产生一个新套接字,并对其行连接,返回新套接字的文件描述符。此刻用所产生的新套接字与客户应用进行通信,而用原来的套接字接收其他连接请求。
如果调用accept()时队列上无连接请求,那么此调用在一个请求到达前阻塞调用者的运行,accept()返回-1,errno置为EWOULDBLOCK,指出无连接请求到达。如果调用accept()时队列上无连接请求,那么此调用在一个请求到达前阻塞调用者的运行,accept()返回-1,errno置为EWOULDBLOCK,指出无连接请求到达。 • 7.read()及write() • read()函数和write()函数是用来接收和发送数据的两个函数。read()函数用于从套接字缓冲区读取数据,write()函数用于往套接字缓冲区写数据,它们的定义分别如下: • int read(int fd,char *buf,int len); • int write(int fd,char *buf,int len);
参数fd是我们要进行读写操作的连接套接字描述符(由connect()函数或accept()函数返回),参数buf是用于存放欲接收或待发送数据的应用缓冲区,参数len指定了欲发送或者接收的数据字节数。参数fd是我们要进行读写操作的连接套接字描述符(由connect()函数或accept()函数返回),参数buf是用于存放欲接收或待发送数据的应用缓冲区,参数len指定了欲发送或者接收的数据字节数。 • 对于read()函数,调用后将从套接字接收缓冲区中读取len字节的数据,如果函数成功返回,返回值将是实际读取数据的字节数,见表2-3。