当前位置: 首页 > news >正文

简单UDP网络程序

目录

UDP网络程序服务端

封装 UdpSocket

服务端创建套接字

服务端绑定

运行服务器

UDP网络程序客户端

客户端创建套接字

客户端绑定

运行客户端


通过上篇文章的学习,我们已经对网络套接字有了一定的了解。在本篇文章中,我们将基于之前掌握的知识进行实际运用,动手实现一个简单的 UDP 网络程序。

UDP网络程序服务端

封装 UdpSocket

服务端创建套接字

我们为了使得代码整体看起来比较的简介,这里进行封装,将服务器封装成一个类。

当我们定义出一个服务器类后,首先要做的就是进行初始化,进行初始化的第一件事就是进行创建套接字。

socket函数

创建套接字的函数叫做socket,该函数的函数原型如下:

int socket(int domain, int type, int protocol);

参数说明:

  • domain:套接字创建时使用的域(也称为协议族),用于指定套接字的类型。该参数对应 struct sockaddr 结构的前16位。若为本地通信,应设置为 AF_UNIX;若为网络通信,则通常使用 AF_INET(IPv4)或 AF_INET6(IPv6)。
  • type:套接字所需的服务类型。常见的有 SOCK_STREAM 和 SOCK_DGRAM 两种。基于 UDP 的网络通信应选用 SOCK_DGRAM(用户数据报服务);而基于 TCP 的网络通信则使用 SOCK_STREAM(流式套接字),提供可靠的流式数据传输服务。
  • protocol:套接字所使用的协议类型。可以显式指定为 TCP 或 UDP,但通常建议直接设置为 0,表示使用默认协议。系统会根据前两个参数(domain 和 type)自动推断应采用的协议。

返回值说明:

  • 套接字创建成功返回一个文件描述符,创建失败返回-1,同时错误码会被设置。

创建套接字时我们需要填入的协议家族就是AF_INET,因为我们要进行的是网络通信,而我们需要的服务类型就是SOCK_DGRAM,因为我们现在编写的UDP服务器是面向数据报的,而第三个参数之间设置为0即可。

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>class UdpServer
{
public:bool InitServer(){// 创建套接字_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){// 创建套接字失败std::cerr << "socket error" << std::endl;return false;}std::cout << "socket create success, sockfd: " << _sockfd << std::endl;return true;}~UdpServer(){if(_sockfd >= 0){close(_sockfd);}}
private:int _sockfd; // 文件描述符
};

除此之外,我们还理应当设置析构函数,当程序结束或者服务器关闭时,理应当关闭对应的文件描述符对应的文件。

测试,检擦是否创建成功:

#include "UdpServer.hpp"int main()
{UdpServer* svr = new UdpServer();svr->InitServer();svr->~UdpServer();return 0;
}

运行结果如下:

运行程序后可以看到套接字是创建成功的,对应获取到的文件描述符就是3,这也很好理解,因为0、1、2默认被标准输入流、标准输出流和标准错误流占用了,此时最小的、未被利用的文件描述符就是3。

服务端绑定

现在套接字已经创建成功了,但作为一款服务器来讲,如果只是把套接字创建好了,那我们也只是在系统层面上打开了一个文件,操作系统将来并不知道是要将数据写入到磁盘还是刷到网卡,此时该文件还没有与网络关联起来。

这里需要调用的函数就是bind函数,同样需要提醒的是,我们写的这个是UDP,所以是不连接的,所以第二件是为绑定,与TCP略有不同。

绑定的函数叫做bind,该函数的函数原型如下:

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数说明:

  • sockfd:绑定的文件的文件描述符。也就是我们创建套接字时获取到的文件描述符。
  • addr:网络相关的属性信息,包括协议家族、IP地址、端口号等。
  • addrlen:传入的addr结构体的长度。

返回值说明:

  • 绑定成功返回0,绑定失败返回-1,同时错误码会被设置。

struct sockaddr_in结构体

在上篇文章中,仅仅介绍了sockaddr的三种结构的适用场景与区别,然后进行了简单的使用介绍,那么下面就看源码进行了解。

因为我们是跨网络通信,所以使用的是sockaddr_in。

在该文件中就可以找到struct sockaddr_in结构的定义,需要注意的是,struct sockaddr_in属于系统级的概念,不同的平台接口设计可能会有点差别。

可以看到,struct sockaddr_in当中的成员如下:

  • sin_family:表示协议家族。
  • sin_port:表示端口号,是一个16位的整数。
  • sin_addr:表示IP地址,是一个32位的整数。

其中sin_addr的类型是struct in_addr,实际该结构体当中就只有一个成员,该成员就是一个32位的整数,IP地址实际就是存储在这个整数当中的。

剩下的字段一般不做处理,当然你也可以进行初始化。

如何理解绑定?

简单的来说,socket就像我们买了一部手机,这部手机本身没有任何身份,它可以用来打给任何人,也可以接听任何电话,但别人不知道如何联系到你。

bind就好比办一个手机卡并公布号码,在这个过程中,bind它将这个“手机号码”(网络地址)与你的“手机”(套接字)绑定在一起。

技术角度来说就是:bind 系统调用的作用是将一个协议地址(IP地址 + 端口号)分配给一个套接字(socket)。

所以bind 的本质就是“挂牌营业”。 它告诉操作系统:“我这个套接字就在这个IP地址的这个端口上提供服务了,所有发往这个地址的数据包都交给我来处理!”

增加IP地址与端口号

所以我们根据bind的参数,得知我们除了要有sockfd,还需要知道IP与端口号。

所以为刚才的类添加成员变量。

// ...class UdpServer
{
public:UdpServer(std::string ip, int port):_sockfd(-1),_port(port),_ip(ip){}bool InitServer(){// ...}~UdpServer(){// ...}
private:int _sockfd; // 文件描述符int _port; //端口号std::string _ip; //IP地址
};

注意: 虽然这里端口号定义为整型,但由于端口号是16位的,因此我们实际只会用到它的低16位。

服务端绑定

套接字初始化完成后,就需要进行绑定了,但在绑定之前,我们需要先自己创建一个

struct sockaddr_in类型的变量,将对应的网络属性信息填充到该结构当中。由于该结构体当中还有部分选填字段,因此我们最好在填充之前对该结构体变量里面的内容进行清空,然后再将协议家族、端口号、IP地址等信息填充到该结构体变量当中。

还需要注意的就是,我们的IP格式,因为我们类中的成员IP变量是string类型的,因为在网络中通信的规定,我们还需要先调用c_str()将其转化为字符串,然后再转化为整形IP的形式,此时我们需要调用inet_addr函数将字符串IP转换成整数IP。除此之外还需要注意的就是网络字节序的问题,因为网络中传输使用的是大端序,所以我们在发送到网络之前需要将端口号设置为网络序列,由于端口号是16位的,因此我们需要使用htons函数将端口号转为网络序列。

当网络属性信息填充完毕后,由于bind函数提供的是通用参数类型,因此在传入结构体地址时还需要将struct sockaddr_in*强转为struct sockaddr*类型后再进行传入。

#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <string>class UdpServer
{
public:UdpServer(std::string ip, int port):_sockfd(-1),_port(port),_ip(ip){}bool InitServer(){// 创建套接字_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){// 创建套接字失败std::cerr << "socket error" << std::endl;return false;}std::cout << "socket create success, sockfd: " << _sockfd << std::endl;//填充网络通信相关信息struct sockaddr_in local;memset(&local, '\0', sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = inet_addr(_ip.c_str());//绑定if (bind(_sockfd, (struct sockaddr*)&local, sizeof(sockaddr)) < 0){ //绑定失败std::cerr << "bind error" << std::endl;return false;}std::cout << "bind success" << std::endl;return true;}~UdpServer(){if(_sockfd >= 0){close(_sockfd);}}
private:int _sockfd; // 文件描述符int _port; //端口号std::string _ip; //IP地址
};

同样我们可以进行再次封装,将初始化的函数整体看起来简便些。

#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <string>class UdpServer
{
public:UdpServer(std::string ip, int port):_sockfd(-1),_port(port),_ip(ip){}bool Socket(){_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){// 创建套接字失败std::cerr << "socket error" << std::endl;return false;}std::cout << "socket create success, sockfd: " << _sockfd << std::endl;return true;}bool Bind(){//填充网络通信相关信息struct sockaddr_in local;memset(&local, '\0', sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = inet_addr(_ip.c_str());//绑定if (bind(_sockfd, (struct sockaddr*)&local, sizeof(sockaddr)) < 0){ //绑定失败std::cerr << "bind error" << std::endl;return false;}std::cout << "bind success" << std::endl;return true;}bool InitServer(){// 检查socket创建是否成功if (!Socket()) {return false;}// 检查绑定是否成功if (!Bind()) {close(_sockfd);_sockfd = -1;return false;}return true;}~UdpServer(){if(_sockfd >= 0){close(_sockfd);}}
private:int _sockfd; // 文件描述符int _port; //端口号std::string _ip; //IP地址
};

运行服务器

UDP服务器的初始化就只需要创建套接字和绑定就行了,当服务器初始化完毕后我们就可以启动服务器了。

服务器持续运行,其核心使命就是周而复始地为客户端提供特定服务。正因如此,服务器程序一旦启动,便通常不会主动退出,其内部逻辑往往通过一个循环结构不断执行,以保持长时间的服务能力。

UDP 服务器采用无连接通信模式,这意味着它无需建立和维护连接状态。一旦启动完成,UDP 服务器即可随时接收来自任何客户端的数据报,直接读取对方发送的信息,并进行相应处理,处理完一个数据后,进行返回,然后执行下一次循环,等待下一次发来的数据,进行周而复始的操作。

所以整体的代码就是一个死循环,可以使用for,也可使用while,这里使用for。

recvfrom函数

接收客户端发来的数据的函数是recvfrom函数。该函数的函数原型如下:

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

参数说明:

  • sockfd:对应操作的文件描述符。表示从该文件描述符索引的文件当中读取数据。
  • buf:读取数据的存放位置。
  • len:期望读取数据的字节数。
  • flags:读取的方式。一般设置为0,表示阻塞读取。
  • src_addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
  • addrlen:调用时传入期望读取的src_addr结构体的长度,返回时代表实际读取到的src_addr结构体的长度,这是一个输入输出型参数。

返回值说明:

  • 读取成功返回实际读取到的字节数,读取失败返回-1,同时错误码会被设置。

注意:

  • 在调用recvfrom读取数据时,必须将addrlen设置为你要读取的结构体对应的大小。
  • 由于recvfrom函数提供的参数也是struct sockaddr*类型的,因此我们在传入结构体地址时需要将struct sockaddr_in*类型进行强转。

启动服务器函数

服务器通过 recvfrom 函数读取客户端发来的数据时,现在视为接收到的数据为字符串。所以为了确保字符串正确终止,应在数据末尾手动添加 '\0'。这样,接收到的内容就可以直接用于输出或后续的字符串操作。

同时,我们还可以获取并输出客户端的地址信息,包括IP地址和端口号。需要注意的是:

  • 获取到的客户端端口号是网络字节序格式,所以要在输出前应当使用 ntohs 函数将其转换为主机字节序。

  • 获取到的客户端IP地址是一个整型的网络字节序地址,应当使用 inet_ntoa 函数将其转换为点分十进制格式的字符串后再进行输出。

#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <string>#define SIZE 128class UdpServer
{
public:UdpServer(std::string ip, int port)// ...{}bool InitServer(){// ...}void Start(){char buffer[SIZE];for(;;){struct sockaddr_in peer;socklen_t len = sizeof(peer);ssize_t size = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);if (size > 0){buffer[size] = '\0';int port = ntohs(peer.sin_port);std::string ip = inet_ntoa(peer.sin_addr);std::cout << ip << ":" << port << "# " << buffer << std::endl;}else{std::cerr << "recvfrom error" << std::endl;}}}~UdpServer(){// ...}
private:int _sockfd; // 文件描述符int _port; //端口号std::string _ip; //IP地址
};

如果调用recvfrom函数读取数据失败,我们可以打印一条提示信息,但是不要让服务器退出,因为考虑到实际情况,服务器不能因为读取某一个客户端的数据失败就退出,不能因为别人的问题,而自己去承担,应该自己的问题自己承担,所以对此应该是客户端去处理。

sendto函数

同样我们还需要对客户端发来的数据进行处理,然后进行返回,这里返回处理后的数据使用到的函数是sendto函数,该函数的函数原型如下:

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

参数说明:

  • sockfd:对应操作的文件描述符。表示将数据写入该文件描述符索引的文件当中。
  • buf:待写入数据的存放位置。
  • len:期望写入数据的字节数。
  • flags:写入的方式。一般设置为0,表示阻塞写入。
  • dest_addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。 addrlen:传入dest_addr结构体的长度。

返回值说明:

  • 写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置。

注意:

  • 由于sendto函数提供的参数也是struct sockaddr*类型的,因此我们在传入结构体地址时需要将struct sockaddr_in*类型进行强转。

补充启动客户端函数

对于数据我们这里为了方便,就将原数据不进行处理,而是再数据前加一个server get->,然后直接返回。

void Start()
{char buffer[SIZE];for(;;){struct sockaddr_in peer;socklen_t len = sizeof(peer);ssize_t size = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);if (size > 0){buffer[size] = '\0';int port = ntohs(peer.sin_port);std::string ip = inet_ntoa(peer.sin_addr);std::cout << ip << ":" << port << "# " << buffer << std::endl;}else{std::cerr << "recvfrom error" << std::endl;}std::string echo_msg = "server get->";echo_msg += buffer;sendto(_sockfd, echo_msg.c_str(), echo_msg.size(), 0, (struct sockaddr*)&peer, len);}
}

那么我们就先对其进行简单的测试,看看有没有语法错误。

这里的参数我是随便写的,对于真正的测试还需要编写客户端的代码,才可以知道start的逻辑是否有错,这里只可以证明初始化没有错,start没有语法错误与越界之类的问题,真正的逻辑问题不能检测。

#include "UdpServer.hpp"int main()
{std::string server_ip = "127.0.0.1";int server_port = 8080;UdpServer* svr = new UdpServer(server_ip, server_port);svr->InitServer();svr->Start();svr->~UdpServer();return 0;
}

但是我们在main函数中设置这一些ip与端口号的信息,多少有点影响美观,所以我们将其设置在.hpp文件内。

设置 IP = "0.0.0.0" 表示服务器将监听本机所有可用的网络接口(网卡)上的指定端口。而不仅仅是我们最一开始设置的127.0.0.1,只可以收到本机发的数据。

到这里我们就简单的实现了一个UDP网络程序,虽然十分简单,但也向前迈出了第一步。

下面我们就紧跟编写客户端的代码,然后进行测试我们写的服务端代码是否真正无错误。

UDP网络程序客户端

同样的,我们把客户端也封装成一个类,当我们定义出一个客户端对象后也是需要对其进行初始化,而客户端在初始化时也需要创建套接字,之后客户端发送数据或接收数据也就是对这个套接字进行操作。

客户端创建套接字

客户端创建套接字时选择的协议家族也是AF_INET,需要的服务类型也是SOCK_DGRAM,当客户端被析构时也可以选择关闭对应的套接字。与服务端不同的是,客户端在初始化时只需要创建套接字就行了,而不需要进行绑定操作。

#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <string>class UdpClient
{
public:bool InitClient(){//创建套接字_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){std::cerr << "socket create error" << std::endl;return false;}return true;}~UdpClient(){if(_sockfd >= 0){close(_sockfd);}}
private:int _sockfd; //文件描述符
};

客户端绑定

与之不同的客户端绑定问题

在网络通信中,通信双方都需要通过IP地址和端口号来定位对方。服务器和客户端虽然都具备各自的IP地址和端口号,但它们在端口号的使用方式上存在重要区别。

服务器作为服务的提供方,必须明确告知外界自己的访问地址。通常,服务器通过域名公开其IP地址,而端口号则往往不会显式地对外公布。因此,服务器必须使用一个众所周知的、固定的端口号,并且在选定之后不能随意更改,否则客户端将无法得知应当连接至哪个端口。这正是服务器需要主动绑定端口的原因——通过调用 bind 系统调用,服务器独占该端口,确保同一时刻只有一个进程能够在此端口上提供服务。

相反,客户端虽然同样需要端口号进行通信,但通常不需要主动绑定端口。客户端访问服务端时,仅要求其端口号在当前系统中是唯一的,而不必与某一特定进程长期关联。

如果客户端绑定了某个固定端口,会导致几个问题:首先,该端口将被独占,即使客户端未运行,其他程序也无法使用该端口;其次,若该端口已被占用,客户端程序将无法启动。因此,客户端的端口分配更适合采用动态方式,无需人工指定。当调用 sendto 等网络接口时,操作系统会自动为其分配合适的、当前未被使用的临时端口号。

也就是说,客户端每次启动时使用的端口号可能是不同的。只要系统中有可用的临时端口,客户端就能正常启动和通信。这种机制既提高了端口资源的利用率,也增加了客户端运行的灵活性。

运行客户端

同样,根服务端一个道理,我们要添加IP地址和端口号成员变量。但对于一个客户端来说,我们是必须要知道服务器端的ip地址与端口号的,因此在客户端类当中引入服务端的IP地址和端口号,此时我们就可以根据传入的服务端的IP地址和端口号对对应的成员进行初始化。

#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <string>class UdpClient
{
public:UdpClient(std::string server_ip, int server_port):_sockfd(-1),_server_port(server_port),_server_ip(server_ip){}bool InitClient(){// ...}~UdpClient(){// ...}
private:int _sockfd; //文件描述符int _server_port; //服务端端口号std::string _server_ip; //服务端IP地址
};

同样跟服务器端一样,当运行起来后,我们就需要处理通信的问题了,对于客户端来说,是应该先发送数据,然后再接收到服务器端处理完后返回的数据。

那么思路就是我们将客户端也设置为死循环,设置为我们自行输入要发送的数据,然后向服务器端发送数据,然后接收到返回的数据后打印出来。按照此逻辑写代码。

    void Start(){std::string msg;struct sockaddr_in peer;memset(&peer, '\0', sizeof(peer));peer.sin_family = AF_INET;peer.sin_port = htons(_server_port);peer.sin_addr.s_addr = inet_addr(_server_ip.c_str());for (;;){std::cout << "Please Enter# ";getline(std::cin, msg);sendto(_sockfd, msg.c_str(), msg.size(), 0, (struct sockaddr*)&peer, sizeof(peer));char buffer[SIZE];struct sockaddr_in tmp;socklen_t len = sizeof(tmp);ssize_t size = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&tmp, &len);if (size > 0){buffer[size] = '\0';std::cout << buffer << std::endl;}}}

然后我们简单的编写一下UDP客户端的.cc文件,这里用到了引入命令行参数,不了解的话,可以去搜一下,理解起来也是不难的。因为这部分的代码不是很难,所以就直接给代码,不讲解了。

#include "UdpClient.hpp"void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << " serverip serverport\n" << std::endl;
}int main(int argc, char *argv[])
{if(argc != 3){Usage(argv[0]);exit(0);}// 同样作为客户端,我们是先发信息,然后经过服务端处理后才会返回让客户端接收到// 同样要创建套接字// int sockfd_;     // 网路文件描述符// std::string ip_; // 任意 ip 地址  表示的是服务器的IP地址。具体来说,ip_ 是用来绑定服务器的网络接口的IP地址。// uint16_t port_;  // 表明服务器进程的端口号std::string server_ip = argv[1];int server_port = atoi(argv[2]);UdpClient* clt = new UdpClient(server_ip, server_port);clt->InitClient();clt->Start();return 0;
}

最后添加Makefile文件

.PHONY:all
all:udpserver udpclientudpserver:main.ccg++ -o $@ $^ -std=c++11
udpclient:UdpClient.ccg++ -o $@ $^ -std=c++11 -lpthread.PHONY:clean
clean:rm -f udpserver udpclient

最后测试结果:

当然我上面代码的封装,其实做的不是很好,UdpServer.hpp与UdpClient.hpp代码还有很多的重复。


文章转载自:

http://NcZTjR6d.cjqqj.cn
http://HleWWHoe.cjqqj.cn
http://XIkz4Jk0.cjqqj.cn
http://kRaAILQF.cjqqj.cn
http://wyQj6Td8.cjqqj.cn
http://UphHudZL.cjqqj.cn
http://Wf1WGscR.cjqqj.cn
http://mUlhQ7wS.cjqqj.cn
http://BthiJ5Nm.cjqqj.cn
http://cNWLBFoY.cjqqj.cn
http://Ambyzdor.cjqqj.cn
http://aN9EIPV6.cjqqj.cn
http://RBR1d0p9.cjqqj.cn
http://uclxB4Jv.cjqqj.cn
http://XJrlN6Ak.cjqqj.cn
http://XjzgkyKX.cjqqj.cn
http://7Koj7JCy.cjqqj.cn
http://RTUENMTj.cjqqj.cn
http://o8fAoncN.cjqqj.cn
http://5mrwnRSv.cjqqj.cn
http://goXbKjTo.cjqqj.cn
http://WivsUq7W.cjqqj.cn
http://jnD4ugAW.cjqqj.cn
http://EtQ1ktFz.cjqqj.cn
http://STmeqFFz.cjqqj.cn
http://HDfTj6mz.cjqqj.cn
http://b5NadIa9.cjqqj.cn
http://YLSWNRbW.cjqqj.cn
http://cyzzMLF9.cjqqj.cn
http://I6UjRkWJ.cjqqj.cn
http://www.dtcms.com/a/382918.html

相关文章:

  • RCE绕过技术:取反与异或的深入解析与实践
  • 算法题(207):最长上升子序列(经典线性dp题)
  • 【Nginx开荒攻略】Nginx主配置文件结构与核心模块详解:从0到1掌握nginx.conf:
  • 操作系统(二) :CPU调度
  • Knockout.js DOM 数据存储模块详解
  • js趣味游戏 贪吃蛇
  • Ajax-day2(图书管理)-弹框显示和隐藏
  • 低代码平台-开发SDK设计
  • Java 线程池面试高频问题全解析
  • 【HarmonyOS】MVVM与三层架构
  • 算法—双指针1.2
  • hcl ac ap 本地转发学习篇
  • Velox:数据界的超级发动机
  • 嵌入式系统启动流程
  • TRAE通用6A规则+敏捷开发5S规则
  • 【Java后端】Spring Boot 集成雪花算法唯一 ID
  • 【知识管理】【科普】新概念的学习路径
  • flask入门(五)WSGI及其Python实现
  • 第17课:自适应学习与优化
  • 详解安卓开发andorid中重要的agp和gradle的关系以及版本不匹配不兼容问题的处理方法-优雅草卓伊凡
  • Linux应用开发(君正T23):三网智能切换及配网功能
  • 华为HarmonyOS开发文档
  • Java 文件io
  • 在Android Studio中配置Gradle涉及到几个关键的文件
  • 基于OpenCV的答题卡自动识别与评分系统
  • 贪心算法应用:出租车调度问题详解
  • 【RK3576】【Android14】如何在Android14下单独编译kernel-6.1?
  • FlashAttention(V2)深度解析:从原理到工程实现
  • ​Prometheus+Grafana监控系统配置与部署全解
  • 电路调试过程中辨认LED正负极并焊接