IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> TCP/IP网络模型之异步选择模型 -> 正文阅读

[网络协议]TCP/IP网络模型之异步选择模型

异步选择模型是基于窗口实现

窗口的创建如下:

1.创建窗口结构体

typedef struct tagWNDCLASSEXW {
  UINT      cbSize;
  UINT      style;
  WNDPROC   lpfnWndProc;
  int       cbClsExtra;
  int       cbWndExtra;
  HINSTANCE hInstance;
  HICON     hIcon;
  HCURSOR   hCursor;
  HBRUSH    hbrBackground;
  LPCWSTR   lpszMenuName;
  LPCWSTR   lpszClassName;
  HICON     hIconSm;
} WNDCLASSEXW, *PWNDCLASSEXW, *NPWNDCLASSEXW, *LPWNDCLASSEXW;

2.注册窗口结构体

ATOM RegisterClassExA(
  const WNDCLASSEXA *unnamedParam1
);

3.创建窗口

HWND CreateWindowExA(
  DWORD     dwExStyle,
  LPCSTR    lpClassName,
  LPCSTR    lpWindowName,
  DWORD     dwStyle,
  int       X,
  int       Y,
  int       nWidth,
  int       nHeight,
  HWND      hWndParent,
  HMENU     hMenu,
  HINSTANCE hInstance,
  LPVOID    lpParam
);

4.显示窗口

BOOL ShowWindow(
  HWND hWnd,
  int  nCmdShow
);

5.更新窗口

BOOL UpdateWindow(
  HWND hWnd
);

6.消息循环

// 获取消息
BOOL GetMessage(
  LPMSG lpMsg,
  HWND  hWnd,
  UINT  wMsgFilterMin,
  UINT  wMsgFilterMax
);
// 翻译消息
BOOL TranslateMessage(
  const MSG *lpMsg
);
// 分发消息
LRESULT DispatchMessage(
  const MSG *lpMsg
);

7.回调函数

LRESULT LRESULT DefWindowProcW(
  HWND   hWnd,
  UINT   Msg,
  WPARAM wParam,
  LPARAM lParam
);

服务端代码:(桌面应用程序)

/*
* 注意事项:
	1.创建的工程是窗口
	2.要将当前项目改成多字符集,要不然接收客户端发来的信息时,会出现乱码

	TCP/IP网络模型之异步选择模型是基于窗口实现的
	异步选择模型核心:
		消息队列:
			操作系统为每个窗口创建一个消息队列并且维护
			所以想要使用消息队列,就必须创建一个窗口
	异步选择模型步骤:
		1.将socket绑定到一个消息上,并投递给系统,由系统帮我们进行监视
			WSAAsyncSelect
		2.创建socket数组,将所有的socket装进数组中,并定义一个变量进行记录装进socket数组的个数
		3.通过回调函数中的参数2处理分类处理消息
		4.通过参数3获取socket
		5.通过参数4中的高位字节获取对应的错误码 HOWORD(lParam)
		6.通过参数4中的低位字节获取对应的消息,进行分类处理事件 FD_ACCEPT FD_READ FD_WRITE FD_CLOSE
	网络模型中的 异步选择模型与事件选择模型的机制比较:
		异步选择模型是处理 消息队列 基于消息机制
		事件选择模型是处理 事件集合 基于事件机制
*/

#define _CRT_SECURE_NO_WARNINGS
#include <windows.h>
#pragma comment(lib, "ws2_32.lib")

// 自定义 函数 WSAAsyncSelect() 函数中参数3所需的宏 消息
#define UM_EVENTSELECTMSG WM_USER + 1	// WM_USER + 1 表示防止自定义的消息ID与系统的消息ID冲突

// 定义客户区HDC 中的y坐标
int y = 0; // 每一次使用之后要 y += 15 防止数据覆盖在一起
// 创建socket数组用于装所有的socket 定义成全局的,方便使用
SOCKET g_allsocks[1024];
// 定义记录将socket往socket数组装的个数变量
int g_count;

/*LRESULT CALLBACK WindowProc(
  _In_ HWND   hwnd,   hwnd获取可操作的客户区 HDC 用于TextOut所需的参数1
  _In_ UINT   uMsg,   处理uMsg参数传入的消息 进行分类处理消息
  _In_ WPARAM wParam, 处理通过wParam传入的socket
  _In_ LPARAM lParam  处理lParam,通过HIWORD(lParam)处理错误 
						通过LOWORD(lParam)进行分类处理事件FD_ACCEPT FD_READ FD_WRITE FD_CLOSE
);*/
// 声明窗口结构体中成员所需的回调函数
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

// 将打开网络库、检验版本、创建socket、绑定地址与端口号、开始监听 封装成函数
SOCKET createSocket(HWND hwnd);

// 窗口的主函数是WinMain
// 参数1:当前窗口的实例句柄(实例句柄是应用程序的名字或者叫ID编号), 
// 参数2:上一个的实例句柄,已经无效,在这里只起到占位作用
// 参数3:命令行参数, 
// 参数4:显示方式(窗口是最小化显示还是其他方式显示)
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE PrehInstance, LPSTR lpCmdLine, int nShowCmd)
{
	// 1.创建窗口的结构体
	WNDCLASSEX ws;
	// 给窗口结构体成员赋值
	ws.cbClsExtra = 0;											// 窗口类的额外空间
	ws.cbSize = sizeof(ws);										// 窗口结构体大小
	ws.lpfnWndProc = WindowProc;								// 回调函数
	ws.hInstance = hInstance;									// 当前窗口的实例句柄 填WinMain中的参数1
	ws.hCursor = NULL;											// 光标(鼠标的样式)
	ws.lpszClassName = "基于窗口的异步选择模型";					// 窗口类名 显示窗口左上角的名字
	ws.cbWndExtra = 0;											// 当前窗口的额外空间 
	ws.hbrBackground = CreateSolidBrush(RGB(227, 231, 249));	// 背景颜色
	ws.hIcon = NULL;											// 左上角的图标
	ws.hIconSm = NULL;											// 窗口图标,程序的图标(显示在桌面下的图标)
	ws.lpszMenuName = NULL;										// 窗口菜单名
	ws.style = CS_HREDRAW | CS_VREDRAW;							// 窗口移动之后的样式:水平刷新和垂直刷新

	// 2.注册窗口结构体
	RegisterClassEx(&ws);

	// 3.创建窗口 CreateWindowEx();
	// 参数1:窗口额外的风格, 参数2:窗口类的名字,注意参数2必须和上面的lpszClassName窗口类名一致,如果不一致会出Bug 点击运行打不开窗口
	// 参数3:窗口名字, 参数4:窗口基本的风格
	// 参数5,6:窗口的起始位置, 参数7,8:窗口宽高,参数9:父窗口,参数10:菜单栏,参数11:当前窗口实例句柄,参数12:回调函数的参数
	HWND hwnd = CreateWindowEx(WS_EX_OVERLAPPEDWINDOW, "基于窗口的异步选择模型", "TCP/IP-服务端", WS_OVERLAPPEDWINDOW,
		100, 0, 500, 1000, NULL, NULL, hInstance, NULL);
	// 判断是否成功创建窗口
	if (NULL == hwnd)
	{
		return 0;
	}

	// 4.显示窗口
	ShowWindow(hwnd, nShowCmd);

	// 5.更新窗口
	UpdateWindow(hwnd);

	// 创建客户区可操作的HDC 用完之后记得要释放
	HDC hdc = GetDC(hwnd);

	// 在创建窗口进行消息循环之前调用封装好的创建socket等网络步骤
	SOCKET socketServer = createSocket(hwnd);
	// 7.将socket绑定到一个消息上,并投递给系统 WSAAsyncSelect();
	/* int WSAAsyncSelect(
		  SOCKET s,		参数1:要绑定的socket
		  HWND   hWnd,	参数2:创建窗口类型,标识在网络事件发生时要接收的窗口句柄
		  u_int  wMsg,	参数3:自定义信号,用于在回调函数中参数 (UINT uMsg) 要进行处理的自定义消息
		  long   lEvent 参数4:要处理的网络事件,FD_ACCEPT FD_READ FD_WRITE FD_CLOSE 
							   这里因为绑定的是服务端的socket,所以事件只填FD_ACCEPT
		);*/
	int nAsyncSel = WSAAsyncSelect(socketServer, hwnd, UM_EVENTSELECTMSG, FD_ACCEPT);
	// 检验是否成功绑定事件并投递给系统
	if (SOCKET_ERROR == nAsyncSel)
	{
		int a = WSAGetLastError();
		// 将int类型转成char 类型
		char buf[10] = { 0 };
		_itoa(a, buf, 10);
		TextOut(hdc, 0, y, buf, strlen(buf));
		//释放socket
		closesocket(socketServer);
		//清理网络库
		WSACleanup();
		return 0;
	}
	// 将socket装进到socket集合中
	g_allsocks[g_count] = socketServer;
	g_count++;	// 注意个数要++

	// 声明GetMessage函数中参数1所需的消息
	MSG msg;
	// 6.消息循环 注意GetMessage函数如果参数2填的是创建窗口的类型 hwnd 的话关闭当前窗口当前应用程序没有被关闭
	// 进程没有结束,还在运行,所以要参数2要填NULL
	while (GetMessage(&msg, NULL, 0, 0))
	{
		// 翻译消息 将消息进行处理一下
		TranslateMessage(&msg);
		// 分发消息 再将消息变量msg传给windows,让windows来调用消息处理函数
		DispatchMessage(&msg);
	}
	// 释放客户区HDC
	ReleaseDC(hwnd, hdc);
	return 0;
}

// 实现窗口结构体中成员所需的回调函数 
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	// hwnd获取可操作的客户区 HDC 用于TextOut所需的参数1
	HDC hdc = GetDC(hwnd);	// 记得用完之后要释放
	// 分类处理通过uMsg参数传入的事件
	switch (uMsg)
	{
	case UM_EVENTSELECTMSG:	// 处理自定义事件
	{
		// 获取通过参数wParam 传入的socket
		SOCKET sock = (SOCKET)wParam;
		// 获取消息 通过参数lParam传递进来的消息
		// LOWORD(lParam);	低位存具体的消息
		// HIWORD(lParam);	高位存对应的错误码
		// 1.通过HIWORD(lParam)判断是否出错
		if (0 != HIWORD(lParam))
		{
			if (WSAECONNABORTED == HIWORD(lParam))
			{
				// 处理客户端下线操作
				TextOut(hdc, 0, y, "client close!!!", strlen("client close!!!"));
				y += 15;
				// 关闭对应的socket上的消息,直接将函数WSAAsyncSelect();中的后两个参数置0就可以
				WSAAsyncSelect(sock, hwnd, 0, 0);
				// 从socket数组中删除对应的socket 
				for (int i = 0; i < g_count; i++)
				{
					// 从socket数组中找sock 
					if (sock == g_allsocks[i])
					{
						// 删除对应的sock在数组中的对应的元素 可以直接将最后第一个数组元素赋给当前sock在数组所在的位置,也就是覆盖
						g_allsocks[i] = g_allsocks[g_count - 1];
						// 注意记录的数组个数要--
						g_count--;
						break;
					}
				}
			}
			break;
		}
		// 2.通过LOWORD(lParam)获取具体的消息 
		// 分类处理事件 FD_ACCEPT FD_READ FD_WRITE FD_CLOSE
		switch (LOWORD(lParam))
		{
		case FD_ACCEPT:		// 处理FD_ACCEPT
		{
			// 调用accept函数接收连接 本质创建客户端socket 注意这里的sock 是服务端的socket
			SOCKET socketClient = accept(sock, NULL, NULL);
			// 检验是否成功创将客户端socket
			if (INVALID_SOCKET == socketClient)
			{
				int a = WSAGetLastError();
				break;
			}
			// 将成功创建出来的客户端socket绑定相应的消息,并投递给系统
			int retAsyncSel =  WSAAsyncSelect(socketClient, hwnd, UM_EVENTSELECTMSG, FD_READ | FD_WRITE | FD_CLOSE);
			// 检验是否成功绑定并投递
			if (SOCKET_ERROR == retAsyncSel)
			{
				// 绑定出错
				int a = WSAGetLastError();
				// 关闭成功创建出来的socket
				closesocket(socketClient);
				break;
			}
			// 打印成功调用accept创建客户端socket的提示
			TextOut(hdc, 0, y, "accept ok", strlen("accept ok"));
			y += 15;
			// 将成功绑定并投递的客户端socket装进到socket数组中,方便之后释放使用
			g_allsocks[g_count] = socketClient;
			g_count++;	// 注意个数要++
		}
		break;
		case FD_READ:		// 处理FD_READ
		{
			// 调用recv函数读取客户端发来的信息
			// 定义接收数据的缓存区字符数组
			char recvBuf[1024] = { 0 };
			int retRecv = recv(sock, recvBuf, sizeof(recvBuf), 0);	// 注意这里的sock表示的是客户端的socket
			// 判断是否成功接收消息
			if (SOCKET_ERROR == retRecv)
			{
				// 接收消息出错
				int a = WSAGetLastError();
				TextOut(hdc, 0, y, "处理FD_READ 调用recv出错", strlen("处理FD_READ 调用recv出错"));
				y += 15;
				break;
			}
			// 打印接收到客户端发来的信息
			TextOut(hdc, 0, y, recvBuf, strlen(recvBuf));
			y += 15;
		}
		break;
		case FD_WRITE:		// 处理FD_WRITE
			// 可以直接send消息个客户端
			// 这里直接打印write ok 表示可以进行send
			TextOut(hdc, 0, y, "write ok", strlen("write ok"));
			y += 15;
			break;
		case FD_CLOSE:		// 处理FD_CLOSE
			// 处理客户端下线操作
			TextOut(hdc, 0, y, "client close!!!", strlen("client close!!!"));
			y += 15;
			// 关闭对应的socket上的消息,直接将函数WSAAsyncSelect();中的后两个参数置0就可以
			WSAAsyncSelect(sock, hwnd, 0, 0);
			// 从socket数组中删除对应的socket 
			for (int i = 0; i < g_count; i++)
			{
				// 从socket数组中找sock 
				if (sock == g_allsocks[i])
				{
					// 删除对应的sock在数组中的对应的元素 可以直接将最后第一个数组元素赋给当前sock在数组所在的位置,也就是覆盖
					g_allsocks[i] = g_allsocks[g_count - 1];
					// 注意记录的数组个数要--
					g_count--;
					break;
				}
			}
			break;
		}

	}
	break;
	case WM_CREATE:	// 初始化,只执行一次
		break;
	case WM_DESTROY:	// 销毁窗口
		PostQuitMessage(0);	// 终止线程请求,并清理内存
		break;
	}

	// 释放hdc
	ReleaseDC(hwnd, hdc);
	
	// 注意DefWindowProc函数参数要和上面WindowProc函数中的参数一一对应
	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

// 将打开网络库、检验版本、创建socket、绑定地址与端口号、开始监听 封装成函数
// 传入的参数用于创建客户区可操作的 HDC
SOCKET createSocket(HWND hwnd)
{
	// 创建客户区可操作的HDC 用完之后记得要释放
	HDC hdc = GetDC(hwnd);

	//1.打开网络库
	WORD wVersion = MAKEWORD(2, 2);//打开2.2版本
	WSADATA WSAData;
	int nRes = WSAStartup(wVersion, &WSAData);
	//判断WSAStartup函数的返回值
	if (0 != nRes)
	{
		switch (nRes)
		{
		case WSASYSNOTREADY:
			// 注意窗口用printf 打印不了 要使用 TextOut
			//printf("请重新启动电脑试试,或者检查网络库\n");
			TextOut(hdc, 0, y, "请重新启动电脑试试,或者检查网络库", strlen("请重新启动电脑试试,或者检查网络库"));
			y += 15;
			break;
		case WSAVERNOTSUPPORTED:
			//printf("请更新网络库\n");
			TextOut(hdc, 0, y, "请更新网络库", strlen("请更新网络库"));
			y += 15;
			break;
		case WSAEINPROGRESS:
			//printf("请重新启动\n");
			TextOut(hdc, 0, y, "请重新启动", strlen("请重新启动"));
			y += 15;
			break;
		case WSAEPROCLIM:
			//printf("请关闭不用的软件,给网络运行提供充足的资源\n");
			TextOut(hdc, 0, y, "请关闭不用的软件,给网络运行提供充足的资源", strlen("请关闭不用的软件,给网络运行提供充足的资源"));
			y += 15;
			break;
		}
		return 0;
	}
	//2、检验版本
	if (2 != HIBYTE(WSAData.wVersion) || 2 != LOBYTE(WSAData.wVersion))
	{
		int a = WSAGetLastError();
		// 将int类型转成char 类型
		char buf[10] = { 0 };
		// 将int类型转成char类型的字符串
		_itoa(a, buf, 10);
		TextOut(hdc, 0, y, buf, strlen(buf));
		//清理网络库
		WSACleanup();
		return 0;
	}
	//3.创建socket
	SOCKET socketServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	//判读socket的返回值
	if (INVALID_SOCKET == socketServer)
	{
		int a = WSAGetLastError();
		// 将int类型转成char 类型
		char buf[10] = { 0 };
		_itoa(a, buf, 10);
		TextOut(hdc, 0, y, buf, strlen(buf));
		y += 15;
		//清理网络库
		WSACleanup();
		return 0;
	}
	//4.绑定地址与端口
	struct sockaddr_in si;
	si.sin_family = AF_INET;
	si.sin_port = htons(12345);
	si.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	if (SOCKET_ERROR == bind(socketServer, (const struct sockaddr*)&si, sizeof(si)))
	{
		int a = WSAGetLastError();
		// 将int类型转成char 类型
		char buf[10] = { 0 };
		_itoa(a, buf, 10);
		TextOut(hdc, 0, y, buf, strlen(buf));
		y += 15;
		//释放socket
		closesocket(socketServer);
		//清理网络库
		WSACleanup();
		return 0;
	}
	//5.开始监听
	if (SOCKET_ERROR == listen(socketServer, SOMAXCONN))
	{
		int a = WSAGetLastError();
		// 将int类型转成char 类型
		char buf[10] = { 0 };
		_itoa(a, buf, 10);
		TextOut(hdc, 0, y, buf, strlen(buf));
		//释放socket
		closesocket(socketServer);
		//清理网络库
		WSACleanup();
		return 0;
	}
	// 释放客户区HDC
	ReleaseDC(hwnd, hdc);

	return socketServer;
}

客户端代码:(控制台应用程序)

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <Winsock.h>
#pragma comment(lib,"ws2_32.lib")

WSADATA WSAData;
// 1,打开网络库
void OpenWSAStarup()
{
	// 1,打开网络库
	WORD version = MAKEWORD(2, 2);	//设置要使用的库的版本 MAKEWORD(主版本,副版本); 
	int n = WSAStartup(version, &WSAData);
	// 检验是否成功打开网络库
	if (0 != n)
	{
		// 打开网络库失败,通过n 返回的错误码,给出提示
		switch (n)
		{
		case WSASYSNOTREADY:
			printf("重启电脑试试,或者检查ws_2_32库是否存在\n");
			break;
		case WSAVERNOTSUPPORTED:
			printf("当前库版本号不支持,尝试更换版本试试\n");
			break;
		case WSAEPROCLIM:
			printf("已达到对Windows套接字实现支持的任务数量的限制\n");
			break;
		case WSAEINPROGRESS:
			printf("正在阻止Windows Sockets 1.1操作,当前函数运行期间,因为某些原因造成阻塞\n");
			break;
		case WSAEFAULT:
			printf("lpWASData参数不是有效的指针,参数写错了\n");
			break;
		default:
			break;
		}
	}
}
// 2.检验版本
void CheckVersion()
{
	if (2 != HIBYTE(WSAData.wVersion) || 2 != LOBYTE(WSAData.wVersion))
	{
		// 打开版本号 2.2 失败
		printf("打开版本号 2.2 失败, 正在退出程序...\n");
		// 关闭网络库
		WSACleanup();
		// 退出程序
		return;
	}
}
// 3.创建socket
SOCKET CreateSocket()
{
	// 3.创建套接字 socket 创建服务端的socket
	SOCKET socketCreate = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	// 检验是否成功创建套接字
	if (INVALID_SOCKET == socketCreate)
	{
		// 创建套接字失败
		// 获取错误码
		int error = WSAGetLastError();
		printf("创建套接字失败,返回错误号为:%d\n", error);
		// 关闭网络库
		WSACleanup();
		// 退出程序
		return 0;
	}
	return socketCreate;
}
// 4.连接服务器
void Connection(SOCKET socket_)
{
	// 4.连接到服务器
	// 创建connect() 函数中参数2所需的结构体 用来装IP地址和端口号
	sockaddr_in client_s;
	//给结构体成员赋值
	client_s.sin_family = AF_INET;		//IP地址类型
	client_s.sin_port = htons(12345);	//端口号
	client_s.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	int nCon = connect(socket_, (struct sockaddr*)&client_s, sizeof(client_s));
	// 检验是否成功连接到服务端
	if (SOCKET_ERROR == nCon)
	{
		// 连接服务端失败
		// 获取错误码
		int error = WSAGetLastError();
		printf("连接服务端失败,返回错误号为:%d\n", error);
		// 关闭套接字
		closesocket(socket_);
		// 关闭网络库
		WSACleanup();
		// 退出程序
		return;
	}
}

// 5.与服务端发送信息
void RecvAndSend(SOCKET socket_)
{
	// 客户端只进行发送消息,因为服务端代码没有给客户端send消息,所以就没有进行接收消息处理
	printf("我是客户端,正在发送信息,请稍等...\n");

	// 5.与服务端进行收发信息
	while (1)
	{
		// 发信息
		char sendBuf[1024] = { 0 };
		scanf_s("%s", sendBuf, 1024);
		// 设置输入 0 时自动退出
		if ('0' == sendBuf[0])
		{
			break;	//退出循环
		}
		send(socket_, sendBuf, 1024, 0);
	}
}

int main()
{
	// 1,打开网络库
	OpenWSAStarup();
	// 2.校验版本
	CheckVersion();
	// 3.创建socket
	SOCKET socketServer = CreateSocket();
	// 4.连接服务器
	Connection(socketServer);
	// 5.与服务端发送信息
	RecvAndSend(socketServer);

	// 关闭套接字
	closesocket(socketServer);
	// 关闭网络库
	WSACleanup();
	system("pause");
	return 0;
}

程序运行结果:

?

  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2021-08-05 17:42:25  更:2021-08-05 17:43:11 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/3 23:59:15-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码