【探索Linux】—— 强大的命令行工具 P.29(网络编程套接字 —— 简单的TCP网络程序模拟实现)

在这里插入图片描述

阅读导航

  • 引言
  • 一、TCP协议
  • 二、TCP网络程序模拟实现
    • 1. 预备代码
      • ⭕ThreadPool.hpp(线程池)
      • ⭕makefile文件
      • ⭕打印日志文件
      • ⭕将当前进程转变为守护进程
    • 2. TCP 服务器端实现(TcpServer.hpp)
    • 3. TCP 客户端实现(main函数)
  • 温馨提示

引言

在前一篇文章中,我们详细介绍了UDP协议和TCP协议的特点以及它们之间的异同点。本文将延续上文内容,重点讨论简单的TCP网络程序模拟实现。通过本文的学习,读者将能够深入了解TCP协议的实际应用,并掌握如何编写简单的TCP网络程序。让我们一起深入探讨TCP网络程序的实现细节,为网络编程的学习之旅添上一份精彩的实践经验。

一、TCP协议

TCP(Transmission Control Protocol)是一种面向连接的通信协议,它要求在数据传输前先建立连接,以确保数据的可靠传输。TCP通过序号、确认和重传等机制来保证数据的完整性和可靠性,同时还实现了拥塞控制和流量控制,以适应不同网络环境下的数据传输需求。由于TCP的可靠性和稳定性,它被广泛应用于网络通信中,包括网页浏览、文件传输、电子邮件等各种应用场景,成为互联网协议套件中的重要组成部分。详介绍可以看上一篇文章:UDP协议介绍 | TCP协议介绍 | UDP 和 TCP 的异同

二、TCP网络程序模拟实现

接下来,我们打算运用线程池技术,模拟实现一个简单的TCP网络程序。通过充分利用线程池,我们能够更有效地管理并发连接,从而提高程序的性能和稳定性。这一实践将有助于加深我们对网络编程关键概念和技术的理解和掌握。在前文中已经提到了线程池,这里就不再赘述其原理和作用。详细可以点击传送门:🚩 线程池

1. 预备代码

⭕ThreadPool.hpp(线程池)

#pragma once#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <pthread.h>
#include <unistd.h>// 线程信息结构体
struct ThreadInfo
{pthread_t tid;  // 线程IDstd::string name;  // 线程名称
};static const int defalutnum = 10;  // 默认线程池大小为10template <class T>
class ThreadPool
{
public:void Lock() // 加锁{pthread_mutex_lock(&mutex_);}void Unlock() // 解锁{pthread_mutex_unlock(&mutex_);}void Wakeup() // 唤醒等待中的线程{pthread_cond_signal(&cond_);}void ThreadSleep() // 线程休眠{pthread_cond_wait(&cond_, &mutex_);}bool IsQueueEmpty() // 判断任务队列是否为空{return tasks_.empty();}std::string GetThreadName(pthread_t tid) // 获取线程名称{for (const auto &ti : threads_){if (ti.tid == tid)return ti.name;}return "None";}public:static void *HandlerTask(void *args) // 线程任务处理函数{ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);std::string name = tp->GetThreadName(pthread_self());while (true){tp->Lock();while (tp->IsQueueEmpty()) // 若任务队列为空,则线程等待{tp->ThreadSleep();}T t = tp->Pop(); // 从任务队列中取出任务tp->Unlock();t(); // 执行任务}}void Start() // 启动线程池{int num = threads_.size();for (int i = 0; i < num; i++){threads_[i].name = "thread-" + std::to_string(i + 1);pthread_create(&(threads_[i].tid), nullptr, HandlerTask, this); // 创建线程}}T Pop() // 从任务队列中取出任务{T t = tasks_.front();tasks_.pop();return t;}void Push(const T &t) // 将任务推入任务队列{Lock();tasks_.push(t);Wakeup();Unlock();}static ThreadPool<T> *GetInstance() // 获取线程池实例{if (nullptr == tp_) // 若线程池实例为空{pthread_mutex_lock(&lock_);if (nullptr == tp_) // 双重检查锁{std::cout << "log: singleton create done first!" << std::endl;tp_ = new ThreadPool<T>(); // 创建线程池实例}pthread_mutex_unlock(&lock_);}return tp_;}private:ThreadPool(int num = defalutnum) : threads_(num) // 构造函数{pthread_mutex_init(&mutex_, nullptr);pthread_cond_init(&cond_, nullptr);}~ThreadPool() // 析构函数{pthread_mutex_destroy(&mutex_);pthread_cond_destroy(&cond_);}ThreadPool(const ThreadPool<T> &) = delete; // 禁用拷贝构造函数const ThreadPool<T> &operator=(const ThreadPool<T> &) = delete; // 禁用赋值操作符,避免 a=b=c 的写法
private:std::vector<ThreadInfo> threads_; // 线程信息数组std::queue<T> tasks_; // 任务队列pthread_mutex_t mutex_; // 互斥锁pthread_cond_t cond_; // 条件变量static ThreadPool<T> *tp_; // 线程池实例指针static pthread_mutex_t lock_; // 静态互斥锁
};template <class T>
ThreadPool<T> *ThreadPool<T>::tp_ = nullptr; // 初始化线程池实例指针为nullptrtemplate <class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER; // 初始化静态互斥锁

以上代码实现了一个简单的线程池模板类 ThreadPool,其中包含了线程池的基本功能和操作。

  1. 首先定义了一个线程信息结构体 ThreadInfo,用来保存线程的ID和名称。

  2. 然后定义了一个模板类 ThreadPool,其中包含了线程池的各种操作和属性:

    • Lock()Unlock() 分别用于加锁和解锁。
    • Wakeup() 用于唤醒等待中的线程。
    • ThreadSleep() 用于使线程进入休眠状态。
    • IsQueueEmpty() 判断任务队列是否为空。
    • GetThreadName() 根据线程ID获取线程名称。
  3. 定义了静态成员函数 HandlerTask,作为线程的任务处理函数。在该函数中,线程会不断地从任务队列中取出任务并执行。

  4. Start() 函数用于启动线程池,创建指定数量的线程,并将线程的任务处理函数设置为 HandlerTask

  5. Pop() 函数用于从任务队列中取出任务。

  6. Push() 函数用于将任务推入任务队列。

  7. GetInstance() 函数用于获取线程池的实例,采用了双重检查锁(Double-Checked Locking)实现单例模式。

  8. 线程池的构造函数和析构函数分别用于初始化和销毁互斥锁和条件变量。

  9. 最后使用静态成员变量初始化了线程池实例指针和静态互斥锁。

⭕makefile文件

.PHONY:all
all:tcpserverd tcpclienttcpserverd:Main.ccg++ -o $@ $^ -std=c++11 -lpthread
tcpclient:TcpClient.ccg++ -o $@ $^ -std=c++11.PHONY:clean
clean:rm -f tcpserverd tcpclient

这段代码是一个简单的 Makefile 文件,用于编译生成两个可执行文件 tcpserverdtcpclient

  1. .PHONY: all:声明 all 为一个伪目标,表示 all 不是一个实际的文件名,而是一个指定的操作。

  2. all: tcpserverd tcpclient:定义了 all 目标,它依赖于 tcpserverdtcpclient 目标。当执行 make all 时,会先编译 tcpserverdtcpclient

  3. tcpserverd: Main.cc:定义了生成 tcpserverd 可执行文件的规则,依赖于 Main.cc 源文件。使用 g++ 编译器进行编译,指定输出文件名为 tcpserverd,使用 C++11 标准,并链接 pthread 库。

  4. tcpclient: TcpClient.cc:定义了生成 tcpclient 可执行文件的规则,依赖于 TcpClient.cc 源文件。同样使用 g++ 编译器进行编译,指定输出文件名为 tcpclient,使用 C++11 标准。

  5. .PHONY: clean:声明 clean 为一个伪目标。

  6. clean: rm -f tcpserverd tcpclient:定义了 clean 目标,用于清理生成的可执行文件。执行 make clean 时将删除 tcpserverdtcpclient 可执行文件。

⭕打印日志文件

#pragma once#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>#define SIZE 1024#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4#define Screen 1
#define Onefile 2
#define Classfile 3#define LogFile "log.txt"class Log
{
public:Log(){printMethod = Screen; // 默认打印方式为屏幕输出path = "./log/"; // 默认日志文件路径为当前目录下的"log/"目录}void Enable(int method){printMethod = method; // 设置打印方式}std::string levelToString(int level){switch (level){case Info:return "Info";case Debug:return "Debug";case Warning:return "Warning";case Error:return "Error";case Fatal:return "Fatal";default:return "None";}}void printLog(int level, const std::string &logtxt){switch (printMethod){case Screen:std::cout << logtxt << std::endl; // 在屏幕上输出日志信息break;case Onefile:printOneFile(LogFile, logtxt); // 将日志信息写入单个文件中break;case Classfile:printClassFile(level, logtxt); // 根据日志级别将日志信息写入不同的文件中break;default:break;}}void printOneFile(const std::string &logname, const std::string &logtxt){std::string _logname = path + logname; // 拼接日志文件路径int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // 打开或创建一个文件,以追加方式写入if (fd < 0)return;write(fd, logtxt.c_str(), logtxt.size()); // 将日志信息写入文件close(fd); // 关闭文件}void printClassFile(int level, const std::string &logtxt){std::string filename = LogFile;filename += ".";filename += levelToString(level); // 生成日志文件名,例如"log.txt.Debug/Warning/Fatal"printOneFile(filename, logtxt); // 将日志信息写入对应级别的文件中}~Log(){}// 重载()运算符,用于输出日志信息void operator()(int level, const char *format, ...){time_t t = time(nullptr);struct tm *ctime = localtime(&t); // 获取当前时间char leftbuffer[SIZE];snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,ctime->tm_hour, ctime->tm_min, ctime->tm_sec); // 格式化左侧部分,包括日志级别和时间信息va_list s;va_start(s, format);char rightbuffer[SIZE];vsnprintf(rightbuffer, sizeof(rightbuffer), format, s); // 格式化右侧部分,即用户自定义的日志内容va_end(s);// 格式:默认部分+自定义部分char logtxt[SIZE * 2];snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer); // 拼接左右两侧的日志内容printLog(level, logtxt); // 打印日志信息}private:int printMethod; // 打印方式std::string path; // 日志文件路径
};

这段代码是一个简单的日志记录类 Log,它提供了几种不同的日志输出方式和日志级别。

  • #pragma once: 使用编译器指令,确保头文件只被编译一次。

  • 定义了一些常量:

    • SIZE: 缓冲区大小为 1024。
    • 日志级别常量:Info, Debug, Warning, Error, Fatal
    • 打印方式常量:Screen, Onefile, Classfile
    • 日志文件名常量:LogFile
  • Log 类包含以下成员函数和变量:

    • printMethod: 记录当前的打印方式,默认为屏幕输出。
    • path: 日志文件路径,默认为"./log/"。
  • 构造函数 Log() 初始化 printMethodpath

  • Enable(int method): 设置日志的打印方式。

  • levelToString(int level): 将日志级别转换为对应的字符串。

  • printLog(int level, const std::string &logtxt): 根据打印方式输出日志信息。

  • printOneFile(const std::string &logname, const std::string &logtxt): 将日志信息写入单个文件中。

  • printClassFile(int level, const std::string &logtxt): 根据日志级别将日志信息写入不同的文件中。

  • 析构函数 ~Log()

  • 重载的函数调用运算符 operator(): 接受日志级别和格式化字符串,格式化输出日志信息到不同的输出位置。

⭕将当前进程转变为守护进程

#pragma once#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>const std::string nullfile = "/dev/null"; // 定义空设备文件路径// 将当前进程变为守护进程的函数
void Daemon(const std::string &cwd = "")
{// 1. 忽略一些异常信号,以避免对守护进程造成影响signal(SIGCLD, SIG_IGN); // 忽略子进程结束信号signal(SIGPIPE, SIG_IGN); // 忽略管道破裂信号signal(SIGSTOP, SIG_IGN); // 忽略终止信号// 2. 创建一个子进程并使父进程退出,确保守护进程不是进程组组长,创建一个新的会话if (fork() > 0)exit(0); // 父进程退出setsid(); // 创建新的会话,并成为该会话的首进程// 3. 更改当前调用进程的工作目录,如果指定了工作目录则切换到相应目录if (!cwd.empty())chdir(cwd.c_str()); // 切换工作目录到指定路径// 4. 将标准输入,标准输出,标准错误重定向至/dev/null,关闭不需要的文件描述符int fd = open(nullfile.c_str(), O_RDWR); // 打开空设备文件if (fd > 0){dup2(fd, 0); // 标准输入重定向至空设备dup2(fd, 1); // 标准输出重定向至空设备dup2(fd, 2); // 标准错误重定向至空设备close(fd); // 关闭打开的文件描述符}
}

这段代码实现了将当前进程转变为守护进程的函数 Daemon

  1. 忽略一些异常信号,避免对守护进程产生影响。
  2. 创建一个子进程并使父进程退出,确保守护进程不是进程组组长,创建一个新的会话。
  3. 更改当前调用进程的工作目录,如果指定了工作目录,则切换到相应目录。
  4. 将标准输入、标准输出和标准错误重定向至 /dev/null,即空设备文件,关闭不需要的文件描述符,确保守护进程不产生输出和错误信息。

2. TCP 服务器端实现(TcpServer.hpp)

#pragma once#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <signal.h>
#include <signal.h>
#include "Log.hpp"
#include "ThreadPool.hpp"
#include "Task.hpp"
#include "Daemon.hpp"const int defaultfd = -1;
const std::string defaultip = "0.0.0.0";
const int backlog = 10; // 最大连接请求队列长度extern Log lg;enum
{UsageError = 1,SocketError,BindError,ListenError,
};class TcpServer;// 线程数据结构,用于传递给线程处理函数
class ThreadData
{
public:ThreadData(int fd, const std::string &ip, const uint16_t &p, TcpServer *t): sockfd(fd), clientip(ip), clientport(p), tsvr(t){}
public:int sockfd;std::string clientip;uint16_t clientport;TcpServer *tsvr;
};// TCP服务器类
class TcpServer
{
public:// 构造函数,初始化端口和IP地址TcpServer(const uint16_t &port, const std::string &ip = defaultip) : listensock_(defaultfd), port_(port), ip_(ip){}// 初始化服务器void InitServer(){// 创建套接字listensock_ = socket(AF_INET, SOCK_STREAM, 0);if (listensock_ < 0){lg(Fatal, "create socket, errno: %d, errstring: %s", errno, strerror(errno));exit(SocketError);}lg(Info, "create socket success, listensock_: %d", listensock_);// 设置套接字选项,允许地址重用int opt = 1;setsockopt(listensock_, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt));// 绑定本地地址struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port_);inet_aton(ip_.c_str(), &(local.sin_addr));if (bind(listensock_, (struct sockaddr *)&local, sizeof(local)) < 0){lg(Fatal, "bind error, errno: %d, errstring: %s", errno, strerror(errno));exit(BindError);}lg(Info, "bind socket success, listensock_: %d");// 监听套接字,开始接受连接请求if (listen(listensock_, backlog) < 0){lg(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));exit(ListenError);}lg(Info, "listen socket success, listensock_: %d");}// 启动服务器void Start(){// 将当前进程变为守护进程Daemon();// 启动线程池ThreadPool<Task>::GetInstance()->Start();lg(Info, "tcpServer is running....");// 循环接受客户端连接并处理while(true){// 获取新连接struct sockaddr_in client;socklen_t len = sizeof(client);int sockfd = accept(listensock_, (struct sockaddr *)&client, &len);if (sockfd < 0){lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); continue;}// 获取客户端IP和端口uint16_t clientport = ntohs(client.sin_port);char clientip[32];inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));// 打印客户端连接信息lg(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d", sockfd, clientip, clientport);// 创建任务对象并加入线程池处理Task t(sockfd, clientip, clientport);ThreadPool<Task>::GetInstance()->Push(t);}}// 析构函数~TcpServer() {}private:int listensock_;  // 监听套接字uint16_t port_;   // 端口号std::string ip_;  // IP地址
};

这段代码是一个简单的TCP服务器的实现,包括了创建套接字、绑定地址、监听连接、接受客户端连接等基本操作。

3. TCP 客户端实现(main函数)

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>void Usage(const std::string &proc)
{std::cout << "\n\rUsage: " << proc << " serverip serverport\n"<< std::endl;
}// ./tcpclient serverip serverport
int main(int argc, char *argv[])
{// 检查命令行参数是否正确if (argc != 3){Usage(argv[0]);exit(1);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);// 设置服务器地址信息struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));while (true){int cnt = 5; // 连接重试次数int isreconnect = false; // 是否需要重连int sockfd = 0;// 创建套接字sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){std::cerr << "socket error" << std::endl;return 1;}do{// 尝试连接服务器int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));if (n < 0){isreconnect = true;cnt--;std::cerr << "connect error..., reconnect: " << cnt << std::endl;sleep(2); // 等待一段时间后重连}else{break;}} while (cnt && isreconnect);if (cnt == 0){std::cerr << "user offline..." << std::endl;break;}// 与服务器建立连接后进行通信while (true){std::string message;std::cout << "Please Enter# ";std::getline(std::cin, message);// 向服务器发送消息int n = write(sockfd, message.c_str(), message.size());if (n < 0){std::cerr << "write error..." << std::endl;}// 从服务器接收消息并显示char inbuffer[4096];n = read(sockfd, inbuffer, sizeof(inbuffer));if (n > 0){inbuffer[n] = 0;std::cout << inbuffer << std::endl;}}// 关闭套接字close(sockfd);}return 0;
}

温馨提示

感谢您对博主文章的关注与支持!如果您喜欢这篇文章,可以点赞、评论和分享给您的同学,这将对我提供巨大的鼓励和支持。另外,我计划在未来的更新中持续探讨与本文相关的内容。我会为您带来更多关于Linux以及C++编程技术问题的深入解析、应用案例和趣味玩法等。如果感兴趣的话可以关注博主的更新,不要错过任何精彩内容!

再次感谢您的支持和关注。我们期待与您建立更紧密的互动,共同探索Linux、C++、算法和编程的奥秘。祝您生活愉快,排便顺畅!
在这里插入图片描述

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

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

相关文章

babyos 学习记录

宏定义头文件 将一个宏定义取不同的数据到不同的数组中&#xff1b; 侵入式链表 struct list_head { struct list_head *next, *prev; }; // 添加&#xff08;list_add_tail/list_add&#xff09;、删除、查找 xx.h // 定义一个用于链表管理的结构体 typedef sturct{ xxx …

matlab矩形薄板小挠度弯曲有限元编程 |【Matlab源码+理论文本】|板单元| Kirchoff薄板 | 板壳单元

专栏导读 作者简介&#xff1a;工学博士&#xff0c;高级工程师&#xff0c;专注于工业软件算法研究本文已收录于专栏&#xff1a;《有限元编程从入门到精通》本专栏旨在提供 1.以案例的形式讲解各类有限元问题的程序实现&#xff0c;并提供所有案例完整源码&#xff1b;2.单元…

分库分表场景下多维查询解决方案(用户+商户)

在采用分库分表设计时&#xff0c;通过一个PartitionKey根据散列策略将数据分散到不同的库表中&#xff0c;从而有效降低海量数据下C端访问数据库的压力。这种方式可以缓解单一数据库的压力&#xff0c;提升了吞吐量&#xff0c;但同时也带来了新的问题。对于B端商户而言&#…

Layui实现删除及修改后停留在当前页

1、功能概述&#xff1f; 我们在使用layui框架的table显示数据的时候&#xff0c;会经常的使用分页技术&#xff0c;这个我们期望能够期望修改数据能停留在当前页&#xff0c;或者删除数据的时候也能够停留在当前页&#xff0c;这样的用户体验会更好一些&#xff0c;但往往事与…

硬核分享|如何将文字转成语音对视频进行配音或旁白解说

硬核分享|如何将文字转成语音对视频进行配音或旁白解说_哔哩哔哩_bilibili 文字转语音工具成为了一种便利而实用的技术应用&#xff0c;它能够将文字内容转化为声音&#xff0c;为我们提供全新的听觉体验。 不论是在阅读、学习、娱乐还是无障碍辅助等方面&#xff0c;文字转语…

【QT入门】 Qt槽函数五种常用写法介绍

声明&#xff1a;该专栏为本人学习Qt知识点时候的笔记汇总&#xff0c;希望能给初学的朋友们一点帮助(加油&#xff01;) 往期回顾&#xff1a; 【QT入门】实现一个简单的图片查看软件-CSDN博客 【QT入门】图片查看软件(优化)-CSDN博客 【QT入门】 lambda表达式(函数)详解-CSDN…

苹果手机更换国内IP地址的方法

在网络世界中&#xff0c;IP地址扮演着极为重要的角色&#xff0c;是互联网通信的基础。很多人在使用苹果手机时&#xff0c;有时候需要更换国内IP地址以获取更多网络资源或保护隐私。那么&#xff0c;是否可以更换国内ip地址&#xff1f;苹果手机更换国内ip地址的方法是怎样的…

Redis学习二--常见问题及处理

基本概念 Redis基本概念数据结构 机制 持久化机制&#xff1a; RDB(内存快照)&#xff1a;某一时刻的内存快照以二进制的方式写入磁盘&#xff0c;可以手动触发和自动触发。 优点&#xff1a;生成文件小&#xff0c;恢复速度快&#xff0c;适用于灾难恢复。 缺点&#xff1a…

Linux docker4--本地jar包生成镜像和docker部署运行

一、通过springboot创建一个java项目&#xff0c;打成出jar包。 二、将jar包生成docker镜像 &#xff08;1&#xff09;、创建Dockerfile文件 创建Dockerfile文件&#xff0c;将如下的代码内容粘贴进去即可。 注意&#xff1a;本例中我打出的jar包是boot.jar。如果你打出的jar…

百度小程序入口在哪里找到怎么打开百度词令关键词口令直达小程序?

百度小程序入口在哪里找到怎么打开百度词令关键词口令直达小程序&#xff1f; 一、百度搜索找到百度词令小程序 打开手机百度搜索「词令」即可找到百度词令关键词口令直达小程序&#xff1b; 二、百度小程序中心找到百度小程序 2.1、打开手机百度&#xff0c;点击底部我的&a…

SQLiteC/C++接口详细介绍sqlite3_stmt类(十三)

返回&#xff1a;SQLite—系列文章目录 上一篇&#xff1a;SQLiteC/C接口详细介绍sqlite3_stmt类&#xff08;十二&#xff09; 下一篇&#xff1a; 待续 51、sqlite3_stmt_scanstatus_reset sqlite3_stmt_scanstatus_reset 函数用于重置指定语句对象最近一次执行的 WHER…

kotlin中使用ViewBinding绑定控件

kotlin中使用ViewBinding绑定控件 什么是ViewBinding&#xff1f; View Binding是Android Studio 3.6推出的新特性&#xff0c;主要用于减少findViewById的冗余代码&#xff0c;但内部实现还是通过使用findViewById。通过ViewBinding&#xff0c;可以更轻松地编写可与视图交互…

String类适合做HashMap的key的原因是什么

在《Java 编程思想》中有这么一句话&#xff1a;设计 hashCode() 时最重要的因素就是对同一个对象调用 hashCode() 都应该产生相同的值。 String 类型的对象对这个条件有着很好的支持&#xff0c;因为 String 对象的 hashCode() 值是根据 String 对象的 内容计算的&#xff…

unity学习(66)——控制器Joystick Pack优化

Joystick Pack这种重力带惯性不利于正常开发。决定进行优化。有一种万事俱备只欠东风的感觉。 源代码如下&#xff1a; 1.在脚本中找到轮盘所输出的方向值 2.把方向的改变值加到鸣人模型身上。 2.1控制器脚本中添加model变量 2.2在unity中赋值 2.3代码中修改位置 using Syst…

Python:使用 jionlp和cpca 实现国内地址文本解析

目录 使用 jionlp解析国内地址文本使用 cpca 实现解析国内地址文本总结 使用 jionlp解析国内地址文本 jionlp: 中文 NLP 预处理、解析工具包&#xff0c;准确、高效、易用 github: https://github.com/dongrixinyu/JioNLP文档&#xff1a;http://www.jionlp.com/ 使用示例 …

vue2源码学习01配置rollup打包环境

1.下载rollup相关依赖 npm i rollup rollup-plugin-babel babel/core babel/preset-env --save-dev 2.新建rollup.config.js配置打包选项 //rollup可以导出一个对象&#xff0c;作为打包的配置文件 import babel from rollup-plugin-babel export default {input: ./src/ind…

搭建Hadoop集群(完全分布式运行模式)

目录 一、准备模板机(最小化安装)二、配置一台纯净的模板机修改主机名固定IP地址通过yum安装方式安装必要的软件关闭防火墙且禁止自启修改hosts映射文件创建普通用户 并让他能用sudo命令在/opt下创建software和module完成 三、搭建完全分布式运行模式3.1克隆第一台机器hadoop10…

新能源汽车充电桩站点烟火AI识别检测算法应用方案

新能源汽车作为现代科技与环保理念的完美结合&#xff0c;其普及和应用本应带给人们更加便捷和绿色的出行体验。然而&#xff0c;近年来新能源汽车充电火灾事故的频发&#xff0c;无疑给这一领域投下了巨大的阴影。这不禁让人深思&#xff0c;为何这一先进的交通工具在充电过程…

【算法】acwing基础课笔记01-快排,归并

第一章&#xff08;基础算法&#xff09;&#xff08;一&#xff09; 以前排序都没怎么自己手写过&#xff0c;这学期终于决定抛弃各种番和游戏好好学一下…记点笔记激励一下自己。 视频知识 00&#xff1a;0000&#xff1a;30快速排序&#xff0c;00&#xff1a;3000&#…

Vue+SpringBoot在线教育考试及管理平台开发(纯原创)后续还在开发,会持续更新

登录页面设计 登录页面设计思路-分为三个角色进行登录&#xff0c;分别为学生&#xff0c;教师&#xff0c;管理员。 前端将登录设计为表单形式&#xff0c;通过选项组件绑定角色参数&#xff0c;向后端传递角色信息&#xff0c;通过表单绑定向后端传递登录者所有信息 <div …