【计算机网络】Linux下简单的UDP服务器(超详细)
套接字接口
我们把服务器封装成一个类,当我们定义出一个服务器对象后需要马上初始化服务器,而初始化服务器需要做的第一件事就是创建套接字。
🌎socket函数
这是Linux中创建套接字的系统调用,函数原型如下:
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就可以了,设置为0表示的就是默认,此时会根据传入的前两个参数自动推导出你最终需要使用的是哪种协议。
返回值说明:
- 套接字创建成功返回一个文件描述符,创建失败返回-1,同时错误码会被设置。
🌎socket函数属于什么类型的接口?
网络协议栈是分层的,按照TCP/IP四层模型来说,自顶向下依次是应用层、传输层、网络层和数据链路层。而我们现在所写的代码都叫做用户级代码,也就是说我们是在应用层编写代码,因此我们调用的实际是下三层的接口,而传输层和网络层都是在操作系统内完成的,也就意味着我们在应用层调用的接口都叫做系统调用接口。
🌎socket函数是被谁调用的?
socket这个函数是被程序调用的,但并不是被程序在编码上直接调用的,而是程序编码形成的可执行程序运行起来变成进程,当这个进程被CPU调度执行到socket函数时,然后才会执行创建套接字的代码,也就是说socket函数是被进程所调用的。
🌎socket函数底层做了什么?
socket函数上面说到是被进程调用的,而每一个进程在系统层面有一个进程地址空间PCB(task_struct),文件描述符表(files_struct)以及对应打开的各种文件。而文件描述符表里包含一个数组fd_array,其中数组的0,1,2下标分别对应,标准输入,标准输出以及标准错误。
当我们调用socket函数创建套接字时,实际相当于我们打开了一个“网络文件”,打开后在内核层面上就形成了一个对应的struct file结构体,同时该结构体被连入到了该进程对应的文件双链表,并将该结构体的首地址填入到了fd_array数组当中下标为3的位置,此时fd_array数组中下标为3的指针就指向了这个打开的“网络文件”,最后3号文件描述符作为socket函数的返回值返回给了用户。
每一个struct file结构体中包含的就是对应打开文件各种信息,比如文件的属性信息、操作方法以及文件缓冲区等。其中文件对应的属性在内核当中是由struct inode结构体来维护的,而文件对应的操作方法实际就是一堆的函数指针(比如read和write)在内核当中就是由struct file_operations结构体来维护的。而文件缓冲区对于打开的普通文件来说对应的一般是磁盘,但对于现在打开的“网络文件”来说,这里的文件缓冲区对应的就是网卡。
对于一般的普通文件来说,当用户通过文件描述符将数据写到文件缓冲区,然后再把数据刷到磁盘上就完成了数据的写入操作。而对于现在socket函数打开的“网络文件”来说,当用户将数据写到文件缓冲区后,操作系统会定期将数据刷到网卡里面,而网卡则是负责数据发送的,因此数据最终就发送到了网络当中。
🌎服务端创建套接字
我们初始化服务器创建套接字时,第一个参数需要填入的就是AF_INET,表明我们要进行网络通信,因为我们是UDP服务器,面向数据报的,所以填入SOCK_DGRAM,第三个参数协议选择,可以自行选择UDP协议,也可以传入0,让socket根据第二个参数自动识别。
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>class UdpServer
{
public://服务器初始化bool InitServer(){//AF_INET,表明需要网络通信//SOCK_DGRAM,面向数据报//0,默认,根据第二个参数为面向数据报,自动识别为UDP协议_sockfd = socket(AF_INET,SOCK_DGRAM,0);if(_sockfd < 0){std::cout << "socket create error!!" << std::endl;return;}std::cout << "socket create success,sockfd : " << _sockfd << std::endl;}~UdpServer(){if(_sockfd >= 0){close(_sockfd);}}
private:int _sockfd;//文件描述符
};
注意: 当析构服务器时,我们可以将sockfd对应的文件进行关闭,但实际上不进行该操作也行,因为一般服务器运行后是就不会停下来的。
这里我们可以做一个简单的测试,看看套接字是否创建成功。
int main()
{UdpServer* srv = new UdpServer();srv->InitServer();return 0;
}
运行程序后可以看到套接字是创建成功的,对应获取到的文件描述符就是3,这也很好理解,因为0、1、2默认被标准输入流、标准输出流和标准错误流占用了,此时最小的、未被利用的文件描述符就是3。
UDP服务器端和客户端均只需1个套接字:TCP 中,套接字是一对一的关系。如要向 10 个客户端提供服务,那么除了负责监听的套接字外,还需要创建 10 套接字。但在 UDP 中,不管是服务器端还是客户端都只需要 1 个套接字。举个例子,负责邮寄包裹的快递公司可以比喻为 UDP 套接字,只要有 1 个快递公司,就可以通过它向任意地址邮寄包裹。同样,只需 1 个 UDP 套接字就可以向任意主机传送数据。
服务端绑定
现在套接字已经创建成功了,但作为一款服务器来讲,如果只是把套接字创建好了,那我们也只是在系统层面上打开了一个文件,操作系统将来并不知道是要将数据写入到磁盘还是刷到网卡,此时该文件还没有与网络关联起来。
UDP中的服务器端和客户端没有连接,UDP 不像 TCP,无需在连接状态下交换数据,因此基于 UDP 的服务器端和客户端也无需经过连接过程。也就是说,不必调用 listen() 和 accept() 函数。UDP 中只有创建套接字的过程和数据交换的过程。
由于现在编写的是不面向连接的UDP服务器,所以初始化服务器要做的第二件事就是绑定。
🌎bind函数
绑定的函数叫做bind,注意它跟c++的bind函数只是同名,功能上没有任何关联,该函数的函数原型如下:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明:
- sockfd:绑定的文件的文件描述符。也就是我们创建套接字时获取到的文件描述符。
- addr:网络相关的属性信息,包括协议家族、IP地址、端口号等。
- addrlen:传入的addr结构体的长度。
返回值说明:
- 绑定成功返回0,绑定失败返回-1,同时错误码会被设置。
🌎struct sockaddr_in结构体
在绑定时需要将网络相关的属性信息填充到一个结构体当中,这里应该填入一个struct sockaddr结构体指针,但我们应该先创建struct sockaddr_in这个结构体,再将其强转到struct sockaddr*填入,具体原因我的上篇博客对这两个结构体解释得很清楚。
附上连接:点击跳转到我的上篇博客
我们可以用grep命令在/usr/include目录下查找该结构,此时就可以找到定义该结构的文件。
可以发现在结构体在/usr/include/linux/in.h文件中,需要注意的是,struct sockaddr_in属于系统级的概念,不同的平台接口设计可能会有点差别。
可以看到,struct sockaddr_in当中的成员如下:
- sin_family:表示协议家族。
- sin_port:表示端口号,是一个16位的整数。
- sin_addr:表示IP地址,是一个32位的整数。
剩下的字段一般不做处理,当然你也可以进行初始化。
其中sin_addr的类型是struct in_addr,实际该结构体当中就只有一个成员,该成员就是一个32位的整数,IP地址实际就是存储在这个整数当中的。
🌎如何理解绑定?
在进行绑定的时候需要将IP地址和端口号告诉对应的网络文件,此时就可以改变网络文件当中文件操作函数的指向,将对应的操作函数改为对应网卡的操作方法,此时读数据和写数据对应的操作对象就是网卡了,所以绑定实际上就是将文件和网络关联起来。
🌎增加IP地址和端口号
由于绑定时需要用到IP地址和端口号,因此我们需要在服务器类当中引入IP地址和端口号,在创建服务器对象时需要传入对应的IP地址和端口号,此时我们就可以根据传入的IP地址和端口号对对应的成员进行初始化。
class UdpServer
{
public:UdpServer(std::string ip,uint16_t port):_sockfd(-1),_port(port),_ip(ip){}//服务器初始化bool InitServer(){//AF_INET,表明需要网络通信//SOCK_DGRAM,面向数据报//0,默认,根据第二个参数为面向数据报,自动识别为UDP协议_sockfd = socket(AF_INET,SOCK_DGRAM,0);if(_sockfd < 0){std::cout << "socket create 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; //文件描述符uint16_t _port; //端口号std::string _ip; //IP地址
};
🌎服务端绑定
绑定之前,需要注意的是,在发送到网络之前需要将端口号设置为网络序列,由于端口号是16位的,因此我们需要使用htons函数将端口号转为网络序列。此外,由于网络当中传输的是整数IP,我们需要调用inet_addr函数将字符串IP转换成整数IP,然后再将转换后的整数IP进行设置。
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdint.h>
#include <string>
#include <memory>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>class UdpServer
{
public:UdpServer(std::string ip,uint16_t port):_sockfd(-1),_port(port),_ip(ip){}//服务器初始化bool InitServer(){//AF_INET,表明需要网络通信//SOCK_DGRAM,面向数据报//0,默认,根据第二个参数为面向数据报,自动识别为UDP协议_sockfd = socket(AF_INET,SOCK_DGRAM,0);if(_sockfd < 0){std::cout << "socket create error!!" << std::endl;return false;}std::cout << "socket create success,sockfd : " << _sockfd << std::endl;//填充网络通信相关信息struct sockaddr_in local;memset(&local,'\0',sizeof(struct sockaddr_in));local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = inet_addr(_ip.c_str());//绑定int ret = bind(_sockfd,(struct sockaddr*)&local,sizeof(struct sockaddr));if(ret < 0){std::cout << "bind error!!" << std::endl;return false;}std::cout << "bind success " << std::endl;return true;}~UdpServer(){if(_sockfd >= 0){close(_sockfd);}}
private:int _sockfd; //文件描述符uint16_t _port; //端口号std::string _ip; //IP地址
};
字符串IP VS 整数IP
IP地址的表现形式有两种:
- 字符串IP:类似于192.168.233.123这种字符串形式的IP地址,叫做基于字符串的点分十进制IP地址。
- 整数IP:IP地址在进行网络传输时所用的形式,用一个32位的整数来表示IP地址。
🌎整数IP存在的意义
网络传输数据时是寸土寸金的,如果我们在网络传输时直接以基于字符串的点分十进制IP的形式进行IP地址的传送,那么此时一个IP地址至少就需要15个字节,但实际并不需要耗费这么多字节。
IP地址实际可以划分为四个区域,其中每一个区域的取值都是0~255,而这个范围的数字只需要用8个比特位就能表示,因此我们实际只需要32个比特位就能够表示一个IP地址。其中这个32位的整数的每一个字节对应的就是IP地址中的某个区域,我们将IP地址的这种表示方法称之为整数IP,此时表示一个IP地址只需要4个字节。
因为采用整数IP的方案表示一个IP地址只需要4个字节,并且在网络通信也能表示同样的含义,因此在网络通信时就没有用字符串IP而用的是整数IP,因为这样能够减少网络通信时数据的传送。
🌎字符串IP和整数IP相互转换的方式
转换的方式有很多,比如我们可以定义一个位段A,位段A当中有四个成员,每个成员的大小都是8个比特位,这四个成员就依次表示IP地址的四个区域,一共32个比特位。
然后我们再定义一个联合体IP,该联合体当中有两个成员,其中一个是32位的整数,其代表的就是整数IP,还有一个就是位段A类型的成员,其代表的就是字符串IP。
由于联合体的空间是成员共享的,因此我们设置IP和读取IP的方式如下:
- 当我们想以整数IP的形式设置IP时,直接将其赋值给联合体的第一个成员就行了。
- 当我们想以字符串IP的形式设置IP时,先将字符串分成对应的四部分,然后将每部分转换成对应的二进制序列依次设置到联合体中第二个成员当中的p1、p2、p3和p4就行了。
- 当我们想取出整数IP时,直接读取联合体的第一个成员就行了。
- 当我们想取出字符串IP时,依次获取联合体中第二个成员当中的p1、p2、p3和p4,然后将每一部分转换成字符串后拼接到一起就行了。
注意: 在操作系统内部实际用的就是位段和枚举,来完成字符串IP和整数IP之间的相互转换的
🌎inet_addr函数
实际在进行字符串IP和整数IP的转换时,我们不需要自己编写转换逻辑,系统已经为我们提供了相应的转换函数,我们直接调用即可。
将字符串IP转换成整数IP的函数叫做inet_addr,该函数的函数原型如下:
in_addr_t inet_addr(const char *cp);
该函数使用起来非常简单,我们只需传入待转换的字符串IP,该函数返回的就是转换后的整数IP。除此之外,inet_aton函数也可以将字符串IP转换成整数IP,不过该函数使用起来没有inet_addr简单。
🌎inet_ntoa函数
将整数IP转换成字符串IP的函数叫做inet_ntoa,该函数的函数原型如下:
char *inet_ntoa(struct in_addr in);
需要注意的是,传入inet_ntoa函数的参数类型是in_addr,因此我们在传参时不需要选中in_addr结构当中的32位的成员传入,直接传入in_addr结构体即可。
运行服务器
UDP服务器的初始化就只需要创建套接字和绑定就行了,当服务器初始化完毕后我们就可以启动服务器了。
服务器实际上就是在周而复始的为我们提供某种服务,服务器之所以称为服务器,是因为服务器运行起来后就永远不会退出,因此服务器实际执行的是一个死循环代码。由于UDP服务器是不面向连接的,因此只要UDP服务器启动后,就可以直接读取客户端发来的数据。
🌎recvfrom函数
UDP服务器读取数据的函数叫做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,同时错误码会被设置。
注意:
- 由于UDP是不面向连接的,因此我们除了获取到数据以外还需要获取到对端网络相关的属性信息,包括IP地址和端口号等。
- 在调用recvfrom读取数据时,必须将addrlen设置为你要读取的结构体对应的大小。
- 由于recvfrom函数提供的参数也是struct sockaddr类型的,因此我们在传入结构体地址时需要将struct
sockaddr_in类型进行强转。
🌎启动服务器函数
现在服务端通过recvfrom函数读取客户端数据,我们可以先将读取到的数据当作字符串看待,将读取到的数据的最后一个位置设置为’\0’,此时我们就可以将读取到的数据进行输出,同时我们也可以将获取到的客户端的IP地址和端口号也一并进行输出。
需要注意的是,我们获取到的客户端的端口号此时是网络序列,我们需要调用ntohs函数将其转为主机序列再进行打印输出。同时,我们获取到的客户端的IP地址是整数IP,我们需要通过调用inet_ntoa函数将其转为字符串IP再进行打印输出。
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdint.h>
#include <string>
#include <memory>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>const int BUFFERSIZE = 128;
class UdpServer
{
public:UdpServer(std::string ip,uint16_t port):_sockfd(-1),_port(port),_ip(ip){}//服务器启动void Start(){char buffer[BUFFERSIZE] = {0};while(true){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){std::cout << "recvfrom error!!" << std::endl;}buffer[size] = '\0';int port = ntohs(peer.sin_port);std::string ip = inet_ntoa(peer.sin_addr);//消息打印std::cout << ip << ":" << port << "# " << buffer << std::endl;}}//服务器初始化bool InitServer(){//AF_INET,表明需要网络通信//SOCK_DGRAM,面向数据报//0,默认,根据第二个参数为面向数据报,自动识别为UDP协议_sockfd = socket(AF_INET,SOCK_DGRAM,0);if(_sockfd < 0){std::cout << "socket create error!!" << std::endl;return false;}std::cout << "socket create success,sockfd : " << _sockfd << std::endl;//填充网络通信相关信息struct sockaddr_in local;memset(&local,'\0',sizeof(struct sockaddr_in));local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = inet_addr(_ip.c_str());//绑定int ret = bind(_sockfd,(struct sockaddr*)&local,sizeof(struct sockaddr));if(ret < 0){std::cout << "bind error!!" << std::endl;return false;}std::cout << "bind success " << std::endl;return true;}~UdpServer(){if(_sockfd >= 0){close(_sockfd);}}
private:int _sockfd; //文件描述符uint16_t _port; //端口号std::string _ip; //IP地址
};
注意: 如果调用recvfrom函数读取数据失败,我们可以打印一条提示信息,但是不要让服务器退出,服务器不能因为读取某一个客户端的数据失败就退出。
🌎引入命令行参数
鉴于构造服务器时需要传入IP地址和端口号,我们这里可以引入命令行参数。此时当我们运行服务器时在后面跟上对应的IP地址和端口号即可。
由于云服务器的原因,后面实际不需要传入IP地址,因此在运行服务器的时候我们只需要传入端口号即可,目前我们就手动将IP地址设置为127.0.0.1。IP地址为127.0.0.1实际上等价于localhost表示本地主机,我们将它称之为本地环回,相当于我们一会先在本地测试一下能否正常通信,然后再进行网络通信的测试。
int main(int argc,char* argv[])
{if(argc != 2){std::cout << "Usage: " << argv[0] << " port" << std::endl;return 1;}std::string ip = "127.0.0.1";//本地环回uint16_t port = atoi(argv[1]);UdpServer* srv = new UdpServer(ip,port);srv->InitServer();srv->Start();return 0;
}
此时带上端口号运行程序就可以看到套接字创建成功、绑定成功,现在服务器就在等待客户端向它发送数据。
虽然现在客户端代码还没有编写,但是我们可以通过netstat命令来查看当前网络的状态,这里我们可以选择携带nlup选项。
netstat常用选项说明:
- -n:直接使用IP地址,而不通过域名服务器。
- -l:显示监控中的服务器的Socket。
- -t:显示TCP传输协议的连线状况。
- -u:显示UDP传输协议的连线状况。
- -p:显示正在使用Socket的程序识别码和程序名称。
此时你就能查看到对应网络相关的信息,在这些信息中程序名称为./server的那一行显示的就是我们运行的UDP服务器的网络信息。
客户端创建套接字
同样的,我们把客户端也封装成一个类,当我们定义出一个客户端对象后也是需要对其进行初始化,而客户端在初始化时也需要创建套接字,之后客户端发送数据或接收数据也就是对这个套接字进行操作。
客户端创建套接字时选择的协议家族也是AF_INET,需要的服务类型也是SOCK_DGRAM,当客户端被析构时也可以选择关闭对应的套接字。与服务端不同的是,客户端在初始化时只需要创建套接字就行了,而不需要进行绑定操作。
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdint.h>
#include <string>
#include <memory>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>class UdpClient
{
public:bool InitClient(){//创建套接字_sockfd = socket(AF_INET,SOCK_DGRAM,0);if(_sockfd < 0){std::cout << "create sockfd error!!" std::endl;return false;}return true;}~UdpClient(){if(_sockfd >= 0){close(_sockfd);}}
private:int _sockfd;
};
客户端的绑定问题
首先,由于是网络通信,通信双方都需要找到对方,因此服务端和客户端都需要有各自的IP地址和端口号,只不过服务端需要进行端口号的绑定,而客户端不需要。
因为服务器就是为了给别人提供服务的,因此服务器必须要让别人知道自己的IP地址和端口号,IP地址一般对应的就是域名,而端口号一般没有显示指明过,因此服务端的端口号一定要是一个众所周知的端口号,并且选定后不能轻易改变,否则客户端是无法知道服务端的端口号的,这就是服务端要进行绑定的原因,只有绑定之后这个端口号才真正属于自己,因为一个端口只能被一个进程所绑定,服务器绑定一个端口就是为了独占这个端口。
而客户端在通信时虽然也需要端口号,但客户端一般是不进行绑定的,客户端访问服务端的时候,端口号只要是唯一的就行了,不需要和特定客户端进程强相关。
如果客户端绑定了某个端口号,那么以后这个端口号就只能给这一个客户端使用,就是这个客户端没有启动,这个端口号也无法分配给别人,并且如果这个端口号被别人使用了,那么这个客户端就无法启动了。所以客户端的端口只要保证唯一性就行了,因此客户端端口可以动态的进行设置,并且客户端的端口号不需要我们来设置,当我们调用类似于sendto这样的接口时,操作系统会自动给当前客户端获取一个唯一的端口号。
也就是说,客户端每次启动时使用的端口号可能是变化的,此时只要我们的端口号没有被耗尽,客户端就永远可以启动。
启动客户端
🌎增加服务端IP地址和端口号
作为一个客户端,它必须知道它要访问的服务端的IP地址和端口号,因此在客户端类当中需要引入服务端的IP地址和端口号,此时我们就可以根据传入的服务端的IP地址和端口号对对应的成员进行初始化。
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdint.h>
#include <string>
#include <memory>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>class UdpClient
{
public:UdpClient(std::string ip,uint16_t port):_sockfd(-1),_server_port(port),_server_ip(ip){}bool InitClient(){//创建套接字_sockfd = socket(AF_INET,SOCK_DGRAM,0);if(_sockfd < 0){std::cout << "create sockfd error!!" << std::endl;return false;}return true;}~UdpClient(){if(_sockfd >= 0){close(_sockfd);}}
private:int _sockfd;uint16_t _server_port; //服务器端口号std::string _server_ip; //服务器ip
};
当客户端初始化完毕后我们就可以将客户端运行起来,由于客户端和服务端在功能上是相互补充的,既然服务器是在读取客户端发来的数据,那么客户端就应该想服务端发送数据。
🌎sendto函数
UDP客户端发送数据的函数叫做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,同时错误码会被设置。
注意:
- 由于UDP不是面向连接的,因此除了传入待发送的数据以外还需要指明对端网络相关的信息,包括IP地址和端口号等。
- 由于sendto函数提供的参数也是struct sockaddr类型的,因此我们在传入结构体地址时需要将struct
sockaddr_in类型进行强转。
🌎启动客户端函数
现在客户端要发送数据给服务端,我们可以让客户端获取用户输入,不断将用户输入的数据发送给服务端。
需要注意的是,客户端中存储的服务端的端口号此时是主机序列,我们需要调用htons函数将其转为网络序列后再设置进struct sockaddr_in结构体。同时,客户端中存储的服务端的IP地址是字符串IP,我们需要通过调用inet_addr函数将其转为整数IP后再设置进struct sockaddr_in结构体。
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdint.h>
#include <string>
#include <memory>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>class UdpClient
{
public:UdpClient(std::string ip,uint16_t port):_sockfd(-1),_server_port(port),_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());while(true){std::cout << "Please Enter# ";getline(std::cin,msg);sendto(_sockfd,msg.c_str(),msg.size(),0,(struct sockaddr*)&peer,sizeof(peer));}}bool InitClient(){//创建套接字_sockfd = socket(AF_INET,SOCK_DGRAM,0);if(_sockfd < 0){std::cout << "create sockfd error!!" << std::endl;return false;}return true;}~UdpClient(){if(_sockfd >= 0){close(_sockfd);}}
private:int _sockfd;uint16_t _server_port; //服务器端口号std::string _server_ip; //服务器ip
};
🌎引入命令行参数
鉴于构造客户端时需要传入对应服务端的IP地址和端口号,我们这里也可以引入命令行参数。当我们运行客户端时直接在后面跟上对应服务端的IP地址和端口号即可。
int main(int argc,char* argv[])
{if(argc != 3){std::cout << "Usage: " << argv[0] << " server_ip server_port" << std::endl;return 1;}std::string serverip = argv[1];uint16_t serverport = atoi(argv[2]);UdpClient* clt = new UdpClient(serverip,serverport);clt->InitClient();clt->Start();return 0;
}
客户端和服务器完整代码
🌎server
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdint.h>
#include <string>
#include <memory>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>const int BUFFERSIZE = 128;
class UdpServer
{
public:UdpServer(std::string ip,uint16_t port):_sockfd(-1),_port(port),_ip(ip){}//服务器启动void Start(){char buffer[BUFFERSIZE] = {0};while(true){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){std::cout << "recvfrom error!!" << std::endl;}buffer[size] = '\0';int port = ntohs(peer.sin_port);std::string ip = inet_ntoa(peer.sin_addr);//消息打印std::cout << ip << ":" << port << "# " << buffer << std::endl;}}//服务器初始化bool InitServer(){//AF_INET,表明需要网络通信//SOCK_DGRAM,面向数据报//0,默认,根据第二个参数为面向数据报,自动识别为UDP协议_sockfd = socket(AF_INET,SOCK_DGRAM,0);if(_sockfd < 0){std::cout << "socket create error!!" << std::endl;return false;}std::cout << "socket create success,sockfd : " << _sockfd << std::endl;//填充网络通信相关信息struct sockaddr_in local;memset(&local,'\0',sizeof(struct sockaddr_in));local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = inet_addr(_ip.c_str());//绑定int ret = bind(_sockfd,(struct sockaddr*)&local,sizeof(struct sockaddr));if(ret < 0){std::cout << "bind error!!" << std::endl;return false;}std::cout << "bind success " << std::endl;return true;}~UdpServer(){if(_sockfd >= 0){close(_sockfd);}}
private:int _sockfd; //文件描述符uint16_t _port; //端口号std::string _ip; //IP地址
};int main(int argc,char* argv[])
{if(argc != 2){std::cout << "Usage: " << argv[0] << " port" << std::endl;return 1;}std::string ip = "127.0.0.1";//本地环回uint16_t port = atoi(argv[1]);UdpServer* srv = new UdpServer(ip,port);srv->InitServer();srv->Start();return 0;
}
🌎client
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdint.h>
#include <string>
#include <memory>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>class UdpClient
{
public:UdpClient(std::string ip,uint16_t port):_sockfd(-1),_server_port(port),_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());while(true){std::cout << "Please Enter# ";getline(std::cin,msg);sendto(_sockfd,msg.c_str(),msg.size(),0,(struct sockaddr*)&peer,sizeof(peer));}}bool InitClient(){//创建套接字_sockfd = socket(AF_INET,SOCK_DGRAM,0);if(_sockfd < 0){std::cout << "create sockfd error!!" << std::endl;return false;}return true;}~UdpClient(){if(_sockfd >= 0){close(_sockfd);}}
private:int _sockfd;uint16_t _server_port; //服务器端口号std::string _server_ip; //服务器ip
};int main(int argc,char* argv[])
{if(argc != 3){std::cout << "Usage: " << argv[0] << " server_ip server_port" << std::endl;return 1;}std::string serverip = argv[1];uint16_t serverport = atoi(argv[2]);UdpClient* clt = new UdpClient(serverip,serverport);clt->InitClient();clt->Start();return 0;
}
本地测试
现在服务端和客户端的代码都已经编写完毕,我们可以先进行本地测试,此时服务器没有绑定外网,绑定的是本地环回。现在我们运行服务器时指明端口号为5678,再运行客户端,此时客户端要访问的服务器的IP地址就是本地环回127.0.0.1,服务端的端口号就是5678。
客户端运行之后提示我们进行输入,当我们在客户端输入数据后,客户端将数据发送给服务端,此时服务端再将收到的数据打印输出,这时我们在服务端的窗口也看到我们输入的内容。
此时我们再用netstat命令查看网络信息,可以看到服务端的端口是8081,客户端的端口是55210。这里客户端能被netstat命令查看到,说明客户端也已经动态绑定成功了,这就是我们所谓的网络通信。
INADDR_ANY
现在我们已经通过了本地测试,接下来就需要进行网络测试了,那是不是直接让服务端绑定我的公网IP,此时这个服务端就能够被外网访问了呢?
理论上确实是这样的,就比如我的服务器的公网IP是47.94.84.249,这里用ping命令也是能够ping通的。
现在我将服务端设置的本地环回改为我的公网IP,此时当我们重新编译程序再次运行服务端的时候会发现服务端绑定失败。
由于云服务器的IP地址是由对应的云厂商提供的,这个IP地址并不一定是真正的公网IP,这个IP地址是不能直接被绑定的,如果需要让外网访问,此时我们需要bind 0。系统当中提供的一个INADDR_ANY,这是一个宏值,它对应的值就是0。
因此如果我们需要让外网访问,那么在云服务器上进行绑定时就应该绑定INADDR_ANY,此时我们的服务器才能够被外网访问。
🌎绑定INADDR_ANY的好处
当一个服务器的带宽足够大时,一台机器接收数据的能力就约束了这台机器的IO效率,因此一台服务器底层可能装有多张网卡,此时这台服务器就可能会有多个IP地址,但一台服务器上端口号为5678的服务只有一个。这台服务器在接收数据时,这里的多张网卡在底层实际都收到了数据,如果这些数据也都想访问端口号为5678的服务。此时如果服务端在绑定的时候是指明绑定的某一个IP地址,那么此时服务端在接收数据的时候就只能从绑定IP对应的网卡接收数据。而如果服务端绑定的是INADDR_ANY,那么只要是发送给端口号为5678的服务的数据,系统都会可以将数据自底向上交给该服务端。
因此服务端绑定INADDR_ANY这种方案也是强烈推荐的方案,所有的服务器具体在操作的时候用的也就是这种方案。
当然,如果你既想让外网访问你的服务器,但你又指向绑定某一个IP地址,那么就不能用云服务器,此时可以选择使用虚拟机或者你自定义安装的Linux操作系统,那个IP地址就是支持你绑定的,而云服务器是不支持的。
🌎更改代码
因此,如果想要让外网访问我们的服务,我们这里就需要将服务器类当中IP地址相关的代码去掉,而在填充网络相关信息设置struct sockaddr_in结构体时,将设置的IP地址改为INADDR_ANY就行了。由于INADDR_ANY的值本质就是0,不存在大小端的问题,因此在设置时可以不进行网络字节序的转换。
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdint.h>
#include <string>
#include <memory>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>const int BUFFERSIZE = 128;
class UdpServer
{
public:UdpServer(std::string ip,uint16_t port):_sockfd(-1),_port(port),_ip(ip){}//服务器启动void Start(){char buffer[BUFFERSIZE] = {0};while(true){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){std::cout << "recvfrom error!!" << std::endl;}buffer[size] = '\0';int port = ntohs(peer.sin_port);std::string ip = inet_ntoa(peer.sin_addr);//消息打印std::cout << ip << ":" << port << "# " << buffer << std::endl;}}//服务器初始化bool InitServer(){//AF_INET,表明需要网络通信//SOCK_DGRAM,面向数据报//0,默认,根据第二个参数为面向数据报,自动识别为UDP协议_sockfd = socket(AF_INET,SOCK_DGRAM,0);if(_sockfd < 0){std::cout << "socket create error!!" << std::endl;return false;}std::cout << "socket create success,sockfd : " << _sockfd << std::endl;//填充网络通信相关信息struct sockaddr_in local;memset(&local,'\0',sizeof(struct sockaddr_in));local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = INADDR_ANY;//绑定INADDR_ANY//绑定int ret = bind(_sockfd,(struct sockaddr*)&local,sizeof(struct sockaddr));if(ret < 0){std::cout << "bind error!!" << std::endl;return false;}std::cout << "bind success " << std::endl;return true;}~UdpServer(){if(_sockfd >= 0){close(_sockfd);}}
private:int _sockfd; //文件描述符uint16_t _port; //端口号std::string _ip; //IP地址
};int main(int argc,char* argv[])
{if(argc != 2){std::cout << "Usage: " << argv[0] << " port" << std::endl;return 1;}std::string ip = "47.94.84.249";//本地环回uint16_t port = atoi(argv[1]);UdpServer* srv = new UdpServer(ip,port);srv->InitServer();srv->Start();return 0;
}
此时当我们再重新编译运行服务器时就不会绑定失败了,并且此时当我们再用netstat命令查看时会发现,该服务器的本地IP地址变成了0.0.0.0,这就意味着该UDP服务器可以在本地读取任何一张网卡里面的数据。
简单的UDP回声服务器
由于在进行网络测试的时候,当客户端发送数据给服务端时,服务端会将从客户端收到的数据进行打印,因此服务端是能够看到现象的。但客户端一直在向服务端发送数据,在客户端这边看不出服务端是否收到了自己发送的数据。
🌎服务端代码改写
鉴于此,我们可以将该服务器改成一个简单的回声服务器。当服务端收到客户端发来的数据后,除了在服务端进行打印以外,服务端可以调用sento函数将收到的数据重新发送给对应的客户端。
需要注意的是,服务端在调用sendto函数时需要传入客户端的网络属性信息,但服务端现在是知道客户端的网络属性信息的,因为服务端在此之前就已经通过recvfrom函数获取到了客户端的网络属性信息。
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdint.h>
#include <string>
#include <memory>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>const int BUFFERSIZE = 128;
class UdpServer
{
public:UdpServer(std::string ip,uint16_t port):_sockfd(-1),_port(port),_ip(ip){}//服务器启动void Start(){char buffer[BUFFERSIZE] = {0};while(true){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){std::cout << "recvfrom error!!" << std::endl;}buffer[size] = '\0';int port = ntohs(peer.sin_port);std::string ip = inet_ntoa(peer.sin_addr);//消息打印std::cout << ip << ":" << port << "# " << buffer << 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);}}//服务器初始化bool InitServer(){//AF_INET,表明需要网络通信//SOCK_DGRAM,面向数据报//0,默认,根据第二个参数为面向数据报,自动识别为UDP协议_sockfd = socket(AF_INET,SOCK_DGRAM,0);if(_sockfd < 0){std::cout << "socket create error!!" << std::endl;return false;}std::cout << "socket create success,sockfd : " << _sockfd << std::endl;//填充网络通信相关信息struct sockaddr_in local;memset(&local,'\0',sizeof(struct sockaddr_in));local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = INADDR_ANY;//绑定INADDR_ANY//绑定int ret = bind(_sockfd,(struct sockaddr*)&local,sizeof(struct sockaddr));if(ret < 0){std::cout << "bind error!!" << std::endl;return false;}std::cout << "bind success " << std::endl;return true;}~UdpServer(){if(_sockfd >= 0){close(_sockfd);}}
private:int _sockfd; //文件描述符uint16_t _port; //端口号std::string _ip; //IP地址
};int main(int argc,char* argv[])
{if(argc != 2){std::cout << "Usage: " << argv[0] << " port" << std::endl;return 1;}std::string ip = "47.94.84.249";//本地环回uint16_t port = atoi(argv[1]);UdpServer* srv = new UdpServer(ip,port);srv->InitServer();srv->Start();return 0;
}
🌎客户端代码改写
服务端的代码改了之后,对应客户端的代码也得改改。当客户端发完数据给服务端后,由于服务端还会将该数据重新发给客户端,因此客户端发完数据后还需要调recvfrom来读取服务端发来的响应数据。
在客户端调用recvfrom函数接收服务端发来的响应数据时,客户端同时也需要读取服务端与网络相关的各种信息。虽然客户端早已知道服务端的网络信息了,此时服务端的网络信息已经不重要了,但还是建议不要把参数设置为空,这样可能会出问题,所以我们还是用一个临时变量将服务端的网络信息读取一下。
而客户端接收到服务端的响应数据后,将数据原封不动的打印出来就行了。此时客户端发送给服务端的数据,除了在服务端会打印显示以外,服务端还会将数据再重新发回给客户端,此时客户端也会接收到响应数据然后将该数据进行打印。
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdint.h>
#include <string>
#include <memory>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>const int BUFFERSIZE = 128;
class UdpClient
{
public:UdpClient(std::string ip,uint16_t port):_sockfd(-1),_server_port(port),_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());while(true){std::cout << "Please Enter# ";getline(std::cin,msg);sendto(_sockfd,msg.c_str(),msg.size(),0,(struct sockaddr*)&peer,sizeof(peer));char buffer[BUFFERSIZE] = {0};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){std::cout << "recvfrom error!!" << std::endl;}buffer[size] = '\0';std::cout << buffer << std::endl;}}bool InitClient(){//创建套接字_sockfd = socket(AF_INET,SOCK_DGRAM,0);if(_sockfd < 0){std::cout << "create sockfd error!!" << std::endl;return false;}return true;}~UdpClient(){if(_sockfd >= 0){close(_sockfd);}}
private:int _sockfd;uint16_t _server_port; //服务器端口号std::string _server_ip; //服务器ip
};int main(int argc,char* argv[])
{if(argc != 3){std::cout << "Usage: " << argv[0] << " server_ip server_port" << std::endl;return 1;}std::string serverip = argv[1];uint16_t serverport = atoi(argv[2]);UdpClient* clt = new UdpClient(serverip,serverport);clt->InitClient();clt->Start();return 0;
}
此时当我们测试回声服务器时,在服务端和客户端就都能够看到对应的现象,这样就能够判断通信是否正常了。
相关文章:

【计算机网络】Linux下简单的UDP服务器(超详细)
套接字接口 我们把服务器封装成一个类,当我们定义出一个服务器对象后需要马上初始化服务器,而初始化服务器需要做的第一件事就是创建套接字。 🌎socket函数 这是Linux中创建套接字的系统调用,函数原型如下: int socket(int domain, int typ…...
Java并发编程实战 Day 3:volatile关键字与内存可见性
【Java并发编程实战 Day 3】volatile关键字与内存可见性 开篇 欢迎来到《Java并发编程实战》系列的第3天!本系列旨在带领你从基础到高级逐步掌握Java并发编程的核心概念和最佳实践。 今天我们将重点探讨volatile关键字及其在多线程程序中确保内存可见性的作用。我…...

华为OD机试真题——报文回路(2025A卷:100分)Java/python/JavaScript/C/C++/GO最佳实现
2025 A卷 100分 题型 本专栏内全部题目均提供Java、python、JavaScript、C、C++、GO六种语言的最佳实现方式; 并且每种语言均涵盖详细的问题分析、解题思路、代码实现、代码详解、3个测试用例以及综合分析; 本文收录于专栏:《2025华为OD真题目录+全流程解析+备考攻略+经验分…...
K8s工作流程与YAML实用指南
K8s 工作流程 K8s 采用声明式管理(用户说"要什么",K8s 负责"怎么做")方式,通过 YAML 文件描述期望的状态,K8s控制平面会自动确保实际状态与期望状态一致。 核心工作流程如下: 用户提交…...

功能丰富的PDF处理免费软件推荐
软件介绍 今天给大家介绍一款超棒的PDF工具箱,它处理PDF文档的能力超强,而且是完全免费使用的,没有任何限制。 TinyTools(PC)这款软件,下载完成后即可直接打开使用。在使用过程中,操作完毕后&a…...

Java补充(Java8新特性)(和IO都很重要)
一、Lambda表达式 1.1、为什么使用Lambda表达式 Lambda表达式起步案例 下面源码注释是传统写法,代码是简写表达式写法 import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.function.Consumer;/* * 学什么…...
pycharm debug的时候无法debug到指定的位置就停住不动了
报错大致是这样的,但是直接run没有问题,debug就停住不动了 Traceback (most recent call last): File "/home/mapengsen/.pycharm_helpers/pydev/_pydevd_bundle/pydevd_comm.py", line 467, in start_client s.connect((host, port)) Timeou…...

分布式流处理与消息传递——Kafka ISR(In-Sync Replicas)算法深度解析
Java Kafka ISR(In-Sync Replicas)算法深度解析 一、ISR核心原理 #mermaid-svg-OQtnaUGNQ9PMgbW0 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-OQtnaUGNQ9PMgbW0 .error-icon{fill:#55222…...
极大似然估计例题——正态分布的极大似然估计
设总体 X ∼ N ( μ , σ 2 ) X \sim N(\mu, \sigma^2) X∼N(μ,σ2),其中 μ \mu μ 和 σ 2 \sigma^2 σ2 是未知参数,取样本观测值为 x 1 , x 2 , ⋯ , x n x_1, x_2, \cdots, x_n x1,x2,⋯,xn,求参数 μ \mu μ 和 σ 2 \sigma^2 σ…...
Pull Request Integration 拉取请求集成
今天我想要把我创建的项目,通过修改yaml里面的内容,让我在main分支下的其他分支拉取请求的时候自动化测试拉取的内容,以及将测试结果上传到控制台云端。 首先我通过修改yaml文件里面的内容 name: Build and Teston:push:branches:- mainjobs:…...

OS10.【Linux】yum命令
目录 1.安装软件的几种方法 直接编译源代码,得到可执行程序 使用软件包管理器 2.yum yum list命令 参数解释 yum install命令 yum remove命令 下载链接存放的位置 扩展yum源 实验:安装sl小火车命令 sl命令的选项 方法1:man sl 方法2:读源代码 3.更新yum源 查看…...
头歌数据库课程实验(角色管理)
第1关:创建角色 任务描述 本关任务:创建角色 role1localhost。 相关知识 为了完成本关任务,你需要掌握MySQL的角色管理。 角色信息存放在数据库 mysql 的 user 表中。 user 表中字段: Host:可以登陆数据库的主机地…...
【android bluetooth 协议分析 03】【蓝牙扫描详解 1】【扫描关键函数 btif_dm_search_devices_evt 分析】
1. 背景 本篇我们来对 btif_dm_search_devices_evt 函数进行分析. 这是系统性分析 Bluetooth 协议栈中的设备扫描流程时必须厘清的一环。 1. 为什么要单独分析 btif_dm_search_devices_evt 函数: btif_dm_search_devices_evt 是 BTIF 层中处理设备扫描࿰…...
SpringBoot使用ThreadLocal保存登录用户信息
Java 多线程,系列文章: 《Java多线程》 《Java创建多线程的3种方法:继承Thread类、实现Runnable接口、实现Callable接口》 《Java多线程的同步:synchronized关键字、Lock接口、volatile关键字》 《Java线程池》 《Java线程池实现秒杀功能》 《SpringBoot使用ThreadLocal保存…...

多模态大语言模型arxiv论文略读(102)
Chat2Layout: Interactive 3D Furniture Layout with a Multimodal LLM ➡️ 论文标题:Chat2Layout: Interactive 3D Furniture Layout with a Multimodal LLM ➡️ 论文作者:Can Wang, Hongliang Zhong, Menglei Chai, Mingming He, Dongdong Chen, Ji…...
Ubuntu系统如何部署Crawlab爬虫管理平台(通过docker部署)
Ubuntu系统如何部署Crawlab爬虫管理平台(通过docker部署) 一、安装docker(ubuntu系统版本20.4) 1、更新apt sudo apt-get update2、安装必要的依赖包 sudo apt-get install ca-certificates curl gnupg lsb-release3、添加 Docker 官方 GPG 密钥(清化大学源) # 添加Docke…...
python常用库-pandas、Hugging Face的datasets库(大模型之JSONL(JSON Lines))
文章目录 python常用库pandas、Hugging Face的datasets库(大模型之JSONL(JSON Lines))背景什么是JSONL(JSON Lines)通过pandas读取和保存JSONL文件pandas读取和保存JSONL文件 Hugging Face的datasets库Hugg…...

高端装备制造企业如何选择适配的项目管理系统提升项目执行效率?附选型案例
高端装备制造项目通常涉及多专业协同、长周期交付和高风险管控,因此系统需具备全生命周期管理能力。例如,北京奥博思公司出品的 PowerProject 项目管理系统就是一款非常适合制造企业使用的项目管理软件系统。 国内某大型半导体装备制造企业与奥博思软件达…...
【Dv3Admin】工具权限配置文件解析
接口级权限控制是后台系统安全防护的核心手段。基于用户角色、请求路径与方法进行细粒度授权,可以有效隔离不同用户的数据访问范围,防止越权操作,保障系统整体稳定性。 本文解析 dvadmin/utils/permission.py 模块,重点关注其在匿…...

AI炼丹日志-22 - MCP 自动操作 Figma+Cursor 自动设计原型
MCP 基本介绍 官方地址: https://modelcontextprotocol.io/introduction “MCP 是一种开放协议,旨在标准化应用程序向大型语言模型(LLM)提供上下文的方式。可以把 MCP 想象成 AI 应用程序的 USB-C 接口。就像 USB-C 提供了一种…...
Python爬虫:AutoScraper 库详细使用大全(一个智能、自动、轻量级的网络爬虫)
更多内容请见: 爬虫和逆向教程-专栏介绍和目录 文章目录 一、AutoScraper概述1.1 AutoScraper介绍1.2 安装1.3 注意事项二、基本使用方法2.1 创建 AutoScraper 实例2.2 训练模型2.3 保存和加载模型2.4 数据提取方法2.5 自定义规则三、高级功能3.1 多规则抓取3.2 分页抓取3.3 代…...
2025.6.1总结
今天又上了一天课,假期三天,上了两天的课,明天还得刷题。利用假期时间上课学习,并没有让我感到有多充实,反而让我感到有些小压抑。 在下午的好消息分享环节,我分享了毕业工作以来的一些迷茫。我不知道自己…...

[嵌入式实验]实验四:串口打印电压及温度
一、实验目的 熟悉开发环境在开发板上读取电压和温度信息使用串口和PC通信在PC上输出当前电压和温度信息 二、实验环境 硬件:STM32开发板、CMSIS-DAP调试工具 软件:STM32CubeMX软件、ARM的IDE:Keil C51 三、实验内容 配置相关硬件设施 &…...
LVS+Keepalived 高可用
目录 一、核心概念 1. LVS(Linux Virtual Server) 2. Keepalived 二、高可用架构设计 1. 架构拓扑图 2. 工作流程 三、部署步骤(以 DR 模式为例) 1. 环境准备 2. 主 LVS 节点配置 (1)安装 Keepali…...

Linux正则三剑客篇
一、历史命令 history 命令 :用于输出历史上使用过的命令行数量及具体命令。通过 history 可以快速查看并回顾之前执行过的命令,方便重复操作或追溯执行过程。 !行号 :通过指定历史命令的行号来重新执行该行号对应的命令。例如,若…...
HTML5 视频播放器:从基础到进阶的实现指南
在现代Web开发中,视频播放功能是许多网站的重要组成部分。无论是在线教育平台、视频分享网站,还是企业官网,HTML5视频播放器都扮演着不可或缺的角色。本文将从基础到进阶,详细介绍如何实现一个功能完善的HTML5视频播放器ÿ…...
鸿蒙HarmonyOS (React Native)的实战教程
一、环境配置 安装鸿蒙专属模板 bashCopy Code npx react-native0.72.5 init HarmonyApp --template react-native-template-harmony:ml-citation{ref"4,6" data"citationList"} 配置 ArkTS 模块路径 在 entry/src/main/ets 目录下创建原生模块&…...
函数栈帧深度解析:从寄存器操作看函数调用机制
文章目录 一、程序运行的 "舞台":内存栈区与核心寄存器二、寄存器在函数调用中的核心作用三、函数调用全流程解析:以 main 调用 func 为例阶段 1:main 函数栈帧初始化**阶段 2:参数压栈(右→左顺序&#x…...

【计算机网络】第3章:传输层—可靠数据传输的原理
目录 一、PPT 二、总结 (一)可靠数据传输原理 关键机制 1. 序号机制 (Sequence Numbers) 2. 确认机制 (Acknowledgements - ACKs) 3. 重传机制 (Retransmission) 4. 校验和 (Checksum) 5. 流量控制 (Flow Control) 协议实现的核心:滑…...
rv1126b sdk移植
DDR rkbin bin/rv11/rv1126bp_ddr_v1.00.bin v1.00 板子2 reboot异常 [ 90.334976] reboot:Restarting system DDR 950804cb85 wesley.yao 25/04/02-15:54:40,fwver: v1.00In Derate1 tREFI1x SR93 PD13 R ddrconf 4 rgef0 rgcsb0 1 ERR: Read gate CS0 err error ERR …...