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

目录

一、前言

二、设计需求

1.服务器需求

2.客户端需求

三、服务端设计

1.项目准备

2.初始化网络库

3.SOCKET创建服务器套接字

[4. bind 绑定套接字](#4. bind 绑定套接字)

[5. listen监听套接字](#5. listen监听套接字)

[6. accept接受客户端连接](#6. accept接受客户端连接)

7.建立套接字数组

[8. 建立多线程与客户端通信](#8. 建立多线程与客户端通信)

[9. 处理线程函数,收消息](#9. 处理线程函数,收消息)

[10. 发消息给客户端](#10. 发消息给客户端)

11.处理断开的客户端

四、客户端设计

1.项目准备

[2. 处理main函数参数](#2. 处理main函数参数)

3.初始化网络库

4.SOCKET创建客户端套接字

[5. 配置IP地址和端口号,连接服务器](#5. 配置IP地址和端口号,连接服务器)

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

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

五、项目运行

1.编译生成可执行文件

2.运行可执行程序

3.进行通讯

六、总代码展示

1.服务端代码:

2.客户端代码:

七、最后


一、前言

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

二、设计需求

1.服务器需求

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

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

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

2.客户端需求

需求一:请求连接上线,

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

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

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

三、服务端设计

1.项目准备

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

cpp 复制代码
#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,这个函数用于初始化网络环境,都是固定写法,必须要有的,直接复制粘贴即可。

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

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

cpp 复制代码
	// 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地址,端口号,以及协议版本。

cpp 复制代码
	// 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参数含义

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

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

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

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

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

cpp 复制代码
    //创建一个互斥对象
    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函数返回的新创建的套接字,就建立一个线程去维护。

cpp 复制代码
//创建一个互斥对象
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就退出循环。

cpp 复制代码
// 处理线程函数, 收发消息
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 函数中,同时还需要注意因为在多线程中,所以要避免多个线程同时访问共享资源时产生数据不一致的问题,需要加互斥锁和解锁。

cpp 复制代码
// 将收到的消息转发给所有客户端
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。

cpp 复制代码
// 处理消息, 收发消息
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.项目准备

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

项目准备代码:

cpp 复制代码
#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函数中的命令行参数作为我们输入的每一个客户端的名字,项目启动在终端开始启动,否则就退出程序。

cpp 复制代码
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.初始化网络库

和服务器端代码一样。

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

以服务器类似。

cpp 复制代码
    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地址和端口号,连接服务器

也是基本固定写法。

cpp 复制代码
	// 配置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 会阻塞主进程代码,直到子进程结束。

cpp 复制代码
	// 定义两个线程 
	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.处理发送消息线程函数

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

cpp 复制代码
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.处理接收消息线程函数

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

cpp 复制代码
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.服务端代码:

如下所示:

cpp 复制代码
// 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.客户端代码:

如下所示:

cpp 复制代码
// 客户端做的事情:
//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;
}

七、最后

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

相关推荐
仍有未知等待探索1 分钟前
Linux 传输层UDP
linux·运维·udp
zeruns8028 分钟前
如何搭建自己的域名邮箱服务器?Poste.io邮箱服务器搭建教程,Linux+Docker搭建邮件服务器的教程
linux·运维·服务器·docker·网站
北城青15 分钟前
WebRTC Connection Negotiate解决
运维·服务器·webrtc
Hugo_McQueen29 分钟前
pWnos1.0 靶机渗透 (Perl CGI 的反弹 shell 利用)
linux·服务器·网络安全
疯狂的大狗1 小时前
docker进入正在运行的容器,exit后的比较
运维·docker·容器
XY.散人1 小时前
初识Linux · 文件(1)
linux·运维·服务器
长天一色1 小时前
【Docker从入门到进阶】01.介绍 & 02.基础使用
运维·docker·容器
伊玛目的门徒1 小时前
docker 搭建minimalist-web-notepad
运维·docker·notepad
叶北辰CHINA2 小时前
nginx反向代理,负载均衡,HTTP配置简述(说人话)
linux·运维·nginx·http·云原生·https·负载均衡
不惑_2 小时前
在 Ubuntu 安装 Python3.7(没有弯路)
linux·运维·ubuntu