3.2.2 IPv4首部
IPv4首部如图3-5所示,IPv4各字段的含义说明如下:
·版本(4位):该字段表示IP协议的具体版本,对于IPv4来说该值固定为0x04。
·首部长度(4位):此处的首部长度包含了首部选项与填充部分,并且以32位(4字节)为最小单位。如果没有首部选项和填充部分,那么该字段的值固定为0x05,也就是说IPv4的首部长度为20字节。
·服务类型:表示所希望的服务质量。该字段在IPv4中并不常用。
·总长度:表示IP数据包的总长度。该字段占2字节,也就是说单个IP数据包的最大长度为65536字节。
·标识:在IP分片中使用,同一个IP数据包的所有分片具有相同的标识编号。
·标志(3位):同样在IP分片中使用。
·分片偏移(13位):同样在IP分片中使用,标识该分片在原始报文中的位置,分片偏移以8字节为最小单位,所以需要将该值乘以8才可以获得原来的位置。
·生存时间:用于防止数据包在路由器之间无限期的流动。每经过一个路由器该值递减1。如果该值减至0,该数据包将被路由器丢弃。
·协议:表示IP负载中的协议类型。例如,协议值为1表示IP负载为ICMP协议,协议值为6表示IP负载为TCP协议,协议值为17表示IP负载为UDP协议。
·首部校验:对于IP首部计算的16位校验和。
·源地址:创建该IP数据分包的设备的32位IP地址。
·目标地址:该IP数据分包的接收设备的32位IP地址。
·选项:长度可变,通常用于实验与诊断。该字段包含安全级别、源路径、路径记录和时间戳等信息。
·填充:如果包括选项部分,那么选项部分的长度必须是32位(4字节)的整数倍,否则需要使用填充部分。
IPv4首部包含的内容较多,但在实际的开发过程中只需要关注IP协议版本号、源地址和目标地址即可。对于IP分片和重组等较为复杂的部分,操作系统内的网络协议栈已经为用户进行处理,在开发过程中工程师并不需要关心。

图3-5 IPv4首部
3.2.3 IPv4地址
在设备间进行网络通信时,使用IP地址识别主机和路由器。为了保证设备间的正常通信,每个设备都应有正确的IP地址。IPv4地址由32位正整数表示。IP地址在计算机或嵌入式系统内部总是以二进制形式保存,但是人们总是不习惯于阅读和使用二进制方式,所以IPv4地址把32位IPv4地址分成8位一组,共分成4组,各组之间通过“.”隔开,每组通过十进制表示。192.168.1.100便是一个合法的IPv4地址。
1.网络ID和主机ID
IPv4地址一般由网络ID和主机ID两部分组成。网络ID在数据链路的每个段将配置不同的值,网络ID必须保证相互连接的每个段的地址不重复。而相同段内的主机必须具有相同的网络标识。这种结构非常有利于路由器的工作,路由器可以很容易地知道目标主机发送的数据在同一个网络中还是在不同的网络中,路由器可以通过比较两个IP地址的网络ID便可做出决策。
网络ID和主机ID的表示方式有两种:后缀法和子网掩码法。例如192.168.1.100/24,24表示网络ID占该IPv4地址的前24位,此时网络ID为192.168.1.0,IPv4地址共32位,那么剩余的8位便是主机ID,所以此时的主机ID为0.0.0.100。除了后缀表示法之外,子网掩码法也非常常用,子网掩码的长度和IPv4地址的长度相同,若某位为比特0表示该位留给网络ID,如果该位为比特1,表示该位留给主机ID。如果IPv4地址192.168.1.100的子网掩码为255.255.255.0,那么它的网络ID为192.168.1.0,主机ID为0.0.0.100。IPv4采用网络ID和主机ID“混合排版”的方式,而IPv6改变了这种方式,把网络ID和主机ID完全分离。图3-6可以非常直观地解释IP地址中网络ID和主机ID的关系。

图3-6 IPv4地址说明
2.私有IPv4地址
通过前面几个章节可以获知,若物联网设备连接网络将会消耗大量的IP地址,但是未等物联网普及,IPv4地址已经不能满足个人计算机和移动手机的需求,IPv4地址已经枯竭。为了解决这种需求冲突出现了多种新技术,其中就包括私有IPv4地址和NAT技术,通过这两项技术可使联网设备并不需要全局唯一的IPv4地址,仅需要在同一个域中保持唯一便可。虽然私有IPv4地址和NAT技术非常普及,但是该技术并不利于物联网设备的发展。
3.2.4 IPv6首部
IPv6首部较IPv4首部发生了巨大的变化,但是这种变化并没有使IP变得更复杂,反而使IPv6显得更简单一些。IPv6首部如图3-7所示。
IPv6首部包含以下内容:
·版本(4位):表示IP的版本号,此处该字段为固定值0x06。
·流量类别:该字段与IPv4中的服务类型比较相似,由于服务类型在IPv4领域中并没有发挥实际的作用,所以原计划在IPv6中删除该部分,但是出于兼容和其他方面的考虑最后还是在IPv6中保留了该字段。
·流标签(20位):流标签字段用于记录从源节点发送至多个目标节点的一系列IPv6数据包的序列,源节点可以利用该字段标志那些请求IPv6路由器进行特殊处理的数据包序列。流标签可以标识同一个流中的所有数据包,从而保证所有的数据包都可以得到IPv6路由器的相同处理。

图3-7 IPv6首部
·净载荷长度:该字段表示IPv6首部之后的负载长度。如果IPv6数据包有一个或多个扩展头,那么该字段也包括这些扩展头的长度。IPv6的净载荷长度和IPv4的数据包总长度存在明显区别。IPv4的数据包总长度包括IPv4首部和IPv4负载,而IPv6净载荷长度为IPv6的有效负载的长度。换句话说,IPv6的首部长度并不需要指示,它的长度总是为40字节。
·下一个首部:下一个首部有两个作用,如果IPv6数据包只有基本首部而没有扩展首部的话,那么下一个首部就是表示IPv6负载中所承载的协议,这一点与IPv4中的协议类型字段非常相似,而且该字段与IPv4首部中的协议类型使用相同的协议值,如下一个首部取值为6时表示IPv6负载为TCP协议,取值为17时表示IPv6负载为UDP协议,取值为58时表示IPv6负载为ICMPv6协议。图3-8很好地解释了下一个首部和IPv6负载之间的关系。
·跳数限制:跳数限制字段与IPv4首部中的生存时间非常类似。该字段的值每经过一个路由器便递减1。
·源地址:IPv6数据包发起设备的128位IPv6地址。
·目标地址:该IPv6数据包的预计接收设备的128位IPv6地址。与IPv4不同,IPv6并没有广播地址,取而代之的是IPv6组播地址。

图3-8 IPv6下一个首部
IPv4和IPv6的最显著区别便是IPv4地址为32位,而IPv6地址一下子增加到128位。
3.2.5 IPv6地址
IPv6地址一般采用x:x:x:x:x:x:x:x格式。每个x都是一个16位数,可使用4个十六进制数字来表示,每个x之间采用冒号隔开。所以IPv6地址包含了8个16位区域,一共为128位,如2020: CA28:0000:0000:0023:0222:0000:2900。
这样的IPv6地址实在太长了,所以非常有必要简化IPv6的写法。IPv6的简化写法可遵循两条规则——省略前导0和省略全0,如图3-9所示。
1.省略前导0
所有16位数中的前导0都可以被省略,请注意该规则只适用于前导0,而尾部的0绝不能被省略。那么2020:CA28:0000:0000:0023:0222:0000:2900将可以简化为:
2020:CA28:0:0:23:222:0:2900
2.省略全0
在IPv6地址定义中可使用双冒号(::)表示任意一段连续的由一个或者多个全零组成的16位数,采用这样的方法可以进一步简化IPv6地址。那么IPv6地址2020:CA28:0:0:23:222:0:2900可以进一步简化为:
2020:CA28::23:222:0:2900
无论是IPv4还是IPv6都是学习CoAP之前需要掌握的基础内容,由于本书篇幅有限无法详细展开,更多的内容请参考《IPv6技术精要》和《深入解析IPv6(第三版)》等书。

图3-9 IPv6地址省略写法
3.3 UDP
虽然IP在互联网和物联网领域中非常重要,但是IP本身并不能组成一个完整应用,IP协议必须与其他协议一起才可以构成一个独立的应用。UDP和TCP正是被广泛使用的传输层协议,下面我们结合几个具体的例子介绍UDP和TCP。
UDP是User Datagram Protocal的简称,可以翻译为用户数据协议。UDP为那些需要简单且快速的传输层协议的应用而设计。UDP非常简单,仅包括了端口和IP地址等部分,而把其他的工作都交给更上一层协议实现。CoAP正是采用UDP作为传输层协议,所以学习CoAP之前必须要了解UDP的相关细节。
3.3.1 动手尝试
本节通过一个示例展现UDP工作的大概流程。在这个动手示例中有两个角色——UDP客户端和UDP服务器,UDP客户端可由Windows主机实现,而UDP服务器由树莓派实现,UDP客户端和服务器端代码均使用Python编写。
1.获取代码
可通过Git工具复制本书提供的示例代码。
# 新建一个名为repo的文件夹
mkdir –p repo
# clone代码仓库
git clone https://github.com/xukai871105/the_beginning_of_coap.git
# 进入目录
cd the_beginning_of_coap
# 进入网络技术回顾示例代码文件夹
cd review_demo
# 进入udp示例目录
cd udp_echo_demo
2.UDP服务器实现
UDP服务器和UDP客户端均使用Python 3编写,示例代码中使用了Python 3.4之后自带的异步IO处理框架asyncio,相比于传统的socket,asyncio可以更方便地完成网络应用开发。
udp_server.py的具体代码如下:
代码清单3-1 udp_server.py
import asyncio
class EchoServerProtocol:
def connection_made(self, transport):
self.transport = transport
def datagram_received(self, data, addr):
message = data.decode()
print('Received %r from %s' % (message, addr))
print('Send %r to %s' % (message, addr))
self.transport.sendto(data, addr)
loop = asyncio.get_event_loop()
print("Starting UDP server")
listen = loop.create_datagram_endpoint(
EchoServerProtocol, local_addr=('0.0.0.0', 5683))
transport, protocol = loop.run_until_complete(listen)
try:
loop.run_forever()
except KeyboardInterrupt:
pass
transport.clo
udp_server.py实现了一个简单的UDP服务器,服务器将把客户端发送给它的内容原样返回至客户端。
(1)创建服务器
通过create_datagram_endpoint方法创建一个UDP服务器,该方法传入两个参数:Echo-ServerProtocol为一个协议实例,该实例中包含多个回调函数,用于处理建立连接、接收数据处理等事件; local_addr用于绑定IP地址和端口号,'0.0.0.0'表示侦听本机的所有网卡,5683为侦听端口号,5683正是CoAP协议的“知名”端口号。
listen = loop.create_datagram_endpoint(
EchoServerProtocol, local_addr=('0.0.0.0', 5683))
(2)接收数据处理
在EchoServerProtocol中,一旦UDP服务器接收到网络数据,那么将会进入datagram_received回调函数,在该回调函数中通过data.decode获取客户端请求内容,最后通过transport.sendto原样返回至客户端。
class EchoServerProtocol:
# 省略部分代码
def datagram_received(self, data, addr):
message = data.decode()
print('Received %r from %s' % (message, addr))
print('Send %r to %s' % (message, addr))
self.transport.sendto(data, addr)
3.UDP客户端实现
UDP客户端的代码和UDP服务器非常相似,读者运行该示例之前需要修改代码中的raspberry_ip_addrss变量,并把该变量替换为树莓派的实际IP地址。
udp_client.py的具体代码如下:
代码清单3-2 udp_client.py
import asyncio
# replace ip address
raspberry_ip_addrss = '192.168.0.8'
class EchoClientProtocol:
def __init__(self, message, loop):
self.message = message
self.loop = loop
self.transport = None
def connection_made(self, transport):
self.transport = transport
print('Send:', self.message)
self.transport.sendto(self.message.encode())
def datagram_received(self, data, addr):
print("Received:", data.decode())
print("Close the socket")
self.transport.close()
def error_received(self, exc):
print('Error received:', exc)
def connection_lost(self, exc):
print("Socket closed, stop the event loop")
loop = asyncio.get_event_loop()
loop.stop()
loop = asyncio.get_event_loop()
message = "Hello World!"
connect = loop.create_datagram_endpoint(
lambda: EchoClientProtocol(message, loop),
remote_addr=(raspberry_ip_addrss, 5683))
transport, protocol = loop.run_until_complete(connect)
loop.run_forever()
transport.close()
loop.close()
udp_client.py实现了客户端的所有功能,一旦客户端和服务器建立连接,udp_client便把“Hello World!”字符串发送至UDP服务器,若udp_client接收到服务器返回的数据则把返回内容打印至控制台,关闭连接并最终结束程序。
(1)创建UDP客户端
通过create_datagram_endpoint方法创建UDP客户端,该方法传入两个参数:EchoClient-Protocol(message,loop)为一个协议示例,该实例处理网络连接、连接丢失和接收数据等事件; remote_addr=(raspberry_ip_addrss,5683)用于指定目标服务器地址和端口号,此处目标服务器为树莓派IP地址,端口号为5683。
connect = loop.create_datagram_endpoint(
lambda: EchoClientProtocol(message, loop),
remote_addr=(raspberry_ip_addrss, 5683))
(2)发送“Hello World!”
通过transport.sendto(message.encode())把“Hello World!”发送至服务器端。
class EchoClientProtocol: # 省略若干代码
def connection_made(self, transport):
self.transport = transport
print('Send:', self.message) self.transport.sendto(self.message.encode())
(3)打印接收数据
一旦接收到服务器返回的数据,那么将进入datagram_received回调函数,data.decode()中便是服务器返回的“Hello World!”,打印服务器返回的数据后关闭UDP连接。
class EchoClientProtocol:
# 省略若干代码
def datagram_received(self, data, addr):
print("Received:", data.decode())
print("Close the socket")
self.transport.close()
4.执行示例代码
在树莓派控制台中输入:
python3 udp_server.py
在Windows控制台中输入:
python3 udp_client.py
树莓派控制台输出以下类似内容:
Starting UDP server
Received 'Hello World!' from ('192.168.0.3', 53495)
Send 'Hello World!' to ('192.168.0.3', 53495)
Windows控制台输出以下类似内容:
Send: Hello World!
Received: Hello World!
Close the socket
Socket closed, stop the event loo



















暂无评论内容