【网络编程】套接字编程——TCP通信

news2024/12/29 10:09:31

文章目录

  • 一、简单的TCP网络程序
    • 1. 单进程版
    • 2. 多进程版
    • 3. 多线程版
  • 二、线程池版TCP网络程序
  • 三、日志与守护进程
    • 1. 日志
    • 2. 守护进程
      • 进程组和会话的引出
      • 守护进程的创建


一、简单的TCP网络程序

1. 单进程版

💕 tcpServer.hpp

#pragma once
#include "err.hpp"
#include <iostream>
#include <string>
#include <functional>
#include <cstring>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>
using namespace std;

namespace ns_server
{
	static const uint16_t defaultport = 8080;
	static const int backlog = 32;

	using func_t = function<string (const string&)>;
	
	class TcpServer
	{
	public:
		TcpServer(func_t func, const uint16_t& port):_func(func), _port(port), _quit(true)
		{}

		void initServer()
		{
			// 创建套接字文件
			_listensock = socket(AF_INET, SOCK_STREAM, 0);
			if(_listensock < 0)
			{
				cerr << "create socket error..." << endl;
				exit(SOCKET_ERR);
			}

			// 绑定
			struct sockaddr_in local;
			memset(&local, 0, sizeof(local));

			local.sin_family = AF_INET;
			local.sin_port = htons(_port);
			local.sin_addr.s_addr = INADDR_ANY;

			if(bind(_listensock, (struct sockaddr*)&local, sizeof(local)) < 0)
			{
				cerr << "bind socket error..." << endl;
				exit(BIND_ERR);
			}

			// 监听
			if(listen(_listensock, backlog) < 0)
			{
				cerr << "listen socket error" << endl;
				exit(LISTEN_ERR);
			}
		}
		void start()
		{

			_quit = false;
			while(!_quit)
			{
				// signal(SIGCHLD, SIG_IGN);

				struct sockaddr_in client;
				socklen_t len = sizeof(client);
				
				// 获取连接
				int sock = accept(_listensock, (struct sockaddr*)&client, &len);
				if(sock < 0)
				{
					cerr << "accept error" << endl;
					continue;
				}

				// 表示获取连接成功,提取client信息
				string clientip = inet_ntoa(client.sin_addr); // 将4字节IP地址转换为字符串IP
				uint16_t clientport = ntohs(client.sin_port);

				// 获取新连接成功,开始进行业务处理
				std::cout << "获取新连接成功: " << sock << " from " << _listensock << ", "
                          << clientip << "-" << clientport << std::endl;
				
				service(sock, clientip, clientport); // 单进程版 
			}
		}
		
		void service(int sock, const string& clientip, const uint16_t& clientport)
		{
			string who = clientip + "-" + to_string(clientport);
			char buffer[1024];
			while(true)
			{
				ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
				if(s > 0)
				{
					buffer[s] = 0;
					string res = _func(buffer);
					cout << who << ">>>" << res << endl;
					write(sock, res.c_str(), res.size());
				}
				else if(s == 0) // 读取到了文件结尾
				{
					// 对方将连接关闭了
					close(sock);
					std::cout << who << " quit, me too" << std::endl;
                    break;
				}
				else
				{
					close(sock);
					cerr << std::cerr << "read error: " << strerror(errno) << std::endl;
                    break;
				}
			}
		}

	private:
		uint16_t _port;
		int _listensock;
		bool _quit;
		func_t _func;
	};
}

在这里插入图片描述
在这里插入图片描述

因为服务器是单进程版的,所以服务端只有完成一个客户端后才会服务另一个客户端。

在这里插入图片描述

主要还是因为当服务端调用service函数进行服务时,在处理一个客户的消息时是一个死循环,只有当一个客户退出后,服务端才会调用accept函数继续获取下一个客户端的连接。

单执行流的服务器一次只能给一个客户端提供服务,此时服务器的资源并没有得到充分利用,因此我们需要将该服务器改为多执行流的。


2. 多进程版

当服务端调用accept函数获取新连接后,我们可以创建新的执行流为该连接提供服务,这时候我们可以创建子进程来执行该服务。因此当父进程fork创建出子进程后,父进程就可以继续监听套接字当中获取新连接,而不需要关心获取上来的连接是否已经服务完毕。

💕 文件描述符的继承

我们知道,文件描述符的生命周期是随进程的,子进程创建后会继承父进程的文件描述符表,当父进程打开一个文件时,该文件对应的文件描述符是3, 创建的子进程的3号文件描述符也会指向该文件。如果子进程再创建一个进程,那么该进程的3号文件描述符也会指向和父进程一样的文件。

在这里插入图片描述

这里我们需要注意的是,当父进程创建子进程后,父子进程间会保持独立性,此时父进程文件描述符的变化不会影响子进程。套接字文件也是一样,当父进程创建的子进程也会继承父进程的套接字文件,此时子进程就能够对特定的套接字文件进行读写操作。进而完成对对应客户端的服务。

💕 父进程等待子进程退出的问题

当父进程创建出子进程后,父进程是需要等待子进程退出的,否则子进程会变成僵尸进程,进而造成内存泄漏。因此服务端创建子进程后需要调用wait或者waitpid函数对子进程进行等待。

  • 如果服务端采用阻塞的方式等待子进程,那么服务端还是需要等待服务完成当前客户端,才能继续获取下一个连接请求,此时服务端仍然是一种串行的方式为客户端提供服务。
  • 如果服务端采用非阻塞的方式等待子进程,虽然在子进程为客户端提供服务期间服务端可以继续获取新连接,但此时服务端就需要将所有的子进程的PID保存下来,并且需要不断花费时间检测子进程是否退出。

因此服务端要等待子进程退出,无论采用阻塞式等待还是非阻塞式等待,都不是很好的选择,此时我们可以选择让服务端不等待子进程退出。

  1. 捕捉SIGCHLD信号,将其处理动作设置为忽略
  2. 让父进程创建子进程,然后让子进程再创建孙子进程,退出子进程,让孙子进程为客户端提供服务

忽略SIGCHLD信号

void start()
{
	signal(SIGCHLD, SIG_IGN);
	_quit = false;
	while(!_quit)
	{

		struct sockaddr_in client;
		socklen_t len = sizeof(client);
		
		// 获取连接
		int sock = accept(_listensock, (struct sockaddr*)&client, &len);
		if(sock < 0)
		{
			cerr << "accept error" << endl;
			continue;
		}

		// 表示获取连接成功,提取client信息
		string clientip = inet_ntoa(client.sin_addr); // 将4字节IP地址转换为字符串IP
		uint16_t clientport = ntohs(client.sin_port);

		// 获取新连接成功,开始进行业务处理
		std::cout << "获取新连接成功: " << sock << " from " << _listensock << ", "
                        << clientip << "-" << clientport << std::endl;

		// ————多进程版本实现——忽略信号方式
		pid_t pid = fork();
		if(pid == 0) // 子进程
		{
			close(_listensock);
			// if(fork() > 0) exit(0);
			service(sock, clientip, clientport);
			exit(0);
		}
		else if(pid < 0)
		{
			close(sock);
			continue;
		}
	}
}

演示效果:

在这里插入图片描述

创建孙子进程

void start()
{
	_quit = false;
	while(!_quit)
	{

		struct sockaddr_in client;
		socklen_t len = sizeof(client);
		
		// 获取连接
		int sock = accept(_listensock, (struct sockaddr*)&client, &len);
		if(sock < 0)
		{
			cerr << "accept error" << endl;
			continue;
		}

		// 表示获取连接成功,提取client信息
		string clientip = inet_ntoa(client.sin_addr); // 将4字节IP地址转换为字符串IP
		uint16_t clientport = ntohs(client.sin_port);

		// 获取新连接成功,开始进行业务处理
		std::cout << "获取新连接成功: " << sock << " from " << _listensock << ", "
                        << clientip << "-" << clientport << std::endl;
		

		// ————多进程版本实现——创建孙子进程
		pid_t pid = fork();
		if(pid == 0) // 子进程
		{
			close(_listensock);
			if(fork() > 0) exit(0);
			service(sock, clientip, clientport);
			exit(0);
		}
		else if(pid < 0)
		{
			close(sock);
			continue;
		}

		// 进程等待
		close(sock);
		pid_t ret = waitpid(pid, nullptr, 0);
		if(ret == pid) cout << "wait child " << pid << " success" << std::endl;  
	}
}

演示效果:

在这里插入图片描述

由于儿子进程创建完孙子进程后就立刻推出了,因此实际为客户端提供服务的孙子进程就变成了孤儿进程,该进程会被系统领养,当孙子进程为客户端提供完服务退出后系统会回收孙子进程,所以服务进程是不需要等待孙子进程退出的。

关闭文件描述符的重要性

服务进程(爷爷进程)调用accept函数获取到新连接后,会让孙子进程为该连接提供服务,此时服务进程已经将文件描述符表继承给了爸爸进程,而爸爸进程又会调用fork函数创建出孙子进程,然后再将文件描述符表继承给孙子进程。同样的,对于爸爸进程和孙子进程来说,它们是不需要关心从服务进程(爷爷进程)继承下来的监听套接字的,因此爸爸进程可以将监听套接字关掉。

  • 对于服务进程来说,当它调用fork函数后就必须将从accept函数获取的文件描述符关掉。因为服务进程会不断调用accept函数获取新的文件描述符(服务套接字),如果服务进程不及时关掉不用的文件描述符,最终服务进程中可用的文件描述符就会越来越少。
  • 而对于爸爸进程和孙子进程来说,还是建议关闭从服务进程继承下来的监听套接字。实际就算它们不关闭监听套接字,最终也只会导致这一个文件描述符泄漏,但一般还是建议关上。因为孙子进程在提供服务时可能会对监听套接字进行某种误操作,此时就会对监听套接字当中的数据造成影响。

完整代码:💕 tcpServer.hpp

#pragma once
#include "err.hpp"
#include <iostream>
#include <string>
#include <functional>
#include <cstring>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>
using namespace std;

namespace ns_server
{
	static const uint16_t defaultport = 8080;
	static const int backlog = 32;

	using func_t = function<string (const string&)>;
	class TcpServer
	{
	public:
		TcpServer(func_t func, const uint16_t& port):_func(func), _port(port), _quit(true)
		{}

		void initServer()
		{
			// 创建套接字文件
			_listensock = socket(AF_INET, SOCK_STREAM, 0);
			if(_listensock < 0)
			{
				cerr << "create socket error..." << endl;
				exit(SOCKET_ERR);
			}

			// 绑定
			struct sockaddr_in local;
			memset(&local, 0, sizeof(local));

			local.sin_family = AF_INET;
			local.sin_port = htons(_port);
			local.sin_addr.s_addr = INADDR_ANY;

			if(bind(_listensock, (struct sockaddr*)&local, sizeof(local)) < 0)
			{
				cerr << "bind socket error..." << endl;
				exit(BIND_ERR);
			}

			// 监听
			if(listen(_listensock, backlog) < 0)
			{
				cerr << "listen socket error" << endl;
				exit(LISTEN_ERR);
			}
		}
		void start()
		{
			_quit = false;
			while(!_quit)
			{

				struct sockaddr_in client;
				socklen_t len = sizeof(client);
				
				// 获取连接
				int sock = accept(_listensock, (struct sockaddr*)&client, &len);
				if(sock < 0)
				{
					cerr << "accept error" << endl;
					continue;
				}

				// 表示获取连接成功,提取client信息
				string clientip = inet_ntoa(client.sin_addr); // 将4字节IP地址转换为字符串IP
				uint16_t clientport = ntohs(client.sin_port);

				// 获取新连接成功,开始进行业务处理
				std::cout << "获取新连接成功: " << sock << " from " << _listensock << ", "
                          << clientip << "-" << clientport << std::endl;

				// ——多进程版本实现
				pid_t pid = fork();
				if(pid == 0) // 子进程
				{
					close(_listensock);
					if(fork() > 0) exit(0);
					service(sock, clientip, clientport);
					exit(0);
				}
				else if(pid < 0)
				{
					close(sock);
					continue;
				}

				// 进程等待
				close(sock);
				pid_t ret = waitpid(pid, nullptr, 0);
				if(ret == pid) cout << "wait child " << pid << " success" << std::endl;  
			}
		}

		void service(int sock, const string& clientip, const uint16_t& clientport)
		{
			string who = clientip + "-" + to_string(clientport);
			char buffer[1024];
			while(true)
			{
				ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
				if(s > 0)
				{
					buffer[s] = 0;
					string res = _func(buffer);
					cout << who << ">>>" << res << endl;
					write(sock, res.c_str(), res.size());
				}
				else if(s == 0) // 读取到了文件结尾
				{
					// 对方将连接关闭了
					close(sock);
					std::cout << who << " quit, me too" << std::endl;
                    break;
				}
				else
				{
					close(sock);
					cerr << std::cerr << "read error: " << strerror(errno) << std::endl;
                    break;
				}
			}
		}

	private:
		uint16_t _port;
		int _listensock;
		bool _quit;
		func_t _func;
	};
}

3. 多线程版

由于创建进程的成本是比较高的,需要创建该进程对应的进程控制块、进程地址空间、页表等数据结构。而线程的创建要比进程的创建成本会小的多,因为线程会共享进程的大部分资源,因此我们在实现多执行流的服务器时最好采用多线程进行实现。

这里我们需要注意的是,当服务进程(主线程)调用accept函数获取到一个文件描述符后,其他创建的新线程是可以直接访问该文件描述符的。但是此时新线程并不知道他所服务的客户端对应的是哪一个文件描述符,因此我们需要将对应的文件描述符传递给该线程。

但是新线程在为客户端提供服务时就是调用service函数,而调用service函数需要传入三个参数:客户端对应的套接字、IP地址和端口号。而实际在调用pthread_create函数创建新线程时,只能传入类型为void*的参数。所以我们需要设计一个参数结构体ThreadDate,将这三个参数放到该结构体中,当主线程创建新线程时就定义一个ThreadDate对象,将客户端的三个参数设计进该对象中。

// ThreadDate结构体
class ThreadDate
{
public:
	ThreadDate(int fd, const string &ip, const uint16_t &port, TcpServer *ts)
	   :sock(fd), clientip(ip), clientport(port), current(ts)
	{}
public:
	int sock;
	string clientip;
	uint16_t clientport;
	TcpServer* current;
};
void start()
{
	// signal(SIGCHLD, SIG_IGN);
	_quit = false;
	while(!_quit)
	{

		struct sockaddr_in client;
		socklen_t len = sizeof(client);
		
		// 获取连接
		int sock = accept(_listensock, (struct sockaddr*)&client, &len);
		if(sock < 0)
		{
			cerr << "accept error" << endl;
			continue;
		}

		// 表示获取连接成功,提取client信息
		string clientip = inet_ntoa(client.sin_addr); // 将4字节IP地址转换为字符串IP
		uint16_t clientport = ntohs(client.sin_port);

		// 获取新连接成功,开始进行业务处理
		std::cout << "获取新连接成功: " << sock << " from " << _listensock << ", "
                        << clientip << "-" << clientport << std::endl;

		// V3版本—— 多线程版本
		pthread_t tid;
		ThreadDate* td = new ThreadDate(sock, clientip, clientport, this);
		pthread_create(&tid, nullptr, threadRoutine, td);
	}
}

static void* threadRoutine(void* args)
{
	pthread_detach(pthread_self()); // 线程分离
	ThreadDate* td = static_cast<ThreadDate*>(args);
	td->current->service(td->sock, td->clientip, td->clientport);
	delete td;
	return nullptr;
}

在这里插入图片描述

完整代码:💕 tcpServer.hpp

#pragma once
#include "err.hpp"
#include <iostream>
#include <string>
#include <functional>
#include <cstring>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>
using namespace std;

namespace ns_server
{
	static const uint16_t defaultport = 8080;
	static const int backlog = 32;

	using func_t = function<string (const string&)>;
	class TcpServer;
	class ThreadDate
	{
	public:
		ThreadDate(int fd, const string &ip, const uint16_t &port, TcpServer *ts)
		   :sock(fd), clientip(ip), clientport(port), current(ts)
		{}
	public:
		int sock;
		string clientip;
		uint16_t clientport;
		TcpServer* current;
	};

	class TcpServer
	{
	public:
		TcpServer(func_t func, const uint16_t& port):_func(func), _port(port), _quit(true)
		{}

		void initServer()
		{
			// 创建套接字文件
			_listensock = socket(AF_INET, SOCK_STREAM, 0);
			if(_listensock < 0)
			{
				cerr << "create socket error..." << endl;
				exit(SOCKET_ERR);
			}

			// 绑定
			struct sockaddr_in local;
			memset(&local, 0, sizeof(local));

			local.sin_family = AF_INET;
			local.sin_port = htons(_port);
			local.sin_addr.s_addr = INADDR_ANY;

			if(bind(_listensock, (struct sockaddr*)&local, sizeof(local)) < 0)
			{
				cerr << "bind socket error..." << endl;
				exit(BIND_ERR);
			}

			// 监听
			if(listen(_listensock, backlog) < 0)
			{
				cerr << "listen socket error" << endl;
				exit(LISTEN_ERR);
			}
		}
		void start()
		{
			_quit = false;
			while(!_quit)
			{

				struct sockaddr_in client;
				socklen_t len = sizeof(client);
				
				// 获取连接
				int sock = accept(_listensock, (struct sockaddr*)&client, &len);
				if(sock < 0)
				{
					cerr << "accept error" << endl;
					continue;
				}

				// 表示获取连接成功,提取client信息
				string clientip = inet_ntoa(client.sin_addr); // 将4字节IP地址转换为字符串IP
				uint16_t clientport = ntohs(client.sin_port);

				// 获取新连接成功,开始进行业务处理
				std::cout << "获取新连接成功: " << sock << " from " << _listensock << ", "
                          << clientip << "-" << clientport << std::endl;

				// —— 多线程版本
				pthread_t tid;
				ThreadDate* td = new ThreadDate(sock, clientip, clientport, this);
				pthread_create(&tid, nullptr, threadRoutine, td);
			}
		}

		static void* threadRoutine(void* args)
		{
			pthread_detach(pthread_self()); // 线程分离
			ThreadDate* td = static_cast<ThreadDate*>(args);
			td->current->service(td->sock, td->clientip, td->clientport);
			delete td;
			return nullptr;
		}

		void service(int sock, const string& clientip, const uint16_t& clientport)
		{
			string who = clientip + "-" + to_string(clientport);
			char buffer[1024];
			while(true)
			{
				ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
				if(s > 0)
				{
					buffer[s] = 0;
					string res = _func(buffer);
					cout << who << ">>>" << res << endl;
					write(sock, res.c_str(), res.size());
				}
				else if(s == 0) // 读取到了文件结尾
				{
					// 对方将连接关闭了
					close(sock);
					std::cout << who << " quit, me too" << std::endl;
                    break;
				}
				else
				{
					close(sock);
					cerr << std::cerr << "read error: " << strerror(errno) << std::endl;
                    break;
				}
			}
		}

	private:
		uint16_t _port;
		int _listensock;
		bool _quit;
		func_t _func;
	};
}

💕 err.hpp

#pragma once

enum
{
    USAGE_ERR = 1,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR,
    CONNECT_ERR
};

💕 tcpServer.cc

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

using namespace ns_server;

static void usage(string str)
{
	std::cout << "Usage:\n\t" << str << " port\n"
              << std::endl;
}

string echo(const string& message)
{
	return message;
}

int main(int argc, char* argv[])
{
	if(argc != 2)
	{
		usage(argv[0]);
		exit(USAGE_ERR);
	}

	uint16_t port = atoi(argv[1]);

	unique_ptr<TcpServer> tsvr(new TcpServer(echo, port));

	tsvr->initServer();
	tsvr->start();
	return 0;
}

💕 tcpClient.cc

#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "err.hpp"
using namespace std;

static void usage(string str)
{
	std::cout << "Usage:\n\t" << str << " serverip serverport\n"
              << std::endl;
}

int main(int argc, char* argv[])
{
	if(argc != 3)
	{
		usage(argv[0]);
		exit(USAGE_ERR);
	}
	
	uint16_t port = atoi(argv[2]);
	string ip = argv[1];

	// 创建socket
	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_port = htons(port);
	inet_aton(ip.c_str(), &(server.sin_addr));

	int cnt = 5;
	while(connect(sock, (struct sockaddr*)&server, sizeof(server)) != 0)
	{
		sleep(1);
		cout << "正在给你尝试重连,重连次数还有: " << cnt-- << endl;
        if(cnt <= 0) break;
	}

	if(cnt <= 0)
	{
		cerr << "重连失败" << endl;
		exit(CONNECT_ERR);
	}

	// 表示连接服务器成功
	char buffer[1024];
	while(true)
	{
		string line;
		cout << "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 << "server quit" << endl;
			break;
		}
		else 
		{
			cerr << "read error:" << strerror(errno) << endl;
			break;
		}
	}

	close(sock);
	return 0;
}

💕 makfile

.PHONY:all
all:tcp_client tcp_server

tcp_client:tcpClient.cc
	g++ -o $@ $^ -std=c++11 -lpthread
tcp_server:tcpServer.cc
	g++ -o $@ $^ -std=c++11 -lpthread

.PHONY:clean
clean:
	rm -f tcp_client tcp_server

二、线程池版TCP网络程序

上面的多线程版服务器有一些问题:

  • 每当有新连接到来时,服务端的主线程都会重新为该客户端创建为其提供服务的新线程,而当服务结束后又会将该新线程销毁。这样做不仅麻烦,而且效率低下,每当连接到来的时候服务端才创建对应提供服务的线程。
  • 如果有大量的客户端连接请求,此时服务端要为每一个客户端创建对应的服务线程。计算机当中的线程越多,CPU的压力就越大,因为CPU要不断在这些线程之间来回切换,此时CPU在调度线程的时候,线程和线程之间切换的成本就会变得很高。此外,一旦线程太多,每一个线程再次被调度的周期就变长了,而线程是为客户端提供服务的,线程被调度的周期变长,客户端也迟迟得不到应答

这里为了解决这个问题我们就需要在服务端引入线程池,线程池的出现解决了处理短时间任务时创建线程与销毁线程的代价,此外,线程池还能保证内核充分利用、防止过度调度。

💕 单例模式线程池的引入:

#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <queue>
#include <unistd.h>
#include "Thread.hpp"
#include "Task.hpp"
#include "lockGuard.hpp"
using namespace std;

const static int N = 5;

// 将此代码设计成单例模式————懒汉模式

template <class T>
class ThreadPool
{
private:
	ThreadPool(int num = N) : _num(num)
	{
		pthread_mutex_init(&_lock, nullptr);
		pthread_cond_init(&_cond, nullptr);
	}
	ThreadPool(const ThreadPool<T>& tp) = delete;
	void operator=(const ThreadPool<T>& tp) = delete;
public:
	// 设计一个静态成员函数来返回创建的对象
	static ThreadPool<T>* getinstance()
	{
		if(_instance == nullptr)
		{
			LockGuard lockguard(&_instance_lock);
			{
				if(_instance == nullptr)
				{
					cout << "线程池单例形成" << endl;
					_instance = new ThreadPool<T>();
					_instance->init();
					_instance->start();
				}
			}
		}
		return _instance;
	}

	pthread_mutex_t *getlock()
	{
		return &_lock;
	}

	void threadWait()
	{
		pthread_cond_wait(&_cond, &_lock);
	}

	void threadWake()
	{
		pthread_cond_signal(&_cond);
	}

	bool isEmpty()
	{
		return _tasks.empty();
	}

	void init()
	{
		for (int i = 0; i < _num; i++)
		{
			_threads.push_back(Thread(i + 1, threadRoutine, this));
			cout << i + 1 << " thread running" << endl;
		}
	}

	void start()
	{
		for (auto &t : _threads)
		{
			t.run();
		}
	}

	void check()
	{
		for (auto &t : _threads)
			cout << t.threadname() << " running..." << endl;
	}

	static void threadRoutine(void *args)
	{
		ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
		while (true)
		{
			T t;
			// 检测此时有没有任务, 如果有任务就处理任务, 否则就挂起等待
			{
				LockGuard lockguard(tp->getlock());
				while (tp->isEmpty())
				{
					tp->threadWait();
				}
				t = tp->popTask();
			}
			t();
		}
	}

	T popTask()
	{
		T t = _tasks.front();
		_tasks.pop();
		return t;
	}

	void pushTask(const T &t)
	{
		LockGuard lockguard(&_lock);
		_tasks.push(t);
		threadWake();
	}

	~ThreadPool()
	{
		for (auto &t : _threads)
		{
			t.join();
		}
		pthread_mutex_destroy(&_lock);
		pthread_cond_destroy(&_cond);
	}

private:
	vector<Thread> _threads;
	int _num;

	queue<T> _tasks; // 使用stl的自动扩容机制
	pthread_mutex_t _lock;
	pthread_cond_t _cond;

	static ThreadPool<T>* _instance;
	static pthread_mutex_t _instance_lock;
};

template<class T>
ThreadPool<T>* ThreadPool<T>::_instance = nullptr;

template<class T>
pthread_mutex_t ThreadPool<T>::_instance_lock = PTHREAD_MUTEX_INITIALIZER;

💕 任务类的设计

这里我们还需要设计一个任务类,该任务类中需要包含客户端中的套接字、IP地址和端口号,表示该任务是为哪一个客户端提供服务,对应操作的套接字是哪一个。

为了有利于软件分层,我们可以给任务类中新增一个仿函数成员,当执行任务类中的 重载() 方法时,就可以以回调的方式处理该任务。

#pragma once
#include <iostream>
#include <string>
#include <functional>
using namespace std;

// using cb_t = function<void (int, const string&, const uint16_t&)>;

class Handler
{
public:
	Handler()
	{}
	~Handler()
	{}
	void operator()(int sock, const string& clientip, const uint16_t& clientport)
	{
		string who = clientip + "-" + to_string(clientport);
		char buffer[1024];
		while(true)
		{
			ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
			if(s > 0)
			{
				buffer[s] = 0;
				cout << who << ">>>" << buffer << endl;
				write(sock, buffer, s);
			}
			else if(s == 0) // 读取到了文件结尾
			{
				// 对方将连接关闭了
				close(sock);
				std::cout << who << " quit, me too" << std::endl;
				break;
			}
			else
			{
				close(sock);
				cerr << std::cerr << "read error: " << strerror(errno) << std::endl;
				break;
			}
		}
	}
};

class Task
{
public:
    Task()
    {}

    Task(int sock, const string& ip, const uint16_t& port)
	:_sock(sock), _ip(ip), _port(port)
    {}

    void operator()()
    {
        _handler(_sock, _ip, _port);
    }

    ~Task()
    {}

private:
    int _sock;
	string _ip;
	uint16_t _port;
	Handler _handler;
};

完整代码:

💕 tcpServer.hpp

#pragma once
#include "err.hpp"
#include <iostream>
#include <string>
#include <functional>
#include <cstring>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>
#include "ThreadPool.hpp"
using namespace std;

namespace ns_server
{
	static const uint16_t defaultport = 8080;
	static const int backlog = 32;

	using func_t = function<string (const string&)>;
	class TcpServer
	{
	public:
		TcpServer(func_t func, const uint16_t& port):_func(func), _port(port), _quit(true)
		{}

		void initServer()
		{
			// 创建套接字文件
			_listensock = socket(AF_INET, SOCK_STREAM, 0);
			if(_listensock < 0)
			{
				cerr << "create socket error..." << endl;
				exit(SOCKET_ERR);
			}
			printf("create socket success, code: %d, error string: %s", errno, strerror(errno));

			// 绑定
			struct sockaddr_in local;
			memset(&local, 0, sizeof(local));

			local.sin_family = AF_INET;
			local.sin_port = htons(_port);
			local.sin_addr.s_addr = INADDR_ANY;

			if(bind(_listensock, (struct sockaddr*)&local, sizeof(local)) < 0)
			{
				cerr << "bind socket error..." << endl;
				exit(BIND_ERR);
			}
			printf("bind socket success, code: %d, error string: %s", errno, strerror(errno));

			// 监听
			if(listen(_listensock, backlog) < 0)
			{
				cerr << "listen socket error" << endl;
				exit(LISTEN_ERR);
			}
			printf("listen socket success, code: %d, error string: %s", errno, strerror(errno));
		}

		void start()
		{

			_quit = false;
			while(!_quit)
			{
				// signal(SIGCHLD, SIG_IGN);

				struct sockaddr_in client;
				socklen_t len = sizeof(client);
				
				// 获取连接
				int sock = accept(_listensock, (struct sockaddr*)&client, &len);
				if(sock < 0)
				{
					cerr << "accept error" << endl;
					continue;
				}

				// 表示获取连接成功,提取client信息
				string clientip = inet_ntoa(client.sin_addr); // 将4字节IP地址转换为字符串IP
				uint16_t clientport = ntohs(client.sin_port);

				// 获取新连接成功,开始进行业务处理
				std::cout << "获取新连接成功: " << sock << " from " << _listensock << ", "
                          << clientip << "-" << clientport << std::endl;
				
				Task t(sock, clientip, clientport, bind(&TcpServer::service, this, placeholders::_1, placeholders::_2, placeholders::_3));
				
				ThreadPool<Task>::getinstance()->pushTask(t);
			}
		}
	private:
		uint16_t _port;
		int _listensock;
		bool _quit;
		func_t _func;
	};
}

💕 err.hpp

#pragma once

enum
{
    USAGE_ERR = 1,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR,
    CONNECT_ERR
};

💕 Thread.hpp

#pragma once

#include <iostream>
#include <cstdlib>
#include <string>
#include <pthread.h>
using namespace std;

class Thread
{
public:
    typedef enum{
        NEW = 0,
        RUNNING,
        EXITED
    } ThreadStatus;
    typedef void (*func_t)(void*);

public:
    Thread(int num, func_t func, void* args) :_tid(0), _status(NEW),_func(func),_args(args)
    {
        char name[128];
        snprintf(name, 128, "thread-%d", num);
        _name = name;
    }

    int status(){ return _status; }
    string threadname(){ return _name; }

    pthread_t get_id()
    {
        if(_status == RUNNING)
            return _tid;
        else
            return 0;
    }

    static void* thread_run(void* args)
    {
        Thread* ti = static_cast<Thread*>(args);
        (*ti)();
        return nullptr;
    }

    void operator()()
    {
        if(_func != nullptr)
            _func(_args);
    }

    void run() // 封装线程运行
    {
        int n = pthread_create(&_tid, nullptr, thread_run, this);
        if(n != 0)
            exit(-1);
        _status = RUNNING; // 线程状态变为运行
    }

    void join() // 疯转线程等待
    {
        int n = pthread_join(_tid, nullptr);
        if(n != 0)
        {
            cout << "main thread join thread: " << _name << "error" << endl;
            return;
        }
        _status = EXITED;
    }

    ~Thread(){}
private:
    pthread_t _tid;
    string _name;
    func_t _func; // 线程未来要执行的回调
    void* _args;
    ThreadStatus _status;
};

💕 Task.hpp

#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <functional>

using cb_t = std::function<void(int , const std::string &, const uint16_t &)>;

class Task
{
public:
    Task()
    {
    }
    Task(int sock, const std::string &ip, const uint16_t &port, cb_t cb)
    : _sock(sock), _ip(ip), _port(port), _cb(cb)
    {}
    void operator()()
    {
        _cb(_sock, _ip, _port);
    }
    ~Task()
    {
    }

private:
    int _sock;
    std::string _ip;
    uint16_t _port;
    cb_t _cb;
};

在这里插入图片描述


三、日志与守护进程

1. 日志

日志(log) 是指在计算机或网络应用中,用于记录系统活动、用户操作、错误信息等的文本或二进制文件。日志文件通常用于故障排除、系统或应用程序性能监控、安全审计等目的。

💕 日志等级

// 枚举日志等级
enum
{
    Debug = 0, // 调试信息
    Info,      // 正常运行
    Warning,   // 报警
    Error,     // 正常错误
    Fatal,     // 严重错误
    Uknown
};

💕 可变参数列表

void logMessage(int level, const char* format, ...)
{
	/*
	    预备
	    va_list p; // char *类型指针
	    int a = va_arg(p, int);  // 根据指定的类型提取参数
	    va_start(p, format); // p指向可变参数部分的起始地址
	    va_end(p); // p = NULL;
	*/
}

💕 打印日志

打印日志时,一般都会有固定的格式,左半部分表示日志的根式,右半部分表示日志的内容。

将可变参数列表元素打印到文件的函数

int vsnprintf(char *str, size_t size, const char *format, va_list ap);
  • 参数str:用于缓存格式化字符串结果的字符数组
  • 参数size:限定最多打印到缓冲区sbuf的字符的个数为size-1个,因为vsnprintf还要在结果的末尾追加\0。如果格式化字符串长度大于size-1,则多出的部分被丢弃。如果格式化字符串长度小于等于size-1,则可以格式化的字符串完整打印到缓冲区sbuf。一般这里传递的值就是sbuf缓冲区的长度。
  • 参数format:格式化限定字符串
  • 参数arg:可变长度参数列表
  • 返回:成功打印到sbuf中的字符的个数,不包括末尾追加的\0。如果格式化解析失败,则返回负数。

获取当前时间的函数

time_t time(time_t*tloc);

在这里插入图片描述

将时间转换成结构体的函数

struct tm* localtime(const time_t* timep);

在这里插入图片描述

返回值:成功就返回struct tm *结构体,失败则返回NULL。

💕 log.hpp

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdio>
#include <ctime>
#include <unistd.h>
#include <sys/types.h>
#include <stdarg.h>
using namespace std;

const string filename = "log/tcpserver.log";

// 枚举日志等级
enum
{
    Debug = 0, // 调试信息
    Info,      // 正常运行
    Warning,   // 报警
    Error,     // 正常错误
    Fatal,     // 严重错误
    Uknown
};

// 字符串形式获取日志等级
static 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 "Uknown";
    }
}

// 获取日志产生时间
static string getTime()
{
    time_t curr = time(nullptr);
    struct tm* tmp = localtime(&curr);

    // 缓冲区
    char buffer[128];
    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];
    string level_string = toLevelString(level);
    string curr_time = getTime();
    snprintf(logLeft, sizeof(logLeft), "[%s] [%s] [%d] ", 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);

    // 打印日志
    printf("%s%s\n", logLeft, logRight);

    // 将日志保存到文件中
    FILE* fp = fopen(filename.c_str(), "a");
    if(!fp) return;
    fprintf(fp, "%s%s\n", logLeft, logRight);
    fflush(fp);
    fclose(fp);
}

2. 守护进程

守护进程 是在操作系统中运行的一种特殊类型的后台进程。它的主要任务是监控其他进程的运行状态,并在必要时重新启动它们。守护进程通常在系统启动时启动,并持续运行,直到系统关闭。

守护进程通常被用来确保关键服务或应用程序始终处于运行状态。当一个进程意外终止或崩溃时,守护进程会检测到这种情况,并根据需要采取适当的行动,例如重新启动该进程,以确保系统的稳定性和可用性。

守护进程在操作系统中起到了重要的作用,它可以在后台默默地工作,为其他进程提供支持和保护,并保持系统的稳定运行。

进程组和会话的引出

下面我们来看一些Linux下的一些操作来引出守护进程

  1. 创建一个后台进程 sleep 1000 &,并查看该进程
    在这里插入图片描述
    PPID:父进程的ID。
    PGID:当前进程所属的进程组
    SID:当前进程的 会话 ID。
    TTY:哪一个终端。
  2. 一下创建两个进程,观察现象
    在这里插入图片描述
    这里我们看到这两个进程的PGID是相同的,他们属于同一个进程组,并且以第一个创建的进程作为进程组的组长。
    在这里插入图片描述
    bash的PID、PGID和SID都是21894,和先前那两个进程的SID相同。

shell中控制进程组的方式

  1. 查询后台任务jobs
    在这里插入图片描述
  2. 将某一任务提到前台运行 fg+任务编号
    在这里插入图片描述
  3. ctrl z 让前台的服务暂停,该任务会自动切换到后台
    在这里插入图片描述
  4. 让后台暂停的任务重新在后台运行起来 bg+任务编号

结论:

  1. 进程组分为前台任务和后台任务
    在这里插入图片描述
  2. 如果将后台任务提到前台,那么老的前台任务就无法运行
    在这里插入图片描述
    因此,在一个会话中,只能有一个前台任务在运行。当我们使用ctrl + c将正在运行的前台任务杀死后,bash就会把自己变成前台任务,此时就又可以运行了。

为什么要存在守护进程?

在这里插入图片描述

在这里登录就相当于创建一个会话,我们可以在会话中可以启动多个任务,当我们退出会话时,可能会影响会话内部的所有任务。

因此,网络服务器为了不受用户登录注销的影响,通常会以守护进程的方式运行。

在这里插入图片描述


守护进程的创建

💕 设置会话的函数 setsid

在这里插入图片描述

设置一个会话,以进程组组长ID作为新的会话ID,这里我们需要注意的是调用setsid函数的进程不可以是进程组的组长。

设置成功,则返回调用进程的PID,若设置失败,则返回-1并设置错误码

💕 使用守护进程的条件

  1. 忽略异常
  2. 让当前进程不要成为进程组组长
  3. 新建会话,让自己成为会话的话首进程
  4. 更改守护进程的工作路径
  5. 对0(标准输入)1(标准输出)2(标准错误)做特殊处理

💕 守护进程化的函数 daemon

在这里插入图片描述

  • 第一个参数表示是否要更改工作目录,默认不更改
  • 第二个参数表示要不要关闭 0、1、2文件描述符,默认表示不关

💕 err.hpp

#pragma once
enum
{
    USAGE_ERR = 1,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR,
    CONNECT_ERR,
    SETSID_ERR,
    OPEN_ERR
};

💕 自己实现守护进程 daemon.hpp

// 守护进程

#pragma once

#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>


#include "log.hpp"
#include "err.hpp"

// 守护进程的本质是孤儿进程的一种

void Daemon()
{
    // 1. 忽略信号
	signal(SIGPIPE, SIG_IGN);
	signal(SIGCHLD, SIG_IGN);

	// 2. 让当前进程不要成为进程组组长
	if(fork() > 0) exit(0);

	// 3. 新建会话,让自己成为会话的话首进程
	pid_t ret = setsid();
	if((int)ret == -1)
	{
		logMessage(Fatal, "deamon error, code: %d, string: %s", errno, strerror(errno));
		exit(SETSID_ERR);
	}

	// 4. 更改守护进程的工作路径 (可不选) —— chdir("/")

	// 5. 处理后续对于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_ERR);
	}
	dup2(fd, 0);
	dup2(fd, 1);
	dup2(fd, 2);
}

在这里插入图片描述

在这里插入图片描述


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

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

相关文章

iPhone手机记笔记工具选择用哪个

iPhone手机大家应该都比较熟悉&#xff0c;其使用性能是比较流畅的&#xff0c;在iPhone手机上记录笔记可以帮助大家快速地进行总结工作、记录工作内容等&#xff0c;在iPhone手机上记笔记工具选择用哪个呢&#xff1f; 可以在iPhone手机上使用的笔记工具是比较多的&#xff0…

OPC UA客户端工具Softing OPC Client使用图文教程

简介 Softing OPC Client 是一个用于与 OPC (OLE for Process Control) 服务器进行通信的工具。下面是一个详细的图文教程&#xff0c;以帮助您使用 Softing OPC Client 进行 OPC 通信。 1. 下载和安装 Softing OPC Client 首先&#xff0c;您需要从 Softing 官方网站下载 S…

csgo饰品市场会崩盘吗?如何评价现在的csgo饰品市场?

如何评价现在的csgo市场&#xff1f; csgo饰品市场会崩盘吗&#xff1f;如何评价现在的csgo饰品市场&#xff1f; 如何来评价现在CSGO市场&#xff1f;哈喽&#xff0c;大家好&#xff0c;我是童话姐姐&#xff0c;那么从长远来看&#xff0c;我觉得这个CSGO市场它一定是呈现一…

渗透测试KAILI系统的安装环境(第八课)

KAILI系统的安装环境(第八课) Kaili是一款基于PHP7的高性能微服务框架&#xff0c;其核心思想是面向服务的架构&#xff08;SOA&#xff09;&#xff0c;支持http、websocket、tcp等多种通信协议&#xff0c;同时还提供了RPC、Service Mesh、OAuth2等功能。Kaili框架非常适合构…

c语言练习85:通讯录的实现(基于顺序表实现)

通讯录的实现(基于顺序表实现&#xff09; 基于动态顺序表实现通讯录 C语⾔基础要求&#xff1a;结构体、动态内存管理、顺序表、⽂件操作 1、功能要求 1&#xff09;⾄少能够存储100个⼈的通讯信息 2&#xff09;能够保存⽤⼾信息&#xff1a;名字、性别、年龄、电话、地址…

Android性能优化,可以从那些方面解决?方案一览

说到Android性能优化大家都很熟悉&#xff0c;这是一个老生畅谈的话题与技术。本篇讲讲Android性能优化需要学习那些&#xff0c;让这些技术做到极致。虽然老生常谈但是一直是一个合格的Android开发人员需要掌握的重点。要想进入大厂也是重要的敲门砖。 Android性能优化重要性…

【FreeRTOS】【STM32】02 FreeRTOS 移植

基于 [野火]《FreeRTOS%20内核实现与应用开发实战—基于STM32》 正点原子《STM32F429FreeRTOS开发手册_V1.2》 准备 基础工程&#xff0c;例如点灯 FreeRTOS 系统源码 FreeRTOS 移植 上一章节已经说明了Free RTOS的源码文件在移植时所需要的&#xff0c;FreeRTOS 为我们提供…

leetcode:1967. 作为子字符串出现在单词中的字符串数目(python3解法)

难度&#xff1a;简单 给你一个字符串数组 patterns 和一个字符串 word &#xff0c;统计 patterns 中有多少个字符串是 word 的子字符串。返回字符串数目。 子字符串 是字符串中的一个连续字符序列。 示例 1&#xff1a; 输入&#xff1a;patterns ["a","abc&…

AlGaN/GaN结构的氧基数字蚀刻

引言 宽带隙GaN基高电子迁移率晶体管(HEMTs)和场效应晶体管(fet)能够提供比传统Si基高功率器件更高的击穿电压和电子迁移率。常关GaN非常需要HEMT来降低功率并简化电路和系统架构&#xff0c;这是GaN HEMT技术的主要挑战之一。凹进的AlGaN/GaN结构是实现常关操作的有用选择之一…

使用IDEA自带功能将WSDL转java

好像IDEA2018版本之后不再支持webservice转java&#xff0c;可以下载2018.3.6版本的IDEA&#xff08;直接IDEA官网下载即可&#xff09;&#xff0c;然后打开一个项目&#xff0c;在根目录处单击右键 选择Generate Java Code From Wsdl...&#xff0c; 选择OK&#xff0c;即可…

二、DMSP/OLS夜光数据校正之饱和校正

一、前言 首先需要将DMSP/OLS夜光数据下载,那么这里方便大家,可以直接私信我获得DMPS/OLS和NPP/VIIRS夜光原始数据,以百度云网盘形式分享给大家。 当把34期DMSP/OLS夜光数数据下载至电脑之后,解压后可以看到如下图的数据。 选择稳定平均灯光数据作为我们研究数据,也就是F…

MYSQL的事务原理

事务基础 事务概念 事务是一组操作的集合&#xff0c;它是一个不可分割的工作单位&#xff0c;事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求&#xff0c;即这些操作要么同时成功&#xff0c;要么同时失败。 事务特性 原子性&#xff08;Atomicity&#xff09…

中兴通讯加入 “数字孪生网络基础框架”开源合作计划

在近日举行的“预见未来——数字孪生网络&#xff08;DTN&#xff09;”分论坛上&#xff0c;中国移动研究院不仅发布了“数字孪生网络基础框架”成果&#xff0c;同时与中兴通讯等合作伙伴正式启动了“数字孪生网络基础框架”开源&#xff08;Open-DTN&#xff09;合作计划。 …

基于SSM的国学文化网站设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…

Lenovo联想笔记本IdeaPad YOGA 710-11IKB(80V6)原装出厂Win10系统镜像

下载链接&#xff1a;https://pan.baidu.com/s/1qAJ6QSQ0NV1Lmwv3YTqwHw?pwdrqxa 系统自带所有驱动、出厂主题壁纸LOGO、Office办公软件、联想电脑管家等预装程序 所需要工具&#xff1a;16G或以上的U盘 文件格式&#xff1a;ISO 文件大小&#xff1a;9.62GB 注&#xff1a;…

API攻防-接口安全SOAPOpenAPIRESTful分类特征导入项目联动检测

文章目录 概述什么是接口&#xff1f; 1、API分类特征SOAP - WSDLWeb services 三种基本元素&#xff1a; OpenApi - Swagger UISpringboot Actuator 2、API检测流程Method&#xff1a;请求方法URL&#xff1a;唯一资源定位符Params&#xff1a;请求参数Authorization&#xff…

关键词搜索1688商品列表数据接口,1688商品列表数据接口

在网页抓取方面&#xff0c;可以使用 Python、Java 等编程语言编写程序&#xff0c;通过模拟 HTTP 请求&#xff0c;获取1688网站上的商品页面。在数据提取方面&#xff0c;可以使用正则表达式、XPath 等方式从 HTML 代码中提取出有用的信息。值得注意的是&#xff0c;1688网站…

docker--使用docker login 报错解决方案

我们在本地使用 docker login 命令登录时报错&#xff0c;可以尝试一下先 docker logout 命令退出登录后&#xff0c;在使用 docker login命令进行登录操作&#xff1b; docker logout

OpenCV4(C++)—— 直方图

文章目录 前言一、计算直方图二、归一化三、直方图均衡化四、直方图匹配 前言 直方图(Histogram)最开始在统计学中被提出&#xff0c;由一系列高度不等的纵向条纹或线段表示数据分布的情况。 一般用横轴表示数据类型&#xff0c;纵轴表示分布情况。在图像领域&#xff0c;直方…

vue3+ts项目03 element-plus、vue-router、pinia

yarn add element-plus yarn add element-plus/icons-vue修改main.ts import { createApp } from vue import App from ./App.vueimport ElementPlus from element-plus import element-plus/dist/index.css import zhCn from element-plus/dist/locale/zh-cn.mjsconst app c…