编程类 第20章 Linux网络编程.docx
《编程类 第20章 Linux网络编程.docx》由会员分享,可在线阅读,更多相关《编程类 第20章 Linux网络编程.docx(82页珍藏版)》请在冰点文库上搜索。
编程类第20章Linux网络编程
第20章基本套接字编程
套接字是Linux系统下主要的网络编程接口。
套接字最初是在BSD版本的UNIX上实现的,随着采用BSDUNIX计算机厂商的日益增多,套接字被广泛认可并逐渐成为事实上的工业标准。
目前,几乎所有的操作系统都提供了对套接字的支持。
Linux系统继承了套接字编程接口,支持BSD套接字。
本章将首先介绍套接字编程的基本概念,并对套接字编程接口函数进行说明。
通过本章的学习,读者应该掌握以下内容。
❑理解套接字的基本概念,特别是对半相关/全相关、地址族/协议族、主机字节序/网络字节序等概念的理解。
❑掌握套接字的数据结构,并能够根据不同的应用环境对结构进行正确赋值。
❑掌握套接字编程接口函数,能够熟练应用接口函数进行网络程序设计。
❑了解套接字选项的作用,并能够在实际应用中合理运用。
❑熟练掌握面向连接和面向无连接的网络编程模式。
20.1套接字编程简述
套接字是位于应用层与TCP/IP协议族通信的中间软件抽象层,它逻辑上位于传输层与应用层之间,实际上由一组网络编程API组成。
套接字的英文名称是Socket,也称为插口或者套接字。
Socket数据传输是一种特殊的I/O。
与文件操作相对应,Socket也可以看作是一种文件描述符。
Socket也具有一个类似于打开文件的函数调用Socket,该函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。
套接字在网络层次中的逻辑位置如图20-1所示。
图20-1套接字在网络层次中的位置
20.1.1半相关与全相关
在网络通信模型中,一个连接一旦建立,则必然包括以下要素:
协议、本地地址、本地端口号、远端地址和远端端口号。
这里的协议是指通讯协议,如TCP协议或UDP协议等。
本地地址和本地端口号定义网络连接的本地参数,而远端地址和远端端口号定义了通讯对端的连接参数。
这样的一组要素,称其为“五元组”,又称为“全相关”。
在上述五元组中,协议、本地地址和本地端口号,这三项要素惟一地标识了网络连接的本地进程,而协议、远端地址和远端端口号则惟一地标识了网络连接的对端进程。
这三项要素又称为“三元组”。
由于三元组指定了一个完整的网络连接的半部分,所以称为“半相关”。
全相关与半相关如图20-2所示。
图20-2全相关与半相关
20.1.2地址族与协议族
Linux套接字支持多种协议族。
协议族也称为域,不同的协议族定义了不同的通信环境。
Linux支持的协议族和协议族的定义包含在头文件bits/socket.h中,本书中介绍的网络编程使用AF_INET协议族。
常见的协议族及其作用的环境如下所示。
PF_UNIX/PF_LOCAL/PF_FILE:
用于主机内进程间通信
PF_INET:
Ipv4网络通信协议,用于远程主机间通信
PF_INET6:
用于Ipv6网络通信
PF_IPX:
用于NovellIPX网络通信
PF_X25:
用于ITU-TX.25/ISO-8208网络通信
根据套接字规范的定义,一个协议族可以支持多个地址族。
但是,到目前为止的套接字实现中,全部都是一个协议族支持一个地址族。
因此,在各个版本的Linux系统中,都把协议族与地址族定义为同一个值。
在实际的编程过程中,考虑到将来的可移植性,在调用创建套接字的函数时,应该使用PF系列宏定义。
而在类似地址绑定的系统调用中,应该使用AP系列宏定义。
20.1.3面向连接与面向无连接
在套接字编程模型中,存在面向连接的服务和面向无连接的服务。
面向连接的服务类似于电话系统,如图20-3所示。
在拨打电话的过程中,首先需要拨号,电话拨通后双方进行通话,通话完毕后挂断电话。
与此相似,在面向连接的服务模式下,每一次完整的数据传输都要经过建立连接、使用连接、终止连接的过程。
无连接服务则类似于电报系统,如图20-4所示。
在发送电报的过程中,并不需要先与目的地建立连接,而是根据目标地址直接发送报文即可。
同样,在面向无连接的服务模式下,只需要知道通信对端的信息,即可直接向目标发送数据。
在上述面向连接的模型中,被叫方相当于TCP服务器,主叫方相当于客户机。
首先,被叫方处于等待呼叫状态(在TCP模型中为服务器处于listen状态);由主叫方拨号,呼叫被叫方(相当于connect);被叫方摘机,接受呼叫(相当于accept)过程,然后双方通话(相当于数据收发);最后完成通话后挂机(相当于close)。
图20-3电话通讯模型(面向连接)
图20-4电报通讯模型(面向无连接)
在上述面向无连接模型中,发报文根据收报文的地址信息直接向收报文发送报文(sendto)。
而收报方收到报文后(recvfrom),根据报文的来源向发报文返回报文(sendto),发报文则接收该响应报文(recvfrom)。
20.1.4套接字类型
在创建套接字时,除需要指定协议族外,还需要指定套接字的类型。
Linux系统支持多种套接字类型,主要包括流式套接字、数据报套接字和原始套接字。
❑流式套接字(SOCK_STREAM):
提供面向连接、可靠的全双工数据传输服务。
流式套接字通过TCP协议实现。
而TCP的差错控制机制可以保证数据无差错、无重复地发送,且按发送顺序接收。
另外,流式套接字有流量控制机制,可以有效避免数据流超限。
Telnet、FTP等应用采用的就是流式套接字。
❑数据报式套接字(SOCK_DGRAM):
提供无连接服务。
数据报套接字通过UDP协议实现。
数据发送时并不经过建立连接的过程,而是直接以数据包形式被发送。
另外,数据报套接字不能保证传输过程中的正确性,并且数据接收顺序可能与发送顺序不一致。
传输内容的正确性和数据顺序要靠应用层编码进行控制。
❑原始式套接字(SOCK_RAW):
该接口允许对较低层协议(如IP、ICMP)进行直接访问。
在某些应用中,使用原始套接字可以构建自定义头部信息的IP报文。
创建原始套接字需要超级用户权限。
20.1.5字节序
字节序是指占内存大于一个字节的类型的数据在内存中的存储顺序,按照不同的顺序可以划分为小端字节序(Littleedian)、大端字节序(Bigedian)两种,统称为主机字节序。
小端字节序指低字节数据存放在内存低地址处,高字节数据存放在内存高地址处;大端字节序是高字节数据存放在低地址处,低字节数据存放在高地址处。
字节序是与计算机硬件的种类相关的,基于Intelx86体系结构的PC机是小端字节序的,而有些平台则是大端字节序的。
如图20-5所示,展示了int型数据0x0001在大字节序主机和小字节序主机的内存中的存储映像。
图20-5小字节序和大字节序存储映像
【示例20-1】查看当前系统的字节序的过程,实现过程如示例代码20-1所示。
示例代码20-1
1#include/*头文件*/
2#include
3intIsLittleEndian()/*定义函数,判断字节序*/
4{
5unsignedshorti=1;/*定义无符号短整型变量*/
6return(1==*((char*)&i));/*取变量i的低8位,与1比较,如果相等,则为小字节序;如果不等,则为大字节序*/
7}
8intmain()/*主函数*/
9{
10if(IsLittleEndian())/*调用函数判断字节序*/
11{
12printf("LittleEndian.\n");/*输出字节序信息*/
13}
14else
15{
16printf("BigEndian.\n");/*输出字节序信息*/
17}
18return0;
19}
【运行结果】经过编译链接,在shell下运行上述程序,其结果如下所示。
20LittleEndian.
【代码解析】在本例中,通过函数IsLittleEndian判断当前主机是否采用小端字节序。
根据图20-1可以知道,如果是小端字节序,在将int数据强制转换为char时,将取内存低端的第1个字节的值,而这个字节的值为1表示当前主机的字节序为小字节序,反之则为大字节序。
源代码的各行解释如下。
❑第1~2行:
头文件信息。
❑第3~7行:
IsLittleEndian函数体。
使用强制类型转换,根据地址低端存储的数据值,判断当前主机的字节序。
❑第8~18行:
主函数。
通过调用IsLittleEndian获取主机的字节序信息。
该示例的输出信息表示当前主机的字节序为小端字节序。
20.1.6套接字连接方式
在面向连接的套接字编程模式下,可以根据应用的需要构建不同的连接方式。
主要包括短连接和长连接两种连接方式:
。
短连接方式是指在每进行一次通信报文收发交易时都需要先建立连接,然后进行数据收发,收发完毕后立即断开连接。
而长连接方式是指客户机与服务器建立好通讯连接,然后进行报文发送和接收。
报文发送与接收完毕后,连接并不断开而继续存在,以便进行下一次的数据收发,因此长连接方式可以连续进行交易报文的发送与接收。
短连接与长接连的模型分别如图20-6和图20-7所示。
图20-6短连接方式
图20-7长连接方式
短连接和长连接都有各自适合应用的场合。
与长连接相比,短连接方式的优点是方式简单,不需要过多考虑长连接方式下的网络故障异常处理,其缺点是每次通信都要有建立连接的过程,处理效率上不如长连接。
而长接连的优点是处理效率高,但是需要考虑各种网络异常的处理,程序逻辑比短连接相对要复杂。
在实际应用中,具体采用何种连接方式应视应用环境确定。
典型的并发服务器采用的是短连接方式。
如果客户机与服务器之间通信频繁,并且对传输的时间要求较高,可以考虑采用长连接方式。
20.1.7数据传输方式
连接建立完成后,在数据发送与接收过程中也存在不同的方式,主要包括同步和异步两种方式。
这里的同步与异步是指从程序的逻辑结构上看数据是同步发送或接收的。
对于同步方式来说,报文发送和接收是同步进行的,即报文发送后,发送方等待接收方处理完成并返回应答报文。
同步方式需要考虑超时问题,报文发出后发送方需要设定超时时间,超时后发送方不再继续等待,而直接返回。
对于异步方式来说,发送方只负责发送数据,不需要等待接收任何返回数据;而接收方只负责接收数据。
通常情况下,异步方式在客户端和服务器端各有两个进程专门负责数据收发。
这两个进程相互独立,互不影响。
在实际网络编程过程中,建立网络通信模型需要综合考虑连接方式和数据传输方式。
比较有价值的网络通信模型包括同步短连接、同步长连接和异步长连接等。
异步长连接的通信模型如图20-8所示。
图20-8异步长连接通信模型
从异步长连接通信模型可以看到,异步长连接包括两条连接。
通常情况下,一条连接负责发送数据,另外一条连接负责接收数据。
每个通信节点实际是由两个子进程组成。
每个子进程负责维持一条通信链路。
异步长连接是相对较为复杂的通信模型,接收和发送分别由不同的进程完成,较高的通信效率也增加了控制的复杂度。
数据的接收和发送是异步完成的,这就存在数据同步的问题。
20.2套接字数据结构
在套接字编程接口函数中,定义了若干数据结构。
这些数据结构大多为结构类型,基本上所有的套接字函数都会用到这些结构的内容。
本节将对这些结构的内容进行详细阐述,读者可以结合后续章节中对接口函数的介绍等内容加深对这些结构的理解。
20.2.1套接字地址结构
在套接字编程模式下,大多接口函数都需要用到套接字地址结构。
套接字地址结构就是包含了套接字的地址和端口等信息的结构体。
该结构在调用大多数套接字函数时是必需的。
Linux操作系统支持多种地址族的套接字,如UNIX、INET、IPX、X25等,而每种套接字又有其特有的地址结构。
在TCP/IP协议下进行网络编程使用的地址族是Internet地址族,即INET。
Internet地址族的地址结构为sockaddr_in,其中的in代表Internet地址族。
该地址结构的定义位于/usr/include/netinet/in.h,数据结构如下所示。
structsockaddr_in
{
__SOCKADDR_COMMON(sin_);
in_port_tsin_port;
structin_addrsin_addr;
/*Padtosizeof'structsockaddr'.
unsignedcharsin_zero[sizeof(structsockaddr)-
__SOCKADDR_COMMON_SIZE-
sizeof(in_port_t)-
sizeof(structin_addr)];
};
参数说明如下。
❑__SOCKADDR_COMMON(sin_):
该成员是一个宏定义,指定地址族,等价于sa_family_tsin_family。
❑sin_port:
端口号,必须为网络字节序。
❑sin_addr:
IP地址,必须为网络字节序。
该成员是一个结构变量,结构中惟一的成员是IP地址,数据类型为uint32_t。
❑sin_zero:
为与通用套接字地址结构保持大小一致而填充的数据。
在调用套接字编程时,往往需要将地址结构进行强制类型转换,转换为通用套接字地址结构进行传递参数。
为满足这一要求,需要保持两个数据结构大小的一致。
该成员内容应设置为空字符“\0”。
20.2.2通用套接字地址结构
Linux系统支持多种不同的地址族,每种地址族的结构内容是各不相同的,例如,internet地址族的地址结构是sockaddr_in,而UNIX地址族的地址结构是sockaddr_un等。
在向套接字的编程接口函数传递地址结构指针时,需要将各不相同的地址结构转换为一个通用的数据结构,这就是通用套接字地址结构。
结构在头文件bits/socket.h中定义,声明如下所示。
structsockaddr
{
__SOCKADDR_COMMON(sa_);
charsa_data[14];
};
参数说明如下。
❑__SOCKADDR_COMMON(sa_):
该成员是一个宏定义,指定地址族,等价于sa_family_tsa_family。
❑sa_data:
地址数据。
对于Internet地址族来说,就是包括sin_port、sin_addr、sin_zero在内的全部地址数据。
20.2.3主机名称数据结构
在进行网络编程时,通常需要用到主机名称(域名)与IP地址转换。
在这种情况下,就需要使用主机名称数据结构hostent,该数据结构定义了主机名与IP地址的对应关系。
在套接字编程模型中,与地址绑定相关的操作都需要使用该结构。
该结构定义包括在头文件netdb.h中,其声明如下所示。
structhostent
{
char*h_name;
char**h_aliases;
inth_addrtype;
inth_length;
char**h_addr_list;
#defineh_addrh_addr_list[0]
};
参数说明如下。
❑h_name:
主机名称。
❑h_aliases:
主机别名列表。
主机别名可能存在多个,该成员为指向别名列表的指针。
❑h_addrtype:
主机地址类型。
在Internet地址族下,该值一般为AF_INET。
❑h_length:
地址的字节长度。
❑h_addr_list:
一个以0结尾的数组,包含该主机的所有地址。
❑h_addr:
在h_addr_list中的第1个地址。
用于获取该结构的接口函数有:
gethostbyname、gethostbyaddr、gethostent等。
其中,gethostbyname函数用于把主机名映射成IP地址,而gethostbyaddr函数的作用则相反。
【示例20-2】查看当前系统的主机名称和IP地址,实现过程如示例代码20-2所示。
示例代码20-2
21#include/*头文件*/
22#include
23#include
24#include
25#include
26intmain()/*主函数*/
27{
28intn;/*循环变量*/
29structhostent*h;/*结构指针变量定义*/
30char**p;/*定义指向字符串的指针*/
31charhostname[PATH_MAX];/*定义字符串数组*/
32if(gethostname(hostname,PATH_MAX)<0)/*得到机器名*/
33{
34fprintf(stderr,"gethostnamefailed:
%s\n",strerror(errno));/*输出错误信息*/
35exit(0);
36}
37if((h=gethostbyname(hostname))==NULL)/*根据机器名解析地址*/
38{
39fprintf(stderr,"gethostbynamefailed:
%s\n",strerror(errno));/*输出错误信息*/
40exit(0);
41}
42fprintf(stderr,"Hostname:
%s\n",h->h_name);/*输出主机名*/
43for(n=0,p=h->h_aliases;*p!
=NULL;p++,n++)/*循环输出所有主机别名*/
44{
45fprintf(stderr,"Aliasname%d:
%s\n",n+1,*p);/*输出别名*/
46}
47for(n=0;nh_length/sizeof(int);n++)/*循环输出所有的IP地址*/
48{
49fprintf(stderr,"Ipaddress%d:
%s\n",n+1,inet_ntoa(*((structin_addr*)(h
50->h_addr_list[n]))));/*解析IP地址并输出*/
51}
52return0;
53}
【运行结果】经过编译链接,在shell下运行上述程序,其结果如下所示。
54Hostname:
Hubery.site
55Aliasname1:
Hubery
56Ipaddress1:
192.168.1.8
【代码解析】在本例中,首先通过系统调用gethostname得到当前主机的主机名,然后根据主机名调用gethostbyname解析/etc/hosts,得到当前主机的详细信息。
源代码的各行解释如下。
❑第1~5行:
头文件信息。
其中netdb.h中包含了gethostbyname函数的原型声明。
❑第12行:
调用gethostname函数获取当前主机的主机名,返回数据存放于局部变量hostname之中。
❑第17行:
调用gethostbyname函数获取主机名的详细信息,返回数据存放于structhostent类型的结构指针之中。
❑第23~26行:
循环输出主机的别名信息。
❑第27~31行:
循环输出主机的IP地址信息。
其中inet_ntoa函数的作用是将地址数据转换为可读性更好的点分十进制字符串。
输出信息的含义具体如下。
❑第1行:
主机名是Hubery.site。
❑第2行:
主机有一个别名Hubery。
❑第3行:
主机定义了一个IP地址192.168.1.8。
提示:
在Linux系统中,主机名称与IP地址的对照关系是在文件/etc/hosts中定义的。
系统调用gethostbyname和gethostbyaddr正是对该文件进行解析获取hostent结构的。
一般情况下hosts文件的每行为一个主机,每行由三节组成,每个节之间由空格隔开。
在Internet系统下,该文件还起到域名解析的作用。
域名解析通过需要通过DNS服务器进行,但如果在该文件中对域名进行了定义,则将不进行DNS解析,直接使用该文件中指定的IP进行访问。
20.2.4服务名称数据结构
在Linux系统中存在一个网络服务配置文件/etc/services,该文件中定义了当前主机提供的网络服务名称、对应的端口号和协议。
文件的每一行定义了一种网络服务,每一行由4节组成,节间由空格分隔。
其中,第1节指定了服务的名称,如Telnet;第2节指定了服务的端口和协议,端口与协议用“/”分隔;第3节定义了服务的别名。
这个文件的作用主要有两个:
一是为Linux超级服务所使用;二是在进行网络编程使用。
在进行服务器网络编程中,需要定义服务绑定的端口号。
如果直接将端口号写到程序中,那么一旦端口号发生变化,则需要修改程序并重新编译。
此时,可以在/etc/services文件中定义服务名称。
在编程时,只需通过相应系统调用根据服务名称获取对应的端口号。
在端口号需要变更时,只需要修改/etc/services文件即可。
Linux系统提供了对/etc/services文件进行操作的一系列函数,包括getservbyname、getservbyport等。
这些函数都需要使用服务名称数据结构,即structservent。
该结构的定义在头文件netdb.h中,声明如下所示。
structservent
{
char*s_name;
char**s_aliases;
ints_port;
char*s_proto;
};
参数说明如下。
❑s_name:
服务名称,如通常的Telnet、FTP等。
❑s_aliases:
服务的别名列表。
❑s_port:
服务的端口号,如Telnet定义在23端口,而FTP定义在21端口。
❑s_proto:
协议名称,如TCP、UDP等。
【示例20-3】查看当前系统中Telnet服务绑定的端口号。
其实现过程如示例代码20-3所示。
示例代码20-3
01#include/*头文件*/
02#include
03#include
04#include
05#include
06intmain()/*主函数*/
07{
08structservent*s;/*结构指针变量定义*/
09if((s=getservbyname("Telnet","tcp"))==NULL)/*根据服务名称得到服务结构*/
10{
11fprintf(stderr,"getservbynamefailed:
%s\n",strerror(errno));/*输出错误信息*/
12exit(0);
13}
14fprintf(stderr,"Port:
%d\n",ntohs(s->s_port));/*输出该服务所对应的端口*/
15return0;
16}
【运行结果】经过编译链接,在shell下运行上述程序,结果如下所示。
01Port:
23
【代码解析】在本例中,首先调用getse