网络套接字编程(三)

网络套接字编程(三)

文章目录

  • 网络套接字编程(三)
    • 简易日志组件
      • 引入日志的原因
      • 日志等级
      • 打印日志函数
      • 将日志组件使用到服务端中
    • 守护进程
      • 概念
      • 进程组、终端、会话
      • 守护进程的实现原理
      • 守护进程化组件
      • 将守护进程化组件使用到服务端中
    • 补充知识
      • 关于inet_ntoa

在上一篇博客 网络套接字编程(二)-CSDN博客中讲解了单执行流、多执行流、线程池版的简易TCP程序的编写,本文将讲解与其有关的组件编写。

简易日志组件

引入日志的原因

在实际开发中,服务端是需要不间断运行来保证无论何时都能给客户端提供网络服务,因此在程序遇到某些不影响程序运行的问题,不会主动终止程序,而是将错误信息以日志的形式打印。服务端维护人员,会通过日志中记录的错误信息,来进行程序错误的定位和解决。

日志等级

在日志系统中,常常使用不同的日志等级来对日志进行分类和标记,以便根据重要性和紧急程度进行过滤、查看和处理。不同的日志等级通常表示了不同的日志消息类型和优先级。

以下是常见的日志等级,按照从高到低的顺序排列:

  1. 致命错误(Fatal):表示严重的错误或故障,导致系统无法正常运行或继续执行。这类错误需要立即解决,并可能需要中断程序的执行。
  2. 错误(Error):表示一些关键操作或功能发生了错误,但系统仍然可以继续运行。这类错误需要进行修复,以确保系统正常运行。
  3. 警告(Warning):表示一些潜在问题或异常情况,可能会影响系统的正常运行或导致错误。这类日志用于提示潜在的风险或不寻常的行为,需要进行检查和调查。
  4. 信息(Info/Information):表示一般的信息性消息,用于记录程序的正常运行状态、关键路径、重要操作等。这类日志用于追踪应用程序的运行情况和关键事件。
  5. 调试(Debug):表示开发过程中的调试信息,用于调试和分析程序的内部逻辑、变量状态等。这类日志通常只在开发和测试阶段启用,可用于排查问题和验证程序行为。

本文采用枚举的方式来表示各个日志等级:

enum
{Debug=0,Info,Warning,Error,Fatal,Unknown
};

打印日志函数

本文想实现的日志组件中,打印的日志格式为[日志等级][时间][进程pid][日志消息体],日志组件的实现中核心部分就是打印日志函数,日志组件的具体实现如下:

#pragma once#include <cstdio>
#include <cstring>
#include <cstdarg>
#include <iostream>
#include <time.h>
#include <sys/types.h>
#include <unistd.h>const std::string filename = "./log.txt";enum
{Debug = 0,Info,Warning,Error,Fatal,Unknown
};static std::string toLevelString(int level)
{switch (level){case Debug:return "Debug";case Info:return "Info";case Warning:return "Warning";case Error:return "Error";case Fatal:return "Fatal";default:return "Unknown";}
}static std::string getTime()
{const time_t cur = time(nullptr);struct tm *tmp = localtime(&cur);char buffer[1024];snprintf(buffer, sizeof(buffer), "%d-%d-%d %d:%d:%d", (tmp->tm_year) + 1900, tmp->tm_mon+1, tmp->tm_mday,tmp->tm_hour, tmp->tm_min, tmp->tm_sec);return buffer;
}// 等级 时间 pid 消息体
void LogMessage(int level, const char *format, ...)
{char logLeft[1024];std::string level_string = toLevelString(level);std::string curr_time = getTime();snprintf(logLeft, sizeof(logLeft), "[%s][%s][%d]\n", level_string.c_str(), curr_time.c_str(), getpid());char logRight[1024];va_list p;va_start(p, format);vsnprintf(logRight, sizeof(logRight), format, p);va_end(p);FILE *fp = fopen(filename.c_str(), "a");if (fp == nullptr)return;fprintf(fp, "%s%s\n", logLeft, logRight);fflush(fp);fclose(fp);
}

识别日志等级函数

我们预期的打印中,日志等级是通过字符串形式打印的,而不是枚举对应的数值,因此需要实现一个识别日志等级函数,将传入的枚举数值转换成字符串形式表示的日志等级。

该函数实现只需要简单的switch case语句将输入的数值对应成的字符串返回即可。

时间获取函数

我们预期的打印中,包含一个以字符串形式打印的时间部分,并且希望这个时间具体到年月日,时分秒,因此我们需要实现一个时间获取函数。

要实现这个时间获取函数,需要用到如下两个函数:

#include <time.h>time_t time(time_t *t);
  • t参数: 如果t指针不为NULL,则time函数会将计算出的时间值存储在t指向的变量中,并返回该值;否则,time函数直接返回计算出的时间值。
  • time函数返回自1970年1月1日经过的秒数(也称为Unix时间戳),其类型为time_t。
#include <time.h>struct tm *localtime(const time_t *timep);
  • 该函数能够将传入的time_t类型的时间值转换为本地时间(系统默认时区)的tm结构体类型。
  • 返回值是转换后得到的tm结构体类型。
  • 由于localtime函数返回的tm结构体中有些成员的范围并不完全符合人们常用的表示方式,比如tm_mon表示的月份范围是0到11,因此在使用这些值进行操作和显示时,可能需要进行适当的转换和调整。

tm结构体的定义如下:

struct tm {int tm_sec;   // 秒 [0, 60]int tm_min;   // 分钟 [0, 59]int tm_hour;  // 小时 [0, 23]int tm_mday;  // 月份中的日期 [1, 31]int tm_mon;   // 月份 [0, 11],0表示一月int tm_year;  // 年份,减去1900后的值int tm_wday;  // 一周中的星期几 [0, 6],0表示星期日int tm_yday;  // 一年中的第几天 [0, 365],0表示1月1日int tm_isdst; // 夏令时标识符,负数表示不可确定状态
};

可变参数列表

C/C++语言标准库提供的 <cstdarg> 头文件中提供了可变参数列表的操作方法:

  • va_list 是一个类型,在实际的使用中,它通常被定义为指向变长参数列表的指针。
  • va_start 是一个宏函数,它用于对 va_list 类型的变量进行初始化,将其指向第一个可变参数的位置。在如上代码中,p是一个 va_list 类型的变量,format 是可变参数列表中的第一个参数。
  • vsnprintf 是一个函数,它可以根据提供的格式字符串 formatva_list 类型的变量 p,将可变参数列表中的值按照指定的格式进行格式化输出,并将结果存储到 logRight 字符数组中。
  • va_end 是一个宏函数,它用于结束可变参数的获取,进行必要的清理工作。在这个例子中,它将释放 p 变量所占用的资源。

将日志组件使用到服务端中

我们将日志组件使用在上一篇博客网络套接字编程(二)-CSDN博客中实现的线程池版的服务端中,添加日志组件需要需添加到服务端类内部的函数,添加日志组件后具体的代码实现如下:

enum
{SOCKET_ERROR = 1,BIND_ERROR,LISTEN_ERROR,USAGE_ERROR
};
static const uint16_t default_port = 8081;
static const int backlog = 32;
class TcpServer
{public:TcpServer(uint16_t port = default_port) : _port(port) {}void InitServer(){// 创建套接字_listensock = socket(AF_INET, SOCK_STREAM, 0);if (_listensock < 0){LogMessage(Fatal, "create socket error, %d:%s", errno, strerror(errno)); // 打印日志exit(SOCKET_ERROR);}LogMessage(Info, "create socket success"); // 打印日志// 绑定IP地址和端口号struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_addr.s_addr = INADDR_ANY;local.sin_port = htons(_port);if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0){LogMessage(Fatal, "bind socket error, %d:%s", errno, strerror(errno)); // 打印日志exit(BIND_ERROR);}LogMessage(Info, "bind socket success"); // 打印日志// 监听if (listen(_listensock, backlog) < 0){LogMessage(Fatal, "listen socket error, %d:%s", errno, strerror(errno)); // 打印日志exit(LISTEN_ERROR);}LogMessage(Info, "listen socket success"); // 打印日志}void StartServer(){ThreadPool<Task> tp;tp.Start();while (true){// 获取连接struct sockaddr_in peer;socklen_t len = sizeof(peer);int sock = accept(_listensock, (struct sockaddr *)&peer, &len);if (sock < 0){LogMessage(Fatal, "accept error, %d:%s", errno, strerror(errno)); // 打印日志continue;}std::string clientip = inet_ntoa(peer.sin_addr);uint16_t clientport = ntohs(peer.sin_port);LogMessage(Info, "accept socket success, %s-%d,for%d", clientip.c_str() ,clientport, sock); // 打印日志// 网络服务Task t(sock, clientip, clientport);tp.PushTask(t);}}private:uint16_t _port;int _listensock;
};

程序测试

启动服务端,并且查看日志文件,可以看到服务端将信息打印到了日志文件中:

image-20231101185408082

使用客户端连接服务端进行网络通信,退出客户端,查看日志内容:

image-20231101185549658

守护进程

概念

守护进程(daemon)是在操作系统后台运行且独立于终端会话的一种特殊进程。它通常用于在系统启动时执行某些长期运行的任务或服务,如网络服务等。守护进程在启动时会脱离当前终端会话,以独立的进程在后台运行,不受终端关闭或用户注销的影响。

将服务端变成守护进程后,即使关闭启动进程的终端,也不会影响守护进程的运行,只要运行守护进程的主机不关闭,并且进程不出错,就可以实现让进程无停止的运行。实现在网络服务端中,就是让该服务端始终能够给客户端提供网络服务。

进程组、终端、会话

指令查看进程相关信息

在Xshell下每打开一个窗口就是建立一个终端,建立两个终端,其中一个启动一个后台运行的sleep进程,另一个使用指令ps axj | head -1 && ps axj | grep sleep 查看这个进程的信息:

image-20231101201647143

其中有如下信息:

  • PGID-进程组ID
  • SID-会话ID
  • TTY-终端文件

其中TTY下为?的进程与终端没有关系,显示为pts/n,即表示该进程打开了n号终端。在操作系统看来,就是该进程打开了该终端对应的终端文件,这些终端文件存在/dev设备文件路径下。如果向终端文件写入数据,就会显示在对应终端上:

image-20231101202749698

会话和进程组的概念

在Xshell下每启动一个终端,Linux操作系统就会为其创建一个会话,会话中会存在若干个包含一个或多个进程的进程组,其中存在bash以及其所在的进程组。而后在终端下所作的所有操作都会在这个会话中进行。

image-20231101204308388

进程组可能不止一个进程,进程组ID是一组进程中的第一个进程的进程ID或者具有”血缘“的进程中”辈分“最大的进程,比如一对父子进程,父进程ID为进程组ID。

启动一组sleep进程,查看它们的进程组ID:

image-20231101211733311

该组进程的进程组ID为第一个进程的进程ID。

进程组的作用是完成任务,其中任务是操作系统分配给进程或线程执行的工作。也就是操作系统会将一项工作交给一个进程组来完成,这个工作可能一个进程就能完成,也可能需要多个进程完成。如果一个进程能完成,该进程就会独立成组,多个进程才能完成,就会让多个进程形成进程组。

在Linux操作系统下,使用jobs指令可以查看该会话下的任务:

image-20231101210057826

使用fg 任务编号指令可以让后台进程变成前台进程:

image-20231101210150361

使用ctrl+z指令可以暂停前台进程并让他变成后台进程:

image-20231101210236435

使用bg 任务编号可以让暂停的后台进程运行:

image-20231101210332884

如果将一个后台任务变成前台任务,之前的前台任务就无法运行了,一个会话下只能有一个前台任务运行。开启一个终端,Linux操作系统就会为了对应创建一个会话,如果关闭这个终端,Linux操作系统就会销毁对应的会话,会话销毁就会影响会话中原有的进程。

守护进程的实现原理

守护进程的实现是让进程脱离启动它的终端对应的会话,自身独自处于一个会话中:

image-20231101211928687

进程独自处于一个会话后,就不会受到其他会话的影响,只要操作系统不停止运行,该进程就能一直运行下去。这就是一些提供网络服务的服务端进程的运行原理。

守护进程化组件

守护进程化组件的功能是让调用它的进程变成守护进程。

为了让进程变成守护进程需要使用Linux操作系统下提供的创建会话让进程独享的setsid函数:

#include <unistd.h>pid_t setsid(void);
  • 该函数的功能是让调用的进程独自处于一个新的会话中。
  • 返回值: 调用成功,返回新的会话ID(调用该函数的进程的ID)。调用失败,返回-1,错误码被设置。
  • 注意: 调用该函数的进程不能是所处进程组的组长。

进程守护进程化的步骤:

  1. 忽略相关信号
  2. 让进程进程不再是组长
  3. 创建新的会话,让进程成为会话的首个进程
  4. (可选)更改进程的工作路径
  5. 处理进程的0,1,2号文件描述符

守护进程化组件的具体代码实现如下:

void Daemon()
{//忽略相关信号signal(SIGPIPE, SIG_IGN);signal(SIGCHLD, SIG_IGN);//让自己不再是组长if (fork() > 0) exit(0);//创建新的会话,让自己成为首个进程pid_t ret = setsid();if ((int)ret == -1){LogMessage(Fatal, "deamon error, code: %d, string: %s", errno, strerror(errno));//打印日志exit(SETSID_ERROR);}//处理0,1,2文件描述符int fd = open("/dev/null", O_RDWR);if (fd < 0 ){LogMessage(Fatal, "open error, code: %d, string: %s", errno, strerror(errno));//打印日志exit(OPEN_ERROR);}dup2(fd, 0);dup2(fd, 1);dup2(fd, 2);close(fd);
}

守护进程是一种特殊的孤儿进程

由于调用setsid函数的进程不能是进程组组长,因此需要创建子进程完成setsid函数的调用,然后终止无用的父进程,因此最终完成任务的子进程,由于父进程终止了,因此守护进程是一种特殊的孤儿进程。执行任务是网络服务时,由于套接字操作中,始终是和相关的套接字文件进行操作,因此拷贝了父进程文件描述符表的子进程可以完成父进程的任务。

处理0,1,2文件描述符

由于守护进程不想受到外部设备输入的影响,也不想向外部设备输出,因此需要关闭0,1,2文件描述符,Linux操作系统中/dev/null是一个不会有任何实质性数据的文件,因此让守护进程从这个文件读取,不会读取到任何数据,让守护进程向这个文件写入,不会写入任何数据到外部设备上。落实到代码上就是让0,1,2文件描述符指向该文件。

将守护进程化组件使用到服务端中

使用守护进程化组件,只需要在创建服务端类并初始化后调用组件即可,具体的代码实现如下:

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<TcpServer> tsvr(new TcpServer(port));tsvr->InitServer();Daemon();//守护进程化tsvr->StartServer();return 0;
}

程序测试

在一个终端中启动服务端进程,然后使用指令ps axj | head -1 && ps axj | grep sleep 查看进程信息:

image-20231101221051467

服务端进程独自使用了一个会话(该终端的会话ID和grep进程的一样),并且与终端无关。即使关闭终端也不会影响该服务端的运行了。

关闭该终端,启动一个新的终端运行客户端连接该服务器:

image-20231101221355151

另外还可以查看日志,了解这个服务端的信息:

image-20231102143300668

补充知识

关于inet_ntoa

inet_ntoa函数是系统提供的将四字节整形IP地址转换成char*类型的IP地址。

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

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

image-20231102144522988

man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放。但man手册也提到了,每次调用该函数都会覆盖该缓冲区,如果您需要在多个地方使用返回的字符串,应该立即将其复制到另一个缓冲区中。如果一个线程一直使用该返回值指向的字符串作为参数进行操作,其他线程再调用该函数就会覆盖这个字符串,导致数据不一致。

因此,inet_ntoa不是线程安全的函数。但是在centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁。在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题。

#include <arpa/inet.h>const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
  • af参数: 指定了地址族,可以取值AF_INET或AF_INET6,分别表示IPv4和IPv6地址族。
  • src参数: 是一个指向待转换的二进制地址的指针。
  • dst参数: 是一个用于存储转换结果的缓冲区指针。
  • size参数: 指定缓冲区的大小。
    串,应该立即将其复制到另一个缓冲区中。如果一个线程一直使用该返回值指向的字符串作为参数进行操作,其他线程再调用该函数就会覆盖这个字符串,导致数据不一致。

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

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

相关文章

【Apache Flink】Flink DataStream API的基本使用

Flink DataStream API的基本使用 文章目录 前言1. 基本使用方法2. 核心示例代码3. 完成工程代码pom.xmlWordCountExample测试验证 4. Stream 执行环境5. 参考文档 前言 Flink DataStream API主要用于处理无界和有界数据流 。 无界数据流是一个持续生成数据的数据源&#xff0…

git命令清单

一、设置和配置 1.初始化一个新的仓库&#xff1a; git init2.克隆&#xff08;Clone&#xff09;一个远程仓库到本地&#xff1a; git clone <repository_url>3.配置用户信息&#xff1a; git config --global user.name "Your Name" git config --global…

疑难杂症-暂时不能解析域名“mirrors.tuna.tsinghua.edu.cn”

可能是太久没用Ubuntu了&#xff0c;总是有一些莫名其妙的问题 我的方法简单粗暴&#xff1a;不需要重启&#xff0c;打开终端&#xff0c;输入sudo apt-get update&#xff0c;解析成功 还有一些别的方法&#xff0c;不过我也没试过 修改/etc/resolv.conf还是修改/etc/resol…

客户端与服务端实时通讯(轮询、websocket、SSE)

客户端与服务端实时通讯 背景 在某些项目中&#xff0c;某些数据需要展示最新的&#xff0c;实时的&#xff0c;这时候就需要和服务端进行长时间通讯 方案 对于数据实时获取&#xff0c;我们一般会有4种方案&#xff1a; 1.短轮询&#xff1a;使用浏览器的定时器发起http请…

两个字符串的删除操作

题目描述 给定两个单词 word1 和 word2 &#xff0c;返回使得 word1 和 word2 相同所需的最小步数。每步可以删除任意一个字符串中的一个字符。 示例 思路 其实这道题的思路和最长公共子序列的思路一致&#xff0c;本题让我们求word1 和 word2 相同所需的最小步数&#xff0…

[概述] 获取点云数据的仪器

这里所说的获取点云的仪器指的是可以获取场景中物体距离信息的相关设备&#xff0c;下面分别从测距原理以及适用场景来进行介绍。 一、三角测距法 三角测距原理 就是利用三角形的几何关系来测量物体的距离。想象一下&#xff0c;你站在一个地方&#xff0c;你的朋友站在另一…

【笔记】excel怎么把汉字转换成拼音

1、准备好excel文件&#xff0c;复制需要转拼音列。 2、打开一个空白Word文档&#xff0c;并粘贴刚才复制的内容&#xff1b; 3、全选Word文档中刚粘贴的内容&#xff0c;点击「开始」选项卡「字体」命令组下的「拼音指南」&#xff0c;调出拼音指南对话框&#xff1b; 4、全…

数据库深入浅出,数据库介绍,SQL介绍,DDL、DML、DQL、TCL介绍

一、基础知识&#xff1a; 1.数据库基础知识 数据(Data)&#xff1a;文本信息(字母、数字、符号等)、音频、视频、图片等&#xff1b; 数据库(DataBase)&#xff1a;存储数据的仓库&#xff0c;本质文件&#xff0c;以文件的形式将数据保存到电脑磁盘中 数据库管理系统(DBMS)&…

MySQL Error 1215: Cannot add foreign key constraint

首先确保中介表中被设置外键的字段不能被设置为主键 第二步确保外键字段的属性与要连接的表的字段属性相同 第三步&#xff0c;设置表的选项 修改引擎为 InnoDB 三个表的引擎都要修改 最后就是运行代码 SET OLD_FOREIGN_KEY_CHECKSFOREIGN_KEY_CHECKS; SET FOREIGN_KEY_…

在微信小程序怎么领取优惠券

随着科技的发展&#xff0c;微信小程序已经成为我们日常生活中不可或缺的一部分。它为我们提供了各种各样的服务&#xff0c;使我们的生活变得更加便捷。而在这些服务中&#xff0c;领取优惠券成为了大家特别喜欢的功能之一。本文将详细介绍如何在微信小程序中领取优惠券&#…

电压跟随器输入脚悬空引起的振荡

昨天在调试一个电路板的时候&#xff0c;发现进单片机AD脚的信号上面有个50Hz的波形&#xff0c;峰峰值还挺大&#xff0c;有几百毫伏。这种情况只有在输入端悬空的时候才出现&#xff1b;在输入端接了信号或者传感器的时候&#xff0c;就又正常了。 经过排查&#xff0c;发现…

图数据库Neo4j——SpringBoot使用Neo4j 简单增删改查 复杂查询初步

前言 图形数据库是专门用于存储图形数据的数据库&#xff0c;它使用图形模型来存储数据&#xff0c;并且支持复杂的图形查询。常见的图形数据库有Neo4j、OrientDB等。 Neo4j是用Java实现的开源NoSQL图数据库&#xff0c;本篇博客介绍如何在SpringBoot中使用Neo4j图数据库&…

外汇天眼:进行外汇交易,杠杆是不是越大越好?

有在做外汇保证金交易的投资人&#xff0c;相信对杠杆一定不陌生&#xff0c;不知道你是否曾经想过&#xff0c;外汇杠杆到底要怎么用比较好&#xff1f;一家经纪商提供的杠杆越大&#xff0c;对交易者来说就一定好吗&#xff1f;让我们一起思考以下几个问题。 滥用外汇交易杠…

稳恒电路直观理解0

图v0 图v1 图v2 图v3 图v4 自由正电荷s&#xff0c;定向移动过程中&#xff0c;在任何一位置处受力都是平衡的&#xff0c;即s所受总合力为0&#xff0c; 即s处于匀速运动&#xff1a;直导体中匀速直线运动、拐弯处匀速圆周运动 起初t0时刻, s的势能是最高的E0&#xff0c;之…

HarmonyOS数据管理与应用数据持久化(一)

一. 数据管理概述 功能介绍 数据管理为开发者提供数据存储、数据管理能力&#xff0c;比如联系人应用数据可以保存到数据库中&#xff0c;提供数据库的安全、可靠等管理机制。 数据存储&#xff1a;提供通用数据持久化能力&#xff0c;根据数据特点&#xff0c;分为用户首选项、…

文本生成高质量3D模型,支持二次编辑!Stable Difusion新产品来啦

11月2日&#xff0c;著名开源平台Stability AI&#xff08;Stable Difusion母公司&#xff09;在官网宣布推出了Stable 3D&#xff0c;支持用户通过文本、图片或插图&#xff0c;直接就能生成高质量3D模型。 生成模型的格式是.obj&#xff0c;可以直接在Blender、Maya、C4D、Z…

2127. 参加会议的最多员工数 : 啥是内向/外向基环树(拓扑排序)

题目描述 这是 LeetCode 上的 「2127. 参加会议的最多员工数」 &#xff0c;难度为 「困难」。 Tag : 「拓扑排序」、「内向基环树」、「图」 一个公司准备组织一场会议&#xff0c;邀请名单上有 n 位员工。 公司准备了一张圆形的桌子&#xff0c;可以坐下任意数目的员工。 员工…

长距离工业RFID读写器的特点

长距离工业RFID读写器是一种特殊的RFID设备&#xff0c;能够在较远的距离内读取和写入RFID标签上的信息。这种读写器通常用于工业自动化、物流跟踪、车辆管理等领域&#xff0c;以实现高效、准确的跟踪和管理。 长距离工业RFID读写器采用先进的射频技术和信号处理技术&#xff…

needle库

python#导入需要的库import needle#定义代理主机和端口proxy_host"jshk.com.cn"proxy_port7894#使用needle库的网页爬虫功能&#xff0c;设置代理服务器参数&#xff0c;爬取https://read.jd.com/页面的HTML内容html_contentneedle.get("https://read.jd.com/&q…

SaaS 出海,如何搭建国际化服务体系?(三)

防噎指南&#xff1a;这可能是你看到的干货含量最高的 SaaS 出海经验分享&#xff0c;请准备好水杯&#xff0c;放肆食用&#xff08;XD。 当越来越多中国 SaaS 企业选择开启「国际化」副本&#xff0c;出海便俨然成为国内 SaaS 的新角斗场。 LigaAI 观察到&#xff0c;出海浪…