第三章笔记
1. 套接字地址:
1.1 分类:
套接字地址分两种,一种是通用套接字地址类型struct sockaddr
,它只是为了在套接字函数中可以传入任意套接字地址而定义的结构体,功能类似于void*,而出现定义套接字函数时还没有出现void*,所以才定义了该结构体。sockaddr
使用unsigned short int sa_family
来表明具体地址类型。另一种是具体协议的套接字地址类型,如果需要具体的套接字地址类型,需要使用sockaddr_in(IPv4)
或sockaddr_in6(IPv6)
类型。
1.2 通用套接字地址结构sockaddr
typedef unsigned short int sa_family_t;
#define __SOCKADDR_COMMON(sa_prefix) \
sa_family_t sa_prefix##family
#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))
//定义位置:/usr/include/bits/sockaddr.h
struct sockaddr
{
__SOCKADDR_COMMON(sa);
char sa_data[14];
}; //定义位置:写代码需#include的头文件:/usr/include/sys/socket.h -> 实际真正定义sockaddr的头文件:/usr/include/bits/socket.h
所以sockaddr的实际定义为:
struct sockaddr
{
unsigned short int sa_family;
unsigned char sa_data[14];
}
(1)其中sa_family表示协议族类型,具体协议族值可以通过man socket查看第一个参数的值,该值就表示协议族类型。
(2)当作为一个参数传递进任何套接字(sockaddr)函数时,套接字地址结构总是以引用形式(也就是以指定该结构的指针)来传递。比如int bind(int,struct sockaddr*,socklen_t);
这就要求对这些函数的任何调用都必须要将指向特定协议的套接字地址结构的指针进行类型强制转换,变成指向某个通用套接字地址结构的指针。 比如:struct sockaddr_in serv; bind(sockfd,(struct sockaddr*) &serv,sizeof(serv));
(3)除了要传递套接字的地址,该结构的长度也要作为一个参数来传递。
当套接字是从进程到内核传递时,结构长度只需传一个整数值即可。这样的函数有3个:bind connect sendto。需要传递长度的原因是:不同的套接字的长度是不同的。比如sockaddr_in和sockaddr_in5的长度就不同。只有给内核传递了长度,内核才知道到底需从进程复制多少数据进来。
当套接字是从内核传递到进程时,结构长度需要传递一个指针。这样的函数有4个:accept recvfrom getsockname getpeername。需要传递长度指针的原因是:
当函数被调用时,结构大小是一个值,它告诉内核该结构的大小,这样内核在写结构时不至于越界;当函数返回时,结构大小又是一个结构,它告诉进程内核在该结构中究竟存储了多少信息。如果套接字地址结构是固定长度的,那么从内核返回的值总是那个固定长度,如sockaddr_in。如果是可变长度的套接字地址结构,返回值可能小于该结构的长度,如sockaddr_un(unix域套接字地址)
1.3 IPV4套接字地址结构sockaddr_in
typedef uint16_t in_port_t
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of `struct sockaddr‘. */
unsigned char sin_zero[sizeof (struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) -
sizeof (struct in_addr)];
}; //定义位置:/usr/include/netinet/in.h
所以sockaddr_in的实际定义为:
struct sockaddr_in
{
unsigned short int sin_family; //协议族类型
uint16_t sin_port; //端口号(可以是TCP或UDP端口号)
uint32_t sin_addr; //IPv4地址
unsigned char sin_zero[sizeof(struct sockaddr) - sizeof(unsigned short int) - sizeof(uint16_t) - sizeof(uint32_t)]; //也就是unsigned char sin_zero[8];
}
(1)对于IPV4来说,sin_family的协议族类型应该是AF_INET
(2) 按照惯例,对套接字结构体赋值前,我们应该总是把整个结构置0,而不是单单把sin_zero字段置0
(3) IPv4的IP地址为32位
4. IPv6套接字地址结构sockaddr_in6
typedef uint16_t in_port_t
struct sockaddr_in6
{
__SOCKADDR_COMMON (sin6_);
in_port_t sin6_port; /* Transport layer port # */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* IPv6 scope-id */
}; //定义位置:/usr/include/netinet/in.h
IPv6的协议族类型是AF_INET6
IPv6的IP地址为128位
1.5 IPv4和IPv6的套接字地址结构图
1.6 新的通用套接字地址结构
sockaddr_storage
,因为现在没用到过这个结构,先pass
2. 字节序
(1)小端:存储具有多字节的类型数据时(比如16位整数),将低序字节存储在指向该数据的起始地址处。
(2)大端:存储具有多字节的类型数据时(比如16位整数),将高序字节存储在指向该数据的起始地址处。
(3)主机字节序:我们把某个给定系统所有的字节序成为主机字节序。
(4)代码:确定主机字节序的程序为:intro/byteorder.c
(5)网际协议使用大端字节序来传递数据。
(6)尽管可以在数据传输时由内核将套接字地址的ip和端口号由主机字节序转换成网络字节序,但由于历史的原因和POSIX规范的规定,套接字地址结构中的某些字段必须按照网络字节序进行维护。所以当需要把套接字地址结构传递给内核时,需要首先把IP地址和端口号由主机字节序转换成网络字节序。从内核获取的套接字类型(比如accept)也是网络字节序,如果要打印,首先先转换成主机字节序
(7)接口函数:
IP地址主机字节序转网络字节序的函数:htonl 网络字节序转主机字节序的函数:ntohl
端口号主机字节序转网络字节序的函数:htons 网络字节序转主机字节序的函数:ntohs
其中:h代表host n代表network s代表short(16位数值) l代表long(现视为32位数值)
3. 字节操作函数
为了操作诸如IP地址这样的字段,这些字段可能包含值为0的字节,但并不是C字符串,需要使用字节操作函数
一组是几乎支持套接字函数的系统都会提供的函数:bzero bcopy bcmp
一组是ANSI C标准提供的函数:memset memcpy memmove memcmp
两组表达的含义相同,使用时可以任意选择。
注意:当拷贝字节串时,如果源字节串与目标字节串重叠时,bcopy能够正确处理,而memcpy不能,需要使用memmove。
memcmp:当两个字节串不相等时,是大于0还是小于0取决于第一个不等的字节,并且比较时是假设两个不等的字节均为无符号字符(unsigned char)的前提下完成的。
4.将IP地址由字符串格式和数值格式进行相互转换的函数
(1)只适用于IPv4的函数:
inet_aton: IP地址由字符串格式转换成数值格式(将四个点分十进制字符串转换成四个字节的数值,比如192.168.1.2->C0 A8 1 2,其中C0 A8分别时192 168的十六进制表示
inet_ntoa: IP地址由数值格式转换成字符串格式(将四个字节的数值转换成四个点分十进制字符串,比如C0 A8 1 2->192.168.1.2,其中C0 A8分别时192 168的十六进制表示)
注意:inet_ntoa:由该函数的返回值所指向的字符串驻留在静态内存中。这意味着该函数是不可重入的。因为静态数据是全局数据,如果是堆栈数据则没有,因为即使相同函数,堆栈数据也是互不影响的,
(2)对IPv4和IPv6都适用的函数:
inet_pton: IP地址由字符串格式转换成数值格式
inet_ntop: IP地址由数值格式转换成字符串格式
可以这样记忆:p代表表达(presentation),表示字符串格式;n代表数值(numeric),表示数值格式
inet_ntop需要指定存储IP字符串的长度。为有助于指定这个长度,在
netinet/in.h
头文件中定义了两个宏,INET_ADDRSTRLEN(16)和INET6_ADDRSTRLEN(46)。可以使用这两个宏定义存储IP字符串的数组。
(3)因为inet_pton inet_ntop对IPv4和IPv6都适用,建议使用这组函数
(4)通用的inet_ntop
虽然inet_ntop对于IPv4和IPv6都适用,但是它们是协议相关的,即将IPv4 和 IPv6的地址由字符串转换成数值,需要不同的代码:
struct sockaddr_in addr;
inet_ntop(AF_INET,&addr.sin_addr,str,sizeof(str));
struct sockaddr_in6 addr6;
inet_ntop(AF_INET6,&addr.sin6_addr,str,sizeof(str));
如果需要,可以实现一个通用的函数sock_ntop,它以指向某个套接字地址结构的指针为参数,查看该结构的内部sa_family,然后调用适当的函数返回该地址的表达格式。
其中可使用通过套接字地址结构sockaddr做参数,并使用该参数的sa_family判断具体是哪个协议。代码位置:lib/sock_ntop.c
5. readn writen readline
字节流套接字上调用read或write输入或输出的字节数可能比请求的数量少,这不是出错的状态。这个现象的原因在于内核中用于套接字的缓冲区可能已达到了极限。此时需要调用者再次调用read或write函数,以输入或输出剩余的字节。
为了不让实现返回一个不足的字节计数值,我们总是改为调用readn writen代替read write
代码见lib/readn.c lib.writen.c lib/readline.c
虽然基于文本行的网络协议相当多,比如SMTP HTTP FTP,然而我们的建议是依照缓冲区而不是文本行的要求来考虑编程。编写从缓冲区中读取数据的代码,当期待一个文本行时,就查看缓冲区中是否含有那一行。
实现readline时即使使用内部缓冲区实现,也可能存在问题。比如select等系统函数仍然不知道readline使用的内部缓冲区,因此编写不严谨的程序很可能发现自己在select上等待的数据早已收到并存放在readline的缓冲区中了。
6. 总结:
(1)通用套接字地址结果为sockaddr,具体套接字结构IPv4为sockaddr_in,IPv6为sockadd_16。其中sockaddr只是为了在套接字函数中可以传入任意套接字地址而定义的结构体,功能类似于void*。
(2)所有的套接字地址结构都有一个共同的变量sa_family,它用来指明具体的地址族的,它是socket接口的第一个参数,具体指可以用man socket查看。sockaddr_in 的地址族类型为AF_INET,并用32位存储IPv4地址,sockaddr_in6的地址族类型为AF_INET6,并用128位存储IPv6地址。而Tcp Udp的端口号都用16位存储
(3)由于历史的原因和POSIX规范的规定,套接字地址结构中的某些字段必须按照网络字节序进行维护。所以当需要把套接字地址结构传递给内核时,需要首先把IP地址和端口号由主机字节序转换成网络字节序。从内核获取的套接字类型(比如accept)也是网络字节序,如果要打印,首先先转换成主机字节序
IP地址和端口号 在主机字节序和网络字节序之间互相转换的四个函数为:htonl ntohl htons ntohs (其中h代表host主机,n代表network,l代表32位数值 s代表16位数值)
(4)将IP地址由字符串格式和数值格式进行相互转换的函数,建议使用inet_pton 和 inet_ntop,这两个函数对于IPv4和IPv6都适用。(其中p代表presentation表达,n代表numeric数值
(5)字节流套接字上调用read或write输入或输出的字节数可能比请求的数量少,这不是出错的状态。但为了不让实现返回一个不足的字节计数值,我们总是改为调用readn writen代替read write