【Linux】来写一个udp的服务端+客户端

news2024/12/22 22:41:46

来写一个udp的代码

1.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);

linux下一切皆文件,socket接口也不例外。其返回值本质上就是一个fd文件描述符,这样我们对网络的发送/接收操作,就转换成了对文件的写入/读取操作了

在这里面有一个比较重要的结构sockaddr需要说明一番

1.1 sockaddr

socket是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4/IPv6。同时,这个接口还可以用于系统内部的通信。这就实现了用一个接口来干两件事。

为此,就必须要在传值中进行一些修改。该接口新增了一个sockaddr,用来接收目标信息。这个值的参数可以是sockaddr_in/scokaddr_un/sockadd_in6之中的任意一个(需要强转指针)

sockaddr本身不存放任何信息。

这个参数可接收的结构体中,固定前16位就是用于标识符的。传到处理函数中,就会判断前16位中的标识符的类型,以确定传入参数的类型,再执行不同的实现

  • 比如传入的scokaddr_un,前16位是AF_UNIX,那么当前使用的就是本地通信
  • sockaddr_in是ipv4通信,sockaddr_in6是ipv6通信

image-20230201150711386

你可能会有疑惑,既然sockaddr不存放信息,那为何不把这个参数设置为一个void*的指针?反正最后都是进了函数之后判断参数类型,void*指针也能达成目标呀🧐

这个问题的答案很简单:当初设计这套接口的时候,C语言还不支持void*😂


1.2 存放位置

因为sockaddr_in这类的结构体,最终都需要被操作系统载入并实现网络操作。所以它们肯定是需要载入内核中的

但这并不意味着这类结构体是存放在内核里面的,而是存放在用户栈,用户态和内核态交换的时候,通过接口传值载入到内核的空间进行使用

2.server

了解了上面的信息,接下来,认识一下如果想建立一个udpserver,需要怎么操作吧!

以下是一个server的类,包含了端口、ip、socker fd三个基本信息

class UdpServer
{
public:
    UdpServer(uint16_t port,const string& ip="")
     : _port((uint16_t)port), _ip(ip), _sockfd(-1)
    {}

private:
    // 服务器端口号
    uint16_t _port;
    // 服务器ip地址
    string _ip;
    // 服务器socket fd信息
    int _sockfd;
};

2.1 创建套接字

这里需要用的是下面这个接口

int socket(int domain, int type, int protocol);
  • 第一个参数domain标识该socker的作用域

可以设置为本地,也可以设置为网络。支持如下参数

Name                Purpose                          Man page
AF_UNIX, AF_LOCAL   Local communication              unix(7)
AF_INET             IPv4 Internet protocols          ip(7)
AF_INET6            IPv6 Internet protocols          ipv6(7)
AF_IPX              IPX - Novell protocols
AF_NETLINK          Kernel user interface device     netlink(7)
AF_X25              ITU-T X.25 / ISO-8208 protocol   x25(7)
AF_AX25             Amateur radio AX.25 protocol
AF_ATMPVC           Access to raw ATM PVCs
AF_APPLETALK        Appletalk                        ddp(7)
AF_PACKET           Low level packet interface       packet(7)

因为我们要创建的是一个网络服务器,所以这里设置为AF_INET,也就是IPV4的服务

  • 第二个参数type指代套接字的类型,决定了通信时的报文类型

这里支持流式(TCP)或者用户数据报(UDP),以及RAW原始格式(能够直接访问协议,方便debug)

SOCK_STREAM   Provides sequenced, reliable, two-way, connection-based byte streams. An out-of-band data  transmission  mechanism  may  be supported.

SOCK_DGRAM    Supports datagrams (connectionless, unreliable messages of a fixed maximum length).

 SOCK_RAW        Provides raw network protocol access.

更多支持的参数参考man手册

  • 第三个参数指代协议,在网络应用中,设置为0即可

返回值是一个linux系统的文件描述符

RETURN VALUE
       On success, a file descriptor for the new socket is returned.  On error, -1 is returned, and errno is set appropriately.

这样,我们就能写出第一行代码,以及对这个代码的返回值判断

_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
    logging(FATAL, "socket:%s:%d", strerror(errno), _sockfd);
    exit(1);
}

因为socket是文件描述符,为了规范,我们还可以在析构函数里面调用一下close

~UdpServer()
{
    close(_sockfd);
}

2.2 配置sockaddr_in

// 2. 绑定网络信息,指明ip+port
struct sockaddr_in local;
memset(&local,0,sizeof(local));//配置为 0

因为用的是ipv4的网络通信,所以这里需要初始化一个sockaddr_in类型

此时在vscode的代码补全中,可以看到4个成员,需要对它们赋值以配置服务器信息

image-20230202152944512

首先是把协议家族设置为IPV4,端口配置为函数参数中的端口

// 协议家族,设置为ipv4
local.sin_family = AF_INET; 
// 端口,需要进行 本地->网络转换
local.sin_port = htons(_port);

随后配置ip

// 如果初始化时候的ip为空,则调用INADDR_ANY代表任意ip。否则对传入的ip进行转换后赋值
local.sin_addr.s_addr = _ip.empty() ? htonl(INADDR_ANY) : inet_addr(ip_.c_str());

这里采用了?:三目操作符,如果类构造的时候传入的ip是空(没有配置ip)那就直接设置为任意ip,否则传入成员变量;

这样对sockaddr_in的配置就完成了。

2.2.1 inet_addr

这里需要使用inet_addr函数对传入的字符串类型的ip(如192.168.0.1)进行转换

in_addr_t inet_addr(const char *cp);//对字符串ip进行转换

因为对于网络来说,它并不认识字符串类型的ip,而是要用网络字节流规定的类型。

/* Internet address.  */
typedef uint32_t in_addr_t;
struct in_addr
  {
    in_addr_t s_addr;
  };

对于该接口的底层做一个简单的说明:其实就是利用位段,将数据转换为一个特殊的类型

//示例,非底层实现
struct ip
{
    uint32_t part1:8;
    uint32_t part2:8;
    uint32_t part3:8;
    uint32_t part4:8;
}

2.2.2 inet_ntoa

同样的,如果我们作为客户端接受到了网络请求中的ip,可以用inet_ntoa将其转换为字符串类型。

char *inet_ntoa(struct in_addr in);

这里就引申出了一个问题:返回值的char*是存在哪里的?是静态区还是malloc

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G26FY7Id-1681715257525)(null)]

手册告诉我们,这个函数是维护了一个static变量来存放返回的ip的。

因此,该函数并不是一个线程安全的函数,在APUE中明确标明了这一点


2.3 bind绑定ip端口

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

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

这个接口的作用就是指定socket和sockaddr进行绑定。第三个参数是addr元素的大小(不是指针大小,别搞错了)

// 2.2 绑定ip端口
if (bind(_sockfd,(const struct sockaddr *)&local, sizeof(local)) == -1)
{
    logging(FATAL, "bind: %s:%d", strerror(errno), _sockfd);
    exit(2);
}
logging(DEBUG,"socket bind success: %d", _sockfd);

绑定了之后,我们的服务器就配置成功了

测试一下,可以看到编译没有报错,也能正常运行!

[muxue@bt-7274:~/git/linux/code/23-02-01 udp]$ make udpServer
g++ -o udpServer udpServer.cpp -std=c++11
[muxue@bt-7274:~/git/linux/code/23-02-01 udp]$ ./udpServer
DEBUG | 1675326460 | muxue | socket create success: 3
DEBUG | 1675326460 | muxue | socket bind success: 3

2.3.1 main

现在先来简单写一下main函数中启动服务的命令行参数吧

int main(int argc,char* argv[])
{
    //参数只有两个(端口/ip)所以参数个数应该是2-3
    if(argc!=2 && argc!=3)
    {
        cout << "Usage: " << argv[0] << " port [ip]" << endl;
        return 1;
    }
    

    string ip;
    // 3个参数,有ip
    if(argc==3)
    {
        ip = argv[2];
    }
    UdpServer s(atoi(argv[1]),ip);
    s.start();

    return 0;
}

为了测试,先把start()函数设置为一个死循环

    void start()
    {
        while(1)
        {
            cout << "running " << getpid() << endl;
            sleep(1);
        }
    }

编译运行,可以看到错误提示是可以用的。正确添加参数之后,也能绑定并开始运行

[muxue@bt-7274:~/git/linux/code/23-02-01 udp]$ make udpServer
g++ -o udpServer udpServer.cpp -std=c++11
[muxue@bt-7274:~/git/linux/code/23-02-01 udp]$ ./udpServer
Usage: ./udpServer port [ip]
[muxue@bt-7274:~/git/linux/code/23-02-01 udp]$ ./udpServer 8080
DEBUG | 1675327610 | muxue | socket create success: 3
DEBUG | 1675327610 | muxue | socket bind success: 3
running 4467
running 4467
running 4467
^C

注意,bind这个函数是不允许你绑定云服务器的公网ip的。因为云服务器并不是直接暴露在公网上的,而是由提供商的入口服务器进入内网,在进入你的服务器。所以他不允许你绑定公网ip;

$ ./udpServer 8080 云服务器公网ip
DEBUG | 1675327690 | muxue | socket create success: 3
FATAL | 1675327690 | muxue | bind: Cannot assign requested address:3

一般情况下,可以选择不绑定ip,或者绑定本地端口127.0.0.1

如果绑定了127.0.0.1,那么服务只有本地可以访问。不绑定端口,就会默认绑定成0.0.0.0,允许本地和远程端口连接

$ ./udpServer 8080 127.0.0.1
DEBUG | 1675327757 | muxue | socket create success: 3
DEBUG | 1675327757 | muxue | socket bind success: 3
running 5067
running 5067

2.3.2 netstat

可以用netstat -lnup命令查看当前开放的端口信息

image-20230202165149257

可以看到,第一行就是我们的udp服务器,本地端口是我们绑定的127.0.0.1:8080,远程端口是0.0.0.0:*,代表允许任何远程ip的任何端口来访问

2.4 开始运行

上面的操作只是初始化了这个udp服务器的信息,并没有让它真正的运行起来;

接下来要做的就是让服务器开始接收信息,并在屏幕上打印出来

2.4.1 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);
  • 第一个参数是前面创建的套接字
  • 第二个参数是用来接收信息的缓冲区
  • 第三个参数是缓冲区的大小
  • 第四个参数是标识符,设置为0,代表阻塞等待
  • 第五个参数,输出型参数,获取发送方的信息
  • 第六个参数,输入输出型参数,需要初始化为sizeof(src_addr)

函数的返回值是接收到的数据的长度,没有接收到或者接受失败,则为-1

示例如下

char inBuf[BUF_SIZE];
struct sockaddr_in peer;      //输出型参数
socklen_t len = sizeof(peer); //输入输出型参数

// peer和len都是输出型参数,用于获取发送方的信息
// len是输入输出型参数,需要以sizeof(peer)初始化后传入
// 第三个参数0为默认等待方式(阻塞等待)
ssize_t s = recvfrom(_sockfd, inBuf, sizeof(inBuf)-1,0,(struct sockaddr *)&peer, &len);
if (s > 0) // s代表获取到的数据长度,不为0代表成功获取
{
    inBuf[s] = '\0'; //末尾追加'\0'
}
else if (s == -1) // -1没有收到信息,错误
{
    logging(WARINING, "recvfrom: %s:%d", strerror(errno), _sockfd);
    continue;
}

这样就能在inBuf中直接获取到发送信息的内容

2.5 服务端start

以下是服务端运行的完整代码

void start()
{
    char inBuf[BUF_SIZE];//接收到信息的缓冲区
    while(1)
    {
        struct sockaddr_in peer;      //输出型参数
        socklen_t len = sizeof(peer); //输入输出型参数

        // peer和len都是输出型参数,用于获取发送方的信息
        // len是输入输出型参数,需要以sizeof(peer)初始化后传入
        // 第三个参数0为默认等待方式(阻塞等待)
        ssize_t s = recvfrom(_sockfd, inBuf, sizeof(inBuf)-1,0,(struct sockaddr *)&peer, &len);
        if (s > 0) // s代表获取到的数据长度,不为0代表成功获取
        {
            inBuf[s] = '\0'; //末尾追加'\0'
        }
        else if (s == -1) // -1没有收到信息,错误
        {
            logging(WARINING, "recvfrom: %s:%d", strerror(errno), _sockfd);
            continue;
        }

        string senderIP = inet_ntoa(peer.sin_addr);// 来源ip
        uint16_t senderPort = ntohs(peer.sin_port); // 来源端口
        logging(NOTICE, "[%s:%d]# %s", senderIP.c_str(),senderPort, inBuf);//打印信息
    }
}

如果你想让另外一台主机访问这个服务,则需要在云服务器控制台和linux系统中同时开放对应的udp端口

参考 【Linux】设置系统防火墙

3.client

有了服务端,也要有对应的客户端来发送消息;除了发送消息的部分,其余操作和服务端基本一致。

3.1 sendto

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

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

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

ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

这里我们要用的是sendto接口

  • 第一个参数是socket套接字
  • 第二个参数是用于输入的缓冲区
  • 第三个参数是缓冲区的类型
  • 第四个参数是标识符,也设置为0
  • 第五个参数和第六个参数与recvfrom一致,为目标服务器的信息

关于flag参数,man手册中有更多选项,这里我们依旧传入0采用默认策略

The flags argument is the bitwise OR of zero or more of the following flags.

3.2 客户端需不需要手动bind?

首先我们要明确一点,bind函数并没有规定一定要是服务端才能使用。也就是说,要不要使用bind是程序猿自己的选择。

答案其实很简单:那就是不需要手动bind

首先我们要知道一点:如果一个网络进程在启动的时候没有手动bind端口,系统是会自动分配一个未使用的端口给它的

  • 对于服务器来说,IP:端口必须固定,否则没有办法给客户端提供稳定的服务。客户又不能拆了你的应用程序修改源码中的端口!
  • 而对于客户端来说,端口应该让系统自动分配。因为这样能避免冲突问题。不然如果有另外一个应用占用了客户端bind的端口,那这个程序就会因为端口冲突而一直打不开!

所以,客户端不需要我们调用bind函数,只需要配置好服务端的目标ip和目标端口就行了

3.3 代码示例

#include <iostream>
#include <string>
#include <cstdlib>
#include <cassert>
#include <unistd.h>
#include <strings.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
using namespace std;

struct sockaddr_in server;

// ./udpClient server_ip server_port
// 客户端要连接server,必须知道server对应的ip和port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        cout << "Usage:\n\t" << argv[0] << " server_ip server_port" << endl;
        return 1;
    }
    // 1. 根据命令行,设置要访问的服务器IP
    string server_ip = argv[1];
    uint16_t server_port = atoi(argv[2]);

    // 2. 创建客户端
    // 2.1 创建socket
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd<0)
    {
        cout << "socket 创建失败" << endl;
        return 2;
    }

    bzero(&server, sizeof(server));//这个函数相当于memset全0

    server.sin_family = AF_INET;//ipv4
    server.sin_port = htons(server_port);//目标服务器端口
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());//目标ip

    // 3. 通讯过程
    string buffer;
    while (true)
    {
        cerr << "Please Enter# ";
        getline(cin, buffer);
        // 发送消息给server
        sendto(sockfd, buffer.c_str(), buffer.size(), 0,
               (const struct sockaddr *)&server, sizeof(server)); 
        // 首次调用sendto函数的时候,client会自动bind自己的ip和port
        // 客户端不应该自己绑定端口,否则端口被占用=客户端不能用
    }
    close(sockfd);

    return 0;
}

3.4 运行测试

这里提供一个makefile,来快速编译服务端/客户端的源码

.PHONY:all
all:udpClient udpServer

udpClient: udpClient.cpp
	g++ -o $@ $^ -std=c++11 -lpthread
udpServer:udpServer.cpp
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f udpClient udpServer

运行服务器,指定8080端口启动。再运行客户端,指定127.0.0.1本地ip和8080端口

image-20230202212859925

可以看到,右侧我们收到的信息,都在左侧被打印了出来,同时显示了来源ip和端口

3.5 windows客户端

让我没想到的是,windows上网络的接口和linux很相似;这里提供一个windows下的udp客户端,向我们的云服务器发送信息

注:进行测试前,一定要在防火墙里面开放云服务器对应的udp端口

#define _WINSOCK_DEPRECATED_NO_WARNINGS 1//屏蔽报错

#include<winsock2.h>
#include<stdio.h>
#include<string.h>
#include<string>
#include<iostream>
using namespace std;
#pragma comment(lib,"ws2_32.lib")
#define BUFFER_SIZE  1024    //缓冲区大小

int main() 
{
    WSADATA WSAData;
    //初始化
    if (WSAStartup(MAKEWORD(2, 2), &WSAData) != 0) 
    {
        printf("初始化失败!");
        return -1;
    }    
    //创建客户端用于通信的Socket
    SOCKET sock_Client = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    //服务器的地址数据结构
    SOCKADDR_IN addr_server;   
    addr_server.sin_family = AF_INET;
    addr_server.sin_port = htons(10000);// 目标端口
    addr_server.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); //目标公网ip(需要改成你自己的)

    string sendBuf;
    while (true) 
    {
        cout << "请输入要传送的数据: ";
        getline(cin,sendBuf);
        sendto(sock_Client, sendBuf.c_str(),sendBuf.size(), 0, (const SOCKADDR*)&addr_server, sizeof(addr_server));
        cout << sendBuf.size() << ": " << sendBuf << endl;
    }
    closesocket(sock_Client);
    WSACleanup();

    return 0;
}

测试一下,可以看到云服务器成功收到了信息,但因为windows和linux的文字编码问题,没能正确显示出中文

image-20230202213301914

发送英文信息是没有问题的!

image-20230202213359805

4.更进一步

4.1 记录用户

有用户给你发送信息,理论上来说,服务端应该记录下用户,以备debug;

这部分并不难,我们记录下用户的ip和端口,还有用户的peer结构体,在服务器里面维护一个map来存放就可以了

    void CheckUser(struct sockaddr_in peer)
    {
        string tmp = inet_ntoa(peer.sin_addr);// 来源ip
        tmp += ':';
        tmp += to_string(ntohs(peer.sin_port));// 来源端口

        // 在map中用ip端口来标识用户
        auto it = _usrMap.find(tmp);
        if(it == _usrMap.end())// 没找到
        {
            _usrMap.insert({tmp,peer});
        }
    }

4.2 客户端接收回信

客户端发送信息给服务器后,可以来接收一下服务器的回信。比如在日常生活中,我们发邮件的时候,需要等待对方回信,这才表明你的信对方确实收到了,而不是丢在半路上了

    // 使用多线程操作,来获取服务器传回的信息
    pthread_t t;
    pthread_create(&t, nullptr, recverAndPrint, (void *)&sockfd);

为了方便,这里采用多线程的方式来操作;客户端在接收到服务器的回信后,会打印出来

void *recverAndPrint(void *args)
{
    while (true)
    {
        int sockfd = *(int *)args;
        char buffer[1024];
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        ssize_t s = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&temp, &len);
        if (s > 0)
        {
            buffer[s] = 0;//将接收到的信息打印出来(服务器返回的)
            cout << "server echo# " << buffer << "\n";
        }
    }
}

4.3 消息路由

所谓消息路由,就是把接收到的消息广播给所有用户。可以理解为一个简单的聊天室。

上面我们已经获取并记录了信息,下面要做的就是把信息重新发给其他用户;操作和客户端的发送是一样的

void MsgRoute(const char* inBuf,size_t len)
{
    struct sockaddr_in user;
    for(auto e:_usrMap)
    {
        user.sin_family = AF_INET;//ipv4
        user.sin_port = e.second.sin_port;//用户端口
        user.sin_addr.s_addr = e.second.sin_addr.s_addr;//用户ip
        // 向用户发送信息
        sendto(_sockfd, inBuf, len, 0,
               (const struct sockaddr *)&user, sizeof(user)); 
    }
}

测试,可以看到,服务端把收到的消息发送给了用户

image-20230203103733686

再新增一个客户端进行测试,可以看到两个客户都收到了服务器的回信

image-20230203104025510

这里对于聊天室来说还有一个小问题,那就是聊天框里面并不会二次出现你的消息。也就是服务器不会把你发送的消息再转发给你。

我们在消息路由函数里面进行判断即可!

void MsgRoute(struct sockaddr_in peer,const char* inBuf,size_t len)
{
    struct sockaddr_in user;
    for(auto e:_usrMap)
    {
        // 如果ip和端口都相等,就代表是发送消息的用户
        if(e.second.sin_port != peer.sin_port || e.second.sin_addr.s_addr != peer.sin_addr.s_addr)
        {
            user.sin_family = AF_INET;//ipv4
            user.sin_port = e.second.sin_port;//用户端口
            user.sin_addr.s_addr = e.second.sin_addr.s_addr;//用户ip
            // 向用户发送信息
            sendto(_sockfd, inBuf, len, 0,
                   (const struct sockaddr *)&user, sizeof(user)); 
        }
    }
}

因为乱序打印的问题,所以看的可能不是很明显。但是我们的目的已经达到了!

image-20230203104506634

这样打印看的不是很清楚,可以使用管道文件来实现输出重定向

mkfifo fifo #创建一个fifo管道文件

运行客户端的时候,指定输出

./udpClient 127.0.0.1 1000 > fifo

在另外一个bash里面,用cat来获取输出

cat < fifo

这就不会出现乱序打印的问题了。

fifo是一个管道文件,需要执行cat后(读端),客户端(写端)才能运行

5.more…

关于udp编程的操作到这里就Over啦,现在我们认识了大部分的网络接口,下一步的目标,就是实现tcp服务器啦!

QQ图片20220416195218

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

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

相关文章

个人邮箱与企业邮箱的区别有哪些?如何选择?

很多用户不了解企业邮箱&#xff0c;认为使用个人邮箱完全可以满足需求&#xff0c;其实这都是错误的观点&#xff0c;企业邮箱不同于个人邮箱&#xff0c;企业邮箱更适于商务应用的邮箱。今天就简单的做个对比。 个人邮箱与企业邮箱的区别&#xff1a; 1、企业形象 企业邮箱&…

STC单片机DS1307+ssd1306 oled时钟显示

STC单片机DS1307+ssd1306 oled时钟显示 📌相关篇《STC单片机DS1302+ssd1306 oled时钟显示》📍《STC单片机对DS1307读写操作》✨效果演示: 🌿实验对象:STC12C5A60S2🌿屏幕型号:I2C ssd1306 0.96“ oled🌿晶振频率:11.059MHz🌿串口波特率:9600📜串口打印读取信…

rk3568 点亮HDMI

rk3568 Android11/12 适配HDMI HDMI&#xff08;High-Definition Multimedia Interface&#xff09;是一种数字化音视频接口标准&#xff0c;用于连接高清电视、电脑、游戏机、蓝光播放器等设备。它是由HDMI联盟&#xff08;HDMI Licensing, LLC&#xff09;制定的&#xff0c…

奥威BI数据可视化大屏分享|多场景、多风格

数据可视化大屏一般应用在品牌推广展示、商务交流、数据分析决策、数据监控等场景&#xff0c;由此催生出各种不同风格的BI数据可视化大屏设计。下面就从奥威BI软件的BI报表模板中截取几个有着不同风格&#xff0c;起着不同作用的BI数据可视化大屏报表&#xff0c;一起来了解一…

【Transformer系列(3)】 《Attention Is All You Need》论文超详细解读(翻译+精读)

前言 哒哒~时隔好久终于继续出论文带读了&#xff0c;这次回归当然要出一手王炸呀——没错&#xff0c;今天我们要一起学习的就是传说中的Transformer&#xff01;在2021年Transformer一经论文《Attention is All You Need》提出&#xff0c;就如龙卷风一般震惊学术界&#xf…

关于ROS机器人-文心一言和CatGPT怎么看-

交流截图&#xff1a; 文字版本如下&#xff08;W-文心&#xff1b;C-猿如意&#xff09;&#xff1a; 如何通过蓝桥云课学习ROS机器人&#xff1f; W&#xff1a; 如果你想通过蓝桥云课学习ROS机器人&#xff0c;可以按照以下步骤进行&#xff1a; 确认ROS机器人的版本和教…

【记录】笔记本新加SSD,重装系统win10,再装双系统ubuntu18.04

记录一下&#xff0c;希望下次装系统可以来的晚一点一、前言二、换SSD三、装Windows1.安装系统2.删除多余系统3.遇到问题&#xff1a;无法关机4.改注册表&#xff0c;开不开机&#xff0c;黑屏有鼠标三、装ubuntu1.安装完&#xff0c;设置root2.更新源3.时间设置4.win和ubuntu共…

大语言模型及其应用

版权声明 本文原创作者&#xff1a;谷哥的小弟作者博客地址&#xff1a;http://blog.csdn.net/lfdfhl 机器学习 机器学习&#xff08;Machine Learning&#xff0c;ML&#xff09;是指从数据中自动学习规律和模式&#xff0c;并利用这些规律和模式&#xff0c;在新的数据中完成…

nssctf web 入门(6)

这里通过nssctf的题单web安全入门来写&#xff0c;会按照题单详细解释每题。题单在NSSCTF中。 想入门ctfweb的可以看这个系列&#xff0c;之后会一直出这个题单的解析&#xff0c;题目一共有28题&#xff0c;打算写10篇。 目录 [SWPUCTF 2021 新生赛]caidao [SWPUCTF 2021 新…

力扣题库刷题笔记64-最小路径和

1、题目如下&#xff1a; 2、个人Python代码实现&#xff1a; 以上代码主要是照抄题解&#xff0c;根据调试bug了解每一步的逻辑&#xff0c;然后注释利于读懂本题&#xff0c;写本篇文章的目的在于初步了解动态规划。 本题的逻辑主要如下&#xff1a; a、列表中每个数字代表当…

【Redis笔记03】Redis运行环境之Cluster集群模式

这篇文章&#xff0c;主要介绍Redis运行环境之Cluster集群模式。 目录 一、Cluster集群模式 1.1、集群模式原理 &#xff08;1&#xff09;普通集群 &#xff08;2&#xff09;什么是分片&#xff1f;&#xff1f;&#xff1f; &#xff08;3&#xff09;如何分片存储&…

基于springboot的音乐网站的设计与实现(带论文)

项目描述 临近学期结束&#xff0c;还是毕业设计&#xff0c;你还在做java程序网络编程&#xff0c;期末作业&#xff0c;老师的作业要求觉得大了吗?不知道毕业设计该怎么办?网页功能的数量是否太多?没有合适的类型或系统?等等。这里根据疫情当下&#xff0c;你想解决的问…

【OCR】CTC loss原理

1 CTC loss出现的背景 在图像文本识别、语言识别的应用中&#xff0c;所面临的一个问题是神经网络输出与ground truth的长度不一致&#xff0c;这样一来&#xff0c;loss就会很难计算&#xff0c;举个例子来讲&#xff0c;如果网络的输出是”-sst-aa-tt-e’, 而其ground truth…

JVM:并发的可达性分析

当前主流编程语言的垃圾收集器基本上都是依靠可达性分析算法来判定对象是否存活的&#xff0c;可达性分析算法理论上要求全过程都基于一个能保障一致性的快照中才能够进行分析&#xff0c;这意味着必须全程冻结用户线程的运行。 在根节点枚举这个步骤中&#xff0c;由于 GC Ro…

0303Kruskal算法和小结-最小生成树-图-数据结构和算法(Java)

1 算法概述 定义。按照边的权重顺序&#xff08;从小到大&#xff09;&#xff0c;将边加入最小生成树中。加入的边不会与已经加入的边构成环&#xff0c;知道树中含有V-1条边为主。这些黑色的边逐渐由一片森林合并为一棵树&#xff0c;也就是最小生成树。这种计算方法被称为Kr…

Redis Lua沙盒绕过命令执行(CVE-2022-0543)

一、描述 影响范围&#xff1a;Debian系得linux发行版本Ubuntu Debian系得linux发行版本 其并非Redis本身漏洞&#xff0c;形成原因在于系统补丁加载了一些redis源码注释了的代码 揭露时间&#xff1a;2022.3.8 二、原理 redis在用户连接后可以通过eval命令执行Lua脚本&#x…

GAN网络系列博客(三):不受坐标限制的GAN(StyleGAN3)

目录 1. 概述 2. 连续信号分析 2.1 等变网络层 3. 具体实现 3.1 傅里叶特征和基础简化 3.2 根据连续插值进行的步骤重建 4.实验 总结 Reference 关于StyleGAN系列博客的收尾之作&#xff0c;StyleGAN3已经鸽了太久了。说实话&#xff0c;前两篇StyleGAN说的什么内容&#xff0…

Web 攻防之业务安全:账号安全案例总结.

Web 攻防之业务安全&#xff1a;账号安全案例总结 业务安全是指保护业务系统免受安全威胁的措施或手段。广义的业务安全应包括业务运行的软硬件平台&#xff08;操作系统、数据库&#xff0c;中间件等&#xff09;、业务系统自身&#xff08;软件或设备&#xff09;、业务所提供…

[ 应急响应篇基础 ] 日志分析工具Log Parser配合login工具使用详解(附安装教程)

&#x1f36c; 博主介绍 &#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 _PowerShell &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【数据通信】 【通讯安全】 【web安全】【面试分析】 &#x1f389;点赞➕评论➕收藏 养成习…

HCIP-6.5BGP路由聚合原理及配置

HCIP-6.5BGP路由聚合原理及配置1、BGP路由聚合原理1.1、自动汇总1.2、手动聚合1.2.1、利用静态路由进行聚合1.2.2、BGP的专有工具Aggregate1、BGP路由聚合原理 在实际运行的网络中&#xff0c;BGP路由表十分庞大&#xff0c;对于设备而言是很大的负担&#xff0c;同时使发生路…