TCP/IP 协议栈实现 Dnet - Ep.1 以太网数据帧的获取

 

本系列 Blog 的初衷是通过自己实现一个 TCP/IP 协议栈的形式来简明易懂地介绍计算机网络相关的知识, 更多细节详见本系列 Ep.0. 相关代码实现开源在 https://github.com/lyc8503/DNet-core.

既然决定开始实现网络协议, 应该从哪里开始呢? 显然, 是物理上的连接到网络开始. 我们的计算机需要通过一些媒介与外界交换信息.

现在这种连接媒介有很多, 比如连接 RJ45 接口的网线, 连接 Wi-Fi, 使用蜂窝网络等. 电脑中二进制的比特会被以特定方式编码成为电, 光等不同形式的信号, 在不同类型的线缆(或是空气)中进行传播. 本系列更在意的是软件层面的实现, 所以不再赘述, 可以参考相关教材(比如经典的这本).


Ep.0 里面已经立了一个很大也很虚无缥缈的 Flag - 实现 TCP/IP 协议栈. 这里再说的详细一些: 我们打算在 Linux 平台用户态使用C++ 语言实现一个网络栈. 也就是说, 最终我们的程序会以一个普通应用程序(而不是内核模块)的形式运行在系统中.

我们马上就发现了问题: 我们的软件是在用户态的, 根本没有办法直接与计算机上的各类物理硬件直接交互, 只能使用一些操作系统 API 委托操作系统完成想要做的工作.


于是我们被迫开始和 Linux API 打交道了…

Linux 有一种叫 TAP虚拟网络设备, 它就相当于一张虚拟网卡, 除了没有实体硬件基本和真实的网卡一致.

我们按照上面链接中文档的做法在 Linux 中创建一张虚拟网卡, 然后通过程序从虚拟网卡中读取或发送数据, 代码如下.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct ifreq ifr{};

if ((fd = open("/dev/net/tap", O_RDWR)) < 0) {
DNET_ERROR("Failed to open TAP device, check permissions or create one with `mknod /dev/net/tap c 10 200`.");
return false;
}

ifr.ifr_flags = IFF_TAP | IFF_NO_PI;
strncpy(ifr.ifr_name, this->dev, IFNAMSIZ);

if (ioctl(fd, TUNSETIFF, (void *) &ifr) < 0) {
DNET_ERROR("Could not ioctl tun: %s", strerror(errno));
close(fd);
return false;
}

/* if the operation was successful, write back the name of the
* interface to the variable "dev". */
strcpy(this->dev, ifr.ifr_name);

我们创建并打开对应的虚拟网络设备后, 得到了对应的文件描述符, 就可以进行对应的读写操作了.

但这时我们的网卡还没有启动, 我们还要启动一下网卡(同时设置一下路由表, 路由表具体是什么会在之后解释).

1
2
ip link set dev dnet0 up
ip route add dev dnet0 10.0.0.0/24

完成后, 我们可以尝试 ping 10.0.0.1, 可以发现每次 ping 都可以从程序中读取到输入.


把上述代码整理封装后, 写成一个类 https://github.com/lyc8503/DNet-core/blob/main/driver/driver.cpp

同时为了以后的需要加入了一些以多线程&回调方式读取数据的方法.

现在对于我们来说读取到的数据还暂时只是一些意义不明的二进制数据, 接下来我们会尝试解析这些数据.

而我们目前完成的内容就相当于虚拟了 OSI 七层模型中的物理层, 或是 TCP/IP协议栈中的网络访问层的一部分.

本文采用 CC BY-NC-SA 4.0 许可协议发布.

作者: lyc8503, 文章链接: https://blog.lyc8503.net/post/tcpip-1-linux-driver/
如果本文给你带来了帮助或让你觉得有趣, 可以考虑赞助我¬_¬