413-
套接字描述符
一个套接字是一个通信端点的抽象,就如同使用文件描述符来访问一个文件一样,应用程序使用套接字描述符来访问套接字。在Linux中实现了一套机制,使套接字的实现与文件描述符实现一样,使应用程序可以像访问文件一样访问套接字。许多用文件描述符访问文件的函数如read和write,也同样可以用于套接字访问。
在应用程序中,为了通过套接字API使用Linux内核的网络功能,首先要创建套接字。创建套接字需调用socket函数,指定用于通信的协议类型。socket函数的原型如下:
#include <sys/socket.h>
int socket(int family,int type,int protocol);
//如果套接字创建成功,函数返回套接字描述符file(socket) descriptor,否则返回错误代码
413-
family参数
调用socket函数需要向它传递3个参数:family,type,protoclol.参数family指定协议族或地址族(该参数又常以domain为参数名),描述了通信的类型。表列出了我们常用地址族(domain)。各地址族符号以AF_为前缀,代表地址族address family.每个地址族都有自己特定的地址格式。
地址族 | 描述 |
AF_INET | IPv4 Internet域 |
AF_INET6 | IPv6 Internet域 |
AF_UNIX | UNIX域 |
AF_UNSPEC | 没有特别的指定 |
在大多数系统中还定义了AF_LOCAL地址域,AF_LOCAL域是AF_UNIX域的别名,AF_UNSPEC域可以广泛地代表任何域
413-
type参数
type参数定义了套接字的类型,套接字类型定义了套接字通信的特点。表给出了Linux常用的套接字类型
套接字类型 | 描述 |
SOCK_DGRAM | 用于固定长度、无连接、不可靠信息网络传送。对于TCP\IP协议栈,在应用程序使用UDP协议通信时,使用这类套接字 |
SOCK_RAW | 与IP接口的裸套接字 |
SOCK_SEQPACKET | 固定长度、顺序传送、可靠、面向连接的消息传送套接字 |
SOCK_STREAM | 顺序传送、可靠、面向连接的字节流套接字。对于TCP/IP协议栈,在应用程序利用TCP协议通信时使用该套接字。 |
414-
protocol参数
通常将protocol参数设置为0,即对给定的域和套接字类型选择默认的协议。当同一个域和套接字类型可以支持多个网络协议时,应用程序可以使用protocol参数来指定使用什么网络协议进行通信。在AF_INET协议族中,protocol参数可以使用的值如表所示。
Protocol值 | 描述 |
IPPROTO_TCP | 传输层TCP协议 |
IPPROTO_UDP | 传输层UDP协议 |
IPPROTO_SCTP | 传输层SCTP协议 |
在AF_INET协议域中,SOCK_STREAM类套接字使用的默认协议是TCP,SOCK_DGRAM类套接字使用的默认通信协议是1UDP.
对于SOCK_DGRAM类型套接字接口,在通信时两个站点之间没有逻辑连接,在传送数据时,需要将数据地址(源地址和目标地址)也传送给通信两端进程的套接字。
数据报(datagram)是一个自包含数据包,发送数据报的网络数据包类似于给某个人发送一封信件,你可以寄出很多信件,但不能保证信件传递顺序,而且有的信件在邮寄过程中还可能丢失。每封信件的信封上都必须有收信人的地址和发信人的地址,信件之间相互独立。
数据报提供的是无连接服务,相反SOCK_STREAM类型的套接字,在两个站点交换数据时,必须在源套接字和目标套接字之间建立逻辑连接。
使用面向连接的协议进行通信就像平时打电话。首先你需要通过拨电话号码与想通信的一方建立连接,连接建立以后,通话双方可以双向地进行通信。连接是点对点形式,但通信的内容中不需要包含地址信息。因为在通信双方端点之间存在一个虚拟连接,连接自身就包含了数据的源地址和目标地址。
在用SOCK_STREAM类套接字传送数据时,应用程序本身不知道传送的数据的1边界,因为这类套接字提供的是字节流服务。即当应用程序从套接字读数据时,它返回的字节数可能与发送进程每次传送的字节数不同,但经过多次函数调用,最终应用程序可以收到全部的发送数据内容。
SOCK_SEQPACKET类套接字与SOCK_STREAM套接字类似,唯一不同的是,SOCK_SEQPACKET套接字提供的是基于消息的服务,而不是字节流服务。即从SOCK_SEQPACKET套接字收到的数据字节数与发送方写的数据长度完全一样。流控制传输协议(SCTP:Stream Control Transmission Protocol)在Internet域中提供顺序数据传送服务。
SOCK_RAW套接字提供的是直接访问网络层数据报接口的套接字(在Internet域的IP层),当使用SOCK_RAW套接字时,如果数据没有经过传输层协议(如TCP和UDP)处理,则应用程序需要自己创建协议头。使用SOCK_RAW套接字需要超级用户权限,以阻止恶意应用绕过系统安全机制传送数据包。
414-
AF_XXX与PF_XXX形式的常数
有时我们会看到socket函数的第一个参数是PF_XXX形式的常数,而不是AF_XXX形式的常数。AF_前缀代表的是地址族address family,PF_前缀代表的是协议族protocol family.最初在开发套接字API时,是为了一个协议族可以支持多个地址族,创建套接字时PF_的值代表协议族,AF_的值用于套接字的地址结构。但实际应用中一个协议族支持多个地址结构从来没有在系统中提供,在<sys/socket.h>头文件中定义了某个协议的PF_值与该协议的AF_的值相同。
套接字上的通信是双向通信,应用程序人员可以调用shutdown函数禁止在套接字上的I/O操作。
#include <sys/socket.h>
int shutdown(int socketfd,int how)
如何关闭套接字上的I/O功能,关闭方式由输入参数how给出。
- SHUT_RD:禁止从套接字上读数据
- SHUT_WR:不能在套接字上发送数据
- SHUT_RDWR:禁止在套接字上读/写数据。
做了以上操作后,程序就可以关闭套接字。为什么我们在关闭套接字前需要调用shutdown?主要有以下几个原因:
- close只会在最后一个引用套接字的进程释放了套接字后才会释放网络端点的连接。即如果某个应用程序复制了套接字,套接字就不会被释放,直到应用程序关闭了最后一个引用套接字的文件描述符。而shutdown函数可以不考虑其它文件描述符对套接字的引用,直接停止套接字的活动。
- 某些时候从某个站点的一端单方面关闭会更方便。例如,一个端点可以单方面关闭套接字的写活动,这样可以让与之通信的进程知道什么时候可以完成数据传送活动,同时自己的进程还可以继续接收其它进程传送的数据。以下为创建TCP协议套接字与UDP协议套接字的示例。
//如何创建一个使用TCP协议传送数据的套接字 #include <sys/socket.h> main() { int sd; sd=socket(AF_INET,SOCK_STREAM,0); if(sd<0) return -1; ... }
在上例中,我们用socket函数创建了一个使用Internet地址族、面向连接的、按字节流传送的协议套接字,协议类型参数为0,即是用SOCK_STREAM类型套接字的默认协议TCP协议。
//创建一个使用UDP协议传送的套接字
#include <sys/socket.h>
main()
{
int sd;
sd=socket(AF_INET,SOCK_DGRAM,0);
if(sd<0)
return -1;
...
}
416-
地址格式
在上一节介绍了如何创建和关闭套接字,应用程序要使用创建好的套接字进行数据传送和接收,大多数套接字函数都需要给出数据包发送/接收的目标地址。套接字的实现独立于网络协议,可以支持多个协议栈的API。不同网络协议族都定义了自己的地址格式,而且数据、地址在网络上传送与在主机中传送的顺序也有不同。所以在进行套接字网络编程的进一步讨论之前,先介绍如何标识我们想与之通信的进程地址。在使用套接字进行网络通信时,识别与自身进程通信的另一个进程需要一下两个部分信息。
- 主机网络地址:用于识别我们想与之通信的,连接在网络上的主机。
- 服务:就是运行在主机上的某个进程,该进程就是本进程要与之通信的另一个进程。
416-
字节顺序
如果在同一主机的进程之间进行通信,就不需要考虑字节顺序的问题。字节顺序与处理器的体系结构有关,表示的是大型数据类型在内存中的存放顺序。例如,现有一个整形数,图给出了一个32位整数的两种不同字节顺序在内存中的存放格式。
如果处理器的体系结构支持big-endian字节顺序,则高位字节地址在least significant byte(LSB)。支持little-endian字节顺序的处理器体系结构正好相反,least significant byte中包含的是低位字节数据。注意,无论CPU支持什么字节顺序,the most significant byte(MSB)总是在左端,least significant byte总是在右端。因此,如果我们分配了一个32位的整数,其值为0x05030209,无论字节顺序是什么,则最高位字节中存放的应该是5,最1低位字节中存放的应该是9.如果用一个强制类型转换的字符串指针(cp :character pointer)来寻址整数的地址,就会看到字节因为顺序不同而带来的差异。在支持little-endian字节顺序的处理器中,cp[0]指向最低位字节,其中包含的数值为9;cp[3]指向最高位字节,其中包含的数值是5;相反在big-endian字节顺序处理器中,cp[0]中包含的值为5,指向最高位字节,而cp[3]中包含的是数值9,指向最低位字节。
网络协议有自身特定的字节顺序,这样不同的计算机系统可以任意交换网络数据,而不必关心网络字节顺序,所以应用程序有时需要在以处理器字节顺序和网络字节顺序之间进行转换。
套接字库函数提供了4个常用函数,为TCP/IP应用在处理器字节顺序和网络字节顺序之间进行转换。
#include <arpa/inet.h>
//将32位的主机字节顺序转换成网络字节顺序
uint32_t htonl(uint32_t hostint32);
//将16位整数的主机字节顺序转换成网络字节顺序
uint16_t htos(uint16_t hostint16);
//将32位整数的网络字节顺序转换成主机字节顺序
uint32_t ntohl(uint32_t netint32);
//将16位整数的网络字节顺序转换成主机顺序
uint16_t noths(uint16_t netint16);
在以上函数中“h”表示“host”字节顺序,即主机字节顺序,“n“表示”network“字节顺序,即网络字节顺序。”l“表示”long“(即4字节)整数,”s“表示”short“(即2字节整数)。所以”htonl“即”host to network long“:长整型数,主机字节顺序转换成网络字节顺序。这4个函数定义在<arpa/inet.h>头文件中。
在应用程序开发中使用这些函数时不需要关心实际的值在主机中的字节顺序与在网络中的字节顺序。需要做的是,调用正确的函数将给定数据在主机字节顺序与网络字节顺序之间进行转换,对于字节顺序与TCP/IP协议栈字节顺序相同的处理器,以上4个函数通常以NULL宏定义。
417-
地址结构
一个地址标识了某个通信协议域中套接字应用程序运行的站点,不同协议域的地址格式不同。所以不同格式的地址传给套接字函数时,地址被强制转换成通用地址格式sockaddr,通用网络地址格式sockaddr结构如下:
struct sockaddr
{
sa_family_t sa_family;
char sa_data[];
};
各操作系统对以上地址格式实现时可自由地在sa_data成员中加入额外的数据成员。例如在Linux系统,以上地址结构的定义为:
struct sockaddr
{
sa_family sa_family; /*地址族*/
char sa_data[14];/*地址,长度可变*/
};
但在FreeBSD系统上,以上地址结构就定义为:
struct sockaddr
{
unsigned char sa_len; /*地址结构的总长度*/
sa_family sa_family; /*地址族*/
char sa_data[14];/*地址,长度可变*/
}
418-
IPv4使用的地址结构
在Linux中支持Internet地址的数据结构定义在<netinet/in.h>库文件中。对于IPv4 Internet域(AF_INET),套接字的地址由struct sockaddr_in数据结构描述:
#include<netinet/in.h>
struct in_addr
{
in_addr_t s_addr;/*IPv4的IP地址*/
};
struct sockaddr_in
{
sa_family_t sin_family; /*地址族*/
in_port_t sin_port; /*端口号*/
struct in_addr sin_addr; /*IPv4网络地址*/
unsigned char sin_zero[8];/*填充字节*/
};
in_port_t数据类型的定义为uint16_t,in_addr_t数据类型的定义为uint32_t.这些整形数据类型指明了这些数据类型所占的字节数,以上整数类型定义在<stdint.h>库文件中。sin_zero成员是填充字段,并不是指其所有值要设置为0.
418-
IPv6协议使用的地址结构
在AF_INET地址族中,Linux中支持的IPv6 Internet协议域(AF_INET6),套接字地址由struct sockaddr_in6数据结构描述:
struct in6_addr
{
uint8_t s6_addr[16];/*IPv6地址*/
};
struct sockaddr_in6
{
sa_family_t sin6_family; /*地址族*/
in_port sin6_port; /*端口号*/
uint32_t sin6_flowinfo; /*流量类和流控制信息*/
struct in6_addr sin6_addr;/*IPv6网络地址*/
uint32_t sin6_scope_id; /*符合范围的接口集*/
};</