每个协议族都定义了自己的套接口地址结构,名字均以sockaddr_
开头,对应协议族的标志结束。大部分套接口函数需要指向套接口地址结构的指针作为参数。
/*
* Internet address (a structure for historical reasons)
*/
struct in_addr {
in_addr_t s_addr; /* 32位的IPv4地址(网络字节序) */
};
/*
* Socket address, internet style.
*/
struct sockaddr_in {
uint8_t sin_len; /* 长度(固定16字节) */
sa_family_t sin_family; /* AF_INET */
in_port_t sin_port; /* 16位的TCP或者UDP端口号(网络字节序) */
struct in_addr sin_addr; /* 32位的IPv4地址(网络字节序) */
char sin_zero[8]; /* 未用 */
};
注:对于结构体类型的,计算其内层数据类型
字段名 | 数据类型 | 长度 |
---|---|---|
|
| 8位 |
|
| 8位 |
|
| 16位 |
|
| 32位 |
|
| 8字节 |
sin_len=(8+8+16+32)/8+8=16 byte
在intro/daytimetcpclo.c
中添加一行打印套接口地址结构大小:
printf("size of servaddr is %zu\n", sizeof(servaddr));
得到结果:
size of servaddr is 16
使用的时候基本只需要这个结构中的3个成员:sin_family
、sin_addr
和sin_port
。
// intro/daytimetcpcli.c
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(13); /* daytime server */
if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
err_quit("inet_pton error for %s", argv[1]);
// intro/daytimetcpsrv.c
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(13); /* daytime server */
IPv4地址和TCP或UDP端口号在套接口地址结构中总是以网络字节序来存储。
/*
* IPv6 address
*/
struct in6_addr {
uint8_t s6_addr8[16]; /* 128位的IPv6地址 */
};
/*
* Socket address for IPv6
*/
#if condition
#define SIN6_LEN
#endif /* condition */
struct sockaddr_in6 {
uint8_t sin6_len; /* 长度(固定24字节) */
sa_family_t sin6_family; /* AF_INET6 */
in_port_t sin6_port; /* 16位的TCP或者UDP端口号(网络字节序) */
uint32_t sin6_flowinfo; /* 32位的IPv6流标 */
/* 低24位是流量标号 */
/* 下4位是优先级 */
/* 再下4位保留 */
struct in6_addr sin6_addr; /* 128位的IPv6地址(网络字节序) */
};
SIN6_LEN
常值必须定义,例如macOS中:#if !defined(_POSIX_C_SOURCE) || defined(_DARWIN_C_SOURCE)
#define SIN6_LEN
#endif /* (_POSIX_C_SOURCE && !_DARWIN_C_SOURCE) */
sockaddr_in6
是64位对齐的,则128位的成员sin6_addr
也是64位对齐的。在一些64位处理机上,如果数据存储在64位便捷的位置,则对64位数据的访问将优化处理。套接口函数,应当是协议无关的,可以处理任何支持的协议族的套接口地址结构。套接口函数是在ANSI C之前定义的,因此它没有使用通用的指针类型void *
,而是定义了一个通用套接口地址结构:
/*
* [XSI] Structure used by kernel to store most addresses.
*/
struct sockaddr {
uint8_t sa_len; /* total length */
sa_family_t sa_family; /* [XSI] address family */
char sa_data[14]; /* [XSI] addr value (actually larger) */
};
于是,套接口函数使用的参数,为指向通用套接口地址结构sockaddr
的指针,例如bind
函数:
int bind(int, const struct sockaddr *, socklen_t);
因此,在调用这些函数时,我们需要将指向特定协议的套接口地址结构的指针类型转换成指向通用套接口地址结构的指针struct sockaddr *
:
bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr));
下图是四种套接口地址结构的对比。为了处理类似Unix域结构和数据链路结构这种可变长度的结构体,我们把指向套接口地址结构的指针以及它的长度作为参数传递给套接口函数。
在Call by value-result
的情形下,在调用函数的时候,参数的值被传入函数,函数返回时,可以修改这个值,使其成为一个返回值。
上面说到,套接口函数中的两个参数,一个是指向套接口地址结构的指针,一个是结构的长度。其中。结构的长度的传递方式,又根据其传递的方向有所不同。
如下面三个函数,最后一个参数都是结构的整数大小(socklen_t
),由于指针和指针所指结构的大小都传递给内核,所以从进程到内核要确切拷贝多少数据是已知的。
int bind(int, const struct sockaddr *, socklen_t);
int connect(int, const struct sockaddr *, socklen_t);
ssize_t sendto(int, const void *, size_t,int, const struct sockaddr *, socklen_t)
下面四个函数,长度的参数则是指向结构的整数的指针(socklen_t *
)。当函数被调用时,告诉内核,它的结构是多大,使内核写这个结构时不会越界。当函数返回时,它的值则被修改为结果——告诉进程内核在此结构中确切存储了多少信息。
int accept(int, struct sockaddr * __restrict, socklen_t * __restrict);
ssize_t recvfrom(int, void *, size_t, int, struct sockaddr * __restrict,socklen_t * __restrict);
int getpeername(int, struct sockaddr * __restrict, socklen_t * __restrict);
int getsockname(int, struct sockaddr * __restrict, socklen_t * __restrict);
不过,尽管如此,对于IPv4和IPv6这两种定长套接口地址结构,那么从内核到进程返回的值也是定长的(分别是16字节和24字节),如果是可变的情况,那么从内核返回的值可能比结构的最大长度小。
考虑一个16位整数0x0102
,它由2个字节组成。内存中存储这两个字节有两种方式:
02
)存储在起始地址,这称为小端(little-endian)字节序。01
)存储在起始地址,这称为大端(big-endian)字节序。上面说的低序和高序,以我们熟悉的十进制来看,从右到左一次是个位,十位,百位,依次增大。二进制和十六进制也是一样,最右侧是最低有效位(LSB),左侧是最高有效位(MSB)。
-----------------------------------
| MSB | 0000 0001 0000 0010 | LSB |
-----------------------------------
术语“小端”和“大端”表示多字节值的哪一端存储在内存的起始地址。
-----------------------------------
| <-----------内存地址增大方向 | 起始 | 小端字节序
-----------------------------------
| |
-----------------------------------
| MSB | 0000 0001 0000 0010 | LSB |
-----------------------------------
| |
-----------------------------------
| 起始 | 内存地址增大方向-----------> | 大端字节序
-----------------------------------
把两字节数0x0102
存储为一个短整数,然后查看两个连续的内存地址的值c[0]
和c[1]
,以确定字节序。
/**
* intro/byteorder.c
*/
#include "unp.h"
int
main(int argc, char **argv)
{
union {
short s;
char c[sizeof(short)];
} un;
un.s = 0x0102;
printf("%s: ", CPU_VENDOR_OS);
if (sizeof(short) == 2) {
if (un.c[0] == 1 && un.c[1] == 2)
printf("big-endian\n");
else if (un.c[0] == 2 && un.c[1] == 1)
printf("little-endian\n");
else
printf("unknown\n");
} else
printf("sizeof(short) = %d\n", sizeof(short));
exit(0);
}
编译运行,可以看到本机是小端字节序:
JACKIELUO-MC0:intro jackieluo$ make byteorder
JACKIELUO-MC0:intro jackieluo$ ./byteorder
i386-apple-darwin17.3.0: little-endian
网际协议在处理多字节整数时,使用大端字节序。因此需要考虑主机字节序和网络字节序间的互相转换,下面是我本机上的相关函数:
#define ntohs(x) __DARWIN_OSSwapInt16(x) // network to host
#define htons(x) __DARWIN_OSSwapInt16(x) // host to network
#define ntohl(x) __DARWIN_OSSwapInt32(x) // network to host
#define htonl(x) __DARWIN_OSSwapInt32(x) // host to network
书中示例程序用bzero
来把套接口地址结构初始化为0:
bzero(&servaddr, sizeof(servaddr));
bzero
函数只有两个参数,便于记忆。
void bzero(void *s, size_t n)
memset
将目标中指定数目的字节置为指定值。
void* memset( void* dest, int ch, std::size_t count );
考虑到bzero
是BSD中的过时函数,可以考虑使用memset
来初始化套接口地址结构:
memset(&servaddr, 0, sizeof(servaddr));
在套接口编程中,我们需要在可读的ASCII字符串的地址,及网络字节序的二进制值间进行转换。书中使用协议无关的inet_pton
和inet_ntop
两个函数进行转换,字母p和n分别代表“presentation”和“numeric”。例如:
/* int
* inet_pton(af, src, dst)
* convert from presentation format (which usually means ASCII printable)
* to network format (which is usually some kind of binary format).
* return:
* 1 if the address was valid for the specified address family
* 0 if the address wasn't valid (`dst' is untouched in this case)
* -1 if some other error occurred (`dst' is untouched in this case, too)
* author:
* Paul Vixie, 1996.
*/
int
inet_pton(af, src, dst)
int af;
const char *src;
void *dst;
{
switch (af) {
case AF_INET:
return (inet_pton4(src, dst));
case AF_INET6:
return (inet_pton6(src, dst));
default:
errno = EAFNOSUPPORT;
return (-1);
}
/* NOTREACHED */
}
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。