【Linux网络】网络编程套接字 -- 基于socket实现一个简单UDP网络程序

news2024/11/18 6:01:55

  • 认识端口号
  • 网络字节序
    • 处理字节序函数 htonl、htons、ntohl、ntohs
  • socket
    • socket编程接口
    • sockaddr结构
    • 结尾实现UDP程序的socket接口使用解析
      • socket
      • 处理 IP 地址的函数
      • 初始化sockaddr_in
      • bind
      • recvfrom
      • sendto
  • 实现一个简单的UDP网络程序
    • 封装服务器相关代码
    • 封装客户端相关代码
    • 实验结果

认识端口号

我们把数据从A主机发送到B主机,是目的吗?不是,真正通信的不是这两个机器!其实是这两台机器上面的软件(人)

数据有IP(公网)标识一台唯一的主机,用谁来标识各自主机上客户或者服务进程的唯一性呢?
为了更好的表示一台主机上服务进程的唯一性,我们采用端口号port标识服务器进程,客户端进程的唯一性!

端口号(port)是传输层协议的内容:

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

ip地址(主机全网唯一性) + 该主机上的端口号,标识该服务器上进程的唯一性
IP保证全网唯一,port保证在主机内部的唯一性

主机上对应的服务进程,在全网中是唯一的一个进程。
网络通信的本质:其实就是进程间通信

  1. 需要让不同的进程,先看到同一份资源—网络
  2. 通信就是在做IO,所以我们所有的上网行为,无外乎两种:我要把我的数据发出去、我要收到别人给我发的数据

进程已经有pid,为什么要有port呢?

  1. 系统是系统,网络是网络,单独设置—系统与网络解耦
  2. 需要客户端每次都能找到服务器进程—服务器的唯一性不能做任何改变— IP+port不能随意改变不会轻易改变。
  3. 不是所有的进程都要提供网络服务或者请求,但是所有的进程都需要pid。

进程+port–>网络服务进程
底层OS如何根据port找到指定的进程:OS内部采用hash方案,在OS内部维护了一个基于端口号的哈希表,key就是端口号,value就是task_struct的地址。有这个端口号就可以找到PCB,继而找到文件描述符表,文件描述符对象,对象找到了那么这个文件的缓冲区也就能找到,然后就可以将数据拷贝到缓冲区,最后就相当于我们将网络数据放到了文件中,如同读文件一样就将数据读上去了。

一个进程可以绑定多个端口号;但是一个端口号不能被多个进程绑定;

理解源端口号和目的端口号:
传输层协议(TCP和UDP)的数据段中有两个端口号,分别叫做源端口号目的端口号。 就是在描述 “数据是谁发的,要发给谁”;

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

  • 传输层协议
  • 有连接(相当于打电话必须先接通才能通话)
  • 可靠传输
  • 面向字节流
    认识UDP协议

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

  • 传输层协议
  • 无连接(相当于发送邮件只需要知道你的邮箱地址不需要你同意直接就能发给你)
  • 不可靠传输
  • 面向数据报

网络字节序

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

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

在这里插入图片描述

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

在这里插入图片描述

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

处理字节序函数 htonl、htons、ntohl、ntohs

在这里插入图片描述
函数原型:

uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

函数作用:
将数据在不同字节序之间进行转换。

函数的详细介绍:

  • htonl() 函数:将一个 32 位无符号整数(unsigned int)从本地字节序转换为网络字节序(大端字节序)。
  • htons() 函数:将一个 16 位无符号整数(unsigned short)从本地字节序转换为网络字节序(大端字节序)。
  • ntohl() 函数:将一个 32 位无符号整数(unsigned int)从网络字节序(大端字节序)转换为本地字节序。
  • ntohs() 函数:将一个 16 位无符号整数(unsigned short)从网络字节序(大端字节序)转换为本地字节序。

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);
  • 网络套接字编程(多应用于:网络跨主机之间多主机通信、支持本地通信)
  • 原始套接字(可以跨过传输层向下访问更底层的接口)
  • unix或间套接字(仅本地通信)

按道理要实现上述三种套接字应该要三套不同的接口,但是设计者只设计了一套接口,通过不同的参数解决所有网络或其他场景下的通信问题

sockaddr结构

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket。然而,各种网络协议的地址格式并不相同
在这里插入图片描述

  • IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型,16位端口号和32位IP地址。
  • IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6。 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。
  • socket API可以都用struct sockaddr *类型表示,在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性,可以接收IPv4,IPv6,以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;

sockaddr 结构:

struct sockaddr
  {
    __SOCKADDR_COMMON (sa_);	/* Common data: address family and length.  */
    char sa_data[14];		/* Address data.  */
  };

sockaddr_in 结构:

/* Structure describing an Internet socket address.  */
struct sockaddr_in
  {
    __SOCKADDR_COMMON (sin_);
    in_port_t sin_port;			/* Port number.  */
    struct in_addr sin_addr;		/* Internet address.  */

    /* Pad to size of `struct sockaddr'.  */
    unsigned char sin_zero[sizeof (struct sockaddr) -
			   __SOCKADDR_COMMON_SIZE -
			   sizeof (in_port_t) -
			   sizeof (struct in_addr)];
  };

虽然socket api的接口是sockaddr,但是我们真正在基于IPv4编程时,使用的数据结构是sockaddr_in;这个结构里主要有三部分信息:地址类型、端口号、IP地址。

in_addr结构:

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

in_addr用来表示一个IPv4IP地址。其实就是一个32位的整数。

显示当前户籍UDP连接状况与端口号的使用情况:

sudo netstat -nuap

结尾实现UDP程序的socket接口使用解析

socket

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

函数作用:
用于创建一个新的网络套接字的系统调用。

函数参数:
domain 参数指定了网络协议族:

  • AF_INET 表示 IPv4 协议
  • AF_INET6 表示 IPv6 协议
  • AF_UNIX 表示 Unix 域协议

type 参数指定了套接字的类型

  • SOCK_STREAM 表示面向连接的流套接字
  • SOCK_DGRAM 表示无连接的数据报套接字
  • SOCK_RAW 表示原始套接字。

protocol 参数指定了使用的协议

  • IPPROTO_TCP 表示 TCP 协议
  • IPPROTO_UDP 表示 UDP 协议
  • 参数设置为 0 时,系统会根据指定的 domain 和 type 参数选择一个默认的协议。这通常是最常用的协议,例如对于 AF_INET 和 SOCK_STREAM 的组合,通常使用的协议是 TCP(即 IPPROTO_TCP)。

使用 socket() 函数的一般流程如下:

  1. 创建一个套接字:调用 socket() 函数,指定 domain、type 和 protocol 参数,返回一个新的套接字描述符。
  2. 绑定套接字到本地地址:调用 bind() 函数,将套接字和一个本地地址绑定,以便其他程序可以通过该地址找到该套接字。
  3. 监听连接请求:如果创建的是面向连接的流套接字,可以调用 listen() 函数,开始监听连接请求。
  4. 接受连接请求:如果创建的是面向连接的流套接字,可以调用 accept() 函数,接受一个连接请求,返回一个新的套接字描述符,用于与客户端通信。
  5. 发送和接收数据:调用 send() 和 recv() 函数,向对方发送数据或接收对方发送的数据。
  6. 关闭套接字:调用 close() 函数,关闭套接字描述符,释放相关资源。

处理 IP 地址的函数

在这里插入图片描述

int inet_aton(const char *cp, struct in_addr *inp);
in_addr_t inet_addr(const char *cp);
in_addr_t inet_network(const char *cp);
char *inet_ntoa(struct in_addr in);
struct in_addr inet_makeaddr(int net, int host);
in_addr_t inet_lnaof(struct in_addr in);
in_addr_t inet_netof(struct in_addr in);
  • inet_aton() 函数:将一个字符串形式的 IP 地址转换为一个二进制形式的 IP 地址。如果转换成功,函数返回非零值,否则返回零。
  • inet_addr() 函数:将一个字符串形式的 IP 地址转换为一个 32 位的整数,该整数表示为网络字节序。如果转换成功,函数返回一个非零值(即返回一个网络字节序表示的 IP 地址),否则返回 INADDR_NONE。
  • inet_network() 函数:将一个字符串形式的 IP 地址的网络部分转换为一个 32 位的整数,该整数表示为网络字节序。
  • inet_ntoa() 函数:将一个二进制形式的 IP 地址转换为一个字符串形式的 IP 地址。注意,该函数返回的是一个指向静态缓冲区的指针,因此不要将其作为返回值传递给其他函数。
  • inet_makeaddr() 函数:根据网络号和主机号创建一个 IP 地址。
  • inet_lnaof() 函数:从一个二进制形式的 IP 地址中提取主机号部分。
  • inet_netof() 函数:从一个二进制形式的 IP 地址中提取网络号部分。

初始化sockaddr_in

sockaddr_in是一个 IPv4 地址结构体,用于存储 IP 地址和端口号信息。在使用套接字函数时,通常需要将地址信息存储在 sockaddr_in 结构体中,并将其作为参数传递给函数

	struct sockaddr_in local; // 定义了一个变量,栈,用户
    bzero(&local, sizeof(local));//用于将指定的内存区域清零
    local.sin_family = AF_INET;//将结构体成员 sin_family 设置为 AF_INET,表示使用 IPv4 地址族
    local.sin_port = htons(_port); 
    //结构体成员 sin_port 设置为要使用的端口号,使用 htons() 函数将端口号转换为网络字节序(大端字节序)
    
    local.sin_addr.s_addr = inet_addr(_ip.c_str());
    //将结构体成员 sin_addr 设置为要使用的 IP 地址,使用 inet_addr() 函数将 IP 地址转换为网络字节序(大端字节序)
    //在实际开发中,可以使用 inet_pton() 函数将字符串形式的 IP 地址转换为一个 struct in_addr 类型的结构体,该结构体包含了 IP 地址的二进制表示

bind

int bind(int socket, const struct sockaddr *address,
              socklen_t address_len);

函数作用:
用于将一个本地地址(IP 地址和端口号)与一个套接字关联起来的函数

函数参数:

  • socket 参数是一个指定了套接字的文件描述符。
  • addr 参数是一个指向 struct sockaddr 类型的结构体的指针,该结构体包含了要绑定的本地地址信息。
  • addrlen 参数是 addr 结构体的长度

函数返回值:
返回值为 0 表示绑定成功,-1 表示绑定失败,错误码保存在 errno 变量中。

recvfrom

ssize_t recvfrom(int socket, void *restrict buffer, size_t length,
              int flags, struct sockaddr *restrict address,
              socklen_t *restrict address_len);

函数作用:
用于从已连接或未连接的套接字接收数据的函数

函数参数:

  • socket 参数是指定了要接收数据的套接字的文件描述符。
  • buf 参数是一个指向接收数据的缓冲区的指针。
  • len 参数是缓冲区的大小。
  • flags 参数是一组标志位,可以用来指定接收数据的行为。为0表示默认
  • address 参数是一个指向 struct sockaddr 类型的结构体的指针,用于存储发送数据的远程地址。
  • addrlen 参数是 src_addr 结构体的长度。

函数返回值:
函数返回值为接收到的数据的字节数,如果没有数据可用,则返回 0。如果发生错误,则返回 -1,错误码保存在 errno 变量中

sendto

ssize_t sendto(int socket, const void *message, size_t length,
              int flags, const struct sockaddr *dest_addr,
              socklen_t dest_len);

函数作用:
用于向已连接或未连接的套接字发送数据的函数

函数参数:

  • socket 参数是指定了要发送数据的套接字的文件描述符。
  • buf 参数是一个指向要发送数据的缓冲区的指针。
  • len 参数是要发送数据的字节数。
  • flags 参数是一组标志位,可以用来指定发送数据的行为。
  • dest_addr 参数是一个指向 struct sockaddr 类型的结构体的指针,用于指定接收数据的远程地址。
  • dest_len 参数是 dest_addr 结构体的长度。

函数返回值:
函数返回值为发送数据的字节数,如果发生错误,则返回 -1,错误码保存在 errno 变量中。


实现一个简单的UDP网络程序

封装服务器相关代码

udpServer.hpp

#pragma once

#include <iostream>
#include <string>
#include <strings.h>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <functional>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

namespace Server
{
    using namespace std;

    static const string defaultIp = "0.0.0.0"; //TODO
    static const int gnum = 1024;

    enum {USAGE_ERR = 1, SOCKET_ERR, BIND_ERR};

    class udpServer
    {
    public:
        udpServer(const uint16_t &port, const string &ip = defaultIp)
        :_port(port), _ip(ip), _sockfd(-1)
        {}
        void initServer()
        {
            // 1. 创建socket
            _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
            if(_sockfd == -1)
            {
                cerr << "socket error: " << errno << " : " << strerror(errno) << endl;
                exit(SOCKET_ERR);
            }
            cout << "socket success: " << " : " << _sockfd << endl;

            // 2. 绑定port,ip(TODO)
            // 未来服务器要明确的port,不能随意改变
            struct sockaddr_in local; // 定义了一个变量,栈,用户
            bzero(&local, sizeof(local));
            local.sin_family = AF_INET;
            local.sin_port = htons(_port);
            local.sin_addr.s_addr = inet_addr(_ip.c_str());
            //local.sin_addr.s_addr = htonl(INADDR_ANY); // 任意地址bind,服务器的真实写法
            int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
            if(n == -1)
            {
                cerr << "bind error: " << errno << " : " << strerror(errno) << endl;
                exit(BIND_ERR);
            }
            // UDP Server 的预备工作完成
        }
        void start()
        {
            // 服务器的本质其实就是一个死循环
            char buffer[gnum];
            for(;;)
            {
                // 读取数据
                struct sockaddr_in peer;
                socklen_t len = sizeof(peer); //必填
                ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
                // 1. 数据是什么 2. 谁发的?
                if(s > 0)
                {
                    buffer[s] = 0;
                    string clientip = inet_ntoa(peer.sin_addr); //1. 网络序列 2. int->点分十进制IP
                    uint16_t clientport = ntohs(peer.sin_port);
                    string message = buffer;

                    cout << clientip <<"[" << clientport << "]# " << message << endl;
                }
            }
        }
        ~udpServer()
        {
        }
    private:
        uint16_t _port;
        string _ip; // 实际上,一款网络服务器,不建议指明一个IP
        int _sockfd;
        // func_t _callback; //回调
    };
}

udpServer.cc

#include "udpServer.hpp"
#include <memory>

using namespace std;
using namespace Server;

static void Usage(string proc)
{
    cout << "\nUsage:\n\t" << proc << " local_port\n\n";
}

// ./udpServer port
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = atoi(argv[1]);
    std::unique_ptr<udpServer> usvr(new udpServer(port));
    usvr->initServer();
    usvr->start();

    return 0;
}

封装客户端相关代码

udpClient.hpp

#pragma once

#include <iostream>
#include <string>
#include <strings.h>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

namespace Client
{
    using namespace std;

    class udpClient
    {
    public:
        udpClient(const string &serverip, const uint16_t &serverport) 
        : _serverip(serverip),_serverport(serverport), _sockfd(-1), _quit(false)
        {}
        void initClient()
        {
            // 创建socket
            _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
            if (_sockfd == -1)
            {
                cerr << "socket error: " << errno << " : " << strerror(errno) << endl;
                exit(2);
            }
            cout << "socket success: " << " : " << _sockfd << endl;

            // 2. client要不要bind[必须要的],client要不要显示的bind,需不需程序员自己bind?不需要
            // 写服务器的是一家公司,写client是无数家公司 -- 由OS自动形成端口进行bind!-- OS在什么时候,如何bind
        }
        void run()
        {
            struct sockaddr_in server;
            memset(&server, 0, sizeof(server));
            server.sin_family = AF_INET;
            server.sin_addr.s_addr = inet_addr(_serverip.c_str());
            server.sin_port = htons(_serverport);

            string message;
            while(!_quit)
            {
                cout << "Please Enter# ";
                cin >> message;

                sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));
            }
        }
        ~udpClient()
        {
        }

    private:
        int _sockfd;
        string _serverip;
        uint16_t _serverport;
        bool _quit;
    };
} // namespace Client

udpClient.cc


#include "udpClient.hpp"
#include <memory>

using namespace Client;

static void Usage(string proc)
{
    cout << "\nUsage:\n\t" << proc << " server_ip server_port\n\n";
}

// ./udpClient server_ip server_port
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]);

    unique_ptr<udpClient> ucli(new udpClient(serverip, serverport));

    ucli->initClient();
    ucli->run();

    return 0;
}

实验结果

在这里插入图片描述


如有错误或者不清楚的地方欢迎私信或者评论指出🚀🚀

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

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

相关文章

TE-L-Tyrosine (FET-precursor),合成蛋白质的必需成分之一,L-Tyrosine

试剂简介&#xff1a;TE-L-Tyrosine (FET-precursor)&#xff0c;L-Tyrosine是一种氨基酸&#xff0c;它是蛋白质合成的必需成分之一。除了在蛋白质合成中的重要作用外&#xff0c;L-Tyrosine还具有多种生理功能。它是肾上腺素、去甲肾上腺素、甲状腺激素等重要激素的前体物质&…

短视频矩阵系统源码|开发者步骤

一、为了开发和部署短视频矩阵系统&#xff0c;首先需要进行以下步骤&#xff1a; 1. 系统设计与开发&#xff1a;根据需求&#xff0c;进行系统架构设计&#xff0c;并选择合适的技术栈进行开发。这可能涉及到前端开发、后端开发、数据库设计等工作。 2. 实现核心功能&#…

AIGC与软件测试的融合

一、ChatGPT与AIGC 生成式人工智能——AIGC&#xff08;Artificial Intelligence Generated Content&#xff09;&#xff0c;是指基于生成对抗网络、大型预训练模型等人工智能的技术方法&#xff0c;通过已有数据的学习和识别&#xff0c;以适当的泛化能力生成相关内容的技术。…

MySQL运行时的可观测性

1.说在前面的话 2.安装employees测试库 3.观测SQL运行状态 3.1 观测SQL运行时的内存消耗3.2 观测SQL运行时的其他开销3.3 观测SQL运行进度 感知SQL运行时的状态 1. 说在前面的话 在MySQL里&#xff0c;一条SQL运行时产生多少磁盘I/O&#xff0c;占用多少内存&#xff0c;是否…

Ctfshow web入门 命令执行RCE篇 web29-web77 与 web118-web124 详细题解 全

Ctfshow 命令执行 web29 pregmatch是正则匹配函数&#xff0c;匹配是否包含flag&#xff0c;if(!preg_match("/flag/i", $c))&#xff0c;/i忽略大小写 可以利用system来间接执行系统命令 flag采用f*绕过&#xff0c;或者mv fl?g.php 1.txt修改文件名&#xff0c…

一文彻底理解时间复杂度和空间复杂度(附实例)

目录 1 PNP&#xff1f;2 时间复杂度2.1 常数阶复杂度2.2 对数阶复杂度2.3 线性阶复杂度2.4 平方阶复杂度2.5 指数阶复杂度2.6 总结 3 空间复杂度 1 PNP&#xff1f; P类问题(Polynomial)指在多项式时间内能求解的问题&#xff1b;NP类问题(Non-Deterministic Polynomial)指在…

数据中心UPS监控,不服不行!

UPS作为关键的电力保障设备&#xff0c;它在电力中断或波动的情况下&#xff0c;为电子设备提供稳定的备用电源&#xff0c;以防止数据丢失、设备损坏或生产中断。 通过远程监控、电池健康检测、负载管理、警报通知等功能&#xff0c;UPS监控确保了系统的高效运行和可靠性。同时…

Windows下安装tomcat无法启动问题汇总和解决

tomcat在初学的时候安装总是出现各种问题&#xff0c;最近重新安装了一次&#xff0c;居然也被一些小问题导致无法启动了&#xff0c;特此写下这篇文章&#xff0c;希望能帮助到大家 导致tomcat启动失败原因 1、未配置tomcat环境变量: CATALINA_HOME&#xff1b;然后path环境…

扭矩张力控制和速度张力控制详细介绍

张力控制的开环和闭环相关算法介绍,请查看下面文章链接: PLC张力控制(开环闭环算法分析)_张力控制plc程序实例_RXXW_Dor的博客-CSDN博客里工业控制张力控制无处不在,也衍生出很多张力控制专用控制器,磁粉制动器等,本篇博客主要讨论PLC的张力控制相关应用和算法,关于绕…

LeetCode(力扣)404. 左叶子之和Python

LeetCode404. 左叶子之和 题目链接代码 题目链接 左叶子之和 代码 递归 # Definition for a binary tree node. # class TreeNode: # def __init__(self, val0, leftNone, rightNone): # self.val val # self.left left # self.right right …

【JavaEE进阶】SpringMVC

文章目录 一. 简单认识SpringMVC1. 什么是SpringMVC?2. SpringMVC与MVC的关系 二. SpringMVC1. SpringMVC创建和连接2. SpringMVC的简单使用2.1 RequestMapping 注解介绍2.2 RequestMapping支持的请求类型2.3 GetMapping 和 PostMapping 3. 获取参数3.1 传递单个参数3.2 传递对…

2023年跑步耳机性价比最高的都在这里了!不容错过

你还在为如何选择合适的跑步耳机而困扰吗&#xff1f;身为一个常年的跑步的我&#xff0c;在选择运动耳机的时候我会从价格、性能、舒适度、耐用性等多个方面去进行了比较&#xff0c;这样才能够找到最适合自己的跑步耳机。那么下面我就给大家推荐几款具有性价比的跑步耳机&…

项目管理工具,让项目团队更高效

随着项目管理的发展&#xff0c;出现个各种各样的项目管理工具&#xff0c;帮助项目团队提高项目管理的效率和质量&#xff0c;降低成本和风险&#xff0c;增强团队的协作和沟通效率&#xff0c;实现项目的成功实施和持续发展。 1、项目计划是项目管理中重要的一环 在传统的…

4.react useContext使用与常见问题

1. 在函数组件实现跨组件通信的方式 2. 注册Context将value传递给子组件let MyContext React.createContext(默认值); <MyContext.Provider value{} > let value useContext(MyContext)<!DOCTYPE html> <html lang"en"><head><meta cha…

【正点原子STM32连载】第十五章 窗口看门狗实验 摘自【正点原子】APM32F407最小系统板使用指南

1&#xff09;实验平台&#xff1a;正点原子stm32f103战舰开发板V4 2&#xff09;平台购买地址&#xff1a;https://detail.tmall.com/item.htm?id609294757420 3&#xff09;全套实验源码手册视频下载地址&#xff1a; http://www.openedv.com/thread-340252-1-1.html# 第十…

认识Mybatis并实现增删查改

目录 一.Mybatis特性 二.常见持久层技术的比较 三.搭建Mybaits环境 四.使用Mybatis 五.通过Mybatis实现增删改 六.实现数据库的查询操作 一.Mybatis特性 定制化SQL&#xff1a;MyBatis允许开发人员编写、优化和管理自定义的SQL语句&#xff0c;可以满足复杂查询和存储过程等…

Vitepress

Vitepress 版本&#xff1a;1.0.0-rc.1 这里使用在Vue3组件库中作为项目文档说明使用&#xff0c;另外版本见差异有点大&#xff0c;如版本不一致请参照官方文档 1、安装&初始化 1.1、安装 yarn add vitepress1.0.0-rc.11.2、初始化 PS D:\WebstromProject\chenxing>…

CloudQuery实战 | 谁说没有一款一体化数据库操作管控云平台了?

文章目录 CloudQuery询盾的地址CloudQuery主页统一入口数据库归纳SQL编辑器权限管控审计中心数据保护数据变更 CloudQuery文档中心了解CloudQuery快速入门安装步骤社区版v2.1.0操作手册1数据查询更新日志 CloudQuery社区和活动 CloudQuery线上实战线上实战主页面展示及数据操作…

CAD哪个版本最好用?学习一下CAD版本转换方法

CAD即计算机辅助设计&#xff0c;是一个制图软件&#xff0c;用于绘制建筑、机械、电子等领域的图纸。CAD文件通常被称为“图纸”或“工程图”。 CAD文件通常在以下方面使用&#xff1a; 1. 建筑&#xff1a;建筑师使用CAD文件来创建建筑物的平面图、立体图和剖面图。 2. 机…

Docker部署LNMP

Docker部署LNMP 一、安装docker1.安装docker2.镜像下载 二、部署MySQL1.获取镜像2.创建启动容器创建启动容器 huahua_mysql 三、部署PHP1.获取镜像2.创建容器3.查看信息 四、安装nginx1.获取镜像2.创建运行容器3.修改nginx配置文件 五、总结1. 安装Docker和Docker Compose&…