【计算机网络】socket 网络套接字

网络套接字

  • 一、端口号
    • 1. 认识端口号
    • 2. socket
  • 二、认识TCP协议和UDP协议
    • 1. TCP协议
    • 2. UDP协议
  • 三、网络字节序
  • 四、socket 编程
    • 1. socket 常见API
    • 2. sockaddr 结构
    • 3. 编写 UDP 服务器
      • (1)socket()
      • (2)bind()
      • (3)recvfrom()
      • (4)sendto()
      • (5)udp 服务端和客户端
    • 4. 地址转换函数
      • (1)相关接口
      • (2)关于 inet_ntoa
    • 5. 编写 TCP 服务器
      • (1)listen()
      • (2)accept()
      • (3)con
      • (4)守护进程
      • (5)tcp 服务端和客户端

一、端口号

1. 认识端口号

实际上我们两台机器在进行通信时,是应用层在进行通信,应用层必定会推动下层和对方的上层进行通信。

其实网络协议栈中的下三层,主要解决的是数据安全可靠的送到远端机器。而用户使用应用层软件,完成数据发送和接收的。那么用户要使用软件,首先需要把这个软件启动起来!所以软件启动起来,本质就是进程!所以两台机器进行通信,本质是两台机器之上的应用层在通信,也就是两个进程之间在互相交换数据!所以网络通信的本质就是进程间通信!只不过在网络通信中的公共资源是网络,通过网络协议栈利用网络资源,让两个不同的进程看到了同一份资源!

在网络协议栈中,在传输层怎么把数据正确交给上层应用层呢?怎么知道交给哪一个应用呢?所以就要求上层应用层和传输层之间必须协商一种方案,让我们把数据准确交给上层,这个方案我们称为端口号。所以在传输层的报头中,必须要有原端口号目的端口号,也就是根据目的端口号就可以决定这个数据的有效载荷要交给上层应用的哪一个!所以对于端口号无论对于客户端和服务端,都能唯一的标识该主机上的一个网络应用层的进程!

我们可以这样理解,其实在传输层当中,操作系统会形成一张哈希表,哈希表中的类型是 task_struct*,每一个应用层都要和该哈希表绑定端口号,本质就是根据端口号在哈希表里做哈希运算,如果该位置已经被占用了,就不能被绑定了,因为一个端口号只能被一个进程绑定;如果该位置没有被使用,就把该进程的pcb地址放在该位置上。

2. socket

因为在公网上,IP地址 能表示唯一的一台主机,端口号 port,用来标识该主机上的唯一的一个进程,所以 IP + port 就可以标识全网唯一的一个进程!那么我们在网络通信时,只需要在对应的报头上填上原IP目的IP原port目的port,就可以将报文交给另一个主机的进程,这种基于 IP + port 的通信方式,我们称为 socket.

那么端口号和进程pid有什么区别呢?进程pid也能标识一台主机上的唯一进程啊?因为首先,不是所有的进程都要通信,但是所有的进程都要有pid!其次是为了使系统和网络功能解耦!

二、认识TCP协议和UDP协议

下面我们先认识一下两个传输层协议:

1. TCP协议

此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识;后面我们再详细讨论 TCP 的一些细节问题。

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

2. UDP协议

此处我们也是对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识;后面再详细讨论。

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

三、网络字节序

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

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

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

在这里插入图片描述

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

四、socket 编程

1. socket 常见API

				// 创建 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);

2. sockaddr 结构

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

在这里插入图片描述

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

3. 编写 UDP 服务器

(1)socket()

下面我们编写一个 UDP 服务器。首先需要做的是创建套接字,使用到的接口是 socket()

在这里插入图片描述

第一个参数是我们创建的套接字的域,即使用 IPv4 的网络协议还是 IPv6 的网络协议,目前我们只需要关注这两个即可,如下图:

在这里插入图片描述

第二个参数表示当前 socket 对应的类型,也就是相当于这个套接字未来给我们提供什么服务,是面向字节流的还是面向用户数据报的,如下:

在这里插入图片描述

第三个参数表示的是协议类型,目前我们不需要传这个参数。

而返回值相当于是一个文件描述符,所以创建一个套接字的本质,在底层就相当于是打开一个文件,只不过以前的 struct file 指向的是键盘、显示器这样的设备;而现在指向的是网卡设备。

在这里插入图片描述

(2)bind()

创建套接字成功之后,接下来就要绑定端口号,使用到的接口是 bind(),如下:

在这里插入图片描述

其中第一个参数就是创建套接字时的返回值;第二个参数是一个结构体;第三个参数是结构体的长度。但是我们在网络套接字编程的时候不用第二个参数类型的结构体,这个结构体它只是设计接口用,我们实际用的是 sockaddr_in 类型的结构体,只需要在传参的时候进行强转即可。我们可以使用 bzero() 接口将该结构体清0;

我们是要使用 bind 来让套接字和我们往该结构体中填充的网络信息要关联起来,所以我们需要想该结构体中填充对应的字段。该结构体中有如下字段:

在这里插入图片描述

对应下图:

在这里插入图片描述

其中 sin_zero 为该结构体的填充字段,也就是这些字段不用填充,当作占位符即可;sin_addr 代表 ip 地址;sin_port 代表服务器所使用的端口号;sin_family 代表该结构体对应的网络协议类型,IPv4 或者 IPv6.

因为我们在给对方发送数据的时候,我们也一定需要让对方知道我们是谁,所以我们需要将端口号携带上,发送给对方,这样对方把数据处理完,就可以给我们响应回来。所以端口号是要在网络里来回发送的,也就是需要保证我们的端口号是网路字节序列,因为该端口号是要给对方发送的。所以这里我们就需要用到主机序列转网络序列的接口,由于端口号是两个字节,所以用到的接口为 htons()

在这里插入图片描述

由于我们用户一般用的都是点分十进制字符串风格的 IP 地址,也就是 0.0.0.0 这种风格,每个点分的范围是 0~255,每个字符一个字节,远远超过结构体中要求的 32 位 ip 地址,也就是四字节。所以我们需要将该字符串类型转换为 uint32_t 的类型,那么用到的接口是 inet_addr(),它的作用就是将字符串风格的 ip 地址转化为网络风格的 uint32_t 类型,如下图:

在这里插入图片描述

同端口号一样,IP 地址也需要保证是网络字节序列。那么它的返回值类型 in_addr_t 其实就是符合网路字节序列的 uint32_t 的类型。

上面我们已经把准备工作做好了,接下来我们就需要使用 bind() 接口进行绑定,本质就是把我们定义的 struct 结构体设置进内核,设置进指定的套接字内部。

(3)recvfrom()

接下来我们就需要在指定的一个套接字里获取数据内容,使用到的接口是 recvfrom(),如下图:

在这里插入图片描述

第一个参数就是网络文件描述符;第二个参数和第三个参数分别表示我们提供的缓冲区和它的长度,读到的数据就会放在缓冲区中;第三个参数设为0就是默认使用阻塞方式;最后两个参数又是熟悉的结构体,由于我们需要知道这些数据是谁给我们发的,因为我们有可能也要将数据给对方返回。所以最后两个参数其实是输出型参数。

返回值成功就是对应的长度,否则就是-1,如下:

在这里插入图片描述

(4)sendto()

将数据发送回给对方使用到的接口为 sendto(),如下:

在这里插入图片描述

参数和 recvfrom() 的参数类似,这里不再介绍了。而最后两个参数是输入型参数,我们要将数据发回给对方,首先需要知道对方是谁,而我们上面已经通过 recvfrom() 获取到了对方的结构体信息,所以直接使用该结构体信息即可。

(5)udp 服务端和客户端

其中通过使用上面的接口编写的一个简单的接收客户端的字符串信息,并进行简单的加工的 udp 服务器代码链接为:UDP.

其中 udp server 的代码如下:

				#pragma once#include <iostream>#include <string>#include <sys/types.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#include <strings.h>#include <unistd.h>#include <cstring>#include <functional>#include "log.hpp"using func_t = std::function<std::string(const std::string&)>;//typedef std::function<std::string(const std::string&)> func_t;std::string default_ip = "0.0.0.0";uint16_t default_port = 8080;log lg;class UdpServer{public:UdpServer(const uint16_t &port = default_port, const std::string &ip = default_ip): _port(port), _ip(ip), _isrunning(false), _sockfd(0){}void Init(){// 1.创建 udp 套接字_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // AF_INET == PF_INETif (_sockfd < 0){lg(Fatal, "socket create faild, sockfd: %d", _sockfd);exit(1);}lg(Info, "socket create success, sockfd: %d", _sockfd);// 2.绑定端口号// 2.1 准备数据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());   // 1.string -> uint32_t  2.保证uint32_t是网络序列local.sin_addr.s_addr = htonl(INADDR_ANY);// 2.2 开始bindint n = bind(_sockfd, (const sockaddr *)&local, sizeof(local));if (n < 0){lg(Fatal, "bind faild, errno: %d, err message: %s", errno, strerror(errno));exit(2);}lg(Info, "bind success, errno: %d, err message: %s", errno, strerror(errno));}void Run(func_t func){_isrunning = true;char buffer[1024];while (_isrunning){// 记录客户端发来时的结构体信息struct sockaddr_in client;socklen_t len = sizeof(client);ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (sockaddr *)&client, &len);if (n < 0){lg(Warning, "recvfrom error, errno: %d, err message: %s", errno, strerror(errno));continue;}buffer[n] = 0;// 对数据进行简单的加工std::string info = buffer;std::string echo_string = func(info);// 发送回给对方sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (const sockaddr *)&client, len);}}~UdpServer(){if (_sockfd > 0)close(_sockfd);}private:int _sockfd;uint16_t _port;std::string _ip;bool _isrunning;};

udp client 的代码如下:

				#include <iostream>#include <cstdlib>#include <unistd.h>#include <strings.h>#include <sys/types.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>using namespace std;void Usage(string proc){cout << "\n\rUsage: " << proc << " serverip serverport\n" << endl;}int main(int argc, char* argv[]){if(argc != 3){Usage(argv[0]);exit(0);}string server_ip = argv[1];uint16_t server_port = stoi(argv[2]);sockaddr_in server;bzero(&server, sizeof(server));server.sin_family = AF_INET;server.sin_addr.s_addr = inet_addr(server_ip.c_str());server.sin_port = htons(server_port);socklen_t len = sizeof(server);// client 也需要 bind,只不过不需要用户显示 bind,一般由OS自由随机选择// 系统会在首次发送数据的时候给我们bindint sockfd = socket(AF_INET, SOCK_DGRAM, 0);if(sockfd < 0){cout << "socker error" << endl;return 1;}string message;char buffer[1024];while(true){cout << "Plase Enter@ ";getline(cin, message);// 发送数据sendto(sockfd, message.c_str(), message.size(), 0, (sockaddr*)&server, len);// 当服务器进行简单的加工处理后会发送回来,此时客户端再次获取sockaddr_in temp;socklen_t size = sizeof(temp);ssize_t s = recvfrom(sockfd, buffer, 1023, 0, (sockaddr*)&temp, &len);if(s > 0){buffer[s] = 0;cout << buffer << endl;}}close(sockfd);return 0;}

main 函数:

				#include <iostream>#include <vector>#include <memory>#include <cstdio>#include "UdpServer.hpp"using namespace std;void Usage(string proc){cout << "\n\rUsage: " << proc << " port[1024+]\n" << endl;}// 处理字符串的方法string Handler(const std::string& str){string res = "Server get a message: ";res += str;cout << res << endl;return res;}// 远程执行指令的方法string ExcuteCommand(const string& cmd){FILE* fp = popen(cmd.c_str(), "r");if(nullptr == fp){perror("popen");return "error";}string result;char buffer[4096];while(true){char* tmp = fgets(buffer, sizeof(buffer), fp);if(tmp == nullptr) break;result = buffer;}pclose(fp);return result;}int main(int argc, char* argv[]){if(argc != 2){Usage(argv[0]);exit(0);}uint16_t port = stoi(argv[1]);unique_ptr<UdpServer> svr(new UdpServer(port, "127.0.0.1"));svr->Init();svr->Run(ExcuteCommand);return 0;}

有关代码中的细节:

  • 有关 IP 地址

云服务器禁止直接bind公网ipbind ip 地址为0,表示的含义是任意地址绑定,这种是比较推荐的做法。当 IP 地址为 127.0.0.1 时,表示进行的是本地传输测试,不会进行跨网传输。

  • 有关 port

其中 0~1023 的端口号是系统内定的端口号,一般都要有固定的应用层协议使用,例如 http:80,https:443;所以我们一般绑端口号,一般绑1024以上的。

  • popen() 系统调用

popen() 是一个被封装起来的管道和子进程执行命令的应用。

在这里插入图片描述

它的第一个参数就是需要执行的命令,在底层它会帮我们进行 fork() 创建子进程,并让父子进程建立管道,然后让子进程把它的运行结果通过管道再返回给调用方。如果调用方想得到 command 指令的运行结果,可以通过文件指针的方式读取。第二个参数相当于是打开这个命令的方式,我们使用 “r” 即可。使用完毕后使用 pclose() 关闭该文件指针即可。

其中,我们可以使用 netstat -nlup 查看系统中所有的 udp 信息,并且把进程信息也显示出来。

我们还可以将以上代码修改成为多线程代码,链接为:多线程UDP.

4. 地址转换函数

(1)相关接口

我们只介绍基于 IPv4socket 网络编程,sockaddr_in 中的成员 struct in_addr sin_addr 表示32位 的 IP 地址,但是我们通常用点分十进制的字符串表示 IP 地址,以下函数可以在字符串表示和 in_addr 表示之间转换。我们在上面的 bind() 中也使用了地址转换函数 inet_addr().

  • 字符串转 in_addr 的函数:

      			#include <arpa/inet.h>int inet_aton(const char* strptr, struct in_addr* addrptr);in_addr_t inet_addr(const char* strptr);int inet_pton(int family, const char* strptr, void* addrptr);
    
  • in_addr 转字符串的函数:

      			char* inet_ntoa(struct in_addr inaddr);const char* inet_ntop(int family, const void* addrptr, char* strptr, size_t len);
    

其中 inet_ptoninet_ntop 不仅可以转换 IPv4in_addr,还可以转换 IPv6in6_addr,因此函数接口是 void* addrptr.

(2)关于 inet_ntoa

inet_ntoa 这个函数返回了一个 char*,很显然是这个函数自己在内部为我们申请了一块内存来保存 ip 的结果,那么是否需要调用者手动释放呢?

在这里插入图片描述

man 手册上说,inet_ntoa 函数,是把这个返回结果放到了静态存储区。这个时候不需要我们手动进行释放。

5. 编写 TCP 服务器

(1)listen()

TCP 是面向连接的,服务器一般是比较被动的,所以服务器一直处于一种等待连接到来的状态,这个工作叫做监听状态,使用到的接口是 listen(),如下:

在这里插入图片描述

第一个参数为指定的套接字,通过该套接字等待新连接的到来。第二个参数我们后面再介绍,暂时设为10左右即可。返回值,成功返回0,失败返回-1.

(2)accept()

因为 TCP 是面向连接的,所以在正式通信之前,先要把连接建立起来,使用到的接口为 accept(),该接口的作用是获取一个新的连接,如下:

在这里插入图片描述

第一个参数为我们刚刚设置为监听状态的套接字;后两个参数和 recvfrom() 的后两个参数一样,都是输出型参数,也就是谁给我们发的 TCP 报文,那么对应的套接字信息就会通过这两个参数返回出来。

而返回值成功返回一个文件描述符;否则返回-1;那么返回值也是一个文件描述符,我们原本也有一个文件描述符,为什么会有两个 sockfd 呢?我们该用哪个呢?其实它们分工是明确的,我们原本定义的 sockfd,即被创建的,被 bind 的,被监听的套接字,它的工作是从底层获取新的连接;而未来真正提供通信服务的,是 accept() 返回的套接字!

至此,我们可以使用 telnet 进行指定服务的一个远程连接,后面跟上 IP 地址和端口号即可;它在底层默认使用的就是 TCP.

(3)con

由于在 TCP 中,客户端是要连接服务器的,所以服务端需要有一个能够向服务器发起连接的接口,该接口为 connect(),如下:

在这里插入图片描述

该接口的作用是通过指定的套接字,向指定的网络目标地址发起连接。后两个参数和 sendto() 的后两个参数一样。返回值成功返回0,失败返回-1.

TCP 客户端也需要 bind,但是和 UDP 一样,不需要显示的 bind,系统会在客户端发起 connect 的时候,进行自动随机 bind.

我们可以使用 netstat -nltp 查看系统中所有 TCP 的信息,并把进程信息显示出来。

(4)守护进程

在我们登录 Linux 的时候,Linux 系统会给我们形成一个会话,而且会为每个会话创建一个 bash 进程,这个 bash 就可以为用户提供命令行服务。每个会话中只能存在一个前台进程,但是可以存在多个后台进程,而键盘信号只能发送给前台进程。前台和后台进程的区别就是是否拥有键盘文件,它们都可以向显示器打印,而只有前台进程才能从键盘,即标准输入获取数据!

如果我们不想后台进程向显示器打印的数据影响我们,我们可以将它的打印数据重定向到文件中,例如:

在这里插入图片描述

其中 [1] 表示后台任务号,后面数字表示进程 PID.

而查看后台任务的指令为:jobs,如下:

在这里插入图片描述

如果我们想把后台进程提到前台,可以使用 fg 任务号,如下:

在这里插入图片描述

如果想把它重新放回后台,我们可以使用 ctrl + z 将该进程暂停。然后使用 bg 任务号 将该进程重新启动,如下:

在这里插入图片描述

接下来我们再运行几个后台进程,例如使用 sleep,方便观察 Linux 中的进程间关系,使用 ps axj | head -1 && ps axj | grep -Ei 'a.out|sleep' 查看它们的进程信息:

在这里插入图片描述

其中 PPID、PID 我们都认识,而 PGID 表示的是进程组IDSID 表示 session id,即会话 id.

而系统中可能会存在多个 session,所以系统需要管理多个 session.

我们可以看到,./a.out 进程的 PIDPGID 是一样的,所以它就是自成进程组的。而三个 sleep 分别是三个不同的进程,但是它们的 PGID 却是同一个,而且是用管道建立的进程的第一个进程的 PID,所以它们三个自成一组,而组长是多个进程中的第一个。那么进程组和任务有什么关系呢?任务是要指派给进程组的!所以我们需要校正一下以前的说法,我们把前台进程称为前台任务,后台进程称为后台任务,因为可能某一个后台任务里面,可能会包含多个进程。但是无论有几个进程组完成对应的任务,在同一个会话内启动的,SID 是一样的!那么上面中的 SID 到底是谁呢?我们可以查看一下:

在这里插入图片描述

如上图,我们可以看到,它是 bash!所以就是以 bashpid 去构建了一个 session

这种后台进程会收到用户登录和退出的影响,如果我们不想受到任何用户登录和注销的影响,我们可以将进程守护进程化。什么是守护进程呢?我们把自成进程组自成会话的进程称为守护进程!那么我们该如何做到呢?下面我们认识一个接口:setsid(),如下:

在这里插入图片描述

该接口的作用就是,哪个进程调用该接口,就把该进程的组ID设置为会话ID,也就是让进程独立成会话。

在这里插入图片描述

返回值成功返回进程的ID,否则返回-1.

注意,该接口不能由进程组的组长直接调用,那么怎么才能保证不是组长调用呢?所以我们可以使用 fork() 创建子进程调用!所以守护进程的本质,也是孤儿进程!

(5)tcp 服务端和客户端

接下来我们结合上面所学的知识,编写一个 TCP 服务器,并将它守护进程化,代码链接:

其中在守护进程中,我们的代码中是充满大量的打印的,而这些打印默认是向标准输出打的,也就是向显示器上打了,而对于守护进程来说,就不应该向显示器上打了,所以我们需要一个解决方案。而 Linux 中存在一种字符文件,叫做 /dev/null,只要我们向该文件写入,都会被该文件丢弃掉,如果我们向该文件读取,什么也读取不到。所以我们只需要将所有的输出向该文件写入即可。我们也可以将打印信息写入文件中。

另外,TCP 在通信时是全双工的,也就是可以同时读写的。在底层操作系统给 TCP 提供两个缓冲区,一个发送缓冲区,一个接收缓冲区,我们在用 TCP 的同时,别人也在用,所以别人也会有上面两个缓冲区,所以当我们发送数据,是先把我们的数据拷贝到我们的 TCP 的发送缓冲区,然后通过网络会发送到对方的接收缓冲区,反过来也同理,如下图:

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

EXCEL 在列不同单元格之间插入N个空行

1、第一步数据&#xff0c;要求在每个数字之间之间插入3个空格 2、拿数据个数*&#xff08;要插入空格数1&#xff09; 19*4 3、填充 4、复制数据到D列 5、下拉数据&#xff0c;选择复制填充这样1-19就会重复4次 6、全选数据D列排序&#xff0c;这样即完成了插入空格 以…

BLUEZ学习笔记_GATT_server_client_简单解析

文章参考了以下内容 蓝牙bluez5的开发方法及入门教程_bluez蓝牙配网demo-CSDN博客文章浏览阅读1w次&#xff0c;点赞15次&#xff0c;收藏99次。1 摘要这篇文章的主要目的是告诉大家应该如何使用bluez进行开发&#xff0c;由于bluez的文档实在太少了&#xff0c;入门门槛实在太…

【JavaScript】实现下--刘谦春晚魔术:约瑟夫环的数学魅力!

2024年春晚刘谦的魔术堪称惊艳全场&#xff0c;那么他这个魔术实现的原理是什么呢&#xff1f;今天&#xff0c;就让咱们使用 JS 是实现这个魔术。 约瑟夫环问题简介&#x1f3b4;&#x1f50e; 约瑟夫环问题源自古罗马&#xff0c;由历史学家约瑟夫斯提出&#xff0c;而它的…

文件上传漏洞--Upload-labs--Pass131415--图片马

声明&#xff1a;Pass13、14、15 都使用相同手段--图片马进行绕过。 一、什么是图片马 顾名思义&#xff0c;图片马即 图片 木马。将恶意代码插入图片中进行上传&#xff0c;绕过白名单。 图片马制作流程&#xff1a; 1、在文件夹中打开 cmd&#xff0c;输入指令。 /b&…

想养猫但对猫毛过敏怎么办?宠物空气净化器可以帮到你!

如今&#xff0c;许多铲屎官都会面临着猫毛过敏的问题&#xff0c;这是一个不能轻视的挑战。我有一个朋友就遇到了这个问题&#xff0c;她多次出现过敏症状&#xff0c;但并没有太在意。可最终&#xff0c;这种对猫毛的过敏反应导致了变异性哮喘的发作。为了她的健康考虑&#…

Java的编程之旅19——使用idea对面相对象编程项目的创建

在介绍面向对象编程之前先说一下我们在idea中如何创建项目文件 使用快捷键CtrlshiftaltS新建一个模块&#xff0c;点击“”&#xff0c;再点New Module 点击Next 我这里给Module起名叫OOP,就是面向对象编程的英文缩写&#xff0c;再点击下面的Finish 点Apply或OK均可 右键src…

pmp考试选择机构有多重要?

关于大家之前提到的机构漏题的问题&#xff0c;其实确实存在&#xff0c;因此我认为选择适合自己的机构仍然需要谨慎选择。很多人不清楚如何选择合适的机构&#xff1f;我会在这里分享一些选择PMP机构的方法&#xff0c;希望能够对有需要的人有所帮助。 一、甄选机构 如何选择…

Stable Diffusion 模型分享:Indigo Furry mix(人类与野兽的混合)

本文收录于《AI绘画从入门到精通》专栏,专栏总目录:点这里。 文章目录 模型介绍生成案例案例一案例二案例三案例四案例五案例六案例七案例八案例九案例十

2024最佳住宅代理IP服务商盘点

跨境出海已成为了近几年的最热趋势&#xff0c;大批量的企业开始开拓海外市场&#xff0c;而海外电商领域则是最受欢迎的切入口。新兴的tiktok、Temu&#xff0c;老牌的Amazon、Ebay&#xff0c;热门的Etsy、Mecari等等都是蓝海一片。跨境入门并不难&#xff0c;前期的准备中不…

SSH tunneling 简明示例

基本概念 SSH tunneling又称为SSH port forwarding。 如果想快速了解其应用场景&#xff0c;这篇文章A short guide to SSH port forwarding 很不错。其详细解释了Client to Server的Local Forwarding。虽然没有涉及Server to Client的Remote Forwarding&#xff0c;但他也指…

ElasticStack安装(windows)

官网 : Elasticsearch 平台 — 大规模查找实时答案 | Elastic Elasticsearch Elastic Stack(一套技术栈) 包含了数据的整合 >提取 >存储 >使用&#xff0c;一整套! 各组件介绍: beats 套件:从各种不同类型的文件/应用中采集数据。比如:a,b,cd,e,aa,bb,ccLogstash:…

图——最小生成树实现(Kruskal算法,prime算法)

目录 预备知识&#xff1a; 最小生成树概念&#xff1a; Kruskal算法&#xff1a; 代码实现如下&#xff1a; 测试&#xff1a; Prime算法 &#xff1a; 代码实现如下&#xff1a; 测试&#xff1a; 结语&#xff1a; 预备知识&#xff1a; 连通图&#xff1a;在无向图…