多线程网络实战之仿qq群聊的服务器和客户端

news2024/12/25 1:18:38

目录

一、前言

二、设计需求

1.服务器需求 

2.客户端需求

三、服务端设计

1.项目准备

 2.初始化网络库

3.SOCKET创建服务器套接字

4. bind 绑定套接字

 5. listen监听套接字

 6. accept接受客户端连接

7.建立套接字数组

8. 建立多线程与客户端通信

9. 处理线程函数,收消息

10. 发消息给客户端

 11.处理断开的客户端

四、客户端设计

1.项目准备

2. 处理main函数参数

 3.初始化网络库

4.SOCKET创建客户端套接字

5. 配置IP地址和端口号,连接服务器

6.创建两线程,发送和接收

 7.处理发送消息线程函数

五、项目运行

1.编译生成可执行文件

2.运行可执行程序

3.进行通讯

六、总代码展示

1.服务端代码:

2.客户端代码:

七、最后


一、前言

        今天我们不学习其他的知识点,主要是复习之前学习过的TCP网络通信和多线程以及线程同步互斥,然后结合这以上知识点设计实现一个小的项目,主要仿照qq群聊的服务器可客户端的实现,下面我将会说明一下设计需求,以下是整个设计示意图。

二、设计需求

1.服务器需求 

        需求一对于每一个上线连接的客户端,服务端会起一个线程去维护。        

        需求二:将服务器受到的消息转发给全部的客户端。例如:服务器接收客户端A的消息后,将立即发送给客户端A,B,C...

        需求三:当某个客户端断开(下线),需要处理断开的链接。

2.客户端需求

        需求一:请求连接上线,   

        需求二:发消息给服务器。

        需求三:客户端等待服务端的消息。

        需求四:等待用户自己的关闭(下线)。

三、服务端设计

1.项目准备

        在创建项目后,引入一些必需的头文件以及创建项目需要的宏,例如:允许客户端连接的最大数量,接收文件字节的大小,客户端连接的个数等等。

#include <stdio.h>
#include <windows.h>
#include <process.h>
#include <iostream>
#pragma comment(lib, "ws2_32.lib")


#define MAX_CLEN 256        // 最大连接数量
#define MAX_BUF_SIZE 1024   // 接收文件大小

SOCKET clnSockets[MAX_CLEN]; // 所有的连接客户端的socket
int clnCnt = 0;   // 客户端连接的个数

// 互斥的句柄
HANDLE hMutex; 
 2.初始化网络库

        WSAStartup初始化Winsock,这个函数用于初始化网络环境,都是固定写法,必须要有的,直接复制粘贴即可。

    // 1. 初始化库
    WSADATA wsaData;
	int stu = WSAStartup(MAKEWORD(2, 2), &wsaData);
	if (stu != 0) {
		std::cout << "WSAStartup 错误:" << stu << std::endl;
		return 0;
	}
3.SOCKET创建服务器套接字

         这和我们之前学的windwos网络一样都是固定写法,重点时查看函数原型以及它的参数,代码如下:

	// 2. socket 创建套接字
	SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);

	if (sockSrv == INVALID_SOCKET)
	{
		std::cout << "socket failed!" << GetLastError() << std::endl;
		WSACleanup(); //释放Winsock库资源
		return 1;
	}
4. bind 绑定套接字

        这个流程主要是绑定服务器的IP地址,端口号,以及协议版本。 

	// 3 bind 绑定套接字
    SOCKADDR_IN addrSrv;
	addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);   // 地址 IP地址any
	addrSrv.sin_family = AF_INET;    // ipv4协议
	addrSrv.sin_port = htons(6000);  // 端口号
	if ( SOCKET_ERROR == bind(sockSrv, (sockaddr*)&addrSrv, sizeof(SOCKADDR)))
	{
		std::cout << "bind failed!" << GetLastError() << std::endl;
		WSACleanup(); //释放Winsock库资源
		return 1;
	}
 5. listen监听套接字

        listen函数最重要的是理解它的第二个参数,为等待连接的最大队列长度 ,这个解释我有专门出过一篇文章windows网络进阶之listen参数含义。

	// 4. 监听
	if (listen(sockSrv, 5) == SOCKET_ERROR) // 5 是指最大的监听数目,执行到listen
	{
		printf("listen error = %d\n", GetLastError());
		return -1;
	}
 6. accept接受客户端连接

        对于每一个被接受的连接请求,accept函数都会创建一个新的套接字,用于与该客户端的后续通信。也都是固定流程,后面互斥和多线程就比较难理解了。

    // 5. accept接受客户端连接
    SOCKADDR_IN addrCli;
	int len = sizeof(SOCKADDR);

	while (true)
	{
		// 接受客户端的连接
		SOCKET sockCon = accept(sockSrv, (sockaddr*)&addrCli, &len);
    }
7.建立套接字数组

        将accept生成的套接字放入全局套接字数组中,同时加上互斥锁。

    //创建一个互斥对象
    hMutex = CreateMutex(NULL, false, NULL);

    while (true)
	{
		// 接受客户端的连接
		SOCKET sockCon = accept(sockSrv, (sockaddr*)&addrCli, &len);
		// 全局变量要加锁
		WaitForSingleObject(hMutex, INFINITE);
		// 将连接放到数组里面
		clnSockets[clnCnt++] = sockCon;
		// 解锁
		ReleaseMutex(hMutex);
    }

	closesocket(sockSrv);
	CloseHandle(hMutex);
	WSACleanup();
	return 0;
8. 建立多线程与客户端通信

        每通过accept函数返回的新创建的套接字,就建立一个线程去维护。

//创建一个互斥对象
hMutex = CreateMutex(NULL, false, NULL);
while (true)
	{
		// 接受客户端的连接
		SOCKET sockCon = accept(sockSrv, (sockaddr*)&addrCli, &len);

		// 全局变量要加锁
		WaitForSingleObject(hMutex, INFINITE);
		// 将连接放到数组里面
		clnSockets[clnCnt++] = sockCon;
		// 解锁
		ReleaseMutex(hMutex);

		// 每接收一个客户端的连接,都安排一个线程去维护
		hThread = (HANDLE)_beginthreadex(NULL, 0, &handleCln, (void*)&sockCon, 0, NULL);

		printf("Connect client IP = %s\n, Num = %d \n", inet_ntoa(addrCli.sin_addr), clnCnt);
	}

	closesocket(sockSrv);
	CloseHandle(hMutex);
	WSACleanup();
	return 0;
9. 处理线程函数,收消息

        上个步骤我们对每一个接受连接的套接字都创建了线程,现在我们开始来写线程函数中的逻辑代码,主要有三个部分:收到客户端的消息,将收到的消息再发给所有客户端,处理断开的客户端。

        下面我们开始完成第一个部分: 收到客户端的消息。

        因为客户端发消息会不止一个,所以我们要建立while循环,通关判断接收到的消息来判断,如果为0就退出循环。

// 处理线程函数, 收发消息
unsigned WINAPI handleCln(void *arg)
{
	SOCKET hClnSock = *((SOCKET *)arg);
	int iLen = 0;
	char recvBuff[MAX_BUF_SIZE] = { 0 };
	
	while (1)
	{
		// iLen 成功时返回接收的字节数(收到EOF时为0),失败时返回SOCKETERROR。
		iLen = recv(hClnSock, recvBuff, MAX_BUF_SIZE, 0);
		// 
		if (iLen >= 0)
		{
			// 将收到的消息转发给所有客户端
			SendMsg(recvBuff,iLen);
		}
		else
		{
			break;
		}
	}
10. 发消息给客户端

        完成第二个部分: 将收到的消息再发给所有客户端

         因为是仿照qq的小demo,所以服务器一旦收到消息,就要再发送给所有的客户端。这段逻辑写在SendMsg 函数中,同时还需要注意因为在多线程中,所以要避免多个线程同时访问共享资源时产生数据不一致的问题,需要加互斥锁和解锁。

// 将收到的消息转发给所有客户端
void SendMsg(char* msg, int len)
{
	int i;
	WaitForSingleObject(hMutex, INFINITE);
	for (i = 0; i < clnCnt; i++)
	{
		send(clnSockets[i], msg, len, 0);
	}
	ReleaseMutex(hMutex);
}
 11.处理断开的客户端

        完成第三个部分: 处理断开的客户端。

        这里也是通过 for 循环遍历 socket 数组,通过匹配每一项,如果相匹配,就然后断开连接。同时  socket 数组 中的数量减 1。

// 处理消息, 收发消息
unsigned WINAPI handleCln(void *arg)
{
	SOCKET hClnSock = *((SOCKET *)arg);
	int iLen = 0;
	char recvBuff[MAX_BUF_SIZE] = { 0 };
	
	while (1)
	{
		// iLen 成功时返回接收的字节数(收到EOF时为0),失败时返回SOCKETERROR。
		iLen = recv(hClnSock, recvBuff, MAX_BUF_SIZE, 0);
		// 
		if (iLen >= 0)
		{
			// 将收到的消息转发给所有客户端
			SendMsg(recvBuff,iLen);
		}
		else
		{
			break;
		}
	}

	printf("此时连接的客户端数量 = %d\n", clnCnt);
	WaitForSingleObject(hMutex, INFINITE);
	for (int i = 0; i < clnCnt; i++)
	{
		// 找到哪个连接下线的,移除这个连接
		if (hClnSock == clnSockets[i])
		{
			while (i++ < clnCnt)
			{
				clnSockets[i] = clnSockets[i + 1];
			}

			break;
		}
	}
	// 断开连接减 1 
	clnCnt--;
	printf("断开连接后连接的客户端数量 = %d\n", clnCnt);
	ReleaseMutex(hMutex);
	// 断开连接
	closesocket(hClnSock);
	return 0;
}

四、客户端设计

1.项目准备

        客户端设计和服务器端其实差别不大,代码有些基本都相同,逻辑也大多一致,所以有些代码不在过多赘述。

        项目准备代码:

#include <stdio.h>
#include <windows.h>
#include <process.h>
#include <iostream>
#pragma comment(lib, "ws2_32.lib")

#define NAME_SIZE 256   
#define MAX_BUF_SIZE 1024

char szName[NAME_SIZE] = "[DEFAULT]"; //  默认的昵称
char szMsg[MAX_BUF_SIZE];    // 收发数据的大小
2. 处理main函数参数

        项目为仿qq群聊,所以我用main函数中的命令行参数作为我们输入的每一个客户端的名字,项目启动在终端开始启动,否则就退出程序。

int main(int argc, char* argv[])
{
	if (argc != 2)
	{
		printf("必须输入两个参数,包括昵称\n");
		printf("例如: WXS\n");
		system("pause");
		return -1;
	}
	sprintf_s(szName, "[%s]", argv[1]);
	printf("this is Client");
}
 3.初始化网络库

        和服务器端代码一样。

    // 初始化库
	WSADATA wsaData;
	int stu = WSAStartup(MAKEWORD(2, 2), &wsaData);
	if (stu != 0) {
		std::cout << "WSAStartup 错误:" << stu << std::endl;
		return 0;
	}
4.SOCKET创建客户端套接字

        以服务器类似。 

    SOCKET sockCli = socket(AF_INET, SOCK_STREAM, 0);

	if (sockCli == INVALID_SOCKET)
	{
		std::cout << "socket failed!" << GetLastError() << std::endl;
		WSACleanup(); //释放Winsock库资源
		return 1;
	}
5. 配置IP地址和端口号,连接服务器

        也是基本固定写法。

	// 配置IP地址 和 端口号
    SOCKADDR_IN addrSrv;
	addrSrv.sin_family = AF_INET;    // ipv4协议
	addrSrv.sin_addr.S_un.S_addr = inet_addr("192.168.1.7"); // 地址 IP地址any
	addrSrv.sin_port = htons(6000);  // 端口号

	// 连接服务器
	int res = connect(sockCli, (sockaddr*)&addrSrv, sizeof(sockaddr));
6.创建两线程,发送和接收

         这里我们创建了两个线程,分别处理发送消息给客户端同时接收消息。同时这个函数WaitForSingleObject 会阻塞主进程代码,直到子进程结束。

	// 定义两个线程 
	HANDLE hSendThread, hRecvThread;
    // 发送消息
	hSendThread = (HANDLE)_beginthreadex(NULL, 0, &SendMsg, (void*)&sockCli, 0, NULL);
	// 接收消息
	hRecvThread = (HANDLE)_beginthreadex(NULL, 0, &RecvMsg, (void*)&sockCli, 0, NULL);
    
    // 阻塞代码,处理子线程执行完后再执行
	WaitForSingleObject(hSendThread,INFINITE);
	WaitForSingleObject(hRecvThread, INFINITE);
 7.处理发送消息线程函数

        我们客户端发送消息是通过控制台程序进行发送的,所以要用到用户输入。同时发送的时候带上自己的名字前缀,也要处理快捷键客户端下线的逻辑,不能一致发送消息。

unsigned WINAPI SendMsg(void* arg)
{
	SOCKET hClnSock = *((SOCKET*)arg);
	char szNameMsg[NAME_SIZE + MAX_BUF_SIZE] = { 0 };   // 昵称和消息

	while (1)
	{
		memset(szMsg, 0, MAX_BUF_SIZE);
		// 阻塞这一句,等待控制台的消息
		//fgets(szMsg, MAX_BUF_SIZE, stdin);
		// 第二种写法
		std::cin >> szMsg;
		if (!strcmp(szMsg, "Q\n") || !strcmp(szMsg, "q\n"))
		{
			// 处理下线
			closesocket(hClnSock);
			exit(0);
		}
		// 拼接  名字和字符串一起发送
		sprintf_s(szNameMsg, "%s %s", szName, szMsg);
		send(hClnSock, szNameMsg, strlen(szNameMsg) + 1, 0);
	}
}

 7.处理接收消息线程函数

        这里接收消息比较简单,和正常接收客户端消息的逻辑差不多,代码如下:

unsigned WINAPI RecvMsg(void* arg)
{
	SOCKET hClnSock = *((SOCKET*)arg);
	char szNameMsg[NAME_SIZE + MAX_BUF_SIZE] = { 0 };   // 昵称和消息
	int len;
	while (1)
	{
		
		len = recv(hClnSock, szNameMsg, sizeof(szNameMsg), 0);
		if (len <= 0)
		{
			break;
			return -2;
		}
		szNameMsg[len] = 0;
		std::cout << szNameMsg << std::endl;
		// fputs(szNameMsg, stdout);
	}
}

五、项目运行

        以上我们分别讲解了服务器和客户端代码的实现逻辑,现在我们来进行步骤验证我们的操作结果。

1.编译生成可执行文件

        如图所示:

2.运行可执行程序

        这里要注意服务器直接运行exe文件即可,而客户端要通过命令行输入运行。

        服务器端:

        客户端运行需要打开终端,输入exe文件的路径,以及名字。另外进行通讯还需要打开多个客户端。

3.进行通讯

         结果展示为:

六、总代码展示

1.服务端代码:

        如下所示:

// 1. 对于每一个上线的客户端,服务端会起一个线程去维护
// 2. 将受到的消息转发给全部的客户端
// 3. 当某个客户端断开(下线),需要处理断开的链接。怎么处理呢?
#include <stdio.h>
#include <windows.h>
#include <process.h>
#include <iostream>
#pragma comment(lib, "ws2_32.lib")


#define MAX_CLEN 256   
#define MAX_BUF_SIZE 1024

SOCKET clnSockets[MAX_CLEN]; // 所有的连接客户端的socket
int clnCnt = 0;   // 客户端连接的个数

HANDLE hMutex; 

// 将收到的消息转发给所有客户端
void SendMsg(char* msg, int len)
{
	int i;
	WaitForSingleObject(hMutex, INFINITE);
	for (i = 0; i < clnCnt; i++)
	{
		send(clnSockets[i], msg, len, 0);
	}
	ReleaseMutex(hMutex);
}

// 处理消息, 收发消息
unsigned WINAPI handleCln(void *arg)
{
	SOCKET hClnSock = *((SOCKET *)arg);
	int iLen = 0;
	char recvBuff[MAX_BUF_SIZE] = { 0 };
	
	while (1)
	{
		// iLen 成功时返回接收的字节数(收到EOF时为0),失败时返回SOCKETERROR。
		iLen = recv(hClnSock, recvBuff, MAX_BUF_SIZE, 0);
		// 
		if (iLen >= 0)
		{
			// 将收到的消息转发给所有客户端
			SendMsg(recvBuff,iLen);
		}
		else
		{
			break;
		}
	}

	printf("此时连接的客户端数量 = %d\n", clnCnt);
	WaitForSingleObject(hMutex, INFINITE);
	for (int i = 0; i < clnCnt; i++)
	{
		// 找到哪个连接下线的,移除这个连接
		if (hClnSock == clnSockets[i])
		{
			while (i++ < clnCnt)
			{
				clnSockets[i] = clnSockets[i + 1];
			}

			break;
		}
	}
	// 断开连接减 1 
	clnCnt--;
	printf("断开连接后连接的客户端数量 = %d\n", clnCnt);
	ReleaseMutex(hMutex);
	// 断开连接
	closesocket(hClnSock);
	return 0;
}

int main(int argc, char* argv[])
{	
	printf("this is Server\n");
	//0. 初始化网络
#if 1
// 0 初始化网络库
// 初始化库
	WSADATA wsaData;
	int stu = WSAStartup(MAKEWORD(2, 2), &wsaData);
	if (stu != 0) {
		std::cout << "WSAStartup 错误:" << stu << std::endl;
		return 0;
	}
#endif
	HANDLE hThread;

	// 1.  创建一个互斥对象
	hMutex = CreateMutex(NULL, false, NULL);

	// 2. socket 创建套接字
	SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);

	if (sockSrv == INVALID_SOCKET)
	{
		std::cout << "socket failed!" << GetLastError() << std::endl;
		WSACleanup(); //释放Winsock库资源
		return 1;
	}

	// 3 bind 绑定套接字
	SOCKADDR_IN addrSrv;
	addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);   // 地址 IP地址any
	addrSrv.sin_family = AF_INET;    // ipv4协议
	addrSrv.sin_port = htons(6000);  // 端口号
	if ( SOCKET_ERROR == bind(sockSrv, (sockaddr*)&addrSrv, sizeof(SOCKADDR)))
	{
		std::cout << "bind failed!" << GetLastError() << std::endl;
		WSACleanup(); //释放Winsock库资源
		return 1;
	}
	// 4. 监听
	if (listen(sockSrv, 5) == SOCKET_ERROR) // 5 是指最大的监听数目,执行到listen
	{
		printf("listen error = %d\n", GetLastError());
		return -1;
	}
	
	// 5
	SOCKADDR_IN addrCli;
	int len = sizeof(SOCKADDR);

	while (true)
	{
		// 接受客户端的连接
		SOCKET sockCon = accept(sockSrv, (sockaddr*)&addrCli, &len);

		// 全局变量要加锁
		WaitForSingleObject(hMutex, INFINITE);
		// 将连接放到数组里面
		clnSockets[clnCnt++] = sockCon;
		// 解锁
		ReleaseMutex(hMutex);

		// 每接收一个客户端的连接,都安排一个线程去维护
		hThread = (HANDLE)_beginthreadex(NULL, 0, &handleCln, (void*)&sockCon, 0, NULL);

		printf("Connect client IP = %s\n, Num = %d \n", inet_ntoa(addrCli.sin_addr), clnCnt);
	}

	closesocket(sockSrv);
	CloseHandle(hMutex);
	WSACleanup();
	return 0;
}
2.客户端代码:

        如下所示:

// 客户端做的事情:
//1 请求连接上线,
//2 发消息
//3 客户端等待服务端的消息
//4 等待用户自己的关闭(下线)
#include <stdio.h>
#include <windows.h>
#include <process.h>
#include <iostream>
#pragma comment(lib, "ws2_32.lib")

#define NAME_SIZE 256   
#define MAX_BUF_SIZE 1024

char szName[NAME_SIZE] = "[DEFAULT]"; //  默认的昵称
char szMsg[MAX_BUF_SIZE];    // 收发数据的大小



unsigned WINAPI SendMsg(void* arg)
{
	SOCKET hClnSock = *((SOCKET*)arg);
	char szNameMsg[NAME_SIZE + MAX_BUF_SIZE] = { 0 };   // 昵称和消息

	while (1)
	{
		memset(szMsg, 0, MAX_BUF_SIZE);
		// 阻塞这一句,等待控制台的消息
		//fgets(szMsg, MAX_BUF_SIZE, stdin);
		std::cin >> szMsg;
		if (!strcmp(szMsg, "Q\n") || !strcmp(szMsg, "q\n"))
		{
			// 处理下线
			closesocket(hClnSock);
			exit(0);
		}

		// 拼接  名字和字符串一起发送
		sprintf_s(szNameMsg, "%s %s", szName, szMsg);
		send(hClnSock, szNameMsg, strlen(szNameMsg) + 1, 0);

	}
}

unsigned WINAPI RecvMsg(void* arg)
{
	SOCKET hClnSock = *((SOCKET*)arg);
	char szNameMsg[NAME_SIZE + MAX_BUF_SIZE] = { 0 };   // 昵称和消息
	int len;
	while (1)
	{
		
		len = recv(hClnSock, szNameMsg, sizeof(szNameMsg), 0);
		if (len <= 0)
		{
			break;
			return -2;
		}
		szNameMsg[len] = 0;
		std::cout << szNameMsg << std::endl;
		// fputs(szNameMsg, stdout);


	}
	
}
int main(int argc, char* argv[])
{
	if (argc != 2)
	{
		printf("必须输入两个参数,包括昵称\n");
		printf("例如: WXS\n");
		system("pause");
		return -1;
	}
	sprintf_s(szName, "[%s]", argv[1]);
	printf("this is Client");
	//0. 初始化网络
#if 1
// 0 初始化网络库
// 初始化库
	WSADATA wsaData;
	int stu = WSAStartup(MAKEWORD(2, 2), &wsaData);
	if (stu != 0) {
		std::cout << "WSAStartup 错误:" << stu << std::endl;
		return 0;
	}
#endif
	
	// 定义两个线程 
	HANDLE hSendThread, hRecvThread;

	// 1. 建立 socket
	SOCKET sockCli = socket(AF_INET, SOCK_STREAM, 0);

	if (sockCli == INVALID_SOCKET)
	{
		std::cout << "socket failed!" << GetLastError() << std::endl;
		WSACleanup(); //释放Winsock库资源
		return 1;
	}

	// 2, 配置IP地址 和 端口号
	SOCKADDR_IN addrSrv;
	addrSrv.sin_family = AF_INET;    // ipv4协议
	addrSrv.sin_addr.S_un.S_addr = inet_addr("192.168.1.7"); // 地址 IP地址any
	addrSrv.sin_port = htons(6000);  // 端口号

	// 3. 连接服务器
	int res = connect(sockCli, (sockaddr*)&addrSrv, sizeof(sockaddr));

	// 4. 发送服务器消息,启动线程
	hSendThread = (HANDLE)_beginthreadex(NULL, 0, &SendMsg, (void*)&sockCli, 0, NULL);
	// 5. 等待
	hRecvThread = (HANDLE)_beginthreadex(NULL, 0, &RecvMsg, (void*)&sockCli, 0, NULL);

	WaitForSingleObject(hSendThread,INFINITE);
	WaitForSingleObject(hRecvThread, INFINITE);

	closesocket(sockCli);
	WSACleanup();

	return 0;
}

七、最后

        制作不易,熬夜肝的,还请多多点赞,拯救下秃头的博主吧!!          

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

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

相关文章

iptables实现端口转发ssh

iptables实现端口转发 实现使用防火墙9898端口访问内网front主机的22端口&#xff08;ssh连接&#xff09; 1. 防火墙配置(lb01) # 配置iptables # 这条命令的作用是将所有目的地为192.168.100.155且目标端口为19898的TCP数据包的目标IP地址改为10.0.0.148&#xff0c;并将目标…

【Java】垃圾回收学习笔记(一):Root Search 根可达算法+垃圾回收的起点

文章目录 1. 引用计数法优点缺点 2. 可达性分析 Root Search2.1 那些对象是GC Roots2.2 引用的分类2.3 回收方法区 3. 实现细节3.1 GC的起点&#xff1a;节点枚举OopMap&#xff1a;帮助高效的根节点枚举 3.2 何时开始GC&#xff1a;安全点与安全区域如何选取安全点如何让程序进…

数据驱动的内容优化:Kompas.ai如何提升内容表现

在数字化营销时代&#xff0c;内容是企业与用户沟通的重要桥梁。然而&#xff0c;随着信息量的爆炸性增长&#xff0c;如何让内容在激烈的竞争中脱颖而出&#xff0c;成为每个营销人员面临的问题。数据驱动的内容优化策略&#xff0c;通过精准分析和科学决策&#xff0c;帮助品…

华为OD机试 - 员工派遣(Java 2024 D卷 200分)

华为OD机试 2024D卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收录于《华为OD机试&#xff08;JAVA&#xff09;真题&#xff08;D卷C卷A卷B卷&#xff09;》。 刷的越多&#xff0c;抽中的概率越大&#xff0c;每一题都有详细的答题思路、详细的代码注释、样例测…

基于字典学习的地震数据降噪(MATLAB R2021B)

稀疏表示基于研究者们提出了许多变换基函数的方法逐渐成型&#xff0c;比如小波域&#xff0c;曲波域&#xff0c;dreamlet 域等&#xff0c;其原理是利用地震信号在变换域内的稀疏性和可分离性以去除噪声。继 Donoho发表非线性去噪方法-小波阈值萎缩方法&#xff0c;在后续的研…

Swift 中 map 和 flatMap 的区别 (入门版)

在 Swift 中&#xff0c;map 和 flatMap 是用于处理集合类型&#xff08;如数组、字典、集合等&#xff09;的两个重要方法。尽管它们看起来相似&#xff0c;但它们在处理集合中的元素和结果时有着不同的行为。 map 方法 map 方法会对集合中的每一个元素应用一个变换&#xff…

C++ 面试宝典之:空类大小究竟是不是 0?

以下内容为本人的学习笔记&#xff0c;如需要转载&#xff0c;请声明原文链接 微信公众号「ENG八戒」https://mp.weixin.qq.com/s/pD4bIjX2kDzo8gbYRPktPQ 首先&#xff0c;空类是什么&#xff1f;空类指的是不包含任何数据成员的类&#xff0c;但可能包含方法成员。 实例化时…

Redis常用命令——Set、Zset篇

文章目录 一、Set相关命令操作 SADD SMEMBERS SISMEMBER SCARD SPOP SMOVE SREM SINTER 与 SINTERSTORE SUNION 与 SUNIONSTORE SDIFF 与 SDIFFSTORE Set命令小结 二、Zset 相关命令操作 ZADD ZCARD ZCOUNT ZRANGE ZREVRANGE ZPOPMAX BZPOPMAX ZPOPMIN 与 BZPOPMIN ZRANK 与 …

AI Earth ——开发者模式案例10:基于 CNN 的 AI 分类模型开发

基于 CNN 的 AI 分类模型开发 本案例主要介绍如何快速利用 AIE Python SDK 创建机器学习建模流程。我们主要使用到 Python SDK的Machine Learning Proxy 模块(下文简称 AieMlProxy )。该模块涵盖了一系列用户与训练集群之间的交互接口,包括:鉴权、数据加载、训练任务提交、…

OpenCV杂记(4):OpenCV之色彩映射(伪彩applyColorMap)

1. 简述 我们在开发基于热成像&#xff08;红外&#xff09;或者做深度估计应用时&#xff0c;为了便于直观的观察&#xff0c;常常将检测结果进行色彩上的映射&#xff0c;这样便可以很直观的看出哪里温度高&#xff0c;哪里温度低&#xff0c;或者哪里深度更深或更浅。 我们将…

【STM32项目】基于Stm32搞怪盒子的设计(完整工程资料)

基于stm32搞怪的盒子设计 前言&#xff1a; 最近我看到一个极具创意的搞怪盒子&#xff0c;设计得相当有意思。作为一个热衷于电子DIY的狂热爱好者&#xff0c;怎能错过这样一个有趣的项目呢&#xff1f;于是&#xff0c;我决定亲自动手&#xff0c;设计一个属于自己的、独一无…

代码随想录——单调递增的数字(Leetcode738)

题目链接 贪心 class Solution {public int monotoneIncreasingDigits(int n) {char[] digits String.valueOf(n).toCharArray();int flag digits.length;for (int i digits.length - 1; i > 0; i--) {if (digits[i] < digits[i - 1]) {flag i;digits[i - 1]--;}}…

KVM把新添加的磁盘扩容到根目录

1、对新增的磁盘进行分区&#xff08;注&#xff1a;可省略&#xff09; PS&#xff1a;使用fdisk或gdisk&#xff08;大于2T时使用&#xff09;对新增磁盘进行分区。 [rootkvm-clinet ~]# fdisk/dev/sdb Welcome to fdisk (util‐linux 2.23.2).4 Changes will remain in …

GISSERVER 管理器发布切片服务

GISSERVER 管理器 1.0(私有化地图离线部署)可以为您发布切片服务&#xff0c;切片服务的概念可以见我以前的文章&#xff1a;如何生成像谷歌高德一样的切片地图。除了我们自己制作的切片外&#xff0c;我们最常见的是利用各类地图下载器下载的在线地图的切片。见&#xff1a;栅…

Qt文档阅读笔记-Queued Custom Type Example

此篇展示了使用Qt编写多线程程序。 概述 此案例创建一Block类&#xff0c;用于存储数据&#xff0c;并且在元对象系统中注册后&#xff0c;在多线程中进行信号与槽函数的连接中充当参数。 Block类 在元对象系统中&#xff0c;注册类&#xff0c;需要类在public部分提供默认构…

56、最近邻向量量化(LVQ) 网络训练对输入向量进行分类

1、LVQ 网络训练对输入向量进行分类简介 1&#xff09;简介 LVQ&#xff08;最近邻向量量化&#xff09;是一种简单而有效的神经网络模型&#xff0c;用于对输入向量进行分类。LVQ网络通过学习一组原型向量&#xff08;也称为代码矢量或参考向量&#xff09;&#xff0c;来表…

如何恢复已删除的音频文件

设备中文件被意外删除并不是什么新鲜事。但是&#xff0c;如果文件是你最喜欢的 MP3 歌曲&#xff0c;那就太令人沮丧了。但你知道吗&#xff0c;有一种方法可以从 Windows 机器中恢复已删除的音乐文件。尝试奇客数据恢复并检索已删除的音频文件。虽然产品名称听起来不像可以帮…

HTML5使用<blockquote>标签:段落缩进

使用<blockquote>标签可以实现页面文字的段落缩进。这一标签也是每使用一次&#xff0c;段落就缩进一次&#xff0c;并且可以嵌套使用&#xff0c;以达到不同的缩进效果。语法如下&#xff1a; <blockquote>文字</blockquote> 【实例】使用<blockquote&…

DC/AC电源模块的节能特点与环保优势

BOSHIDA DC/AC电源模块的节能特点与环保优势 在过去的几十年里&#xff0c;人们对电力的需求不断增加&#xff0c;这导致了电力系统的扩展和增加&#xff0c;为了满足这些需求&#xff0c;传统的交流&#xff08;AC&#xff09;电源系统在全球范围内广泛使用。然而&#xff0c…

8.1 栅格图层符号化整体说明

文章目录 前言栅格图层符号化总结 前言 地图制图是QGIS的优势所在&#xff0c;QGIS包含完整的地图制图功能&#xff0c;如标注与注记、符号化与地图综合等多种功能。 在地图制图过程中&#xff0c;必须经过符号化和地图综合两个最基本的操作。符号化&#xff08;Symbolization&…