以太网编程实践(C语言)

本节的目标是,实现一个命令 send_ether ,用于通过网卡发送以太网数据帧。我们将从最基础的知识开始,一步步朝着目标努力。

send_ether 在前面章节已经用过,并不陌生,基本用法如:表格-1。

表格-1 命令行选项
选项含义
-i –iface发送网卡名
-t –to目的MAC地址
-T –type类型
-d –data待发送数据

下面是一个命令行执行实例:

  1. $ send_ether -i enp0s8 -t 0a:00:27:00:00:00 -T 0x1024 -d "Hello, world!"

处理命令行参数

我们要解决的第一问题是,如何获取命令行选项。在 C 语言中,命令行参数通过 main 函数参数 argc 以及 argv 传递:

  1. int main(int argc, char *argv[]);

以上述命令为例,程序 main 函数获得的参数等价于:

  1. int argc = 9;
  2. char *argv[] = {
  3. "send_ether",
  4. "-i",
  5. "enp0s8",
  6. "-t",
  7. "0a:00:27:00:00:00",
  8. "-T",
  9. "0x1024",
  10. "-d",
  11. "Hello, world!",
  12. };

这时,你可能要开始对 argv 进行解析,各种判断 -i-t 啦。当然了,如果是学习或者编程练习,这样做是可以的,编程的诀窍就是勤练习嘛。

但更推荐的方式是,站在巨人的肩膀上——使用 GNU 提供的 Argp 。下面以解析 send_ether 参数为例,介绍 Argp 的用法。

首先,定义一个结构体 arguments 用于存放解析结果,结构体包含 ifacetotype 以及 data 总共 4 个字段:

/_src/c/ethernet/send_ether.c

  1. /**
  2. * struct for storing command line arguments.
  3. **/
  4. struct arguments {
  5. // name of iface through which data is sent
  6. char const *iface;
  7. // destination MAC address
  8. char const *to;
  9. // data type
  10. unsigned short type;
  11. // data to send
  12. char const *data;
  13. };

接着,实现一个选项处理函数 opt_handlerArgp 每成功解析出一个命令行选项,将调用该函数进行处理:

/_src/c/ethernet/send_ether.c

  1. /**
  2. * opt_handler function for GNU argp.
  3. **/
  4. static error_t opt_handler(int key, char *arg, struct argp_state *state) {
  5. struct arguments *arguments = state->input;
  6. switch(key) {
  7. case 'd':
  8. arguments->data = arg;
  9. break;
  10. case 'i':
  11. arguments->iface = arg;
  12. break;
  13. case 'T':
  14. if (sscanf(arg, "%hx", &arguments->type) != 1) {
  15. return ARGP_ERR_UNKNOWN;
  16. }
  17. break;
  18. case 't':
  19. arguments->to = arg;
  20. break;
  21. default:
  22. return ARGP_ERR_UNKNOWN;
  23. }
  24. return 0;
  25. }

其中,参数 key 是命令行选项配置键,一般为短选项值;参数 arg 是选项参数值(如果有);参数 state 是解析上下文,从中可以取到存放解析结果的结构体 arguments 。处理函数逻辑非常简单,根据解析到选项,将参数值存放到 arguments 结构体。

最后,实现一个解析函数 parse_arguments ,接收参数 argc 以及 argv ,返回解析结果: arguments

/_src/c/ethernet/send_ether.c

  1. /**
  2. * Parse command line arguments given by argc, argv.
  3. *
  4. * Arguments
  5. * argc: the same with main function.
  6. *
  7. * argv: the same with main function.
  8. *
  9. * Returns
  10. * Pointer to struct arguments if success, NULL if error.
  11. **/
  12. static struct arguments const *parse_arguments(int argc, char *argv[]) {
  13. // docs for program and options
  14. static char const doc[] = "send_ether: send data through ethernet frame";
  15. static char const args_doc[] = "";
  16. // command line options
  17. static struct argp_option const options[] = {
  18. // Option -i --iface: name of iface through which data is sent
  19. {"iface", 'i', "IFACE", 0, "name of iface for sending"},
  20. // Option -t --to: destination MAC address
  21. {"to", 't', "TO", 0, "destination mac address"},
  22. // Option -T --type: data type
  23. {"type", 'T', "TYPE", 0, "data type"},
  24. // Option -d --data: data to send, optional since default value is set
  25. {"data", 'd', "DATA", 0, "data to send"},
  26. { 0 }
  27. };
  28. static struct argp const argp = {
  29. options,
  30. opt_handler,
  31. args_doc,
  32. doc,
  33. 0,
  34. 0,
  35. 0,
  36. };
  37. // for storing results
  38. static struct arguments arguments = {
  39. .iface = NULL,
  40. .to = NULL,
  41. //default data type: 0x0900
  42. .type = 0x0900,
  43. // default data, 46 bytes string of 'a'
  44. // since for ethernet frame data is 46 bytes at least
  45. .data = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
  46. };
  47. argp_parse(&argp, argc, argv, 0, 0, &arguments);
  48. return &arguments;
  49. }

解析函数执行以下步骤:

  • 定义程序文档 doc 以及位置参数文档 args_doc ,用于参数解析失败时输出程序用法提示用户;
  • 定义命令行选项配置,总共 4 个选项,配置的每个字段含义见表格-2;
  • 申明结构体 argp 用于存放先前定义的各种配置;
  • 申明结构体 arguments 用于存放解析结构,并填充默认值;
  • 调用库函数 argp_parse 进行解析,参数请参考 Argp 文档;
表格-2 选项配置字段含义
字段名含义
name选项名,一般为长选项
key选项键,一般为短选项
arg
flags选项标志位,OPTION_ARG_OPTIONAL表示可选
doc选项文档(用法描述)
group选项组,这里省略

这样,在 main 函数里,只需要调用 parse_arguments 便可获得解析结果。如果,用户给出了错误的选项,程序将输出提示信息并退出。解决方案很完美!

以太网帧

接下来,重温 以太网帧 ,看到下图应该不难回忆:

../_images/97c13f044de260baf0ed8051091dd251.png以太网帧:目的地址、源地址、类型、数据、校验和

从数学的角度,重新审视以太网帧结构:每个字段有固定或不固定的 长度 ,单位为字节。字段开头与帧开头之间的距离称为 偏移量 ,第一个字段偏移量是 0 ,后一个字段偏移量是前一个字段偏移量加长度,依次类推。

各字段 长度 以及 偏移量 列举如下:

表格-3 以太网帧字段长度及偏移量
字段长度(字节)偏移量(字节)
目的地址60
源地址66
类型212
数据46-150014

在程序编写中,可能会经常用到这些常量。如果每次都直接使用数值,很考验记忆能力,出错是迟早的事情。

C 语言中,可以用 宏定义 将这些常量固化下来。定义一次,无限使用:

以太网宏定义

  1. #define MAX_ETHERNET_DATA_SIZE 1500
  2. #define ETHERNET_HEADER_SIZE 14
  3. #define ETHERNET_DST_ADDR_OFFSET 0
  4. #define ETHERNET_SRC_ADDR_OFFSET 6
  5. #define ETHERNET_TYPE_OFFSET 12
  6. #define ETHERNET_DATA_OFFSET 14
  7. #define MAC_BYTES 6

转换MAC地址

mac_ntoa

函数 mac_ntoaMAC 地址由二进制形式转化成可读形式(冒分十六进制),形如 08:00:27:c8:04:83

void mac_ntoa(unsigned char n, char a)

  1. /**
  2. * Convert binary MAC address to readable format.
  3. *
  4. * Arguments
  5. * n: binary format, must be 6 bytes.
  6. *
  7. * a: buffer for readable format, 18 bytes at least(`\0` included).
  8. **/
  9. void mac_ntoa(unsigned char *n, char *a) {
  10. // traverse 6 bytes one by one
  11. for (int i=0; i<6; i++) {
  12. // format string
  13. char *format = ":%02x";
  14. // first byte without leading `:`
  15. if(0 == i) {
  16. format = "%02x";
  17. }
  18. // format current byte
  19. a += sprintf(a, format, n[i]);
  20. }
  21. }

参数 n 为二进制形式,长度为 6 字节;参数 a 为存放可读形式的缓冲区,长度至少为 18 字节(包含末尾 \0 字节)。

mac_ntoa 函数体,逐一遍历 MAC 地址 6 个字节,调用 C 库函数 sprintf 将字节十六进制输出到缓冲区。注意到,除了首字节,需要额外输出前缀冒号 :

mac_aton

可读形式转化为二进制形式稍微有点复杂,因为需要做合法性检查。08:00:27:c8:04:83 是一个合法的 MAC 地址,而 08:00:27:c8:04:8g 就不是( g 超出十六进制范围),08-00-27-c8-04-83 也不是(不是冒号 : 分隔)。

因此,需要先判断一个字符是不是合法的十六进制字符,可以通过一个宏解决:

IS_HEX(c)

  1. #define IS_HEX(c) ( \
  2. (c) >= '0' && (c) <= '9' || \
  3. (c) >= 'a' && (c) <= 'f' || \
  4. (c) >= 'A' && (c) <= 'F' \
  5. )

十六进制字符必须在 09 之间,或者 af 之间,或者 AF 之间。宏 IS_HEX 就是上述定义的程序语言表达,看似很长很复杂,其实很简单。

那么,两个字节的可读十六进制如何转换成其表示的原始字节呢?以 c8 为例,需要转换成字节 0xc8 ,计算方式如下:

  1. 0xc8 == 12 * 16 + 8 == (12 << 4) | 8

那么,从字符 c 如何得到数值 12 呢?计算方式如表格-4(有所省略):

表格-4 十六进制字符数值计算方式
字符数值计算方式
‘0’0‘0’ - ‘0’
‘1’1‘1’ - ‘0’
‘A’10‘A’ - ‘A’ + 10
‘B’11‘B’ - ‘A’ + 10
‘a’10‘a’ - ‘a’ + 10
‘b’11‘b’ - ‘a’ + 10

现在,可以通过一个宏 HEX 来完成十六进制字符到数值的转换,定义如下:

HEX(c)

  1. #define HEX(c) ( \
  2. ((c) >= 'a') ? ((c) - 'a' + 10) : ( \
  3. ((c) >= 'A') ? ((c) - 'A' + 10) : ((c) - '0') \
  4. ) \
  5. )

需要注意,需要先判断是否是小写字符,大写字母次之,数字最后,因为三者在 ASCII 表就是这个顺序。有了宏 HEX 之后,转换不费吹灰之力:

  1. (HEX(high_byte) << 4) | HEX(low_byte)

注意到,这里使用位运算代替乘法以及加法,因为位运算更高效。

做了这么多准备,终于可以操刀 mac_aton 函数了:

int mac_aton(const char a, unsigned char n)

  1. /**
  2. * Convert readable MAC address to binary format.
  3. *
  4. * Arguments
  5. * a: buffer for readable format, like "08:00:27:c8:04:83".
  6. *
  7. * n: buffer for binary format, 6 bytes at least.
  8. *
  9. * Returns
  10. * 0 if success, -1 or -2 if error.
  11. **/
  12. int mac_aton(const char *a, unsigned char *n) {
  13. for (int i=0; i<6; i++) {
  14. // skip the leading ':'
  15. if (i > 0) {
  16. // unexpected char, expect ':'
  17. if (':' != *a) {
  18. return -1;
  19. }
  20. a++;
  21. }
  22. // unexpected char, expect 0-9 a-f A-f
  23. if (!IS_HEX(a[0]) || !IS_HEX(a[1])) {
  24. return -2;
  25. }
  26. *n = ((HEX(a[0]) << 4) | HEX(a[1]));
  27. // move to next place
  28. a += 2;
  29. n++;
  30. }
  31. return 0;
  32. }

参数 a 是可读形式,形如 08:00:27:c8:04:83 ,至少 18 字节(末尾 \0 );参数 n 是用于存储二进制形式的缓冲区,需要 6 字节。

函数体执行 6 次循环,每次处理一个字节。第一个字节之后,需要检查冒号 : 并跳过。转换前,先检查高低两个字节是否都是合法十六进制。转换时,调用刚刚讨论的转换算法,并移动缓冲区。

当然了,用通过 C 库函数,一行代码就可以完成转换过程:

int mac_aton(const char a, unsigned char n)

  1. /**
  2. * Convert readable MAC address to binary format.
  3. *
  4. * Arguments
  5. * a: buffer for readable format, like "08:00:27:c8:04:83".
  6. *
  7. * n: buffer for binary format, 6 bytes at least.
  8. *
  9. * Returns
  10. * 0 if success, -1 if error.
  11. **/
  12. int mac_aton(const char *a, unsigned char *n) {
  13. int matches = sscanf(a, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx", n, n+1, n+2,
  14. n+3, n+4, n+5);
  15. return (6 == matches ? 0 : -1);
  16. }

弄清来龙去脉之后,使用库函数是不错的:①开发效率更高;②代码更健壮。mac_ntoa 函数也可以用一行代码完成,留作读者练习。

获取网卡地址

发送以太网帧,我们需要 目的地址源地址类型 以及 数据目的地址 以及 数据 分别由命令行参数 -t 以及 -d 指定。那么, 源地址 从哪来呢?

别急, -i 参数不是指定发送网卡名吗?——发送网卡物理地址就是 源地址 !现在的问题是,如何获取网卡物理地址?

Linux 下可以通过 ioctl 系统调用获取网络设备信息,request 类型是 SIOCGIFHWADDR 。下面,写一个程序 show_mac ,演示查询网卡物理地址的方法。show_mac 需要接收一个参数,以指定待查询网卡名:

  1. $ show_mac enp0s8
  2. IFace: enp0s8
  3. MAC: 08:00:27:c8:04:83

show_mac 程序源码如下:

/_src/c/ethernet/show_mac.c

  1. /**
  2. * FileName: show_mac.c
  3. * Author: Chen Yanfei
  4. * @contact: fasionchan@gmail.com
  5. * @version: $Id$
  6. *
  7. * Description:
  8. *
  9. * Changelog:
  10. *
  11. **/
  12. #include <net/if.h>
  13. #include <stdio.h>
  14. #include <string.h>
  15. #include <sys/ioctl.h>
  16. #include <sys/socket.h>
  17. /**
  18. * Convert binary MAC address to readable format.
  19. *
  20. * Arguments
  21. * n: binary format, must be 6 bytes.
  22. *
  23. * a: buffer for readable format, 18 bytes at least(`\0` included).
  24. **/
  25. void mac_ntoa(unsigned char *n, char *a) {
  26. // traverse 6 bytes one by one
  27. for (int i=0; i<6; i++) {
  28. // format string
  29. char *format = ":%02x";
  30. // first byte without leading `:`
  31. if(0 == i) {
  32. format = "%02x";
  33. }
  34. // format current byte
  35. a += sprintf(a, format, n[i]);
  36. }
  37. }
  38. int main(int argc, char *argv[]) {
  39. // create a socket, any type is ok
  40. int s = socket(AF_INET, SOCK_STREAM, 0);
  41. if (-1 == s) {
  42. perror("Fail to create socket");
  43. return 1;
  44. }
  45. // fill iface name to struct ifreq
  46. struct ifreq ifr;
  47. strncpy(ifr.ifr_name, argv[1], 15);
  48. // call ioctl to get hardware address
  49. int ret = ioctl(s, SIOCGIFHWADDR, &ifr);
  50. if (-1 == ret) {
  51. perror("Fail to get mac address");
  52. return 2;
  53. }
  54. // convert to readable format
  55. char mac[18];
  56. mac_ntoa((unsigned char *)ifr.ifr_hwaddr.sa_data, mac);
  57. // output result
  58. printf("IFace: %s\n", ifr.ifr_name);
  59. printf("MAC: %s\n", mac);
  60. return 0;
  61. }

程序先定义函数 mac_ntoa 用于将 MAC 地址从二进制形式转换成可读形式,浅析网卡地址一节介绍过,不再赘述。

接着是程序入口 main 函数,主体逻辑如下:

  • 创建一个套接字,类型不限( 47 - 51 行);
  • 将待查询网卡名填充到 ifreq 结构体( 54 - 55 行);
  • 调用 ioctl 系统调用查询网卡物理地址( SIOCGIFHWADDR ),内核将物理地址填充到 ifreq 结构体( 58 - 62 行);
  • ifreq 结构体取出 MAC 地址并转换成可读形式( 65 - 66 行);
  • 输出结果( 69 - 70 行);

编译

好了,程序编写完成!那么,怎么让程序代码跑起来呢?对于 C 语言,需要先将源代码编译成可执行程序,方可执行。Linux 下,可以使用 gcc 来编译代码:

  1. fasion@ubuntu:~/lnp$ ls
  2. _build c docs python README.md
  3. fasion@ubuntu:~/lnp$ cd c/ethernet/
  4. fasion@ubuntu:~/lnp/c/ethernet$ ls
  5. send_ether.c show_mac.c
  6. fasion@ubuntu:~/lnp/c/ethernet$ gcc -o show_mac show_mac.c
  7. fasion@ubuntu:~/lnp/c/ethernet$ ls
  8. send_ether.c show_mac show_mac.c
  9. fasion@ubuntu:~/lnp/c/ethernet$ ./show_mac enp0s8
  10. IFace: enp0s8
  11. MAC: 08:00:27:c8:04:83

如上,主要步骤包括:

  • 进入源码 show_mac.c 所在目录 c/ethernet/ ( 3 行);
  • 运行 gcc 命令编译程序, -o 指定生成可执行文件名,( 6 行);
  • 运行程序 show_mac ( 9 行);

代码复用

更进一步,可以将代码重构成获取网卡地址的通用函数 fetch_iface_mac ,以便在后续的开发中复用:

/_src/c/ethernet/send_ether.c

  1. /**
  2. * Fetch MAC address of given iface.
  3. *
  4. * Arguments
  5. * iface: name of given iface.
  6. *
  7. * mac: buffer for binary MAC address, 6 bytes at least.
  8. *
  9. * s: socket for ioctl, optional.
  10. *
  11. * Returns
  12. * 0 if success, -1 if error.
  13. **/
  14. int fetch_iface_mac(char const *iface, unsigned char *mac, int s) {
  15. // value to return, 0 for success, -1 for error
  16. int value_to_return = -1;
  17. // create socket if needed(s is not given)
  18. bool create_socket = (s < 0);
  19. if (create_socket) {
  20. s = socket(AF_INET, SOCK_DGRAM, 0);
  21. if (-1 == s) {
  22. return value_to_return;
  23. }
  24. }
  25. // fill iface name to struct ifreq
  26. struct ifreq ifr;
  27. strncpy(ifr.ifr_name, iface, 15);
  28. // call ioctl to get hardware address
  29. int ret = ioctl(s, SIOCGIFHWADDR, &ifr);
  30. if (-1 == ret) {
  31. goto cleanup;
  32. }
  33. // copy MAC address to given buffer
  34. memcpy(mac, ifr.ifr_hwaddr.sa_data, MAC_BYTES);
  35. // success, set return value to 0
  36. value_to_return = 0;
  37. cleanup:
  38. // close socket if created here
  39. if (create_socket) {
  40. close(s);
  41. }
  42. return value_to_return;
  43. }

fetch_iface_mac 函数总共有 3 个参数:

  • iface :指定待查询网卡名;
  • mac :用于存放 MAC 地址的缓冲区,至少 6 字节;
  • s :套接字,可以复用已有实例,避免创建开销;

注解

如果没有现成套接字可用,可以给 s 参数传特殊值 -1 。函数将创建临时套接字,用完销毁。这个套路在其他函数封装中也会用到,后续不再赘述。

接下来,看看 fetch_iface_mac 函数体部分,逻辑与 show_main 程序 main 函数类似。注意到,在函数开头,需要视情况创建临时套接字。在函数结尾处,需要对临时套接字进行回收。套接字创建后,后续系统调用如果失败,函数需要提前返回,千万别忘了回收临时套接字!函数 fetch_iface_mac 中,使用 goto ( 34 行)将程序逻辑跳转到资源回收处,这个套路在 C 语言中也算经典。

好了, fetch_iface_mac 函数开发大功告成!在接下来的开发中,我们将看到 代码复用 的强大威力!

发送以太网帧

Linux 下,发送以太网帧,需要通过原始套接字。创建一个类型为 SOCK_RAW 的套接字,与发送网卡进行绑定,便可发送数据了。

先来看看套接字如何与发送网卡绑定:

int bind_iface(int s, char const *iface)

  1. /**
  2. * Bind socket with given iface.
  3. *
  4. * Arguments
  5. * s: given socket.
  6. *
  7. * iface: name of given iface.
  8. *
  9. * Returns
  10. * 0 if success, -1 if error.
  11. **/
  12. int bind_iface(int s, char const *iface) {
  13. // fetch iface index
  14. int if_index = fetch_iface_index(iface, s);
  15. if (-1 == if_index) {
  16. return -1;
  17. }
  18. // fill iface index to struct sockaddr_ll for binding
  19. struct sockaddr_ll sll;
  20. bzero(&sll, sizeof(sll));
  21. sll.sll_family = AF_PACKET;
  22. sll.sll_ifindex = if_index;
  23. sll.sll_pkttype = PACKET_HOST;
  24. // call bind system call to bind socket with iface
  25. int ret = bind(s, (struct sockaddr *)&sll, sizeof(sll));
  26. if (-1 == ret) {
  27. return -1;
  28. }
  29. return 0;
  30. }

bind_iface 函数接收两个参数: s 是待绑定套接字, iface 是发送网卡名。

通过 bind 系统调用将套接字与发送网卡绑定,但不能直接用网卡名,需要先获取网卡序号( ifindex )。获取网卡序号套路与 获取网卡地址 类似,这里不再赘述。

最后,再来看看 send_ether 函数:

int send_ether(char const iface, unsigned char const to, short type, char const *data, int s)

  1. /**
  2. * Send data through given iface by ethernet protocol, using raw socket.
  3. *
  4. * Arguments
  5. * iface: name of iface for sending.
  6. *
  7. * to: destination MAC address, in binary format.
  8. *
  9. * type: protocol type.
  10. *
  11. * data: data to send, ends with '\0'.
  12. *
  13. * s: socket for ioctl, optional.
  14. *
  15. * Returns
  16. * 0 if success, -1 if error.
  17. **/
  18. int send_ether(char const *iface, unsigned char const *to, short type,
  19. char const *data, int s) {
  20. // value to return, 0 for success, -1 for error
  21. int value_to_return = -1;
  22. // create socket if needed(s is not given)
  23. bool create_socket = (s < 0);
  24. if (create_socket) {
  25. s = socket(PF_PACKET, SOCK_RAW | SOCK_CLOEXEC, 0);
  26. if (-1 == s) {
  27. return value_to_return;
  28. }
  29. }
  30. // bind socket with iface
  31. int ret = bind_iface(s, iface);
  32. if (-1 == ret) {
  33. goto cleanup;
  34. }
  35. // fetch MAC address of given iface, which is the source address
  36. unsigned char fr[6];
  37. ret = fetch_iface_mac(iface, fr, s);
  38. if (-1 == ret) {
  39. goto cleanup;
  40. }
  41. // construct ethernet frame, which can be 1514 bytes at most
  42. unsigned char frame[1514];
  43. // fill destination MAC address
  44. memcpy(frame + ETHERNET_DST_ADDR_OFFSET, to, MAC_BYTES);
  45. // fill source MAC address
  46. memcpy(frame + ETHERNET_SRC_ADDR_OFFSET, fr, MAC_BYTES);
  47. // fill type
  48. *((short *)(frame + ETHERNET_TYPE_OFFSET)) = htons(type);
  49. // truncate if data is to long
  50. int data_size = strlen(data);
  51. if (data_size > MAX_ETHERNET_DATA_SIZE) {
  52. data_size = MAX_ETHERNET_DATA_SIZE;
  53. }
  54. // fill data
  55. memcpy(frame + ETHERNET_DATA_OFFSET, data, data_size);
  56. int frame_size = ETHERNET_HEADER_SIZE + data_size;
  57. ret = sendto(s, frame, frame_size, 0, NULL, 0);
  58. if (-1 == ret) {
  59. goto cleanup;
  60. }
  61. // set return value to 0 if success
  62. value_to_return = 0;
  63. cleanup:
  64. // close socket if created here
  65. if (create_socket) {
  66. close(s);
  67. }
  68. return value_to_return;
  69. }

函数主要逻辑如下:

  • 创建套接字,类型为 SOCK_RAW ( 26 行);
  • 调用 bind_iface 函数绑定发送网卡( 33 行);
  • 分配 char 数组用于填充待发送数据帧( 47 行);
  • 根据字段偏移量填充数据帧,数据必要时截断( 48 - 64 行);
  • 计算数据帧总长度( 66 行);
  • 调用 sendto 系统调用发送数据帧( 68 行);整个程序代码有点长,就不在这里贴了,请在 GitHub 上查看:c/ethernet/others/send_ether.v1.c

数据帧封装

我们可以进一步优化,将 以太网帧 封装成一个结构体:

struct ethernet_frame

  1. /**
  2. * struct for an ethernet frame
  3. **/
  4. struct ethernet_frame {
  5. // destination MAC address, 6 bytes
  6. unsigned char dst_addr[6];
  7. // source MAC address, 6 bytes
  8. unsigned char src_addr[6];
  9. // type, in network byte order
  10. unsigned short type;
  11. // data
  12. unsigned char data[MAX_ETHERNET_DATA_SIZE];
  13. };

这样一来,帧字段与结构体字段一一对应,更加清晰。而且,填充以太网帧不需要手工指定偏移量,只需填写结构体相关字段即可:

fill ethernet frame

  1. // construct ethernet frame, which can be 1514 bytes at most
  2. struct ethernet_frame frame;
  3. // fill destination MAC address
  4. memcpy(frame.dst_addr, to, MAC_BYTES);
  5. // fill source MAC address
  6. memcpy(frame.src_addr, fr, MAC_BYTES);
  7. // fill type
  8. frame.type = htons(type);
  9. // truncate if data is to long
  10. int data_size = strlen(data);
  11. if (data_size > MAX_ETHERNET_DATA_SIZE) {
  12. data_size = MAX_ETHERNET_DATA_SIZE;
  13. }
  14. // fill data
  15. memcpy(frame.data, data, data_size);

同样,全量代码可以在 GitHub 上查看:c/ethernet/send_ether.c

总结

本节,我们从 处理命令行参数 开始,重温 以太网帧 ,学习如何 转换MAC地址 以及如何 获取网卡地址 ,一步步实现终极目标: 发送以太网帧

此外,在 编译 小节,我们第一次编译并执行 C 语言程序。在 代码复用 小节,我们将零散的代码逻辑封装成可复用的通用函数,并涉猎一些 C 语言经典设计方式。由于篇幅有限,讲解点到即止,但也足以作为一个不错的起点。

下一步

本节以 C 语言为例,演示了以太网编程方法。如果你对其他语言感兴趣,请按需取用:

订阅更新,获取更多学习资料,请关注我们的 微信公众号

../_images/wechat-mp-qrcode.png小菜学编程

参考文献