netmap: UDP 协议栈的实现

news2024/11/26 17:39:58

文章目录

    • 1、获取以太网数据
      • 1.1、netmap 原理
      • 1.2、netmap 环境搭建
    • 2、udp 协议栈的实现
      • 2.1、以太网帧
      • 2.2、ip 协议
      • 2.3、udp 协议
      • 2.4、问题分析
    • 3、ARP 协议的实现
    • 4、icmp 协议的实现
    • 5、netmap 代码实现

1、获取以太网数据

自定义协议栈,需要获取原始的以太网数据,获取方式有:

  • raw socket 原始套接字
  • 实现一个网卡驱动 driver
  • 旁路:netmap dpdk
  • hook 机制:bpf, ebpf

这里以 netmap 为例。

1.1、netmap 原理

netmap 采用 mmap 的方式,将网卡驱动的 ring 内存空间映射到用户空间。这样用户态可以直接操作内存,获取原始的数据,避免了内核和用户态的两次拷贝(网卡 -> 内核协议栈 -> 内存)

在这里插入图片描述

1.2、netmap 环境搭建

安装 netmap

# 安装 netmap
git clone https://github.com/luigirizzo/netmap.git
cd netmap/LINUX
./configure
make && make install

# 将头文件拷贝到 /usr/include/net
cd ./netmap/sys/net/ # netmap 头文件位置
cp * /usr/include/net  

启动 netmap

# 开启 netmap
insmod netmap.ko 
ls /dev/netmap -l
# 关闭 netmap
rmmod netmap.ko

2、udp 协议栈的实现

2.1、以太网帧

在这里插入图片描述

// 以太网数据帧头,字节对齐: sizeof = 16
struct ethhdr {
	unsigned char dmac[ETH_ADDR_LENGTH];	 // 目的mac地址
	unsigned char smac[ETH_ADDR_LENGTH]; 	 // 源mac地址
	unsigned short protocol; 			    // 协议:上层协议的类型,ip:0x0800
};

2.2、ip 协议

在这里插入图片描述

// ip 数据包首部
struct iphdr {
    unsigned char version:4,    // ip协议版本,IPv4:0100
                hdrlen:4;       // 首部长度,* 4
    unsigned char tos;          // 服务类型
    unsigned short totlen;      // 总长度,* 1,最大65535字节,超过MTU(1500)分片
    unsigned short id;          // 标识,相同表示数据包来源于同一报文
    unsigned short flag:3,      // 标志,MF:more frag、DF:don't frag、未用
                flag_offset:13; // 片偏移,标识该数据包在上层数据报文中的偏移量
    unsigned char ttl;          // 生存时间 time to live,默认是64,避免环路
    unsigned char type;         // 协议,上层协议的类型,udp, tcp
    unsigned short check;       // 首部校验和
    unsigned int sip;           // 源ip
    unsigned int dip;           // 目的ip
};

2.3、udp 协议

在这里插入图片描述

// udp报文首部
struct udphdr {
    unsigned short sport;       // 源端口
    unsigned short dport;       // 目的端口
    unsigned short length;      // udp 报文长度
    unsigned short check;       // udp 校验
};

协议栈中用户数据经过逐层封装,增加各层的首部,得到 udp 数据报

在这里插入图片描述

// udp 报文,sizeof(struct udppkt) == 42
struct udppkt {
    struct ethhdr eh;       // 以太网帧首部
    struct iphdr ip;        // ip 首部
    struct udphdr udp;      // udp 首部
    
    unsigned char payload[0];  // 应用层数据,柔性数组(零长数组)
};

零长数组(柔性数组):柔性数组是定义结构体时创建一个空数组,运行时可以动态进行结构体的扩展。注意零长数组必须声明为结构体的最后一个成员,且不能作为结构体的唯一成员,sizefo返回的结构体的大小不包括柔性数组的内存。

那么,为什么使用柔性数组?下面两种定义方式存在问题:

// 1、指针分配的内存是不连续的,分配内存:结构体->指针,释放内存:指针->结构体
unsigned char* payload; 
// 2、若数据不够存储,发生越界,内存泄漏。
unsigned char* payload[65535];  

使用柔性数组的优势

  • 无需初始化,数组名就是偏移
  • 不占用空间
  • 空间一次分配,分配连续的内存

柔性数组的适用场景

  • 可以计算出长度
  • 内存初始被分配好

2.4、问题分析

实现 udp 协议后,运行代码,过了一段时间,产生了两个问题:

  • 服务端不能继续接收数据:在客户端使用 arp -a 发现没有服务端的 ip 和 mac,这需要我们手动实现ARP协议
  • 服务端不能 ping 通:需要手动实现 icmp 协议

3、ARP 协议的实现

在这里插入图片描述

// ARP 首部
struct arphdr {
    unsigned short type;        // 硬件类型
    unsigned short protocol;    // 协议类型
    unsigned char addrlen;      // 硬件地址长度
    unsigned char protolen;     // 协议地址长度
    unsigned short oper;  // 操作类型,ARP请求1,ARP响应2,RARP请求3,RARP响应4 
    unsigned char smac[ETH_ADDR_LENGTH];   // 源 mac 地址
    unsigned int sip;                       // 源 ip 地址
    unsigned char dmac[ETH_ADDR_LENGTH];   // 目的 mac 地址
    unsigned int dip;                       // 目的 ip 地址
};

// ARP 数据包
struct arppkt {
    struct ethhdr eh;   // 以太网帧首部
    struct arphdr arp;  // ARP 首部
};

在客户端测试 arp -a,前后的对比:

在这里插入图片描述

注:如果不用代码的实现形式,可以采用静态绑定 arp 地址

4、icmp 协议的实现

在这里插入图片描述

// icmp 首部,icmp分为差错报文和查询报文,由类型和代码共同决定
struct icmphdr {
	unsigned char type;		// 类型
	unsigned char code;		// 代码
	unsigned short check;	// 首部校验和
	unsigned short id;		// 标识
	unsigned short seq;		// 序列号
	unsigned char data[32];	// 数据
};

// icmp 数据包
struct icmppkt {
	struct ethhdr eh;		// 以太网帧首部
	struct iphdr ip;		// ip 首部
	struct icmphdr icmp;	// icmp 首部
};

在客户端 ping服务端,前后的对比:

在这里插入图片描述

5、netmap 代码实现

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

#include <sys/poll.h>
#include <arpa/inet.h>

#define NETMAP_WITH_LIBS

#include <net/netmap_user.h>	// netmap 开启
#pragma pack(1)					// 以1个字节对齐

#define ETH_ADDR_LENGTH	6		// 以太网 mac 地址长度
#define PROTO_IP	0x0800		// ip协议
#define PROTO_ARP	0x0806		// ARP协议

#define PROTO_UDP	17
#define PROTO_ICMP	1
#define PROTO_IGMP	2

#define UDP		1		
#define ICMP	1		
#define ARP		1

// 以太网数据帧头,字节对齐: sizeof = 16
struct ethhdr {
	unsigned char dmac[ETH_ADDR_LENGTH];	 // 目的mac地址
	unsigned char smac[ETH_ADDR_LENGTH]; // 源mac地址
	unsigned short protocol; 				 //协议:上层协议的类型,ip:0x0800
};

// ip 数据包首部
struct iphdr {
    unsigned char version:4,    // ip协议版本,IPv4:0100
                  hdrlen:4;     // 首部长度,* 4
    unsigned char tos;          // 服务类型
    unsigned short tot_len;      // 总长度,* 1,最大65535字节,超过MTU(1500)分片
    unsigned short id;          // 标识,相同表示数据包来源于同一报文
    unsigned short flag:3,      // 标志,MF:more frag、DF:don't frag、未用
                   flag_offset:13; // 片偏移,标识该数据包在上层数据报文中的偏移量
    unsigned char ttl;          // 生存时间 time to live,默认是64,避免环路
    unsigned char protocol;         // 协议,上层协议的类型,udp, tcp
    unsigned short check;       // 首部校验和
    unsigned int sip;           // 源ip
    unsigned int dip;           // 目的ip
};

// udp报文首部
struct udphdr {
    unsigned short sport;       // 源端口
    unsigned short dport;       // 目的端口
    unsigned short len;      // udp报文长度
    unsigned short check;       // udp 校验
};

// udp 报文,sizeof(struct udppkt) == 42
struct udppkt {
    struct ethhdr eh;    // 以太网帧首部
    struct iphdr ip;        // ip 首部
    struct udphdr udp;      // udp 首部
    
    unsigned char payload[0];  //应用层数据,柔性数组(零长数组)
};

// ARP 首部
struct arphdr {
    unsigned short type;        // 硬件类型
    unsigned short protocol;    // 协议类型
    unsigned char addrlen;      // 硬件地址长度
    unsigned char protolen;     // 协议地址长度
    unsigned short oper;        // 操作类型,ARP请求1,ARP响应2,RARP请求3,RARP响应4    
    unsigned char smac[ETH_ADDR_LENGTH];   // 源 mac 地址
    unsigned int sip;                       // 源 ip 地址
    unsigned char dmac[ETH_ADDR_LENGTH];   // 目的 mac 地址
    unsigned int dip;                       // 目的 ip 地址
};

// ARP 数据包
struct arppkt {
    struct ethhdr eh;   // 以太网帧首部
    struct arphdr arp;  // ARP 首部
};

// icmp 首部,icmp报文分为差错报文和查询报文,由类型和代码共同决定其类型
struct icmphdr {
	unsigned char type;		// 类型
	unsigned char code;		// 代码,指定类型中的一个功能
	unsigned short check;	// 首部校验和
	unsigned short id;		// 标识
	unsigned short seq;		// 序列号
	unsigned char data[32];
};

// icmp 数据包
struct icmppkt {
	struct ethhdr eh;		// 以太网帧首部
	struct iphdr ip;		// ip 首部
	struct icmphdr icmp;	// icmp 首部
};

// 字符串"FF:...:FF"转 mac 地址(十六进制数字)
int str2mac(char *mac, char *str) {
	char *p = str;
	unsigned char value = 0x0; // 十六进制起始
	
	int i = 0;
	while (*p != '\0') {
		 // : 分割符
		if (*p == ':') {
			mac[i++] = value;	
			value = 0x0; 
		} else {
			// 处理数字
			unsigned char temp = *p;
			if (temp <= '9' && temp >= '0') {
				temp -= '0';
			} else if (temp <= 'f' && temp >= 'a') {
				temp -= 'a';
				temp += 10;
			} else if (temp <= 'F' && temp >= 'A') {
				temp -= 'A';
				temp += 10;
			} else {	
				break;
			}
			value <<= 4;
			value |= temp;
		}
		p ++;
	}

	mac[i] = value;
	return 0;
}

// 回复 arp 
void echo_arp_pkt(struct arppkt *arp, struct arppkt *arp_rt, char *hmac) {

	memcpy(arp_rt, arp, sizeof(struct arppkt));

	memcpy(arp_rt->eh.dmac, arp->eh.smac, ETH_ADDR_LENGTH);
	str2mac(arp_rt->eh.smac, hmac);
	arp_rt->eh.protocol = arp->eh.protocol;

	arp_rt->arp.addrlen = 6;
	arp_rt->arp.protolen = 4;
	arp_rt->arp.oper = htons(2);
	
	str2mac(arp_rt->arp.smac, hmac);
	arp_rt->arp.sip = arp->arp.dip;
	
	memcpy(arp_rt->arp.dmac, arp->arp.smac, ETH_ADDR_LENGTH);
	arp_rt->arp.dip = arp->arp.sip;
}

// 回复 udp,bad udp,估计是校验的问题
void echo_udp_pkt(struct udppkt *udp, struct udppkt *udp_rt) {

	memcpy(udp_rt, udp, sizeof(struct udppkt));

	memcpy(udp_rt->eh.dmac, udp->eh.smac, ETH_ADDR_LENGTH);
	memcpy(udp_rt->eh.smac, udp->eh.dmac, ETH_ADDR_LENGTH);

	udp_rt->ip.sip = udp->ip.dip;
	udp_rt->ip.dip = udp->ip.sip;

	udp_rt->udp.sport = udp->udp.dport;
	udp_rt->udp.dport = udp->udp.sport;
}

// icmp 校验和
unsigned short in_cksum(unsigned short *addr, int len) {
	register int nleft = len;
	register unsigned short *w = addr;
	register int sum = 0;
	unsigned short answer = 0;

	while (nleft > 1)  {
		sum += *w++;
		nleft -= 2;
	}

	if (nleft == 1) {
		*(u_char *)(&answer) = *(u_char *)w ;
		sum += answer;
	}

	sum = (sum >> 16) + (sum & 0xffff);	
	sum += (sum >> 16);			
	answer = ~sum;
	
	return (answer);

}

// 回复 icmp 
void echo_icmp_pkt(struct icmppkt *icmp, struct icmppkt *icmp_rt) {

	memcpy(icmp_rt, icmp, sizeof(struct icmppkt));

	icmp_rt->icmp.type = 0x0; 
	icmp_rt->icmp.code = 0x0; 
	icmp_rt->icmp.check = 0x0;

	icmp_rt->ip.sip = icmp->ip.dip;
	icmp_rt->ip.dip = icmp->ip.sip;

	memcpy(icmp_rt->eh.dmac, icmp->eh.smac, ETH_ADDR_LENGTH);
	memcpy(icmp_rt->eh.smac, icmp->eh.dmac, ETH_ADDR_LENGTH);

	icmp_rt->icmp.check = in_cksum((unsigned short*)&icmp_rt->icmp, sizeof(struct icmphdr));
	
}

int main() {
	
	struct ethhdr *eh;
	struct pollfd pfd = {0};
	struct nm_pkthdr h;
	unsigned char *stream = NULL;

	struct nm_desc *nmr = nm_open("netmap:eth0", NULL, 0, NULL);
	if (nmr == NULL) {
		return -1;
	}

	pfd.fd = nmr->fd;
	pfd.events = POLLIN;

	while (1) {
		int ret = poll(&pfd, 1, -1);
		if (ret < 0) continue;
		
		if (pfd.revents & POLLIN) {
			// 从内存的 ringbuf 中取出一个包
			stream = nm_nextpkt(nmr, &h);
			eh = (struct ethhdr*)stream;

			// 若以太网帧携带的是 ip 数据
			if (ntohs(eh->protocol) == PROTO_IP) {

				struct udppkt *udp = (struct udppkt*)stream;
				// 1、ip数据包携带的是 udp 报文
				if (udp->ip.protocol == PROTO_UDP) {

					struct in_addr addr;
					addr.s_addr = udp->ip.sip;

					int udp_length = ntohs(udp->udp.len);
					printf("%s:%d: udp_length:%d, ip_len:%d -->\n", inet_ntoa(addr), ntohs(udp->udp.sport), 
						 udp_length, ntohs(udp->ip.tot_len));

					udp->payload[udp_length - 8] = '\0';	// udp报文长度=udp长度-udp首部
					printf("udp pkt: %s\n", udp->payload);
#if 1
					struct udppkt udp_rt;
					echo_udp_pkt(udp, &udp_rt);
					nm_inject(nmr, &udp_rt, sizeof(struct udppkt));
#endif
				} 
#if ICMP		
				// 2、ip数据包携带的是 icmp 报文
				else if (udp->ip.protocol == PROTO_ICMP) {
					
					struct icmppkt *icmp = (struct icmppkt*)stream;

					printf("icmp ---------- --> %d, %x\n", icmp->icmp.type, icmp->icmp.check);
					if (icmp->icmp.type == 0x08) {
						struct icmppkt icmp_rt = {0};
						echo_icmp_pkt(icmp, &icmp_rt);

						//printf("icmp check %x\n", icmp_rt.icmp.check);
						nm_inject(nmr, &icmp_rt, sizeof(struct icmppkt));
					}
					
				} 
#endif
				else if (udp->ip.protocol == PROTO_IGMP) {

				} 
				else {
					printf("other ip packet");
				}
				
			}  
#if ARP
			// 若以太网帧携带的是 arp 数据
			else if (ntohs(eh->protocol) == PROTO_ARP) {

				struct arppkt *arp = (struct arppkt *)stream;
				struct arppkt arp_rt;
				
				// 若访问的是本机的ip,回复arp数据包
				if (arp->arp.dip == inet_addr("192.168.0.104")) {
					echo_arp_pkt(arp, &arp_rt, "00:0c:29:18:ef:9d"); // 本地的mac
					nm_inject(nmr, &arp_rt, sizeof(struct arppkt));
				}
			}
#endif
		} 
	}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/143440.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

第50问:从连接判断应用访问数据库的异常行为

问 我发现应用有一根访问数据库的连接有异常流量&#xff0c;如何判断是应用哪个逻辑导致了异常行为 实验 先起锅烧一个数据库实例&#xff1a; 我们用 mysqlslap 作为应用&#xff1a; 假设在 MySQL 中&#xff0c;我们认为这根连接有异常流量&#xff1a; 通过 ss 找到这根…

阿里一面 | 说说你对 MySQL 死锁的理解

1、什么是死锁&#xff1f; 死锁指的是在两个或两个以上不同的进程或线程中&#xff0c;由于存在共同资源的竞争或进程&#xff08;或线程&#xff09;间的通讯而导致各个线程间相互挂起等待&#xff0c;如果没有外力作用&#xff0c;最终会引发整个系统崩溃。 2、Mysql出现死…

cordova-Toast的使用 -官方插件和自定义插件

前言&#xff1a;cordova是使用前端技术来开发app,可以节省成本和快速发布。不需要了解原生app开发 加载web的方式&#xff0c;可以兼容生成Android、ios以及浏览器等各种平台的项目 前文&#xff1a;cordova开发流程 一、官方提示浮动框 cordova-plugin-x-toast 1.cordova pl…

二、GtkApplication and GtkApplicationWindow

1 GtkApplication 1.1 GtkApplication and g_application_run 人们编写编程代码来开发应用程序。什么是应用程序?应用程序是使用库运行的软件&#xff0c;其中包括操作系统、框架等。在GTK 4编程中&#xff0c;GTK应用程序是使用GTK库运行的程序(或可执行程序)。 编写GtkAp…

屏幕录制有快捷键吗?录屏快捷键ctrl加什么

我们日常使用的电脑是自带录屏功能&#xff0c;可以方便我们将玩游戏的精彩画面&#xff0c;或者是电影某个片段给录制下来。为了不错过这些精彩片段&#xff0c;可以使用录屏快捷键录制。那电脑录屏快捷键ctrl加什么&#xff1f;今天本文就简单地给大家介绍电脑录屏快捷键&…

【错误记录】IntelliJ IDEA 编译 Java 文件报错 ( 错误: 非法字符: ‘\ufeff‘ )

文章目录一、报错信息二、修改方案一、报错信息 报错信息 : D:\002_Project\003_Java_Work\Xxx\src\main\java\cn\Xxx.java:1: 错误: 非法字符: \ufeff &#xfeff;package xxx;出现该问题的原因是 IntelliJ IDEA 在创建文件时 , 为文件添加了 BOM 隐藏字符 , 这是 文件的 字…

(七)汇编语言——更灵活的定位内存地址的方法

目录 and和or ASCII码 [bxidata] SI和DI寄存器 [bxsi]和[bxdi] [bxsiidata]和[bxdiidata] 总结 例子&#xff08;双重循环的解决方案&#xff09; 我们知道&#xff0c;对于汇编来说&#xff0c;内存是极为重要的&#xff0c;所以&#xff0c;能精准且巧妙地定位内存地…

进程间通信——信号

目录 1 概念 2 信号类型 linux的基本信号类型 操作 常用的信号 3 怎么操作信号 signal kill raise alarm pause 注意 范例1&#xff08;自己用信号发送书写sleep函数实现定时炸弹&#xff09; 范例2&#xff08;用信号发送书写功能检测用户是否输入&#xff0c;如…

OAuth2.0协议流程与授权模式、协议流程

什么是OAuth2.0OAuth&#xff08;Open Authorization&#xff09;是一个关于授权&#xff08;authorization&#xff09;的开放网络标准&#xff0c;允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息&#xff0c;而不需要将用户名和密码提供给第三方移动应用或分…

Java 如何优雅的导出 Excel

前言 公司项目最近有一个需要&#xff1a;报表导出。整个系统下来&#xff0c;起码超过一百张报表需要导出。这个时候如何优雅的实现报表导出&#xff0c;释放生产力就显得很重要了。下面主要给大家分享一下该工具类的使用方法与实现思路。 实现的功能点 对于每个报表都相同…

看零信任如何基于PKI体系实现数字信任

谈到安全&#xff0c;在万物互联的数字世界里&#xff0c;生活和工作之间的界限正在变得越来越模糊&#xff0c;传统的边界不再适用&#xff0c;传统的安全手段也不再适用。数字转型的步伐虽在不断加速&#xff0c;但威胁载体也在不断发展壮大&#xff0c;在数字互动过程中&…

从0到1完成一个Vue后台管理项目(三、使用SCSS/LESS,安装图标库)

往期 从0到1完成一个Vue后台管理项目&#xff08;一、创建项目&#xff09; 从0到1完成一个Vue后台管理项目&#xff08;二、使用element-ui&#xff09; 安装CSS预处理器 Sass cnpm i sass-loader7 node-sass4 -S 安装完就可以使用了&#xff0c;通过lang的方式 这里注意自…

C#,桌面编程入门(01)——按钮Button属性与事件、动态创建、快捷键、控件数组及自定义Button

本文是《桌面编程入门》系列文章的开山之篇。《桌面编程入门》系列文章主要介绍桌面编程的各种组件&#xff0c;Button&#xff0c;ComboBox&#xff0c;Panel&#xff0c;WebBrowser。。。。。。类似的文章非常多&#xff0c;深度不同&#xff0c;水平不等。1 桌面编程1.1 图形…

第六天作业部分

使用的文件内容 1&#xff1a;passwd root:x:0:0:root:/root:/bin/bash bin:x:1:1:bin:/bin:/sbin/nologin daemon:x:2:2:daemon:/sbin:/sbin/nologin adm:x:3:4:adm:/var/adm:/sbin/nologin lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin sync:x:5:0:sync:/sbin:/bin/sync shut…

0. 【R、Rstudio、Rtools 】软件的安装 + 在Rstudio上永久修改(默认工作路径 + 默认安装包路径)

文章目录1.R软件安装2. Rstudio的安装3. Rtools 的安装4. 初始化Rstudio4.1 更改Rtudio的镜像为中国清华大学&#xff0c;下载包更快4.2. 修改默认工作路径 默认安装包路径全部都在官网下&#xff0c;都是免费的 1.R软件安装 访问官网&#xff1a;https://www.r-project.org/ …

React(coderwhy)- 04(css)

React中CSS的概述 组件化天下的CSS ◼ 前面说过&#xff0c;整个前端已经是组件化的天下&#xff1a;  而CSS的设计就不是为组件化而生的&#xff0c;所以在目前组件化的框架中都在需要一种合适的CSS解决方案。 ◼ 在组件化中选择合适的CSS解决方案应该符合以下条件&#xff…

为外贸软件花钱前,需要考虑的几大因素

当外贸企业发展壮大到一定程度时&#xff0c;购买一款适合公司、产品、业务的办公软件帮助员工更好的处理工作是必然的选择。如何挑选一款真正便捷高效的软件产品&#xff0c;特别是在花费几万甚至十几万购买外贸软件之前&#xff0c;一定要谨慎考虑以下几个问题。 一、使用便捷…

如何选择合适的文档管理解决方案?

如何选择合适的文档管理解决方案&#xff1f; 合适的文档管理解决方案有助于保护您的数据、数字化纸质流程、保持业务连续性、满足合规性标准并为审计做好准备。 但并非所有文档管理解决方案都相同 —— 要真正实现这些好处&#xff0c;您需要一个满足某些条件的系统。 文档管…

【Linux修炼】12.深入了解系统文件

每一个不曾起舞的日子&#xff0c;都是对生命的辜负。 文件fd一. 重新谈论文件1. 共识的问题2. 重谈C语言文件操作2.1 概要2.2 C语言文件实操2.3 OS接口open的使用&#xff08;比特位标记&#xff09;2.4 写入操作2.5 追加操作2.6 只读操作二. 如何理解文件1. 提出问题2. 文件描…

Java 空指针异常的若干解决方案

Java 中任何对象都有可能为空&#xff0c;当我们调用空对象的方法时就会抛出 NullPointerException 空指针异常&#xff0c;这是一种非常常见的错误类型。我们可以使用若干种方法来避免产生这类异常&#xff0c;使得我们的代码更为健壮。本文将列举这些解决方案&#xff0c;包括…