网络——套接字编程UDP

目录

端口号

源端口号和目的端口号

认识TCP协议和UDP协议

网络字节序

socket编程接口

socket常见接口

sockaddr结构

UDP

socket

bind

recvfrom

sendto

编写客户端

绑定INADDR_ANY

实现聊天功能


端口号

        在这之前我们已经说过源IP地址和目的IP地址,还有源MAC地址和目的MAC地址。接下来我们再来谈一谈什么是端口号。

源端口号和目的端口号

        首先我们要知道的是IP地址标定了主机的唯一性,当我们在打游戏或者刷视频的时候,一定是通过手机或者电脑,里面一定会有对应的App,这叫做客户端软件,运行起来后就是客户端进程,我们通过请求服务端进程来执行某种功能,通过TCP/IP协议把数据送到对方的主机,但是这不是真正的网络通信的过程,它本质上是在进行进程间通信,将数据在主机间转发只是过程,所以机器收到后需要将数据交付给指定的进程

        端口号(port)就是标识特定主机上进程的唯一性,他是一个2字节16位的整数。但是之前我们说过,进程pid也是标识进程的唯一性,那么为什么不用pid代替端口号呢?原因是,进程是进程,端口号是端口号,这两个概念本来就没有什么关系,我们不希望让两个没有关系的概念揉在一起。

        所以在网络上两台主机要进行通信就要有源IP地址和源端口号,还要有目的IP和目的端口号,通过IP地址和端口号就可以标识网络中唯一一个进程,我们把他们就叫做套接字


认识TCP协议和UDP协议

        在应用层下就是传输层,传输层使用的是由操作系统提供的系统调用接口,而传输层最典型的两种协议就是TCP和UDP协议。

UDP协议:

        UDP协议叫做用户数据报协议(User Datagram Pool),它无需建立连接,而且不可靠面向数据报的传输层协议,可能会出现丢包、数据包乱序的问题。

TCP协议:

        TCP协议叫做传输控制协议(Transmission Control Protocol),它是一种可连接可靠的面向字节流的传输层通信协议

        看着UDP是不是没有TCP可靠,但是在现在的主流网络丢包的问题概率并不大,有些是可以容忍的。可不可靠只是他们两个的一个特点,不可靠不会为了让网络通信变得可靠而做过多的工作,可靠性是需要大量的编码和数据处理的TCP为了保证可靠性一定要设计更多的策略,这些都要用户自己去完成,虽然保证了可靠性,但是它就会更复杂,维护成本也更高。


网络字节序

        在计算机中是有大小端的概念的,大端就是高字节内容放在低地址处低字节内容放在高地址处小端就是高字节内容放在高地址处低字节内容放在低地址处

        如果编写的程序在本地上是不需要考虑这些问题的,如果是网络通信,就不确定对方的主机是哪种存储方式。

        由于我们不能保证通信双方存储数据的方式是一样的,所以就规定网络当中传输的数据必须采用大端字节序,无论是大端机还是小端机,都必须转换成大端。

        所以小端机器发送时要将数据转换成大端,接受数据时再把数据转换成小端。大端机器就不需要考虑这些。

为了解决这个问题,也有了这些接口:

  • h代表host,主机的意思
  • n代表network,网络的意思
  • l 代表long,32位的意思
  • s代表short,16位的意思

所以上面这些接口无非就是主机转网络,或者是网络转主机。


socket编程接口

socket常见接口

// 创建 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结构

        套接字不仅支持网络的进程间通信,还支持本地的进程间通信(域间套接字)。在进行网络通信时我们需要传递IP地址和端口号,而本地通信则不需要,因此套接字提供了sockaddr_in结构体和sockaddr_un结构体,其中sockaddr_in结构体是用于跨网络通信的,而sockaddr_un结构体是用于本地通信的。

        因为设计师不想设计不同的结构对应不同的通信方式,于是就出现了sockaddr结构体,他们三个虽然结构不同,但是三个结构体前16位比特位都是一样的,这叫做套接字的域或者叫协议家族,之后我们也是要填充这个字段的。

在传参时统一传的都是sockaddr结构体。通过前16个比特位确定通信方式,


UDP

作为一个服务器,那就一定要有服务端和用户端,我们就先来写一下服务端。

socket

要进程网络通信,第一步就要创建socket套接字。

参数:

  • domain:创建套接字的域或者叫协议家族,网络通信就设置为AF_INET(IPV6折纸为AF_INET6),本地通信就设置为AF_UNIX,这其实是一个宏。
  • type:创建套接字的服务类型,字节流SOCK_STREAM或数据报SOCK_DGRAM。
  • protocol:创建套接字的协议类别,可以指明值TCP或UDP。一般只设置为0。

返回值:成功返回一个文件描述符,失败返回-1,错误码被设置。

    _sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (_sock < 0)
    {
        logMessage(FATAL, "%d:%s", errno, strerror(errno));
        exit(2);
    }

bind

作用:将ip和port在内核中和当前进程强关联。

参数:

  • sockfd:套接字文件描述符
  • addr:我们要填充的服务端网络属性信息,包括协议家族、端口号、IP地址,因为我们使用的是sockaddr_in,所以最后要取地址强转为struct sockaddr*
  • addrlen:addr结构体的长度

返回值:绑定成功返回0,失败返回-1,错误码被设置。

        套接字文件描述符我们是有的,addrlen可以使用sizeof,那就剩addr了,除了memset可以初始化,bzero也是可以初始化的,这些接口也多用一下。

struct sockaddr_in local就是我们要填充的字段。

三个参数看着也很熟悉:

  • sin_family:这是域或者协议家族。
  • sin_port:这是端口号,16位整数。
  • sin_addr:这是IP地址,32位整数。
typedef unsigned short int sa_family_t;
#define	__SOCKADDR_COMMON(sa_prefix) \
  sa_family_t sa_prefix##family

typedef uint16_t in_port_t;

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

struct sockaddr_in
{
    __SOCKADDR_COMMON (sin_);   // 协议家族
    in_port_t sin_port;			// 16位端口号
    struct in_addr sin_addr;	// 32位IP地址

    // ...
};

        未来要把数据发送给服务端,也要把自己的IP和端口号发送到网络,所以一定要改变网络字节序。不过我们要注意的是,IP地址通常是以字符串形式出现的,所以还要再做一下处理,使用inet_addr函数就可以把IP地址从char*类型转换成网络序列的32位整数。

至此我们初始化的工作就做好了,我们把它封装成一个类中的成员函数。

class UdpServer
{
public:
    UdpServer(uint16_t port, std::string ip = "0.0.0.0")
        :_port(port)
        ,_ip(ip)
        ,_sock(-1)
    {} 

    bool initServer()
    {
        // 1. 创建套接字
        _sock = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sock < 0)
        {
            // 这就是使用到我们进程池写的日志文件了
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(2);
        }

        // 2. bind绑定,将ip和port在内核中和当前进程强关联
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        // 主机到网络的转换,port是16位的使用host to net short
        local.sin_port = htons(_port);
        // 把IP地址从点分十进制字符串变成4字节32位整数,再变成网络序列
        local.sin_addr.s_addr = inet_addr(_ip.c_str());

        if (bind(_sock, (struct sockaddr*)&local, sizeof local) < 0)
        {
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "%s", "init udp server done ... ", strerror(errno));
    }

private:
    // 服务器必须要有ip地址和端口号
    uint16_t _port; // 192.168.16.1
    std::string _ip;
    int _sock;
};

recvfrom

        这个时候我们的服务器就已经可以启动了,UDP的初始化只需要创建套接字加绑定就可以了,服务器就是一直为用户提供某种服务,所以服务器运行起来后就永远不会退出,实际上执行的就是一个死循环代码。

        由于UDP服务器是不面向连接,也不需要其他操作,而UDP服务器读取客户端数据的函数就是这个。

参数:

  • sockfd:套接字文件描述符
  • buf:读取的数据要放到的缓冲区
  • len:要读取的字节数
  • flags:读取的方式,一般设置为0,表示阻塞式读取
  • src_addr:输出型参数,对端网络的属性
  • addrlen:调用时传入的src_addr结构体的长度,返回实际读取到的实际长度,这是一个输入输出型参数

返回值:读取成功返回实际读取的字节数,读取失败返回-1,错误码被设置。

        因为UDP不面向连接,所以一定要获取对方的网络信息,如IP和端口号,recvfrom函数提供的参数是struct sockaddr*类型的,所以一定要取地址强转。

使用inet_ntoa函数就可以把struct in_addr中的IP从32位整数的网络序列转化为本机的char*类型。

// 作为一款服务器来说,一定是永远不退出的
// 所以该进程一定是一个常驻进程,永远在内存中存在。除非进程终止了

#define SIZE 1024

char buffer[SIZE];
for (;;)
{
    struct sockaddr_in peer;    // 对端网络的属性信息
    bzero(&peer, sizeof(peer)); // 初始化
    socklen_t len = sizeof(peer);
    // 读取数据
    ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
    if (s > 0)
    {
        buffer[s] = 0; // 传过来的数据
        // 1.输出信息
        // 2.是谁发的
        uint16_t cli_port = ntohs(peer.sin_port); // 从网络中来的数据要转换成本机字节序
        std::string cli_ip = inet_ntoa(peer.sin_addr); // 4字节的32位整数IP网络序列->本主机的字符串点分十进制IP
        printf("[%s:%d]# %s\n", cli_ip, cli_port, buffer);
    }

    // ...
}

sendto

UDP服务器返回给客户端数据的函数

参数:

  • sockfd:套接字文件描述符
  • buffer:要把那个缓冲区中的数据返回对端
  • len:缓冲区字符的个数
  • flags:一般为0,表示阻塞写入
  • dest_addr:对端网络相关的属性信息
  • addrlen:dest_addr结构体的长度

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

// udp_server.hpp

#include <iostream>
#include <string>

#include <unistd.h>
#include <cerrno>
#include <cstring>
#include <cstdio>
#include <cstdio>

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "log.hpp"

#define SIZE 1024

class UdpServer
{
public:
    UdpServer(uint16_t port, std::string ip = "0.0.0.0")
        :_port(port)
        ,_ip(ip)
        ,_sock(-1)
    {} 

    bool initServer()
    {
        // 1. 创建套接字
        _sock = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sock < 0)
        {
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(2);
        }

        // 2. bind绑定,将ip和port在内核中和当前进程强关联
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        // 主机到网络的转换,port是16位的使用host to net short
        local.sin_port = htons(_port);
        // 把IP地址从点分十进制字符串变成4字节32位整数,再变成网络序列
        local.sin_addr.s_addr = inet_addr(_ip.c_str());

        if (bind(_sock, (struct sockaddr*)&local, sizeof local) < 0)
        {
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "init udp server done ... %s", strerror(errno));

        return true;
    }

    void Start()
    {
        // 作为一款服务器来说,一定是永远不退出的
        // 所以该进程一定是一个常驻进程,永远在内存中存在。除非进程终止了
        char buffer[SIZE];
        for (;;)
        {
            struct sockaddr_in peer;    // 对端网络的属性信息
            bzero(&peer, sizeof(peer)); // 初始化
            socklen_t len = sizeof(peer);
            // 读取数据
            ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
            if (s > 0)
            {
                buffer[s] = 0; // 传过来的数据
                // 1.输出信息
                // 2.是谁发的
                uint16_t cli_port = ntohs(peer.sin_port); // 从网络中来的数据要转换成本机字节序
                std::string cli_ip = inet_ntoa(peer.sin_addr); // 4字节的32位整数IP网络序列->本主机的字符串点分十进制IP
                printf("[%s:%d]# %s\n", cli_ip.c_str(), cli_port, buffer);
            }
            // 分析数据

            // 写回数据
            sendto(_sock, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, len);
        }
    }

    ~UdpServer()
    {
        if (_sock >= 0) close(_sock);
    }
    
private:
    // 服务器必须要有ip地址和端口号
    uint16_t _port; // 192.168.16.1
    std::string _ip;
    int _sock;
};

至此,我们的UDP服务端就已经写好了,封装了一下服务端。

编写客户端

// udp_client.cc

#include <iostream>
#include <string>

#include <cstring>
#include <cstdio>

#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

// .udp_client server_ip server_port
static void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << " ip port\n" << std::endl;
}

int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        exit(1);
    }

    // 1.创建套接字
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }

    // client不需要主动bind,这是一个客户端,普通用户不知道,如果程序员写了bind,那么一定bind了一个固定的端口号
    // 如果这个端口号被其他进程占用了呢,所以就让OS自动随机选择端口号

    std::string message;
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(atoi(argv[2]));
    server.sin_addr.s_addr = inet_addr(argv[1]);

    char buffer[1024];
    while (true)
    {
        std::cout << "请输入:";
        std::getline(std::cin, message);
        // 当client首次发送消息给服务器的时候,OS会自动给client bind他的IP和PORT
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));

        // 读取
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        ssize_t s = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr*)&temp, &len);
        if (s > 0)
        {
            std::cout << "Server echo# " << buffer << std::endl;
        }
    }
    return 0;
}

        首先就来说一下客户端绑定的问题,网络通信中IP地址和端口号一定要的,服务器是给别人提供服务的,所以就必须要告诉别人服务器的IP和端口号。因为服务器是一个死循环的进程,选定的端口号不能轻易改变,所以服务端必须要bind。客户端不需要绑定的原因就是他在访问服务器的时候,只需要确定此时的端口号唯一,如果他绑定了,那么客户端就只能通过这个端口号访问服务器,所以写客户端的时候就不要bind,当调用sendto的时候操作系统会自动给当前客户端获取一个唯一端口号

        写好了之后就可以启动服务端和客户端了。

// udp_server.cc

#include "udp_server.hpp"
#include <memory>
#include <cstdlib>

// .udp_server ip port
void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << " ip port\n" << std::endl;
}

int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        exit(1);
    }
    std::string ip = argv[1];
    uint16_t port = atoi(argv[2]);
    std::unique_ptr<UdpServer> svr(new UdpServer(port, ip));

    svr->initServer();

    svr->Start();

    return 0;
}

        我们通过netstat命令查看网络状态,netstat的选项:

  • -n:直接使用IP地址,而不通过域名服务器。
  • -l :显示监控中的服务器的Socket
  • -t :显示TCP传输协议的连线状况。
  • -u:显示UDP传输协议的连线状况。
  • -p:显示正在使用Socket的程序识别码和程序名称。

        我们的服务端流程,通过设置本机IP和固定端口号初始化UdpServer,创建套接字,再bind将IP和port进行强关联,编辑本机的sockaddr;运行服务器,recvfrom读取数据,接受客户端的sockaddr信息,处理过后再调用sendto给客户端发送回去。

        客户端流程,设置要访问的服务端IP和端口号,编辑服务端sockaddr,再调用sendto向服务端发送信息,最后读取服务端的信息。

        使用netstat -nlup查看当前网络信息。

绑定INADDR_ANY

        客户端和服务端在本机中已经可以实现通信了,如果想要在网络中通过客户端访问服务端绑定自己的公网IP,但是我现在用的是一个云服务器,它的IP地址并不是真正的公网IP,所以不能被绑定,让外网访问就要绑定0,系统中提供了一个宏就是INADDR_ANY,它的值就是0。

        一个服务器中,可能放了多张网卡,那么就会有多个IP地址,但是端口号只有一个,服务器在接收数据的时候,多张网卡都收到了数据,如果bind的时候是指定IP地址的,那就只能从这个IP地址中接收数据,如果bind的是INADDR_ANY,只要是发送给指定端口号的,就可以从不同的IP地址接收交给服务端。

// 2. bind绑定,将ip和port在内核中和当前进程强关联
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
// 主机到网络的转换,port是16位的使用host to net short
local.sin_port = htons(_port);
// 把IP地址从点分十进制字符串变成4字节32位整数,再变成网络序列
local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());

实现聊天功能

        我们现在想要实现一个聊天功能,一个客户端向服务端发送信息,服务端收到信息,并把这个用户对应的信息保存起来,再把消息从服务端发送回客户端;此时我们再来一个用户,一样的操作,这时候两个客户端应该可以看到两个人发送的信息。

// 添加一个成员变量
std::unordered_map<std::string, struct sockaddr_in> _users; // IP-PORT : sockaddr_in

// 修改一下Start成员函数

char buffer[SIZE];
char key[64]; // 将ip-port写到key中
for (;;)
{
    // ...
    ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
    if (s > 0)
    {
        // ...
        snprintf(key, sizeof(key), "%s-%u", cli_ip.c_str(), cli_port);
        logMessage(NORMAL, "key: %s", key);
        auto it = _users.find(key); // 把信息写到map中
        if (it == _users.end())
        {
            logMessage(NORMAL, "add new user: %s", key);
            _users.insert({key, peer});
        }
    }

    // 写回数据
    for (auto& iter : _users)
    {
        std::string sendMessage = key;
        sendMessage += "# ";
        sendMessage += buffer; 
        logMessage(NORMAL, "push message to %s", iter.first.c_str());
        sendto(_sock, sendMessage.c_str(), sendMessage.size(), 0, (struct sockaddr*)&iter.second, sizeof(iter.second));
    }
}

        想法很美好,但是现实往往和想象中的不一样,一开始还行,后面打印的都是什么东西,原因就是IO被阻塞了,就是当我们getline拿用户输入的数据的时候,后面的sendto和recvfrom都不会执行,所以现在就可以使用多线程,一个线程发数据,另一个线程负责收数据。

        这就有一个要注意的点,不管是读数据还是写数据都要用sock,如果使用多线程就要把sock设置成全局的,或者再把客户端封装成一个类,成员变量对于整个类也是全局的。那会不会有线程安全的问题呢,这也是没有的,因为在多线程之前就要创建出socket,而线程只是用这个socket,并不会修改它,所以可以并发访问。

        我们再把之前已经封装好的线程拿过来,这样sock会直接传入ThreadData中,所以也就不需要把sock定义成全局的。

#include "thread.hpp"

// port、ip
uint16_t serverport = 0;
std::string serverip;

// .udp_client ip port
static void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << " ip port\n" << std::endl;
}

static void *udpSend(void *args)
{
    // 拿到sock
    int sock = *(int *)((ThreadData *)args)->args_;
    std::string name = ((ThreadData *)args)->name_;

    // 填充服务端信息
    std::string message;
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    server.sin_addr.s_addr = inet_addr(serverip.c_str());

    while (true)
    {
        // 输入数据,发送
        std::cerr << "请输入:";
        std::getline(std::cin, message);
        if (message == "quit")
            break;
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));
    }

    return nullptr;
}

static void *udpRecv(void *args)
{
    int sock = *(int *)((ThreadData *)args)->args_;
    std::string name = ((ThreadData *)args)->name_;

    char buffer[1024];
    while (true)
    {
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        ssize_t s = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr *)&temp, &len);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << buffer << std::endl;
        }
    }

    return nullptr;
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        exit(1);
    }

    // 1.创建套接字
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }

    serverport = atoi(argv[2]);
    serverip = argv[1];

    std::unique_ptr<Thread> sender(new Thread(1, udpSend, (void *)&sock)); // 发送线程
    std::unique_ptr<Thread> recver(new Thread(2, udpRecv, (void *)&sock)); // 接收线程

    sender->start();
    recver->start();

    sender->join();
    recver->join();

    close(sock);

    return 0;
}

        至此多线程就写好了,虽然用的socket都是同一个,但是没有读写冲突的情况,因为UDP是全双工的,可以同时进行收和发,不会受到干扰。

        我们在目录下使用mkfifo创建两个管道文件client1和client2,将客户端输出重定向到管道文件,再使用cat输出重定向,这就好像类似于一个输入框和一个显示框。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Ubuntu安装教程——Desktop版本(细致入微的操作)

目录 前言 一、安装Ubuntu桌面版操作系统 二、UbuntuLive版安装 1.语言选择 2.键盘布局 3.版本选择 4.网络配置 5.代理配置 6.镜像地址 7.磁盘划分 8.设置用户信息 9.ssh 10.选择软件包 11.安装界面 12.基础配置 12.1root用户 12.2时区 12.3包管理工具 12…

2015年认证杯SPSSPRO杯数学建模C题(第一阶段)荒漠区动植物关系的研究全过程文档及程序

2015年认证杯SPSSPRO杯数学建模 C题 荒漠区动植物关系的研究 原题再现&#xff1a; 环境与发展是当今世界所普遍关注的重大问题, 随着全球与区域经济的迅猛发展, 人类也正以前所未有的规模和强度影响着环境、改变着环境, 使全球的生命支持系统受到了严重创伤, 出现了全球变暖…

一款比Typora更简洁优雅的Markdown编辑器神器(完全开源免费)

前言 自从Typora收费以后经常有朋友会问有没有一个好用、简洁、免费的Markdown编辑器推荐的&#xff0c;今天大姚给大家分享一款比Typora更简洁优雅的、完全开源免费&#xff08;MIT License&#xff09;Markdown编辑器神器&#xff1a;MarkText。 MarkText简介 Typora的完美替…

CTK插件框架学习-新建插件(02)

CTK插件框架学习-源码下载编译(01)https://mp.csdn.net/mp_blog/creation/editor/136891825 开发环境 window11、vs17、Qt5.14.0、cmake3.27.4 开发流程 新建ctk框架调用工程&#xff08;CTKPlugin&#xff09; 拷贝CTK源码编译完成后的头文件和库文件到工程目录&#xff0…

微信小程序开发技巧:canvas实现电子签名

在微信小程序中实现电子签名功能方式很多,本文采用canvas绘制的方式实现。具体实现步骤如下: 在页面中添加canvas元素 <view class"container"><canvas canvas-id"signCanvas" class"canvas" disable-scrolltrue touchstart"sta…

OSCP靶场--Cockpit--待续

OSCP靶场–Cockpit 考点(sql注入绕过sudo tar提权) 1.nmap扫描 ## ┌──(root㉿kali)-[~/Desktop] └─# nmap 192.168.229.10 -Pn -sV -sC --min-rate 2500 Starting Nmap 7.92 ( https://nmap.org ) at 2024-03-25 01:40 EDT Nmap scan report for 192.168.…

上班几周了,

过年回来后&#xff0c;时间变得飞快&#xff0c;很多事情都是马上要去干&#xff0c;而且又是很着急的事&#xff0c;呵呵&#xff0c;真的要干趴了 然后——经历了第一次年后的周末连续加班出版本保量产&#xff0c;经历了加班到凌晨3点调试问题&#xff0c;经历我们在疯狂的…

【字典树】【字符串】【 前缀】100268. 最长公共后缀查询

作者推荐 视频算法专题 本文涉及知识点 字典树 字符串 前缀 LeetCode 100268. 最长公共后缀查询 给你两个字符串数组 wordsContainer 和 wordsQuery 。 对于每个 wordsQuery[i] &#xff0c;你需要从 wordsContainer 中找到一个与 wordsQuery[i] 有 最长公共后缀 的字符串…

Linux课程_____网络管理

一、查看接口信息 1. ifconfig 查看所有活动网络接口的信息 ifconfig -a 查看所有网络接口信息 ifconfig 直接加网络接口 查看指定网络接口信息 1.1查看指定接口IP [rootlocalhost ~]# ip addr show ens160 1.2设置网络接口的IP地址 # ifconfig eth0 192.168.152.133 …

基于Springboot+Vue的前后端分离的简单Demo案例(二)

前端搭建 Vue router 来动态构建左侧菜单 导航1 页面1页面2导航2 页面3页面4导航3 页面5页面6 在views目录下创建四个页面 PageOne.vue <template><h1>这是页面1</h1> </template> <script> export default {name: "PageOne", }; …

Java String类深入了解JDK各个版本进阶版本

Java String类深入了解JDK各个版本进阶版本 一&#xff0c;底层类型 在jdk11中 String value 存储字符串值 是byte[] 数组 &#xff0c;String中存储字节码的是coder 也是byte类型&#xff0c;因此String的底层数据存储类型成为了byte类型 而在jdk8中String 的String value 存…

shell编程-jq命令详解

文章目录 前言一、jq简介1. 简介2. 语法3. 命令选项 二、用于处理json数据1. 过滤1.1 标识运算符1.2 基本过滤1.3 获取对象属性1.3 迭代数组元素1.4 获取数组元素1.5 使用运算符 2. 类型和值2.1 数组构造2.2 对象构造2.3 递归下降 3. 内置运算符和函数3.1 算术运算符3.2 函数3.…

Makefile的override

今天在编译opengauss extension时遇到一个报错&#xff1a; 简单的理解就是编译时要加 -fPIC&#xff0c;告诉编译器生成Position Independent Code&#xff0c;试过 make CPPFLAGS-fPIC 可以成功编译&#xff0c;不过看到其它的解决方案是在Makefile中加 override CPPFLAG…

代码随想录算法训练营第三十天 | 332.重新安排行程,51. N皇后 ,37. 解数独

这道题是一道欧拉路径/ 欧拉回路的一笔画问题&#xff0c;需要找出开销最小的一笔画方案 这种一笔画的问题&#xff0c;以前学数据结构的时候我们习惯把图放进二维数组中存储&#xff0c;但对于这种无规律的图结构&#xff0c;我们可以使用二维的哈希表来存储&#xff0c;这样…

【4月】CDA Club 第2期数据分析组队打卡学习活动开启!

活动名称 CDA Club 第2期数据分析组队打卡学习活动 活动介绍 本次打卡活动由CDA俱乐部旗下学术部主办。目的是通过数据分析科普内容&#xff0c;为数据分析爱好者提供学习和交流的机会。方便大家利用碎片化时间在线学习&#xff0c;以组队打卡的形式提升学习效果&#xff0c…

水离子雾化壁炉的原理和技术解析

水离子雾化壁炉采用超声波雾化技术将水分子雾化成微细的水离子&#xff0c;然后通过风扇吹出再经过UVC紫外线杀菌产生安全仿真的火焰效果。以下是水离子雾化壁炉的原理和技术解析&#xff1a; 超声波雾化技术&#xff1a; 水离子雾化壁炉利用超声波振动器产生高频振动&#xf…

[Java、Android面试]_13_map、set和list的区别

本人今年参加了很多面试&#xff0c;也有幸拿到了一些大厂的offer&#xff0c;整理了众多面试资料&#xff0c;后续还会分享众多面试资料。 整理成了面试系列&#xff0c;由于时间有限&#xff0c;每天整理一点&#xff0c;后续会陆续分享出来&#xff0c;感兴趣的朋友可关注收…

TSN协议原理!看完这一篇就够了(1)——时钟同步IEEE802.1AS-2020

▎前言 在许多应用场景中&#xff0c;一个本地局域网中互联的设备集群需要共享同一个时间&#xff0c;以支持各设备的协同工作。例如&#xff1a;音频设备与视频设备的配合播放&#xff0c;雷达与摄像头的数据融合等&#xff1b;这样一个看似简单的域功能&#xff0c;细化成为…

好书推荐 :《 提问的艺术:让 ChatGPT 给出高质量答案 》

AGI 时代降临&#xff01;还不知如何向 ChatGPT 提问&#xff1f; 恰当的提示至关重要&#xff01;《提问的艺术—让 ChatGPT 给出高质量答案》一书&#xff0c;共 24 章&#xff0c;系统介绍了如何向 ChatGPT 提问以获取优质答案&#xff0c;是 ChatGPT 时代的入门指南&#x…

【 Mysql8.0 忘记登录密码 可以试试 】

** Mysql8.0 忘记登录密码 可以试试 ** 2024-3-21 段子手168 1、首先停止 mysql 服务 &#xff0c;WIN R 打开运行&#xff0c;输入 services.msc 回车打开服务&#xff0c;找到 mysql 服务&#xff0c;停止。 然后 WIN R 打开运行&#xff0c;输入 CMD 打开控制台终端输…