网络套接字编程(一)

news2024/11/19 1:51:17

网络套接字编程(一)

文章目录

  • 网络套接字编程(一)
    • 预备知识
      • 源IP地址和目的IP地址
      • 端口号
      • TCP/UDP协议特点
      • 网络字节序
    • socket编程
      • socket常用API
      • sockaddr结构
    • 简易UDP网络程序
      • 服务端创建套接字
      • 服务端绑定IP地址和端口号
      • 字符型IP地址VS整型IP地址
      • 服务端运行
      • 客户端创建套接字
      • 客户端绑定问题
      • 启动客户端
      • 程序测试

预备知识

源IP地址和目的IP地址

在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址。

源IP地址的作用是让接收方能够将数据返回过来。

目的IP地址的作用进行路由选择和确认数据是否传输到指定位置。

端口号

端口号

在了解端口号前要明确一些概念,首先,网络通信使得网络中的一台主机可以将数据传输给另一台主机,其目的是为了让多台主机协同完成工作。其次,在网络协议栈中由应用层产生数据,交付给下层进行传输。而网络应用层中产生数据的其实是进程,进程为了完成一个工作通过网络将数据交付给其他主机的进程,从而完成多个进程协同工作,因此网络通信的本质是进程间通信

如下图,客户端进程通过网络将数据传输给服务端进程,让服务端进程对所得到的数据进行处理,从而完成任务:

image-20231021152108069

既然,网络通信的本质是进程间通信,因此在网络传输时,必须知道数据要定位接收方进程,因此使用端口号定位主机中的一个进程,又因为IP地址能够定位网络中的一台主机,因此IP地址+端口号可以定位网络中的唯一一个进程。完整的IP中包含IP地址和端口号。

端口号(port)作为传输层协议的内容,其内容如下:

  • 端口号是一个2字节16位的整数。
  • 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理。
  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程。
  • 一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定。

不采用进程PID定位进程而采用端口号的原因

网络的管理在操作系统中属于文件管理的范畴,而进程PID是属于进程管理的范畴,并且网络在文件管理中属于一个单独的模块,如果使用进程PID就会将网络管理和进程管理关联起来,会使得二者耦合度变高,不利于维护。

网络将数据交付给进程的原理

网络的管理在操作系统中属于文件管理的范畴,进程的管理属于进程管理的范畴,网络要将数据交给进程需要将数据加载到文件的缓冲区中,然后让进程使用文件操作读取缓冲区的内容,最终得到网络传输过来的数据。

TCP/UDP协议特点

应用层进程主要是从传输层获取网络传输的数据,传输层中有两种协议:TCP协议、UDP协议。

TCP协议特点

TCP(Transmission Control Protocol 传输控制协议):

  • 传输层协议
  • 有连接
  • 可靠传输
  • 面向字节流

注: 可靠传输是指使用TCP协议传输数据时,如果数据丢失了,会采用重传数据等策略保证接收端接收到数据。

UDP协议特点

UDP(User Datagram Protocol 用户数据报协议):

  • 传输层协议
  • 无连接
  • 不可靠传输
  • 面向数据报

注: 不可靠传输是指使用UCP协议传输数据时,即使数据丢失了,也不会采取任何措施。

说明一下: 可靠传输和不可靠传输是TCP协议和UDP协议的特点,而不是优缺点,因为执行可靠传输一定要付出协议复杂,传输时间长等的代价,不可靠传输由于协议简单,会有传输时间短等的优点。

网络字节序

内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏 移地址也有大端小端之分, 网络数据流同样有大端小端之分。那么如何定义网络数据流的地址呢?

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按存地址从低到高的顺序保存;
  • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
  • TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。
  • 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
  • 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主字节序的转换:

#include <arpa/inet.h>

uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
  • 这些函数名很好记,h表示host(主机),n表示network(网络),l表示32位长整数,s表示16位短整数。
  • 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;。
  • 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。

socket编程

socket常用API

#include <sys/types.h>         
#include <sys/socket.h>

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器) 
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

sockaddr结构

socket编程中用于定义网络地址的结构体种类分为struct sockaddr_instruct sockaddr_un,其中struct sockaddr_in用于IPv4网络,struct sockaddr_un用于Unix域(本地局域网),为了兼容两种结构体,采用struct sockaddr作为socket接口的参数,在接收参数时,会判断前16位地址类型是AF_INET还是AF_UNIX,区分使用的是那种结构体。

image-20231021165106439

IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示;包括16位地址类型,16位端口号和32位IP地址.

sockaddr_in的结构:

struct sockaddr_in
{
    sa_family_t _sinfamily;//unsigned short int类型参数
    in_port_t sin_port;//端口号
    struct in_addr sin_addr;//IP地址
    //填充字段
    unsigned char sin_zero[sizeof (struct sockaddr) -
                           __SOCKADDR_COMMON_SIZE -
                           sizeof (in_port_t) -
                           sizeof (struct in_addr)];
};

typedef uint32_t in_addr_t;
struct in_addr
{
    in_addr_t s_addr;
};

简易UDP网络程序

为了更好的理解socket编程,编写一组简易的UDP网络程序,其包含客户端和服务端,客户端发送数据给服务端,服务端将接收的数据回传给客户端。

image-20231029184309956

服务端创建套接字

我们把服务器封装成一个类,当我们定义出一个服务器对象后需要马上初始化服务器,而初始化服务器需要做的第一件事就是创建套接字。创建套接字需要使用socket函数:

//socket函数所在的头文件和函数声明
#include <sys/types.h>
#include <sys/socket.h>

int socket(int domain, int type, int protocol);
  • 该函数用于创建网络套接字。
  • domain参数: 指明数据传输域,传入AF_INET为网络通信,传入AF_UNIX为本地通信。
  • type函数: 指明套接字种类,传入SOCK_STREAM为流式套接,传入 SOCK_DGRAM 为数据报套接。
  • protocol参数: 指明所使用的协议,默认为0,该函数会自动识别所使用的协议。
  • 返回值: 调用成功返回一个文件描述符,调用失败返回-1,错误码被设置。

socket函数属于什么类型的接口?

在计算机软硬体系结构中,程序员编程形成程序都是在操作系统之上的用户层进行的,对应TCP/IP网络协议栈的应用层,因此,socket函数是操作系统提供属于应用层的系统接口。

socket函数底层做了什么?

socket函数是被引用层的进程所调用的,而每一个进程在系统层面上都有一个进程地址空间PCB(task_struct)、文件描述符表(files_struct)以及对应打开的各种文件。而文件描述符表里面包含了一个数组fd_array,其中数组中的0、1、2下标依次对应的就是标准输入、标准输出以及标准错误。
image-20231029171121435

在调用socket函数后,操作系统会为该进程创建该套接字对应的文件,将其记录在该进程的文件描述符表中:

image-20231029171516441

将数据写入该套接字对应的文件中,该文件刷新缓冲区的数据后,就会将数据写入网卡设备中,网卡会将数据传输出去。

image-20231029171611894

创建套接字

enum
{
    SOCKET_ERR=1
};
class UdpServer
{
    public:
    void InitServer()
    {
        //创建套接字
        _sock = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sock < 0)
        {
            std::cerr << "socket create error:" << strerror(errno) << std::endl;
            exit(SOCKET_ERR); 
        }
    }
    private:
    int _sock; // 网络文件描述符
};

服务端绑定IP地址和端口号

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

//bind函数所在的头文件和函数声明
#include <sys/types.h>         
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 该函数用于绑定套接字的IP地址和端口号。
  • sockfd参数: 要接收数据的套接字,即调用socket函数返回的文件描述符。
  • addr参数: 用于传入一个指向存有目标IP地址和端口号的sockaddr类型指针。
  • addlen参数: sockaddr类型变量的长度。
  • 返回值: 调用成功返回一个文件描述符,调用失败返回-1,错误码被设置。
  • 云服务不需要调用bind函数绑定指定IP地址,因为云服务可以存在多个网卡设备,需要让云服务自身制定IP地址。可以在将sockaddr类型中的IP地址字段赋值为INADDR_ANY让云服务绑定任意IP地址。

sockaddr_in的结构

前面提到了使用网络通信时,采用的sockaddr结构中的sockaddr_in结构,sockaddr_in具体的数据结构如下:

typedef unsigned short int sa_family_t;
typedef uint16_t in_port_t;

typedef uint32_t in_addr_t;
struct in_addr
{
    in_addr_t s_addr;
};

struct sockaddr_in
{
    sa_family_t _sinfamily;//unsigned short int类型参数
    in_port_t sin_port;//端口号
    struct in_addr sin_addr;//IP地址
    //填充字段
    unsigned char sin_zero[sizeof (struct sockaddr) -
                           __SOCKADDR_COMMON_SIZE -
                           sizeof (in_port_t) -
                           sizeof (struct in_addr)];
};
  • _sinfamily: 标识该sockaddr结构要进行的是网络通信,还是本地通信.网络通信时赋值为AF_INET,本地通信时赋值为AF_UNIX.
  • sin_port: 16位无符号整型表示的端口号。
  • s_addr: 32位无符号正式表示的IP地址。

给进程绑定IP地址和端口号的原理

进程在应用层将要绑定的IP地址和端口号写入到对应的sockaddr结构中,然后调用操作系统提供的bind系统接口,让操作系统完成进程的IP地址和端口号的绑定工作。

本地端口号和网络字节序的转化

进程执行时定义一个无符号的16位整型port变量记录要绑定的端口号后,想要将其写入sockaddr结构前,需要调用系统提供的网络字节序接口htons接口,将port转换成符合网络字节序的16位端口号,完成网络字节序的转化后,才能将其写入sockaddr结构中,并使用其绑定端口号。

绑定IP地址和端口号

enum
{
    SOCKET_ERR = 1,
    BIND_ERROR
};
class UdpServer
{
    public:
    UdpServer(uint16_t port):_port(port) {}
    void InitServer()
    {
        // 创建套接字
        _sock = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sock < 0)
        {
            std::cerr << "socket create error:" << strerror(errno) << std::endl;
            exit(SOCKET_ERR);
        }
        // 绑定IP地址和端口号
        struct sockaddr_in local;//创建sockaddr结构写入IP地址和端口号
		memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_addr.s_addr = INADDR_ANY;//云服务器IP地址赋值
        local.sin_port = htons(_port);
        socklen_t len = sizeof(local);
        int n = bind(_sock, (struct sockaddr*)&local, len);//端口号绑定
        if (n < 0)
        {
            std::cerr << "bind error:" << strerror(errno) << std::endl;
            exit(BIND_ERROR);
        }
    }
    private:
    int _sock; // 网络文件描述符
    uint16_t _port;//端口号
};

字符型IP地址VS整型IP地址

IP地址是由 . 分割,由四个部分形成的,每个部分的取值范围位[0~255],如果使用字符型记录,至少需要12个字节(不记录.)也就是96个比特位,如果采用整型记录只需要4个字节,也就是32位,具体的记录方式是将一个4字节的无符号整形数据按照字节划分成4个部分,每个部分都占一个字节的空间,而一个字节的空间刚好能记录[0~255]的数据:

image-20231029182319364

操作系统提供了字符型IP地址和整形IP地址的转换函数,我们直接调用即可:

inet_addr函数

inet_addr函数的功能是将字符串IP转换成整数IP。

//inet_addr函数所在的头文件和函数声明
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);

该函数使用起来非常简单,我们只需传入待转换的字符串IP,该函数返回的就是转换后的整数IP。除此之外,inet_aton函数也可以将字符串IP转换成整数IP,不过该函数使用起来没有inet_addr简单。

inet_ntoa函数

inet_ntoa函数的功能是将整数IP转换成字符串IP。

//inet_ntoa函数所在的头文件和函数声明
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
char *inet_ntoa(struct in_addr in);

需要注意的是,传入inet_ntoa函数的参数类型是in_addr,因此我们在传参时不需要选中in_addr结构当中的32位的成员传入,直接传入in_addr结构体即可。

服务端运行

服务器运行起来后,需要完成从网络中接收数据和将数据回传给客户端的任务,接收数据需要用到recvfrom函数:

recvfrom函数

//recvfrom函数所在的头文件和函数声明
#include <sys/types.h>
#include <sys/socket.h>

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, 
 				socklen_t *addrlen);
  • 该函数的功能是从一个指定的套接字接收数据,并将数据存储到指定的缓冲区中。
  • sockfd参数: 要接收数据的套接字,即调用socket函数返回的文件描述符。
  • buf参数: 指向用来存储接收数据的缓冲区。
  • len参数: 缓冲区长度(即最多可以接收的字节数)。
  • flags参数: 用于控制recvfrom函数的行为,默认为0,阻塞读取。
  • src_addr参数: 存储发送方的地址信息(IP地址和端口号)的sockaddr类型变量的地址。
  • addrlen参数: 指向存放发送方地址信息的sockaddr类型变量的长度的变量的地址。
  • 返回值: 成功接收数据时,返回接收到的字节数。连接关闭时,返回0。发生错误时,返回-1,并设置errno变量以指示具体的错误原因。

服务端接受数据后要发送数据,发送数据需要使用sendto函数:

sendto函数

//sendto函数所在的头文件和函数声明
#include <sys/types.h>
#include <sys/socket.h>

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, 					   socklen_t addrlen);
  • 该函数的功能是将指定缓冲区中的数据发送到指定的套接字。
  • sockfd参数: 要发送数据的套接字,即调用socket函数返回的文件描述符。
  • buf参数: 指向要发送数据的缓冲区。
  • len参数: 要发送的数据长度(字节数)。
  • flags参数: 用于控制sendto函数的行为,默认为0。
  • dest_addr参数: 指定目标地址(即接收方的地址信息,包括IP地址和端口号)的sockaddr类型变量的指针。
  • addrlen参数: 指定目标地址信息的大小,即dest_addr的长度。
  • 返回值: 成功发送数据时,返回成功发送的字节数。发生错误时,返回-1,并设置errno变量以指示具体的错误原因。

启动服务器函数

启动服务器函数的功能让其从网络中接收数据,并将数据回传给客户端。

class UdpServer
{
    public:
    void StartServer()//服务端运行
    {
        char buffer[128];
        while (true)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);//必须写明
            ssize_t n = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (n > 0)
            {
                buffer[n] = 0;
            }
            else
                continue;
            std::string clientip = inet_ntoa(peer.sin_addr);
            uint16_t clientport = ntohs(peer.sin_port);
            std::cout << clientip << "-" << clientport << "send#" << buffer << std::endl;
            sendto(_sock, buffer, strlen(buffer), 0, (struct sockaddr *)&peer, len);
        }
    }
    private:
    int _sock; // 网络文件描述符
    uint16_t _port;//端口号
};

recvfrom函数接收sockaddr结构

recvfrom函数使用sockaddr结构从网络中接收数据时,就是按照网络字节序接收的,因此再交给sendto函数发送数据时,无需进行网络字节序的转化。

缓冲区问题

使用buffer缓冲区从网络中接收数据时,按照C语言规定需要为缓冲区预留一个字节用于存储’\0’。

使用buffer缓冲区向网络中发送数据时,无需发送’\0’,因为那是C语言的规定不是网络的规定。

recvfrom函数接收数据时会将网络字节序转换为主机序列写入缓冲区,sendto函数向网络发送数据时会将缓冲区数据从主机序列转换成网络字节序发送。

运行服务端

调用服务端类内部的函数进行服务端的初始化,并启动服务端。为了给错误启动服务端纠错,引入了命令行参数,在启动服务端时做纠错提示:

void Usage(const char *proc)
{
    std::cout << "Usage:\n\t" << proc << "port\n" << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
    }
    uint16_t port = atoi(argv[1]);

    std::unique_ptr<UdpServer> ustr(new UdpServer(port));
    ustr->InitServer();
    ustr->StartServer();
    return 0;
}

启动服务端

启动服务端后,使用netstat -naup指令查看服务端进程信息:

image-20231030143051235

客户端创建套接字

同样的,将客户端封装成类,在使用客户端时,只需要创建类对象,然后调用对应的函数即可使用客户端。在创建客户端类对象后的第一步就是初始化客户端,在初始化客户端时,首先就需要创建套接字:

enum
{
    SOCKET_ERR = 1,
    BIND_ERROR
};
class Udp_Client
{
    public:
    void InitClient()
    {
        //创建套接字
        _sock = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sock < 0)
        {
            std::cerr << "socket create error:" << strerror(errno) << std::endl;
            exit(SOCKET_ERR);
        }
    }
    private:
    int _sock;
};

客户端绑定问题

使用socket进行网络通信,是需要通过IP地址和端口号确定唯一进程,然后再进行通信的,客户端如果不进行IP地址和端口号的绑定,服务端就无法将数据再回传给客户端,因此客户端是一定需要绑定IP地址和端口号的。

一台主机上会存在大量的客户端进程,如果每个客户端进程都要指定绑定端口号,可能会因为客户端端口号冲突造成客户端启动失败的问题,并且客户端只要能够实现和服务端进行网络通信的功能即可,端口号的具体值并不重要,因此客户端不能绑定指定的端口号,需要让操作系统来完成客户端端口号的绑定。

服务端是给众多客户端提供网络服务的,服务端的端口号如果随意改变,客户端就会因为服务端的端口号的改变导致无法找到服务端。因此服务端的端口号一定需要自主绑定。

客户端在首次调用发送数据的系统调用时,操作系统会自动选择端口号和自身的IP地址绑定到客户端。

启动客户端

运行客户端函数

运行客户端函数的功能是接受用户输入的数据将其发送给服务端,然后接受服务端回传的数据。

class Udp_Client
{
    public:
    Udp_Client(std::string serverip, uint16_t serverport):_serverip(serverip), _serverport(serverport)
    {}
    void StartClient()
    {
        while(true)
        {
            std::cout << "Please enter message#";
            std::string message;
            getline(std::cin, message);
            struct sockaddr_in peer;//指明服务端IP地址和端口号
            peer.sin_family = AF_INET;
            peer.sin_addr.s_addr = inet_addr(_serverip.c_str());
            peer.sin_port = htons(_serverport);
            sendto(_sock, message.c_str(), message.size(), 0, (struct sockaddr*)&peer, sizeof(peer));
            struct sockaddr_in temp;
            socklen_t tlen;
            char buffer[128];
            ssize_t n = recvfrom(_sock, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&temp, &tlen);
            if (n > 0)
            {
                buffer[n] = 0;
                std::cout << "server echo:" << buffer << std::endl;
            }
        }
    }
    private:
    int _sock;
    std::string _serverip;
    uint16_t _serverport;
};

revfrom函数的注意事项

recvfrom函数最后两个参数src_addr,addrlen都是输出型参数,在函数中会进行赋值操作,因此不能传空指针。

启动客户端

和服务端相同,调用客户端类内部的函数进行客户端的初始化,并启动客户端。为了给错误启动客户端纠错,引入了命令行参数,在启动客户端时做纠错提示:

void Usage(const char *proc)
{
    std::cout << "Usage:\n\t" << proc << "port\n" << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERROR);
    }
    uint16_t port = atoi(argv[1]);

    std::unique_ptr<UdpServer> ustr(new UdpServer(port));
    ustr->InitServer();
    ustr->StartServer();
    return 0;
}

程序测试

本地测试

现在服务端和客户端的代码都已经编写完毕,我们可以先进行本地测试,此时服务器没有绑定外网,绑定的是本地环回。现在我们运行服务器时指明端口号为8080,再运行客户端,此时客户端要访问的服务器的IP地址就是本地环回127.0.0.1,服务端的端口号就是8080。

image-20231030160930214

客户端运行之后提示我们进行输入,当我们在客户端输入数据后,客户端将数据发送给服务端,此时服务端再将收到的数据打印输出,这时我们在服务端的窗口也看到我们输入的内容。

image-20231030160726907

此时我们再用netstat命令查看网络信息,可以看到服务端的端口是8080,客户端的端口是。这里客户端能被netstat命令查看到,说明客户端也已经动态绑定成功了,这就是我们所谓的网络通信。

image-20231030161029355

网络测试

网络测试和本地测试的方式类似,只是网络测试输入的IP地址得是服务端的IP地址:

image-20231030162533334

不同于本地测试的是,可以使用其他主机访问该服务端,只需要让其他主机获取该客户端程序,然后在其他主机运行客户端时输入服务端IP地址和端口号即可完成网络通信。

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

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

相关文章

python使用ffmpeg来制作音频格式转换工具(优化版)

简介:一个使用python加上ffmpeg模块来进行音频格式转换的工具。 日志: 20231030:第一版,设置了简单的UI布局和配色,实现音频转为Mp3、AAC、wav、flac四种格式。可解析音频并显示信息,可设置转换后的保存路径 UI界面: 编程平台:visual studio code 编程语言:python 3…

如何把编辑器编辑好的文章分享给别人?

哈喽&#xff0c;大家好&#xff0c;今天小编给大家分享一个非常实用的功能&#xff0c;当我们在使用微信编辑器编辑排版文章的时候&#xff0c;需要在不发表的前提下分享给好友或者需要先给领导看一下的时候该怎么办呢&#xff1f;有三个方法&#xff0c;都可以在不发表的前提…

Ortec974A EPICS IOC程序

1&#xff09; 创建一个用户存放这个IOC程序结构的目录&#xff1a; rootorangepi4-lts:/usr/local/EPICS/program# mkdir ortec974A rootorangepi4-lts:/usr/local/EPICS/program# cd ortec974A/ rootorangepi4-lts:/usr/local/EPICS/program/ortec974A# ls2&#xff09;使用…

系列四十二、Spring的事务传播行为案例演示(二)#REQUIRED

一、演示Spring的默认传播行为&#xff08;REQUIRED&#xff09; 1.1、运行之前表中的数据 1.2、StockServiceImpl /*** Author : 一叶浮萍归大海* Date: 2023/10/30 15:43* Description:*/ Service(value "stockServiceREQUIRED") public class StockServiceImpl…

Mysql设置了更新时间自动更新,指定更新部分sql时不进行时间更新

现象&#xff1a; 因为字段设置了自动更新&#xff0c;所以sql语句一进行修改此字段就会自动更新时间&#xff0c;但是呢我们的有部分定时任务是半夜执行&#xff0c;并且不能让这个任务修改到数据的更新时间 解决&#xff1a; <update id"updateCreative">ALT…

element-plus DateTimePicker日期选择器,限制指定日期和时间不可选择

element-plus日期选择器&#xff0c;在指定日期时间前不可选择。 限制日期选择&#xff0c;使用disabled-date 限制小时选择&#xff0c;使用disabled-hours 限制分钟选择&#xff0c;使用disabled-minutes 限制毫秒选择&#xff0c;使用disabled-seconds 指定日期当天的时间有…

一带一路10周年:爱创科技加速中国药企国际化征程

“源自中国&#xff0c;属于世界”。 共建“一带一路”倡议提出10周年来&#xff0c;中国与沿线国家经济深度融合&#xff0c;在共商共建共享的基本原则下&#xff0c;“一带一路”形成了国际合作的平台和机制&#xff0c;跨国经济合作已基本形成。 随着“一带一路”合作日益加…

【脚本笔记】AssetDatabase

AssetDatabase是编辑器下的处理资源操作的重要类&#xff0c;主要用于访问资源并针对资源执行操作的接口。 这里面所有的操作路径都是基于Unity项目的相对路径也就是Assets/xxx或者Assets/xxx.jpg这种。CacheServer 主要解决的是缩短大型团队导入资源的时间。当配置后&#xff…

K8S部署应用时从harbor拉取镜像失败

问题描述 K8S部署服务拉取镜像失败&#xff1a;ImagePullBackOff K8S拉取镜像提示&#xff1a;Failed to pull image “IP:PORT/zcy-project/nginx:1.16.1”: rpc error: code Unknown desc Error response from daemon: unauthorized: unauthorized to access repository: …

mediapipe 训练自有图像数据分类

参考&#xff1a; https://developers.google.com/mediapipe/solutions/customization/image_classifier https://colab.research.google.com/github/googlesamples/mediapipe/blob/main/examples/customization/image_classifier.ipynb#scrollToplvO-YmcQn5g 安装&#xff1a…

配音软件怎么选?推荐三款自用好评的

会刷短视频的小伙伴肯定知道&#xff0c;很多创作者想让自己的视频更加丰富&#xff0c;呈现更加完美的视频效果&#xff0c;往往会在视频里的空境部分加入一些旁白解说&#xff0c;你们难道就不好奇吗&#xff1f;这些声音为什么这么想真人说的话&#xff0c;而且还可以这么自…

虚幻C++基础 day1

虚幻C概念 虚幻C类的继承结构 虚幻引擎C类层级结构(Hierarchy) 这些基本类又派生出了很多子类&#xff0c;例&#xff1a; UE中的反射与垃圾回收系统 例如一个创建了一个Actor类&#xff0c;有一个Actor类型指针去指向这个Actor类&#xff0c;如果的指针被销毁了&#xff…

编译环境里存在yaml-cpp的多个版本时可能引起的问题

有时要编译的程序自带了特定版本的yaml-cpp&#xff0c;同时系统目录下也安装了更高版本的yaml-cpp&#xff0c;这时可能引起编译错误&#xff0c;就是某些yaml-cpp的API不认识&#xff0c;例如&#xff1a; 出现这种问题倒好办&#xff0c;正常情况下不可能&#xff0c;肯定能…

目标检测算法发展史

前言 比起图像识别&#xff0c;现在图片生成技术要更加具有吸引力&#xff0c;但是要步入AIGC技术领域&#xff0c;首先不推荐一上来就接触那些已经成熟闭源的包装好了再提供给你的接口网站&#xff0c;会使用别人的模型生成一些图片就能叫自己会AIGC了吗&#xff1f;那样真正…

电脑突然提示找不到msvcp140.dll怎么办,解决msvcp140.dll丢失的办法

当我们在电脑上运行某些软件或游戏时&#xff0c;可能会遇到一个常见的错误消息&#xff1a;“找不到msvcp140.dll”。出现这样的情况通常意味着系统缺少一个重要的动态链接库文件&#xff0c;而这可能会导致程序无法正常启动。如果你现在遇到了这个问题&#xff0c;哪有可以用…

人大与加拿大女王大学金融管理硕士项目:开启国际视野,成就金融领袖

生活中&#xff0c;我们总会遇到各种各样的困难和挑战。有时候&#xff0c;我们会感到沮丧、迷茫甚至绝望。但是&#xff0c;正是这些困难和挑战&#xff0c;让我们变得更加坚强、勇敢和成熟。在这个职场竞争愈发激烈的时代&#xff0c;不断地充实自己是非常重要的。如果你从事…

Echats-页面切换时echats图表刷新

在src文件夹下的composables创建echats.ts echats.ts的内容为&#xff1a; import { nextTick, effectScope, onScopeDispose, ref, watch } from vue; import type { ComputedRef, Ref } from vue; import * as echarts from echarts/core; import { BarChart, GaugeChart, L…

Golang | Zinx学习笔记(一)

参考 http://zinx.me/ https://www.kancloud.cn/aceld/zinx/1960213 https://www.yuque.com/aceld/tsgooa/gx01meg5ow4pftac 说明 zinx是一个基于Golang的轻量级并发服务器框架。 目前zinx已经在很多企业进行开发使用&#xff0c;具体使用领域包括:后端模块的消息中转、长链…

Mybatis-Plus通用枚举功能 [MyBatis-Plus系列] - 第493篇

历史文章&#xff08;文章累计490&#xff09; 《国内最全的Spring Boot系列之一》 《国内最全的Spring Boot系列之二》 《国内最全的Spring Boot系列之三》 《国内最全的Spring Boot系列之四》 《国内最全的Spring Boot系列之五》 《国内最全的Spring Boot系列之六》 S…