chapter03_基本套接字简介
chapter_03 传输层:基本套接字简介
这篇文章是我自己看的。阅读《unix网络编程》,整理自己的思路,仅放在网上便于保存&&分享。
相对于书而言,内容没有什么价值。书上的内容全面可靠。
PS:文章是建立在我的知识体系之上。因而,文中也不会有多余解释。(如果有不清楚的阅读书本/google)
一些必要的内容放在正文中。还有些相对而言非主线的内容放在附录中。这两个集合之外的内容,可能没有写出,或者随它而去。
文章目录
一、内容
1 摘要(概述)
主要讲述,套接字地址结构和地址转换函数。
1.1、套接字地址结构
套接字地址结构:套接字的地址结构。而这里的套接字,指网络套机字。
网络套接字(英语:Network socket;又译网络套接字、网络接口、网络插槽)在计算机科学中是电脑网络中行程间数据流的端点。使用以网际协议(Internet Protocol)为通信基础的网络套接字,称为网际套接字(Internet socket)。因为网际协议的流行,现代绝大多数的网络套接字,都是属于网际套接字。 (来自wiki百科)
其实不用这么术语话,我们大体清楚就好。
1.2 、地址转换函数
地址转换函数作用:地址的文本表达和它们存放在套接字地址结构中的二进制值之间进行转换。 eg:
文本表达式:127.0.0.1 、二进制:01111111 00000000 00000000 000000000
二进制总共4×8=32位。所以用一个uint32_t
就可以存储。
2、套接字的地址结构
“罪魁祸首”是intro/daytimetcpcli.c
中 struct sockaddr_in servaddr;
这一小结围绕着sockaddr_in
展开。确实是由薄变厚。
2.1 、IPv4套接字的地址结构
我列出的是代码中显示的,和书上有稍微区别。本质是一样的。而且我的代码中没有长度字段sin_len。
POSIX规范只需要这个结构中的3个字段: sin_family、 sin_addr和sin_port。
/* Structure describing an Internet socket address. */
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)];
};
/**************************下面是分析部分**********************/
// __SOCKADDR_COMMON (sin_)的分析--->存储的是哪种协议。比如ipv4对应的是AF_INET(常量)
#define __SOCKADDR_COMMON(sa_prefix) \
sa_family_t sa_prefix##family
宏定义:__SOCKADDR_COMMON (sin_) ---> sa_family_t sin_family
其中 “##”起连接作用
//in_port_t分析 --->存储的是:以网络字节序来存储的端口号。
typedef uint16_t in_port_t;
//in_addr分析:至于为什么放在一个结构体中,这是历史原因;详细见书上对应章节 ---》存储ip
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
//sin_zero[]
sin_zero字段未曾使用, 不过在填写这种套接字地址结构时, 我们总是把该字段置为0。
很明显:sockaddr_in和sockaddr大小相同。
2.2、 IPv6套接字地址结构
参考文章: 内存对齐
这是一篇很好的介绍。可惜没有用gdb等方式,真实的展现变量内存状态。当然,我也没有这么干过。????
结构中字段的先后顺序做过编排, 使得如果sockaddr_in6结构本身是64位对齐的, 那么128位的sin6_addr字段也是64位对齐的。 在一些64位处理机上, 如果64位数据存储在某个64位边界位置, 那么对它的访问将得到优化
处理。 (没看出来。。)
结构体中sin6_flowinfo
: sin6_flowinfo字段分成两个字段:低序20位是流标(flow label) 、高序12位保留。我不清楚它的使用,暂时不介绍。
#if !__USE_KERNEL_IPV6_DEFS
/* Ditto, for IPv6. */
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 */
};
#endif /* !__USE_KERNEL_IPV6_DEFS */
#if !__USE_KERNEL_IPV6_DEFS
/* IPv6 address */
struct in6_addr
{
union
{
uint8_t __u6_addr8[16];
uint16_t __u6_addr16[8];
uint32_t __u6_addr32[4];
} __in6_u;
#define s6_addr __in6_u.__u6_addr8
#ifdef __USE_MISC
# define s6_addr16 __in6_u.__u6_addr16
# define s6_addr32 __in6_u.__u6_addr32
#endif
};
#endif /* !__USE_KERNEL_IPV6_DEFS */
/***********************分析**********************/
在结构体in6_addr中定义了一个名为__in6_u的union。
至于可以enable unino中的哪一项,由宏定义决定。
无论哪种存法,毫无疑问的可以存储下128位的ipv6
2.3 、不同套接字地址结构对比
3、地址转换函数
3.1、前提:字节序转换建立统一
字节序,即字节存放的顺序。一种是将低序字节存储在起始地址, 这称为小端(little-endian) 节序; 另一种方法是将高序字节存储在起始地址, 这称为大端(bigendian) 字节序。
在网络应用中,字节序是一个必须被考虑的因素,因为不同机器类型可能采用不同标准的字节序,所以均按照网络标准转化。
字节之间,有大端和小端。字节之内的8位没有这个区分。最左边的是高位。位序一般用于串行设备的传输顺序。
关于字节序的判断,可以见附录3。这里我们要做的是字节序之间的转换。
网际协议使用大端字节序来传送这些多字节整数。
从理论上说, 具体实现可以按主机字节序存储套接字地址结构中的各个字段, 等到需要在这些字段和协议首部相应字段之间移动时, 再在主机字节序和网络字节序之间进行互转, 让我们免于操心转换细节。 然而由于历史的原因和POSIX规范的规定, 套接字地址结构中的某些字段必须按照网络字节序进行维护。 因此我们要关注如何在主机字节序和网络字节序之间相互转换。
h代表host, n代表network, s代表short, l代表long
均返回: 网络字节序的值 | 均返回: 主机字节序的值 |
---|---|
uint16_t htons(uint16_t host16bitvalue); | uint16_t ntohs(uint16_t net16bitvalue); |
uint32_t htonl(uint32_t host32bitvalue); | uint32_t ntohl(uint32_t net32bitvalue); |
3.2 、准备:字节操纵函数
c语言字符串的结尾为:\0; 在ascii中为0;IP如果按照字符串来处理,在遇到\0会提前终止。所以我们不能用字符串来处理。用字节来处理。
以空字符结尾的C字符串是由在<string.h>头文件中定义、 名字以str(表示字符串) 开头的函数处理的。
名字以b(表示字节) 开头的第一组函数起源于4.2BSD, 几乎所有现今支持套接字函数的系统仍然提供它们。
名字以mem(表示内存) 开头的第二组函数起源于ANSI C标准, 支持ANSI C函数库的所有系统都提供它们。
以b(表示字节) 开头 | 以mem(表示内存) 开头 |
---|---|
void bzero(void *dest, size_t nbytes); | void *memset(void *dest, int c, size_t len); |
void bcopy(const void *src, void *dest, size_t nbytes); | void *memcpy(void *dest, const void *src, size_t nbytes); |
int bcmp(const void *ptr1, const void *ptr2, size_t nbytes); | int memcmp(const void *ptr1, const void *ptr2, size_t nbytes); |
3.3上手:地址转换函数
inet_pton和inet_ntop;
这两个函数是随IPv6出现的新函数, 对于IPv4地址和IPv6地址都适用。 函数名中p和n分别代表表达
(presentation) 和数值(numeric) 。 地址的表达格式通常是ASCII字符串, 数值格式则是存放到套接字地址结构中的二进制值
转换成数值模式 | 转换成表达式模式 |
---|---|
int inet_pton(int family, const char *strptr, void *addrptr); | const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len); |
返回: 若成功则为1, 若输入不是有效的表达格式则为 0, 若出错则为-1 | 返回: 若成功则为指向结果的指针, 若出错则为NULL |
二、代码
此时,可以修改上一章的代码。
//修改客户端代码:使用readn(),进行读取;
//修改服务端代码:使用write(),进行写入
//具体代码略
附录
1、IPv4通用套接字地址结构
在没有void*
之前,用来作为强制转换的。以保证:以这样的指针作为参数之一的任何套接字函数必须处理来自所支持的任何协议族的套接字地址结构 。但是现在有了void *
。这个也就没有必要了。
void 有什么好讲的呢?如果你认为没有,那就没有;但如果你认为有,那就真的有。有点像“色即是空,空即是色”。
struct sockaddr
{
__SOCKADDR_COMMON (sa_); /* Common data: address family and length. */
char sa_data[14]; /* Address data. */
};
2、新的通用套接字地址结构
作为IPv6套接字API的一部分而定义的新的通用套接字地址结构克服了现有struct sockaddr的一些缺点。 不像struct sockaddr, 新的structsockaddr_storage足以容纳系统所支持的任何套接字地址结构。sockaddr_storage结构在<netinet/in.h>头文件中定义
3、大端字节序和小端字节序
参考文章:理解字节序
阮一峰 的介绍很好。很流畅。????
首先,为什么会有小端字节序?
答案是,计算机电路先处理低位字节,效率比较高,因为计算都是从低位开始的。所以,计算机的内部处理都是小端字节序。
但是,人类还是习惯读写大端字节序。所以,除了计算机的内部处理,其他的场合几乎都是大端字节序,比如网络传输和文件储存。
通过代码来判断大端/小端。代码也很巧妙。
#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);
}
//输出:x86_64-unknown-linux-gnu: little-endian
文中参考汇总
当将局部变量声明为静态局部变量的时候,也就改变了局部变量的存储位置,即从原来的栈中存放改为静态存储区存放。这让它看起来很像全局变量,其实静态局部变量与全局变量的主要区别就在于可见性,静态局部变量只在其被声明的代码块中是可见的。
上一篇: Delta Lake 实践(离线篇)
下一篇: 关于Java方法的一些注意事项