1. 套接字描述符
套接字是通信端点的抽象。如同使用文件描述符访问文件一样,应用程序用套接字描述符访问套接字。其实在UNIX中套接字描述符就是一种文件描述符,许多处理文件描述符的函数(如read、wirte)可以用于处理套接字描述符。
套接字通信是双向的。即可以通过一个套接字接收或发送数据
1.1 socket函数
通过socket函数创建一个套接字。成功调用返回的文件描述符将是当前未为进程打开的最低编号的文件描述符。
int socket(int domain, int type, int protocol);
**参数domain(域,地址族):**指定一个通信域,确定通信的特性,包括地址格式。表示各个域的常数都以AF_开头,意为地址族。
域 | 描述 |
---|---|
AF_INET | IPv4因特网域 |
AF_INET6 | IPv6因特网域 |
AF_UNIX | UNIX域 |
AF_UPSPEC | 未指定(可以代表任何域) |
**参数type:**确定套接字类型,进一步确定通信特性。
类型 | 描述 |
---|---|
SOCK_DGRAM | 固定长度的、无连接的、不可靠的报文传递 |
SOCK_RAW | IP协议的数据包接口 |
SOCK_SEQPACKET | 固定长度的、有序的、可靠的、面向连接的报文传递 |
SOCK_STREAM | 有序的、可靠的、面向连接的字节流 |
-
SOCK_DGRAM:两个进程之间通信时不需要逻辑连接,只需要向对方所使用的套接字送出一个报文即可。不能保证传递的次序,也无法保证能成功送达。
面向无连接的 UDP协议是面向报文的有边界的报文的协议。发送方的UDP对应用程序交下来的报文,在添加首部后就向下交付给IP层。既不拆分,也不合并,而是保留这些报文的边界,因此,应用程序需要选择合适的报文大小。
-
SOCK_STREAM:在交换数据之前,本地套接字和与之通信的套接字之间建立一个逻辑连接。SOCK_STREAM提供字节流服务,所以应用程序分辨不出报文的界限。这意味着从SOCK_STREAM套接字读数据时,也许不会返回所有由发送进程所写的字节数。最终可以获得发送过来的所有数据,但也许要通过若干次函数调用才能得到。
面向连接的TCP协议属于无边界的字节流协议,用户每次调用接收发送函数接口时,不一定都能接收发送一条完整的消息,而是必须对裸字节流进行拆分、组合(与基于有边界报文的UDP协议的应用程序有很大差别)。
-
SOCK_SEQPACKET:和SOCK_STREAM套接字很类似,只是从该套接字得到的是基于报文的服务而不是字节流服务。因此从该套接字接收的数据量与对方所发送的一致。
-
SOCK_RAW:提供了一个接口,用于直接访问下面的网络层(即IP协议)。使用该接口时,应用程序负责构造自己的协议头部(因为传输层协议如TCP/UDP被绕过了)。需要超级用户权限,防止恶意应用程序绕过内建安全机制来创建报文。
**参数protocol:**协议类型。通常为0,因为一般情况下有了前两个参数就可以创建套接字了,操作系统会自动推演出协议类型,这时第三个参数设为0即可。
当同一域和套接字类型支持多个协议时,可以通过protocol参数选择一个特定协议。在AF_INET通信域中,套接字类型SOCK_STREAM的默认协议是TCP;在AF_INET通信域,套接字类型SOCK_DGRAM的默认协议是UDP。
1.2 以套接字描述符作为参数的函数行为
socket函数与调用open类似,都可获得用于I/O的文件描述符。当不再需要该文件描述符时,调用close来关闭对该套接字的访问,并且释放该描述符以便重新使用。但是注意,只有最后一个引用该套接字的描述符被close后,才真正释放该套接字。
虽然套接字描述符本质上是一个文件描述符,但不是所有参数为文件描述符的函数都可以接受套接字描述符。有些使用文件描述符作为参数的函数被套接字描述符调用时,函数的行为会有所不同。下图中"未指定和由实现定义"通常意味着该函数对套接字描述符无效。如lseek就不能以套接字描述符作为参数,因为套接字不支持文件偏移量概念
1.3 shutdown函数
套接字通信是双向的。即可以通过一个套接字接收或发送数据
可以通过shutdown函数禁止一个套接字的I/O。这里与close区别开来,shutdown只是使一个套接字处于不活跃的状态,而非关闭它。且shutdown是作用于套接字的,与引用该套接字的描述符数量无关,不像close那样:close最后一个引用该套接字的描述符才会真正关闭该套接字。
int shutdown(int sockfd, int how);
how参数:该函数具体行为
- SHUT_WR:关闭写端,无法使用套接字发送数据
- SHUT_RD:关闭读端,无法使用套接字读数据
- SHUT_RDWR:即无法读数据,又无法发送数据
2. 寻址
通过两个部分标识一个TCP/IP网络上的通信进程:
- 计算机IP地址。用于标识网络上我们想与之通信的计算机
- 端口号。标识该计算机上特定进程。
2.1 字节序
字节序是处理器架构特性,用于指示像整数这样的大数据类型内部的字节如何排序。处理器架构要么支持大端字节序,要么支持小端字节序。
大端法和小端法:
网络字节序:
网络字节顺序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释,网络字节顺序采用大端排序方式。(即取高字节的数据存放在低地址)。需要区别的是很多机器是小端的,如X86处理器。
对于TCP/IP应用程序,有4个用来在处理器字节序和网络字节序之间转换的函数
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
其中h
表示主机字节序,n
表示网络字节序,l
表示长整数,s
表示短整数。
2.2 地址格式
2.2.1 sockaddr结构
一个地址(IP地址+端口号)标识一个特定通信域的套接字端点。不同通信域(地址族)使用不同的结构体(如sockaddr_in)表示其地址格式,但是传递给函数时为了统一,都需要强制转换成一个通用的地址结构sockaddr
struct sockaddr
{
sa_family_t sa_family; /* 地址族 */
char sa_data[14]; /* 地址数据(包括IP地址和端口号) */
};
对于IPv4因特网域(AF_INET),套接字地址用结构sockaddr_in表示,在传递给函数时将sockaddr_in指针类型强制转换为sockaddr指针类型。
struct sockaddr_in
{
sa_family_t sa_family; /* 地址族 */
in_port_t sin_port; /* 端口号 */
struct in_addr sin_addr; /* IPv4地址 */
char sin_zero[8];/* 不使用,一般用0填充 */
};
struct in_addr
{
in_addr_t s_addr; /* IPv4地址 */
};
注意,其中sin_zero为填充字段,应该全部被置0。
对于IPv6因特网域(AF_INET6),套接字地址用结构sockaddr_in6表示,在传递给函数时将sockaddr_in6指针类型强制转换为sockaddr指针类型。
struct sockaddr_in6
{
sa_family_t sin6_family; /* 地址族 */
in_port_t sin6_port; /* 端口号 */
uint32_t sin6_flowinfo; /* IPv6流信息 */
struct in6_addr sin6_addr; /* IPv6地址 */
uint32_t sin6_scope_id; /* IPv6接口范围ID */
};
struct in6_addr {
uint8_t s6_addr[16] /* IPv6地址 */
};
2.2.2 二进制网络字节序与点分十进制字符串(如"127.0.0.1")之间的相互转换
inet_addr、inet_ntoa、inet_aton函数可用于二进制地址格式与点分十进制字符串相互转换,但是缺陷是这三个函数只适用于IPv4地址。
inet_addr函数
若输入字符串有效(点分十进制,如"127.0.0.1")则将字符串转换为32位二进制网络字节序的IPV4地址并返回(即返回输入字符串对应的32位数),否则返回INADDR_NONE(通常是-1)。
但是由于-1也可以表示"255.255.255.255",因此不建议用这个函数。
in_addr_t inet_addr(const char* strptr);
/* 示例 */
struct sockaddr_in mysock;
mysock.sin_addr.s_addr = inet_addr("192.168.1.0"); //设置地址
inet_ntoa函数
将一个32位网络字节序的二进制IP地址转换成相应的点分十进制的IP地址
char *inet_ntoa(struct in_addr in);
inet_aton函数(推荐)
将一个字符串IP点分十进制地址转换为一个32位的网络序列IP地址。如果这个函数成功,函数的返回值非零,如果输入地址不正确则会返回零。
int inet_aton(const char *string, struct in_addr*addr);
/* 示例 */
struct sockaddr_in adr_inet;
inet_aton("127.0.0.1", &adr_inet.sin_addr);
参数:
- const char *string:传入的点分十进制字符串,如"127.0.0.1"
- struct in_addr*addr:第一个参数的转换结果会保存在第二个参数中
返回值:成功返回非零值,否则返回零。
可以使用inet_ntop和inet_pton,它们支持IPv4和IPv6地址
int inet_pton(int domain, const char *src, void *dst);
const char *inet_ntop