前言
不总结出来睡不着觉啊md,本来想着1点就能写完,没想到3点才写完
本文主要介绍比特序在大小端机器上的排布,以及网卡是如何收发比特的,文末简单介绍了位域的约定。文章主要学习与参考字节序(byte order)和位序(bit order) 和 网络字节序之大小端(字节序与比特序),这两篇文章,在其基础上修正错误,并绘制新图。
本专栏知识点是通过零声教育的线上课学习,进行梳理总结写下文章,对c/c++linux课程感兴趣的读者,可以点击链接 C/C++后台高级服务器课程介绍 详细查看课程的服务。
引出疑惑
在网络编程中经常会提到网络字节序和主机序,即网络字节序是大端,主机序常为小端,所以在接收或者发送数据的时候常常要进行大小端转换。字节序不再赘述,直接看大端与小端概念、多字节之间与单字节多部分的大小端转换详解(此文看大小端概念和多字节之间即可,后面的单字节写的太垃圾了,还是看本文详细的比较好)
我知道字节序,大小端我懂,但是当我看到websocket的结构体定义,ip头的结构体定义的时候,我真的晕了。一个字节的8个bit居然也需要区分大小端,我真的百思不得其姐,为什么要这样做
昨天夜里我一直在想,难道一个字节还需要大小端??难道8个bit的存储一个是从左往右,一个是从右往左??
一搜还真是,看了好几篇文章,但是感觉看的文章讲的都不够清楚。上面超链接里面写的单字节多部分大小端其实写的不好(因为写的时候还不知道比特序和位域),所以特地写此文详细解释比特序和位域
。
typedef struct _ws_ophdr { unsigned char opcode: 4, rsv3: 1, rsv2: 1, rsv1: 1, fin: 1; unsigned char payload_len: 7, mask: 1; } ws_ophdr;
struct iphdr { #if defined(__LITTLE_ENDIAN_BITFIELD) __u8 ihl:4, version:4; #elif defined (__BIG_ENDIAN_BITFIELD) __u8 version:4, ihl:4; #else #error "Please fix <asm/byteorder.h>" #endif __u8 tos; __be16 tot_len; __be16 id; __be16 frag_off; __u8 ttl; __u8 protocol; __sum16 check; __be32 saddr; __be32 daddr; /*The options start here. */ };
字节序
本文的重点不再字节序之上,但是后续内容是建立在此之上的。如果不懂字节序的请先看====》大端与小端概念、多字节之间与单字节多部分的大小端转换详解《====看这篇文章的大端与小端概念、多字节之间内容即可,单字节多部分的大小端转换的原理需要看本文。
这里我就截一张核心图放着以方便上下文阅读了。
比特序
说实话我都大二开学就大三了,我居然最近才知道比特序
这个名词,属实知识有点空洞了。那么何谓比特序呢,很好理解,就是一个字节内的8个bit之间的顺序
。
- 字节序:多个字节的顺序,字节与字节之间的顺序
- 比特序:一个字节内,多个bit的顺序,bit与bit之间的顺序
在一般情况下,比特序的顺序,与字节序是保持一致的。即如果是小端字节序,那么高位bit存在高地址,低位bit存在低地址。
我有一个很好用的办法来区分大小端,即
从左往右看,如果符合我们的预期则是大端
从右往左看,如果符合我们的预期则是小端
那么问题就来了,既然比特序也区分大小端,那么下面的主机序与网络序的字节序转换函数,是否将比特序也一并进行转换了呢?
uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort);
就比如上图的194,用大端读是194,用小端读是35,即从左往右看和从右往左看,值是不同的。做了测试之后发现,上面4个函数都没有对比特序做转换。而且写了这么久的代码了,也没见系统提供比特序转换的函数,这就很奇怪了。
我小端接收大端的数据,大端的比特序和小端的顺序不一样,那读出来的字面值肯定不一样啊,咋明明没有对比特序做任何处理,程序安然无恙呢?
在我一开始接触字节序的时候我就在想会不会有比特序这种东西,会不会大小端是反的,不知道读者是否有过这种思考。
网卡-比特的发送和接收顺序
既然在程序中无感知,那么要么是内核,要么是网卡硬件帮我们处理了。比特的发送、接收顺序是指一个字节中的bit在网络电缆中是如何发送、接收的。在以太网(Ethernet)中,是从最低比特位到最高比特位
的发送顺序,也就是最低比特位首先发送
。
可以看出发送顺序其实是按照小端序的顺序来发送的。从图中我们可以发现,先发低位bit再发高位bit,这样对于接收方来说,无需知道对端是大端还是小端,这个数据最左边一定是低位bit,右边一定是高位bit。
牢记这句人能看懂的话: 协议规定了先发送的bit是低位bit,后发送的是高位bit。那么先接收的一定是低位bit,后接收的一定是高位bit。至于接收之后怎么转换顺序,就看主机是什么端。
比特的发送、接收顺序对CPU、软件都是不可见的,因为我们的网卡会给我们处理这种转换,在发送的时候按照先发低位bit再发高位bit的顺序发送
比特位,在接收的时候会把接收到的比特序转,换成主机的比特序
。
所以按照这一规定,就能保证在不同的机器之间进行通信不会发生前面担心的字节值发生变化的问题。
大端序发送给小端序
小端序发送给大端序
位域
何为位域?就是将一个字节,分成多个区域,如下面结构体所示,一个字节8个bit,被分成了5个区域。
在计算机中可寻址的最小单位为字节,bit是无法寻址的,但是为了抽象我们可以把计算机的最小寻址单位变成bit,也就是我们可以单独获得一个bit位。
位域有一个约定:在C语言的结构体中如果包含了位域,如果位域A定义在位域B之前,那么位域A总是出现在低地址的比特位。 这就决定了网络编程中位域在定义时必须处理大小端问题。(同样,结构体中前面的成员也处于较低的地址)
struct bit_order{ unsigned char a: 2, b: 3, c: 3; };
我们发现大小端序不同的话,abc对应的值也会不同
unsigned char ch = 121; struct bit_order *ptr = (struct bit_order *)&ch;
定义协议的万能公式
一般网络协议都是大端序,大端序低地址存储高位,所以如果主机是小端序,则按照协议规定反着定义位域即可。因为大小端序转换的话,bit位置就是逆序
举个例子,在websocket的第一个字节中,拿FIN举例,在大端序中它是最低地址0,在小端序中它是最高地址7。
typedef struct _ws_ophdr { unsigned char opcode: 4, rsv3: 1, rsv2: 1, rsv1: 1, fin: 1; unsigned char payload_len: 7, mask: 1; } ws_ophdr;