【Linux】TCP套接字编程

目录

前言

UDP服务器的完善

线程的封装

结构定义

接口实现

环形队列

结构定义

接口实现

加锁

信号量的申请与释放

入队与出队

整体组装 

初始化与析构

信息接收线程

消息发送线程

TCP套接字

创建套接字

listen

accept

收发操作

客户端的编写

进一步完善

多进程

多线程

总结


前言

上篇文章中我们介绍了套接字编程,简单构建了一个 UDP 服务器,今天在此基础上添加并行的模块,之后再进行 TCP 套接字的介绍,并同样完成一个服务端的构建。

UDP服务器的完善

🎃之前的版本中,我们将数据的读取和发送同样写在一个串行的逻辑之中,因此若此时我们未向命令行输入数据则无法收到服务器发送的信息

🎃因此,我们可以使用线程分别进行消息的读取和发送,同时我们想要将这个程序构建成一个小型的聊天室,因此服务器接收到一个数据时要向已连接上的所有用户进行广播。所以我们还需要使用一个哈希表将用户的相关数据管理起来。

🎃同时,我们还可以维护一个环形队列,收线程就作为生产者而发线程为消费者,二者分别进行队列的插入与读取,当一方不满足条件时阻塞即可。

线程的封装

结构定义

🎃对于一个线程,我们需要封装他的 tid 、回调函数、当前状态,还可以给线程取个名字。

🎃在初始化列表完成成员的初始化,而构造函数中根据传入线程的序列号生成一个线程名。

class Thread
{
public:typedef enum{NEW = 0,RUNNING,EXIT} ThreadStatus;using func_t = std::function<void()>;Thread(int num, func_t func) : _tid(0), _status(NEW), _func(func){char name[128];snprintf(name, sizeof(name), "thread-%d", num);_name = name;}~Thread(){}private:pthread_t _tid;std::string _name;func_t _func;ThreadStatus _status;
};

接口实现

🎃之后我们只需要简单封装对应的接口即可,首先便是成员的获取。而获取 tid 时需要判断一下当前线程是否在运行,否则直接返回 0

int status() { return _status; }std::string name() { return _name; }pthread_t threadid()
{if (_status == RUNNING)return _tid;elsereturn 0;
}

🎃接下来就是线程运行的老三样了,即运行、等待、回调

🎃很明显,在构造函数时我们并未创建线程,因为线程创建的这个过程在运行的函数中进行实现。

void run()
{int n = pthread_create(&_tid, nullptr, RunHelper, this);if (n != 0)exit(1);_status = RUNNING;
}

🎃接下来实现线程运行的逻辑,首先将传进来的参数转换回 Thread* 再回调函数。

🎃而我们还进行了运算符重载,可以直接回调,但在调用前应先判断对应的函数是否为空。

static void* RunHelper(void* args) // 由于本函数是静态成员函数,线程调用时还需要外部传this指针进来
{Thread* ts = (Thread*)args; // 强转恢复this指针(*ts)();return nullptr;
}void operator()()
{if (_func != nullptr)_func();
}

🎃最后就是线程的运行和等待了,run 函数中我们直接创建一个线程,入口函数就为上面写的 RunHelper ,之后将线程的状态设置为运行状态。join 就是对接口进行简单的封装,若等待出错就进行报错。

void run()
{int n = pthread_create(&_tid, nullptr, RunHelper, this);if (n != 0)exit(1);_status = RUNNING;
}void join()
{int n = pthread_join(_tid, nullptr);if (n != 0){std::cout << n << std::endl;std::cerr << "main thread join thread " << _name << " error" << std::endl;}
}

环形队列

结构定义

🎃环形队列的底层其实就是一个数组,通过维护下标获取插入和读取的位置,同时其为一个临界资源,涉及到生产者间与消费者间的访问冲突,因此还需要两个锁,接着我们还可以使用信号量维护环形队列中对应的数据量。

🎃在构造函数中我们完成各个成员的初始化,对于数据和空间的信号量,显然一开始环形队列为空,因此数据量为 0,而空间的量为环形队列的大小。同时,在析构时就需要完成信号量和锁的销毁

const int N = 50;
template <class T>
class RingQueue
{
public:RingQueue(int num = N) : _ring(num), _cap(num){sem_init(&_data_sem, 0, 0);sem_init(&_space_sem, 0, num);pthread_mutex_init(&_c_mutex, nullptr);pthread_mutex_init(&_p_mutex, nullptr);_c_step = _p_step = 0;}~RingQueue(){sem_destroy(&_data_sem);sem_destroy(&_space_sem);pthread_mutex_destroy(&_c_mutex);pthread_mutex_destroy(&_p_mutex);}
private:std::vector<T> _ring;int _cap;sem_t _data_sem;  // 数据的信号量sem_t _space_sem; // 空间的信号量int _c_step;      // 消费位置int _p_step;      // 生产位置pthread_mutex_t _c_mutex; // 消费者间的锁pthread_mutex_t _p_mutex; // 生产者间的锁
};

接口实现

加锁

🎃就是简单对对应接口的封装,两个锁都可以直接调用进行加锁的操作。

void Lock(pthread_mutex_t &m) // 加锁
{pthread_mutex_lock(&m);
}void UnLock(pthread_mutex_t &m) // 解锁
{pthread_mutex_unlock(&m);
}
信号量的申请与释放

🎃这里同样是对系统接口的再次封装,经由这些步骤便可使得我们的代码更加规范,能够用统一的视角来看待变量。

void P(sem_t& s) // 申请信号量
{sem_wait(&s);
}void V(sem_t& s) // 释放信号量
{sem_post(&s);
}
入队与出队

🎃当我们要插入数据时,先申请空间信号量,若无空余便阻塞,之后直接加锁接下来我们便能够进行数据的插入,插入位置迭代后需要 % 上环形队列的大小才能进行环形的读取与插入。数据插入完毕后便可以解锁,最后释放一个数据的信号量,表示我们成功插入一个数据。

void push(const T in)
{P(_space_sem);         // 申请空间信号量Lock(_p_mutex);        // 生产者加锁_ring[_p_step++] = in; // 数据写入_p_step %= _cap;UnLock(_p_mutex);V(_data_sem); // 释放数据的信号量
}

🎃而读取的操作类似,只不过申请的是数据的信号量,释放的是空间的信号量,同时这里使用的是输出型参数,读取的数据填充到指针里即可。

void pop(T* out)
{P(_data_sem);            // 查看数据数量Lock(_c_mutex);          // 消费者加锁*out = _ring[_c_step++]; // 读取数据_c_step %= _cap;UnLock(_c_mutex);V(_space_sem); // 释放空间的信号量
}

整体组装 

🎃增加了那么多新的组件,接下来我们进行服务器功能的完善。

🎃首先便是需要增加成员,因为要维护在线用户因此需要一个哈希表进行管理,同时,我们这个的表也是被多线程访问的,因此还需要一个进行保护。接着还需要使用两个线程分别进行数据的读取和发送,以及一个环形队列存储要发送的信息。

private:int _sock;uint16_t _port;pthread_mutex_t lock;std::unordered_map<std::string, struct sockaddr_in> OnlineUser;RingQueue<std::string> rq;Thread* c;Thread* p;

初始化与析构

🎃因为新增了许多成员,我们需要在构造函数中进行部分成员的初始化。这里线程的入口函数分别为接下来要实现的收发操作,同时因为二者为成员函数,因此我们需要手动为其绑定 this 指针作为第一个参数。

UdpServer(int port = defaultport): _port(port)
{pthread_mutex_init(&lock, nullptr);p = new Thread(1, std::bind(&UdpServer::Recv, this));c = new Thread(2, std::bind(&UdpServer::Broadcast, this));
}

🎃在网络前期准备后,便可以直接进行线程的运行。

if (bind(_sock, (sockaddr*)&local, sizeof(local)))
{std::cout << "bind socket error: " << strerror(errno) << std::endl;exit(BIND_ERR);
}
std::cout << "bind socket success" << std::endl;c->run();
p->run();

 🎃而析构时就进行线程的等待,待到线程终止就删除 new 出来的对象。

~UdpServer()
{pthread_mutex_destroy(&lock);c->join();p->join();delete c;delete p;
}

信息接收线程

🎃前面的操作大部分的都讲过了,接收到数据后提取发送方的相关信息,将其加入到表中,而要发送回用户的字符串则放进环形队列中。

void Recv()
{char buffer[1024];while (true){// 接收信息struct sockaddr_in peer;socklen_t len = sizeof(peer);int n = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (sockaddr*)&peer, &len);if (n > 0)buffer[n] = '\0';elsecontinue;// 提取客户端信息std::string clientip = inet_ntoa(peer.sin_addr);uint16_t port = ntohs(peer.sin_port);std::cout << "get message from " << clientip << "-" << port << ": " << buffer << std::endl;std::string name = clientip;name += "-";name += std::to_string(port);addUser(name, peer);std::string message = name + "echo# " + buffer;rq.push(message);}
}

🎃因为用户的注册表是一个临界资源,因此在访问前一定要先加锁,这里使用的是封装的一个类,类似于智能指针的作用,直接理解为加锁函数结束时解锁即可,若此时注册表中未有对应名称的用户,就将用户的 ip 信息插入进去。

void addUser(const std::string& name, const struct sockaddr_in& peer)
{LockGuard lg(&lock);if (!OnlineUser.count(name))OnlineUser[name] = peer;
}

消息发送线程

🎃而这个广播线程就时时刻刻从环形队列中拿取数据,通过遍历注册表将消息发给所有用户。

🎃这里有个小细节,我们使用一个 vector 先将用户的 ip 信息记录下来,临界区域结束后再进行发送,因为若是直接在锁内进行数据的发送就会占用过久临界资源,进而影响程序的运行效率。

void Broadcast()
{while (true){std::string sendstring;rq.pop(&sendstring);std::vector<sockaddr_in> v;{LockGuard lockguard(&lock);for (auto& usr : OnlineUser){v.push_back(usr.second);}}for (auto& usr : v){sendto(_sock, sendstring.c_str(), sendstring.size(), 0, (sockaddr*)&usr, sizeof(usr));}}
}

🎃于客户端而言,将接收操作交由一个线程处理,便能够做到即便未发数据,也能够同步收到服务器发送的消息。

TCP套接字

🎃接下来我们就进行 TCP 套接字编程的讲解,之前就讲过 TCP有连接可靠的传输方式,自然也代表着其通信过程相较于 UDP 更为复杂。

创建套接字

🎃UDP 在创建套接字时做的工作,TCP 也都要做,即创建套接字 fd 、填充 sockaddr_inbind,但需要注意的是创建 socket 时使用的是 SOCK_STREAM 选项。

socket(AF_INET, SOCK_STREAM, 0);

listen

🎃接下来 TCP 服务器还需要调用 listen 才能完成前置准备工作。

🎃第一个参数即为前面创建的 socket 文件描述符,第二个参数表示为挂起连接队列的最大长度,定义一个不大不小的值即可,这里就设置成了 32。

static const int backlog = 32;void initserver()
{_listensock = socket(AF_INET, SOCK_STREAM, 0);if (_listensock < 0){std::cerr << "create socket error" << std::endl;exit(SOCKET_ERR);}struct sockaddr_in local;local.sin_family = AF_INET;local.sin_addr.s_addr = INADDR_ANY;local.sin_port = htons(_port);if (bind(_listensock, (sockaddr *)&local, sizeof(local)) != 0){std::cerr << "bind socket error" << std::endl;exit(BIND_ERR);}if (listen(_listensock, backlog) != 0){std::cerr << "listen socket error" << std::endl;exit(LISTEN_ERR);}
}

accept

🎃若说 UDP 的传输方式像发快递,那么 TCP 的传输方式就像餐馆的运行方式。

🎃一个餐馆的运行的方式可以分成两部分,分别是餐馆内部和餐馆外部,外部有人负责揽客,而内部则有服务员负责处理用户就餐的请求。

🎃同样,前面我们创建的套接字负责的就是揽客工作,当有外部有请求尝试连接便会通过 accept 函数返回。

🎃而我们之后用于通信的 fd 其实是 accept 返回的,接下来就可以凭借这个 fd 进行消息的收发了。

void start()
{while (true){struct sockaddr_in client;socklen_t len = sizeof(client);int sock = accept(_listensock, (sockaddr *)&client, &len);if (sock < 0){std::cerr << "accept socket error" << std::endl;continue;}std::string clientip = inet_ntoa(client.sin_addr);uint16_t port = ntohs(client.sin_port);std::string name = clientip;name += "-";name += std::to_string(port);std::cout << "获取新连接成功 " << sock << " from " << _listensock << "," << name << std::endl;service(sock, clientip, port);}
}

收发操作

🎃因为 TCP 通信面向字节流,而流式服务都可以直接使用 read 进行读取,这里我们便直接使用 read 即可。

🎃同时,read 的返回值有三种不同的情况,大于 0 时表示为读取数据的字节数,等于 0 表示断开连接,小于 0 就表示出错。

🎃读取的时候记得给 /0 留一个位置,接下来根据业务进行处理后便可以发回给用户了,同样我们可以直接使用 write 进行数据发送。

void service(int sock, const std::string &ip, const uint16_t &port)
{char buffer[1024];std::string name = ip + '-' + std::to_string(port);while (true){ssize_t s = read(sock, buffer, sizeof(buffer) - 1);if (s > 0){buffer[s] = '\0';std::string res = _func(buffer);    //调用回调函数进行业务处理std::cout << name << "# " << res << std::endl;write(sock, buffer, sizeof(buffer) - 1);}else if (s == 0){close(sock);std::cout << ip << " quit" << std::endl;break;}else{close(sock);std::cout << "recv error" << std::endl;break;}}
}

🎃经过这几个步骤,TCP 服务器的通信框架便搭建起来了,接下来就进行客户端的编写。 

客户端的编写

🎃首先我们从命令行参数中获取服务器的 ip端口号,接着创建套接字并填充 sockaddr_in 结构,接下来便可以与服务器进行连接了。

int main(int args, char *argv[])
{if (args != 3){usage(argv[0]);exit(USAGE_ERR);}std::string server_ip = argv[1];uint16_t server_port = atoi(argv[2]);int sock = socket(AF_INET, SOCK_STREAM, 0);if (sock < 0){cerr << "create socket error" << endl;exit(SOCKET_ERR);}struct sockaddr_in server;server.sin_family = AF_INET;server.sin_addr.s_addr = inet_addr(server_ip.c_str());server.sin_port = htons(server_port);
}

🎃接下来就需要使用 connect 函数进行与服务器的连接了。

🎃参数于我们而言相当熟悉了,我们还可以写一个简单的重连逻辑,若连接不上就直接使进程退出了。

int cnt = 1;
while (connect(sock, (sockaddr *)&server, sizeof(server)) != 0)
{cout << "正在重连(" << cnt++ << ")" << endl;sleep(1);if (cnt > 5)break;
}if (cnt > 5)
{cerr << "连接失败" << endl;exit(CONNECT_ERR);
}

🎃建立连接后便可以进行数据的收发了,还是与服务端相同的操作,而当 read 返回值为 0 时就代表断开连接。

char buffer[1024];
while (true)
{std::string line;cout << "please Enter# ";getline(cin, line);write(sock, line.c_str(), line.size());ssize_t s = read(sock, buffer, sizeof(buffer) - 1);if (s > 0){buffer[s] = '\0';cout << "server echo# " << buffer << endl;}else if (s == 0){cerr << "服务器崩溃" << endl;break;}else{cerr << "read error:" << strerror(errno) << endl;break;}
}

进一步完善

🎃经过上面的构建,虽然我们能够构建出通信的环境,但我们会发现只有第一个用户能够连接上服务器

🎃这是因为我们在 accept 后直接就串行地为用户提供服务,又因为服务逻辑是一个死循环,因此就无法再次接收新用户连接了。

🎃所以接下来我们就使用多线程或多进程的方法使其能够支持多用户的连接。

多进程

🎃需要注意的一点是,无论是线程还是进程,若是再进行等待,同样会使主进程/线程阻塞,因此需要忽略掉子进程/线程返回的相关信息。

🎃对于多进程而言可以直接使用 signal 进行忽略。

signal(SIGCHLD, SIG_IGN);

🎃但这里我使用了另一种方法, 在子进程创建出来后,再创建一个子进程并使其父进程退出,使其成为一个孤儿进程。这样就将其与主进程分离了。

🎃同时,在创建进程后我们需要将不需要的文件描述符关闭。

// 多进程
int id = fork();
if (id < 0)
{close(sock);  continue;
}
else if (id == 0) // 子进程
{close(_listensock);    if (fork() > 0)exit(0);service(sock, clientip, port);exit(0);
}

多线程

🎃为了方便使用,我们维护了一个结构用于存储线程需要使用到的信息,之后创建线程将这个结构传进去就行。

class ThreadData
{
public:ThreadData(int fd, const std::string &ip, uint16_t port, TcpServer *ts): socket(fd), clientip(ip), clientport(port), current(ts){}public:int socket;std::string clientip;uint16_t clientport;TcpServer *current;
};

🎃先构建出对应结构,接下来便可以进行线程的创建。

// 多线程
pthread_t tid;
ThreadData *td = new ThreadData(sock, clientip, clientport, this);
pthread_create(&tid, nullptr, threadRoutine, td);

🎃线程进入入口函数的第一件事便是将自身独立出来,将参数转换出来后便可以通过 this 指针调用服务。

static void *threadRoutine(void *args)
{pthread_detach(pthread_self());ThreadData *td = static_cast<ThreadData *>(args);td->current->service(td->socket, td->clientip, td->clientport);delete td;return nullptr;
}

🎃由此我们便完成了 TCP 服务端的简单构建。 

总结

🎃对于 UDP 服务器的构建,需要以下三步前置工作:

  1. 创建套接字
  2. 构建 sockaddr_in 结构
  3. bind

🎃而 TCP 服务器的构建则需要再此基础上再添加两步:

  1. listen
  2. accept

🎃同时,其有连接的性质,注定了服务端需要使用并行的方式进行用户连接与数据收发。

🎃客户端而言,TCP 连接时只需要比 UDP 多出一步与服务器 connect

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

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

相关文章

C语言中的格式化输出符号:%d %c %p %x等

文章目录 概览%d%c%d和%c的区别%p%x %X输出浮点数参考 概览 C语言中的格式化输出符号有很多&#xff0c;以下是一些常见的&#xff1a; %d 或 %i&#xff1a;用于输出十进制整数。 %u&#xff1a;用于输出无符号十进制整数。 %f&#xff1a;用于输出浮点数。 %s&#xff1a;用…

酷开科技 | 酷开系统,让家庭娱乐方式焕然一新!

在这个快节奏的社会&#xff0c;家庭娱乐已成为我们日常生活中不可或缺的一部分&#xff0c;为了给家庭带来更多欢笑与感动&#xff0c;酷开科技发力研发出拥有丰富内容和技术的智能电视操作系统——酷开系统&#xff0c;它集合了电影、电视剧、综艺、游戏、音乐等海量内容&…

基于SpringBoot的企业客户管理系统的设计与实现

摘 要 本论文主要论述了如何使用JAVA语言开发一个企业客户管理系统&#xff0c;本系统将严格按照软件开发流程进行各个阶段的工作&#xff0c;采用B/S架构&#xff0c;面向对象编程思想进行项目开发。在引言中&#xff0c;作者将论述企业客户管理系统的当前背景以及系统开发的目…

Spinnaker 基于 docker registry 触发部署

docker registry 触发部署 Spinnaker可以通过Docker镜像的变化来触发部署&#xff0c;这种方法允许你在Docker镜像发生变化时自动启动新的部署流程。 示例原理如下图所示&#xff1a; 以下是如何在Spinnaker中实现基于Docker Registry触发部署的配置流程。最终实现的效果如下…

JavaScript 的初步学习下篇

函数 语法格式 创建函数/函数声明/函数定义 function 函数名(形参列表) {函数体return 返回值; }函数调用 函数名(实参列表) // 不考虑返回值 返回值 函数名(实参列表) // 考虑返回值 注: 函数定义并不会执行函数体内容, 必须要调用才会执行. 调用几次就会执行几次. js 中…

Windows11系统下MemoryCompression导致内存占用率过高

. # &#x1f4d1;前言 本文主要是win11系统下CPU占用率过高如何下降的文章&#xff0c;如果有什么需要改进的地方还请大佬指出⛺️ &#x1f3ac;作者简介&#xff1a;大家好&#xff0c;我是青衿&#x1f947; ☁️博客首页&#xff1a;CSDN主页放风讲故事 &#x1f304;每日…

如何在财税行业查找批量客户?

现在市场上代记账公司也不算少&#xff0c;做过这行的都知道&#xff0c;最初呢行业竞争不强&#xff0c;都是靠地推、老客户转介绍&#xff0c;或者长期以往的蹲守各个地区的工商注册服务中心&#xff0c;找那些才注册企业的老板或者创业者。但是&#xff0c;随着市场经济的发…

iceoryx(冰羚)-publisher注册RouDi进程

1、发UserApp启动&#xff0c;发布REG消息过程 2、初始化状态转换 3、pub->RouDi 4、sub->RouDi 5、IPC通信 IPC通信支持socket和管道&#xff0c;IpcInterfaceUser ->RouDi进程的socket&#xff0c; IpcInterfaceCreator接收RouDi进程的消息socket。 optional<…

结构体||联合体

1.结构体 1.1实际生活中一些东西往往有多个元素组成。如一名学生有身高、体重、名字、学号等。这时候就需要用到结构体。 结构体是一些值的结合&#xff0c;这些值被称为成员变量。结构体的每个成员可以是不同类型的变量&#xff0c;如&#xff1a;标量、数组、指针、甚至是其…

基于SSM的网上书城

简介 本系统主要分为前台和后台&#xff0c;前台网页主要是面向用户的&#xff0c;用户注册登录后网上书城可以进行下单购买图书&#xff0c;主要功能有图书基本信息的查询、用户登录和注册、图书搜索、添加购物车、购买、订单查询等功能&#xff0c;后台是管理员进入的&#x…

Python基础学习快速入门

文章目录 Number变量String字符串Def函数Class类List列表Tuple元组Dictionary字典Set集合值与引用类型if条件控制Loop循环 Number变量 python直接赋值&#xff0c;不需要定义变量类型。不需要**,逗号结尾符 使用print**直接进行输出 #赋值 a 1.0 print(a)a 7 print(a)p…

Selenium 学习(0.17)——软件测试之测试用例设计方法——白盒测试——逻辑覆盖法(条件覆盖和条件判定覆盖)

条件覆盖 设计测试用例&#xff0c;使每个判断中每个条件的可能取值至少满足一次。 条件判定覆盖 通过设计足够的测试用例&#xff0c;满足如下条件&#xff1a; 所有条件的可能至少执行一次的取值 所有判断的可能结果至少执行一次 条件判定覆盖同时满足判定覆…

12.1平衡树(splay),旋转操作及代码

平衡树 变量定义 tot表示结点数量&#xff0c;rt表示根的编号 v[i]表示结点i的权值 fa[i]表示结点i的父亲节点 chi[i][2]表示结点i的左右孩子 cnt[i]表示结点i的权值存在数量&#xff0c;如1123&#xff0c;v[3]1&#xff0c;则cnt[3]2;就是说i3的三号结点的权值为1&…

中国湖泊面积-水位长时序数据产品(2000-2020)

今天我们分享中国湖泊面积-水位长时序数据产品&#xff08;2000-2020&#xff09; 该数据集包含中国典型湖泊2000-2020年最大水体面积、多年平均面积、水位变化速率及空间分布矢量。 数据溯源信息 「数据来源描述」Landsat、HJ、ZY、Jason、ENVISAT、Cryosat、ICESat和HY 「数…

hls实现播放m3u8视频将视频流进行切片 HLS.js简介

github官网GitHub - video-dev/hls.js: HLS.js is a JavaScript library that plays HLS in browsers with support for MSE.HLS.js is a JavaScript library that plays HLS in browsers with support for MSE. - GitHub - video-dev/hls.js: HLS.js is a JavaScript library …

无桌面版docker在Ubuntu系统上安装

目录 注意 系统要求 卸载旧版本 安装 使用apt存储库安装 1. 设置 Docker 的apt存储库。 2. 安装Docker软件包 3. 通过运行镜像来验证Docker Engine安装是否成功 hello-world。 从包中安装 1. 进入 https://download.docker.com/linux/ubuntu/dists/。 2. 在列表中选择…

ubuntu22.04新机使用(换源,下载软件,安装显卡驱动,锁屏长亮)

换源 国内有很多Ubuntu的镜像源&#xff0c;包括阿里的、网易的&#xff0c;还有很多教育网的源&#xff0c;比如&#xff1a;清华源、中科大源。推荐使用中科大源&#xff0c;快得很。 /etc/apt/sources.list编辑/etc/apt/sources.list文件, 在文件最前面添加以下条目(操作前…

KEPserver和S7-200SMART PLC通信配置

KEPserver和S7-1200PLC通信配置,请查看下面文章链接: https://rxxw-control.blog.csdn.net/article/details/134683670https://rxxw-control.blog.csdn.net/article/details/134683670 1、OPC通信应用 2、选择Siemens驱动 3、添加S7-200设备

k8s部署jenkins

1.先决条件 1.因为国内的容器镜像加速器无法实时更新docker hub上的镜像资源.所以可以自己进行jenkins的容器镜像创建,. 2.这里用到了storageClass k8s的动态制备.详情参考: k8s-StoargClass的使用-基于nfs-CSDN博客 3.安装docker服务.(用于构建docker image) 2.构建jenki…

C++学习之路(十六)C++ 用Qt5实现一个工具箱(为屏幕颜色提取功能增加一个点击复制的功能)- 示例代码拆分讲解

上篇文章&#xff0c;我们用 Qt5 实现了在小工具箱中添加了《颜色代码转换和屏幕颜色提取功能》功能。今天我们把屏幕颜色提取的功能再扩展一下&#xff0c;让它可以点击复制吧。下面我们就来看看如何来规划开发这样的小功能并且添加到我们的工具箱中吧。 老规矩&#xff0c;先…