查看“Network Stack”的源代码
←
Network Stack
跳到导航
跳到搜索
因为以下原因,您没有权限编辑本页:
您请求的操作仅限属于该用户组的用户执行:
用户
您可以查看和复制此页面的源代码。
{{In_Progress}} 本文是关于编写TCP/IP协议栈的,即使用链路层(如以太网卡)来处理IP、[[Address Resolution Protocol|ARP]]、TCP、UDP等协议的数据包的子系统。 ==扫描PCI设备== 首先要做的是扫描安装在机器上的PCI设备,以便通过查看特定的供应商ID和设备ID来检测以太网卡。 有关更多详细信息,请参见[[PCI]]页面。 ==为NIC编写驱动程序== 找到以太网卡后,需要实现一个驱动程序,使其能够发送和接收数据。 如果你使用的是仿真器,Intel E1000是一款很好的驱动程序编写卡,因为它可以在各种仿真器上使用,比如VirtualBox - osdev.org上的这里对进行了更多介绍(请参阅[[Intel Ethernet i217]])。 如果在实现E1000驱动程序时遇到问题,可以从[[RTL8139]]开始,这是一种更简单的旧以太网卡。 从以太网卡出来的第一件事是机器的MAC地址。 在本地网络上交换数据需要这个6字节的地址。 你能做的最简单的测试就是在网络上发送ARP广播。 你可以用[https://www.wireshark.org/ Wireshark]既可以捕获有效ARP请求的示例,也可以验证目标主机是否已接收到你自己的请求。 就接收数据而言,你的网卡应该捕获通过本地网络发送的数据,即使这些数据没有发送到你的机器。 ==网络协议== 一旦你可以通过NIC发送和接收数据,并获得机器的MAC地址,你就必须实施(至少部分实施)几种相互共存的网络协议: * Ethernet以太网协议:这是使用MAC地址将数据发送到本地网络上另一台机器的基本协议。 这是所有其他功能的基础,因为如果你想与外界通信,你需要向路由器发送数据。 ** [[ARP]](Address Resolution Protocol 地址解析协议):允许将IPv4地址转换为MAC地址 ** IP(Internet Protocol)协议:它位于以太网之上,需要在给定IP地址的互联网上发送数据。 最常见的版本是使用32位IP地址的IPv4,但IPv6(使用128位IP地址)也正在获得越来越多的注意。 请注意,IP提供了发送数据包的“最大努力(best effort)”发送,但不能保证数据包将成功到达目的地,也不能保证数据包将按发送顺序接收 *** [[ICMP]](Internet Control Message Protocol互联网控制消息协议):由ping或traceroute等工具使用 *** UDP(User Datagram Protocol 用户数据报协议):一种无连接传输协议,将源端口和目标端口的概念添加到IP中。 应用程序服务可以订阅一个或多个端口,以便在UDP消息发送到该端口时收到通知 **** DHCP(Dynamic Host Configuration Protocol动态主机配置协议):允许请求机器网络配置信息,如其IP地址、本地路由器的IP地址、DNS等。 **** DNS(Domain Name System 域名系统):获取给定域名的IP地址 ***TCP(Transmission Control Protocol 传输控制协议):与UDP一样,它添加了源端口和目标端口的概念。 然而,TCP更为复杂,因为它创建了自己的会话机制,并确保使用它的应用程序将按顺序接收数据包,如果需要,还会重新发送数据包。 **** [[SSL/TLS]](可选):在建立安全连接时使用 ***** HTTP(HyperText Transfer Protocol 超文本传输协议):定义一种请求和响应机制,用于传输网页、图像和其他资源。 ***** Telnet:使用命令行Shell远程访问机器的协议。 [https://www.wireshark.org/ Wireshark]是可以帮助你的首选工具,它是一个免费的网络嗅探器和分析器。 因为它非常详细地解释了数据包的每个字节对应于什么,可以用来理解各种网络协议是如何编码的,。 请注意,在Windows上,Wireshark不会捕获环回(loopback)流量(即从本地主机到本地主机的流量),因此可能不会捕获模拟器和主机之间的网络流量。 但是,你可以使用Rawcap将网络流量捕获到文件中,并使用Wireshark对其进行检查。 ==网络栈== 网络协议被组织成一个栈,每一层调用下一层。 通过网络发送的数据包将由多个报头组成,每一层对应一个报头。 考虑DHCP请求的例子。 这是你可能希望尽早实施的协议之一,因为它允许你的机器查找其IP地址,获取本地路由器IP地址,DNS IP地址- 这样就能够获得通过网络正确通信的基本信息。 实现这些的一种方法如下: * 操作系统决定发送DHCP请求,因此调用DHCP层 ** DHCP层要求UDP层创建一个数据包,其目标是IP地址255,255,255,255(广播到整个本地网络)、端口53,有效负载大小为300字节(长度可能不同) *** UDP层要求IP层创建一个UDP类型的数据包,地址为255,255,255,255,大小为308字节 **** IP层要求以太网层创建一个长度为328字节的IPv4类型的数据包,其目标是IP地址255,255,255,255 ***** 以太网层创建一个大小为342字节的数据包,并在前14个字节中写入以太网报头,包括源地址(机器的MAC地址)、目标MAC地址FF:FF:FF:FF:FF:FF(从IP地址255,255,255,255翻译过来)并将其发送回IP层 **** IP层将IP报头写入以太网报头后的20字节中,并将其发送到UDP层 *** UDP层在IP报头之后的8个字节中写入报头,并将其发送到DHCP层 ** DHCP层将其请求写入剩下的300字节,并将其发送回UDP层 *** UDP层通过写入校验和(包含DHCP消息)来完成报头,并将其发送到IP层 **** IP层将其发送到以太网层 ***** 以太网层将数据包发送到以太网卡,以太网卡通过网络发送消息 通过网络实际发送的数据包如下所示: {| {{wikitable}} |- |以太网报头(14字节) |- |IPv4报头(20字节) |- |UDP报头(8字节) |- |DHCP请求(300字节有效负载) |} DHCP响应的格式与请求的格式相同,应按如下方式处理: * 以太网卡驱动程序将验证目标MAC是当前机器的,如果是,则将数据包发送到以太网层 * 以太网层将查看以太网报头,检查服务类型(应该是IP),并将数据包(去掉以太网报头)发送到IP层 * IP层将检查IP报头,验证校验和,因为其类型是UDP,所以将数据包(不带IP报头)转发到UDP层。 * UDP层将检查UDP报头,验证校验和,并根据目标端口将有效负载发送到正确的服务——在本例中是DHCP层(再次剥离UDP报头) * DHCP层将读取DHCP消息,验证消息类型是否为响应(即,它是来自路由器的响应),并将检索其IP地址、路由器IP地址和其他网络配置信息。 请注意,根据定义,网络协议是异步的,即在网络上发送请求,需要等待其响应。 特别是,如果有回应,也你无法预测什么时候会有回应。 由于传入的数据包由中断处理程序处理,因此它可以随时中断你的代码。 ==小端和大端== 按照惯例,任何在互联网上编码的消息都使用big-endian(最高有效字节排在第一位)。 对于在英特尔和AMD处理器上开发的人来说,这是一件需要时刻牢记的事情,因为x86处理器使用little endian编码数字。 因此,你必须经常转换数字。 以下两个函数用于将endian转换为16位和32位整数: uint16_t switch_endian16(uint16_t nb) { return (nb>>8) | (nb<<8); } uint_t switch_endian32(uint_t nb) { return ((nb>>24)&0xff) | ((nb<<8)&0xff0000) | ((nb>>8)&0xff00) | ((nb<<24)&0xff000000); } ==校验和(Checksums)== 一些网络协议使用校验和来验证消息在传输过程中是否被意外更改。 如果没有有效的校验和,数据包可能会被忽略。 校验和是一个16位数字,可以使用的计算方式如下: *将消息拆分为16位的校验和块 * 累加那些块 * 如果消息的字节数为奇数,则最后一个字节应计为较高的字节,以得到一个完整16位的数字(例如,如果最后一个字节为0x42,则添加0x4200) * 如果总和不适合16位数字(即大于0xFFFF),则去掉前16位并将其添加到低16位。 重复最后一步,直到得到16位和 * 返回该和的二进制反转 IP校验和只覆盖它自己的报头。 UDP和TCP校验和比较复杂,因为它们包括UDP/TCP报头、有效负载(即UDP/TCP报头之后的任何内容)以及由源和目标IP地址、IP类型(0x11代表UDP,0x06代表TCP)和UDP/TCP消息长度(从UDP/TCP报头开始)组成的“伪报头”。 正确计算校验和可能很棘手,Wireshark可以帮助你。 为此,通过进入编辑/首选项/协议,确保它正在验证校验和(默认情况下未启用该选项),选择所需的协议(例如UDP、TCP、IPv4),并确保选中“如果可能,验证校验和”。 这样,Wireshark将告诉你校验和是否有效,如果无效,其值应该是多少。 == ARP == ARP协议将是你需要实现的首批协议之一。 没有它,你将无法在本地网络上进行通信,更不用说在互联网上了。 幸运的是,这是一个简单的协议,只需要实现几个功能: * 发送请求和处理回复:你的操作系统需要执行一个请求,将IP地址转换成MAC地址,这是与你的本地路由器通信所必需的。 这意味着不仅要发送一个请求包,还要在收到回复时进行处理,以便操作系统可以更新其ARP表 *接收请求并发送回复:你的操作系统还需要能应答按其方式发送的请求(例如,当有人询问其MAC地址时)。 特别是,本地路由器会定期向你的机器发送ARP请求。 如果没有响应,路由器会认为你的机器坏了,不会再转发更多的流量了。 == TCP == TCP是最复杂的网络协议之一。 首先,它在客户端和服务器之间创建了一个虚拟连接。 为了实现这一点,TCP报头包含多个标志,双方将使用这些标志来通信该连接的状态:SYN(同步 synchronize)、ACK(确认 acknowledge)、PSH(推送 Push)、FIN(完成 finish)和其他标志。 除此之外,TCP还试图解决这样一个困境:IP不能保证数据包按发送顺序被接收,更不用说完整接收了。 这就是为什么它跟踪实际发送的数据量,要求各方定期确认他们收到的数据,并在需要时重新发送数据包的原因。 为此,TCP报头包含一个序列号和一个确认号。 在TCP连接过程中,双方都可以互相发送一些数据,这些数据分为多个数据包。 考量当前通信到什么阶段的一种方法是发送该通信中的位置(按字节数)。 TCP数据包中的序列号(sequence number)是当前数据包所在的位置。 同样,确认号(acknowledgement number)表示一方希望另一方发送的位置(仍以字节为单位)。 当任何一方接收到序列号为S、确认号为A、负载大小为N的TCP数据包时,其发送的下一个数据包应具有序列号A(即发送另一方期望的数据)和确认号S+N(如果N为空,则为S+1)。 ===建立连接=== 通过以下三次握手建立TCP连接: * 客户端向服务器发送SYN请求(即设置了SYN标志的消息) * 服务器以SYN+ACK请求进行响应(该标准还允许它分别发送ACK和SYN,尽管这种情况很少发生)。 * 客户端发送ACK响应。 SYN包中使用的序列号是初始序列号; 所有进一步的数据包应使用初始序列号的增量序列号。 序列号可以通过发送带有新序列号的新SYN数据包来重置。 初始SYN和SYN+ACK数据包“可能”还包含要发送到应用程序的数据,但很少使用。 TCP规范规定,在建立连接之前(即,在接收到最终ACK响应数据包之后),不得将该数据发送到应用程序。 ===传输数据=== 要发送数据,任何一方都可以发送PSH、ACK消息,实际数据在TCP报头之后。 另一方需要发送ACK消息以确认其已收到数据包。 如果没有,发送方将再次发送数据包。 这就是一些TCP/IP实现的不同之处 - 有些可能会在发送ACK之前等待或多或少的时间。 ===关闭连接=== 连接的终止: * 想要关闭连接的一方发送一个带有FIN标志的数据包 * 另一方发送FIN,ACK消息 * 一开始主动关闭发送确认消息 与用于建立连接的数据包一样,这些数据包不包含任何有效负载,只包含TCP报头。 ===一个例子=== 让我们来看一个HTTP GET请求的TCP通信示例: {| {{wikitable}} |- ! 源->目的地 ! 目的地->源 ! 说明 |- | Flag: SYN seq_nb=0, ack_nb=0 | |TCP握手的开始。 它正在发送字节 #0(没有有效负载的数据包将被视为至少有一个字节的通信),并且尚未从服务器接收任何数据 |- | | Flags: SYN, ACK seq_nb=0, ack_nb=1 | |- | Flag: ACK seq_nb=1, ack_nb=1 | |TCP握手完成后,可以开始通信 |- | Flags: PSH, ACK seq_nb=1, ack_nb=1, len=77 | | 这是客户端发送的HTTP GET请求。这是第一个具有实际有效载荷的数据包 |- | | Flag: ACK seq_nb=1, ack_nb=78 | 服务器确认HTTP请求:它已成功读取字节#77,因此希望下一次通信从字节#78开始 |- | | Flags: PSH, ACK seq_nb=1, ack_nb=78, len=1009 |这是主体的HTML |- | Flag: ACK seq_nb=78, ack_nb=1010 | |客户端确认服务器发送的消息:它正在发送字节#78,并且已接收到字节#1009,因此希望下一次通信从字节#1010开始 |- | Flags: FIN, ACK seq_nb=78, ack_nb=1010 | |客户端终止TCP连接 |- | | Flags: FIN, ACK seq_nb=1010, ack_nb=79 | |- | Flag: ACK seq_nb=79, ack_nb=1011 | | TCP通信的结束 |} ==需要关注什么== 协议栈的形式会因设计决策而有所不同。这些决策点可能包括 * 数据包是否在一个缓冲区中的处理层之间传递,或者在通过层边界时是否复制到新的缓冲区; * 针对无论是入站帧还是出站帧,使用专用线程与链路层通信,该线程是都完全包含在中断处理程序中,还是单线程环境中的循环中; * 帧(如以太网帧)是立即处理还是排队处理; * 无论你想要TCP支持还是UDP支持,或者仅仅是IP支持;TCP是协议栈中最复杂的部分,在lwip实现中,有一半代码是特定于TCP的。 例如,一个协议栈可能 * 让NIC的API提供三个功能:设置NIC、轮询帧和发送帧; * 在一个线程中与NIC进行入帧和出帧通信; * 在另一个线程中从接收队列解编入站帧。 ==一般考虑事项== * 在以太网上编写协议栈时,你可能希望提供对ARP协议和解析功能的支持。 * 为了模块化,工作站的IP最好存储在nic_info结构中,而不是作为全局变量。 * 你也许会希望使用Wireshark或其他数据包嗅探器来检查通信和netcat,一旦你获得UDP或TCP支持,netcat将转储从操作系统发送的调试数据。 此外,在调试arp代码时,工具命令arping也很有用。 你也许需要编写一个触发器,例如,在收到ARP表示已为所选IP设置了地址时,重新启动你的网络系统。 * 你可以在一台计算机上使用专用以太网卡,通过交叉电缆连接到另一台计算机(哪里运行着你自制的操作系统),并使用静态IP。 其他可选的办法包括在为其提供的网络设备实现驱动程序后,在bochs或qemu下进行测试。 ==另见== ===文章=== * [[Diskless Booting|无盘启动]] * [[:Category:Network_Hardware|网络硬件]] ===论坛主题=== ===外部链接=== * [http://focus.ti.com/lit/an/slaa137a/slaa137a.pdf 有关嵌入式web服务器实现的详细信息,包括硬件、TCP/IP套件、TCP/IP栈及其API的概述。] * [http://www.faqs.org/rfcs/rfc793.html RFC 793 - Transmission Control Protocol] * [http://www.amazon.com/TCP-Illustrated-Protocols-Addison-Wesley-Professional/dp/0201633469/ref=pd_bbs_sr_1?ie=UTF8&s=books&qid=1229662804&sr=8-1 TCP/IP Illustrated - 任何网络类型的必备书,很棒的参考书] 许多tcp/ip协议栈都附带了它们的实现文档;读起来不错。 * [http://www.cs.northwestern.edu/~pdinda/minet/ Minet: A User-level TCP/IP Stack] * [http://www.sics.se/~adam/uip/index.php/Main_Page uIP tiny stack for 8-bit embedded microcontrollers] * [https://lwip.fandom.com/wiki/LwIP_Wiki lwIP轻量级实现] [[Category:Networking]]
本页使用的模板:
模板:In Progress
(
查看源代码
)
模板:Wikitable
(
查看源代码
)
返回至“
Network Stack
”。
导航菜单
个人工具
登录
命名空间
页面
讨论
变体
已展开
已折叠
查看
阅读
查看源代码
查看历史
更多
已展开
已折叠
搜索
导航
首页
最近更改
随机页面
MediaWiki帮助
工具
链入页面
相关更改
特殊页面
页面信息