经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » C++ » 查看文章
C++ ASIO 实现异步套接字管理
来源:cnblogs  作者:lyshark  时间:2023/8/30 9:12:21  对本文有异议

Boost ASIO(Asynchronous I/O)是一个用于异步I/O操作的C++库,该框架提供了一种方便的方式来处理网络通信、多线程编程和异步操作。特别适用于网络应用程序的开发,从基本的网络通信到复杂的异步操作,如远程控制程序、高并发服务器等都可以使用该框架。该框架的优势在于其允许处理多个并发连接,而不必创建一个线程来管理每个连接。最重要的是ASIO是一个跨平台库,可以运行在任何支持C++的平台下。

本章笔者将介绍如何通过ASIO框架实现一个简单的异步网络套接字应用程序,该程序支持对Socket套接字的存储,默认将套接字放入到一个Map容器内,当需要使用时只需要将套接字在容器内取出并实现通信,客户端下线时则自动从Map容器内移除,通过对本章知识的学习读者可以很容易的构建一个跨平台的简单远控功能。

AsyncTcpClient 异步客户端

如下这段代码实现了一个基本的带有自动心跳检测的客户端,它可以通过异步连接与服务器进行通信,并根据不同的命令返回不同的数据。代码逻辑较为简单,但为了保证可靠性和稳定性,实际应用中需要进一步优化、处理错误和异常情况,以及增加更多的功能和安全性措施。

首先我们封装实现AsyncConnect类,该类内主要实现两个功能,其中aysnc_connect()方法用于实现异步连接到服务端,而port_is_open()方法则用于验证服务器特定端口是否开放,如果开放则说明服务端还在线,不开放则说明服务端离线此处尝试等待一段时间后再次验证,在调用boost::bind()函数绑定套接字时通过&AsyncConnect::timer_handle()函数来设置一个超时等待时间。

进入到主函数中,首先程序通过while循环让程序保持持续运行,并通过hander.aysnc_connect(ep, 5000) 每隔5秒验证是否与服务端连接成功,如果连接了则进入内循环,在内循环中通过hander.port_is_open("127.0.0.1", 10000, 5000)验证特定端口是否开放,这主要是为了保证服务端断开后客户端依然能够跳转到外部循环继续等待服务端上线。而当客户端与服务端建立连接后则会持续在内循环中socket.read_some()接收服务端传来的特定命令,以此来执行不同的操作。

  1. #define BOOST_BIND_GLOBAL_PLACEHOLDERS
  2. #include <iostream>
  3. #include <string>
  4. #include <boost/asio.hpp>
  5. #include <boost/bind.hpp>
  6. #include <boost/array.hpp>
  7. #include <boost/date_time/posix_time/posix_time_types.hpp>
  8. #include <boost/noncopyable.hpp>
  9. using namespace std;
  10. using boost::asio::ip::tcp;
  11. // 异步连接地址与端口
  12. class AsyncConnect
  13. {
  14. public:
  15. AsyncConnect(boost::asio::io_service& ios, tcp::socket &s)
  16. :io_service_(ios), timer_(ios), socket_(s) {}
  17. // 异步连接
  18. bool aysnc_connect(const tcp::endpoint &ep, int million_seconds)
  19. {
  20. bool connect_success = false;
  21. // 异步连接,当连接成功后将触发 connect_handle 函数
  22. socket_.async_connect(ep, boost::bind(&AsyncConnect::connect_handle, this, _1, boost::ref(connect_success)));
  23. // 设置一个定时器 million_seconds
  24. timer_.expires_from_now(boost::posix_time::milliseconds(million_seconds));
  25. bool timeout = false;
  26. // 异步等待 如果超时则执行 timer_handle
  27. timer_.async_wait(boost::bind(&AsyncConnect::timer_handle, this, _1, boost::ref(timeout)));
  28. do
  29. {
  30. // 等待异步操作完成
  31. io_service_.run_one();
  32. // 判断如果timeout没超时,或者是连接建立了,则不再等待
  33. } while (!timeout && !connect_success);
  34. timer_.cancel();
  35. return connect_success;
  36. }
  37. // 验证服务器端口是否开放
  38. bool port_is_open(std::string address, int port, int timeout)
  39. {
  40. try
  41. {
  42. boost::asio::io_service io;
  43. tcp::socket socket(io);
  44. AsyncConnect hander(io, socket);
  45. tcp::endpoint ep(boost::asio::ip::address::from_string(address), port);
  46. if (hander.aysnc_connect(ep, timeout))
  47. {
  48. io.run();
  49. io.reset();
  50. return true;
  51. }
  52. else
  53. {
  54. return false;
  55. }
  56. }
  57. catch (...)
  58. {
  59. return false;
  60. }
  61. }
  62. private:
  63. // 如果连接成功了,则 connect_success = true
  64. void connect_handle(boost::system::error_code ec, bool &connect_success)
  65. {
  66. if (!ec)
  67. {
  68. connect_success = true;
  69. }
  70. }
  71. // 定时器超时timeout = true
  72. void timer_handle(boost::system::error_code ec, bool &timeout)
  73. {
  74. if (!ec)
  75. {
  76. socket_.close();
  77. timeout = true;
  78. }
  79. }
  80. boost::asio::io_service &io_service_;
  81. boost::asio::deadline_timer timer_;
  82. tcp::socket &socket_;
  83. };
  84. int main(int argc, char * argv[])
  85. {
  86. try
  87. {
  88. boost::asio::io_service io;
  89. tcp::socket socket(io);
  90. AsyncConnect hander(io, socket);
  91. boost::system::error_code error;
  92. tcp::endpoint ep(boost::asio::ip::address::from_string("127.0.0.1"), 10000);
  93. // 循环验证是否在线
  94. go_: while (1)
  95. {
  96. // 验证是否连接成功,并定义超时时间为5秒
  97. if (hander.aysnc_connect(ep, 5000))
  98. {
  99. io.run();
  100. std::cout << "已连接到服务端." << std::endl;
  101. // 循环接收命令
  102. while (1)
  103. {
  104. // 验证地址端口是否开放,默认等待5秒
  105. bool is_open = hander.port_is_open("127.0.0.1", 10000, 5000);
  106. // 客户端接收数据包
  107. boost::array<char, 4096> buffer = { 0 };
  108. // 如果在线则继续执行
  109. if (is_open == true)
  110. {
  111. socket.read_some(boost::asio::buffer(buffer), error);
  112. // 判断收到的命令是否为GetCPU
  113. if (strncmp(buffer.data(), "GetCPU", strlen("GetCPU")) == 0)
  114. {
  115. std::cout << "获取CPU参数并返回给服务端." << std::endl;
  116. socket.write_some(boost::asio::buffer("CPU: 15 %"));
  117. }
  118. // 判断收到的命令是否为GetMEM
  119. if (strncmp(buffer.data(), "GetMEM", strlen("GetMEM")) == 0)
  120. {
  121. std::cout << "获取MEM参数并返回给服务端." << std::endl;
  122. socket.write_some(boost::asio::buffer("MEM: 78 %"));
  123. }
  124. // 判断收到的命令是否为终止程序
  125. if (strncmp(buffer.data(), "Exit", strlen("Exit")) == 0)
  126. {
  127. std::cout << "终止客户端." << std::endl;
  128. return 0;
  129. }
  130. }
  131. else
  132. {
  133. // 如果连接失败,则跳转到等待环节
  134. goto go_;
  135. }
  136. }
  137. }
  138. else
  139. {
  140. std::cout << "连接失败,正在重新连接." << std::endl;
  141. }
  142. }
  143. }
  144. catch (...)
  145. {
  146. return false;
  147. }
  148. std::system("pause");
  149. return 0;
  150. }

AsyncTcpServer 异步服务端

接着我们来实现异步TCP服务器,首先我们需要封装实现CAsyncTcpServer类,该类使用了多线程来支持异步通信,每个客户端连接都会创建一个CTcpConnection类的实例来处理具体的通信操作,该服务器类在连接建立、数据传输和连接断开时,都会通过事件处理器来通知相关操作,以支持服务器端的业务逻辑。其头文件声明如下所示;

  1. #ifdef _MSC_VER
  2. #define BOOST_BIND_GLOBAL_PLACEHOLDERS
  3. #define _WIN32_WINNT 0x0601
  4. #define _CRT_SECURE_NO_WARNINGS
  5. #endif
  6. #pragma once
  7. #include <thread>
  8. #include <array>
  9. #include <boost\bind.hpp>
  10. #include <boost\noncopyable.hpp>
  11. #include <boost\asio.hpp>
  12. #include <boost\asio\placeholders.hpp>
  13. using namespace boost::asio;
  14. using namespace boost::asio::ip;
  15. using namespace boost::placeholders;
  16. using namespace std;
  17. // 每一个套接字连接,都自动对应一个Tcp客户端连接
  18. class CTcpConnection
  19. {
  20. public:
  21. CTcpConnection(io_service& ios, int clientId) : m_socket(ios), m_clientId(clientId){}
  22. ~CTcpConnection(){}
  23. int m_clientId;
  24. tcp::socket m_socket;
  25. array<BYTE, 16 * 1024> m_buffer;
  26. };
  27. typedef shared_ptr<CTcpConnection> TcpConnectionPtr;
  28. class CAsyncTcpServer
  29. {
  30. public:
  31. class IEventHandler
  32. {
  33. public:
  34. IEventHandler(){}
  35. virtual ~IEventHandler(){}
  36. virtual void ClientConnected(int clientId) = 0;
  37. virtual void ClientDisconnect(int clientId) = 0;
  38. virtual void ReceiveData(int clientId, const BYTE* data, size_t length) = 0;
  39. };
  40. public:
  41. CAsyncTcpServer(int maxClientNumber, int port);
  42. ~CAsyncTcpServer();
  43. void AddEventHandler(IEventHandler* pHandler){ m_EventHandlers.push_back(pHandler); }
  44. void Send(int clientId, const BYTE* data, size_t length);
  45. string GetRemoteAddress(int clientId);
  46. string GetRemotePort(int clientId);
  47. private:
  48. void bind_hand_read(CTcpConnection* client);
  49. void handle_accept(const boost::system::error_code& error);
  50. void handle_read(CTcpConnection* client, const boost::system::error_code& error, size_t bytes_transferred);
  51. private:
  52. thread m_thread;
  53. io_service m_ioservice;
  54. io_service::work m_work;
  55. tcp::acceptor m_acceptor;
  56. int m_maxClientNumber;
  57. int m_clientId;
  58. TcpConnectionPtr m_nextClient;
  59. map<int, TcpConnectionPtr> m_clients;
  60. vector<IEventHandler*> m_EventHandlers;
  61. };

接着来实现AsyncTcpServer头文件中的功能函数,此功能函数的实现如果读者不明白原理可自行将其提交给ChatGPT解析,这里就不再解释功能了。

  1. // By: 朱迎春 (基础改进版)
  2. #include "AsyncTcpServer.h"
  3. // CAsyncTcpServer的实现
  4. CAsyncTcpServer::CAsyncTcpServer(int maxClientNumber, int port)
  5. : m_ioservice()
  6. , m_work(m_ioservice)
  7. , m_acceptor(m_ioservice)
  8. , m_maxClientNumber(maxClientNumber)
  9. , m_clientId(0)
  10. {
  11. m_thread = thread((size_t(io_service::*)())&io_service::run, &m_ioservice);
  12. m_nextClient = make_shared<CTcpConnection>(m_ioservice, m_clientId);
  13. m_clientId++;
  14. tcp::endpoint endpoint(tcp::v4(), port);
  15. m_acceptor.open(endpoint.protocol());
  16. m_acceptor.set_option(tcp::acceptor::reuse_address(true));
  17. m_acceptor.bind(endpoint);
  18. m_acceptor.listen();
  19. // 异步等待客户端连接
  20. m_acceptor.async_accept(m_nextClient->m_socket, boost::bind(&CAsyncTcpServer::handle_accept, this, boost::asio::placeholders::error));
  21. }
  22. CAsyncTcpServer::~CAsyncTcpServer()
  23. {
  24. for (map<int, TcpConnectionPtr>::iterator it = m_clients.begin(); it != m_clients.end(); ++it)
  25. {
  26. it->second->m_socket.close();
  27. }
  28. m_ioservice.stop();
  29. m_thread.join();
  30. }
  31. // 根据ID号同步给特定客户端发送数据包
  32. void CAsyncTcpServer::Send(int clientId, const BYTE* data, size_t length)
  33. {
  34. map<int, TcpConnectionPtr>::iterator it = m_clients.find(clientId);
  35. if (it == m_clients.end())
  36. {
  37. return;
  38. }
  39. it->second->m_socket.write_some(boost::asio::buffer(data, length));
  40. }
  41. // 根据ID号返回客户端IP地址
  42. string CAsyncTcpServer::GetRemoteAddress(int clientId)
  43. {
  44. map<int, TcpConnectionPtr>::iterator it = m_clients.find(clientId);
  45. if (it == m_clients.end())
  46. {
  47. return "0.0.0.0";
  48. }
  49. std::string remote_address = it->second->m_socket.remote_endpoint().address().to_string();
  50. return remote_address;
  51. }
  52. // 根据ID号返回端口号
  53. string CAsyncTcpServer::GetRemotePort(int clientId)
  54. {
  55. map<int, TcpConnectionPtr>::iterator it = m_clients.find(clientId);
  56. char ref[32] = { 0 };
  57. if (it == m_clients.end())
  58. {
  59. return "*";
  60. }
  61. unsigned short remote_port = it->second->m_socket.remote_endpoint().port();
  62. std::string str = _itoa(remote_port, ref, 10);
  63. return str;
  64. }
  65. void CAsyncTcpServer::handle_accept(const boost::system::error_code& error)
  66. {
  67. if (!error)
  68. {
  69. // 判断连接数目是否达到最大限度
  70. if (m_maxClientNumber > 0 && m_clients.size() >= m_maxClientNumber)
  71. {
  72. m_nextClient->m_socket.close();
  73. }
  74. else
  75. {
  76. // 发送客户端连接的消息
  77. for (int i = 0; i < m_EventHandlers.size(); ++i)
  78. {
  79. m_EventHandlers[i]->ClientConnected(m_nextClient->m_clientId);
  80. }
  81. // 设置异步接收数据
  82. bind_hand_read(m_nextClient.get());
  83. // 将客户端连接放到客户表中
  84. m_clients.insert(make_pair(m_nextClient->m_clientId, m_nextClient));
  85. // 重置下一个客户端连接
  86. m_nextClient = make_shared<CTcpConnection>(m_ioservice, m_clientId);
  87. m_clientId++;
  88. }
  89. }
  90. // 异步等待下一个客户端连接
  91. m_acceptor.async_accept(m_nextClient->m_socket, boost::bind(&CAsyncTcpServer::handle_accept, this, boost::asio::placeholders::error));
  92. }
  93. void CAsyncTcpServer::bind_hand_read(CTcpConnection* client)
  94. {
  95. client->m_socket.async_read_some(boost::asio::buffer(client->m_buffer),
  96. boost::bind(&CAsyncTcpServer::handle_read, this, client, boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred));
  97. return;
  98. client->m_socket.async_receive(boost::asio::buffer(client->m_buffer),
  99. boost::bind(&CAsyncTcpServer::handle_read, this, client, boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred));
  100. boost::asio::async_read(client->m_socket, boost::asio::buffer(client->m_buffer),
  101. boost::bind(&CAsyncTcpServer::handle_read, this, client, boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred));
  102. }
  103. void CAsyncTcpServer::handle_read(CTcpConnection* client, const boost::system::error_code& error, size_t bytes_transferred)
  104. {
  105. if (!error)
  106. {
  107. // 发送收到数据的信息
  108. for (int i = 0; i < m_EventHandlers.size(); ++i)
  109. {
  110. m_EventHandlers[i]->ReceiveData(client->m_clientId, client->m_buffer.data(), bytes_transferred);
  111. }
  112. bind_hand_read(client);
  113. }
  114. else
  115. {
  116. // 发送客户端离线的消息
  117. for (int i = 0; i < m_EventHandlers.size(); ++i)
  118. {
  119. m_EventHandlers[i]->ClientDisconnect(client->m_clientId);
  120. }
  121. m_clients.erase(client->m_clientId);
  122. }
  123. }

AsyncTcpServer 类调用

服务端首先定义CEventHandler类并继承自CAsyncTcpServer::IEventHandler接口,该类内需要我们实现三个方法,方法ClientConnected用于在客户端连接时触发,方法ClientDisconnect则是在登录客户端离开时触发,而当客户端有数据发送过来时则ReceiveData方法则会被触发。

方法ClientConnected当被触发时自动将clientId客户端Socket套接字放入到tcp_client_id全局容器内存储起来,而当ClientDisconnect客户端退出时,则直接遍历这个迭代容器,找到序列号并通过tcp_client_id.erase将其剔除;

  1. // 客户端连接时触发
  2. virtual void ClientConnected(int clientId)
  3. {
  4. // 将登录客户端加入到容器中
  5. tcp_client_id.push_back(clientId);
  6. }
  7. // 客户端退出时触发
  8. virtual void ClientDisconnect(int clientId)
  9. {
  10. // 将登出的客户端从容器中移除
  11. vector<int>::iterator item = find(tcp_client_id.begin(), tcp_client_id.end(), clientId);
  12. if (item != tcp_client_id.cend())
  13. tcp_client_id.erase(item);
  14. }

ReceiveData一旦收到数据,则直接将其打印输出到屏幕,即可实现客户端参数接收的目的;

  1. // 客户端获取数据
  2. virtual void ReceiveData(int clientId, const BYTE* data, size_t length)
  3. {
  4. std::cout << std::endl;
  5. PrintLine(80);
  6. std::cout << data << std::endl;
  7. PrintLine(80);
  8. std::cout << "[Shell] # ";
  9. }

相对于接收数据而言,发送数据则是通过同步的方式进行,当我们需要发送数据时,只需要将数据字符串放入到一个BYTE*字节数组中,并在调用tcpServer.Send时将所需参数,套接字ID,缓冲区Buf数据,以及长度传递即可实现将数据发送给指定的客户端;

  1. // 同步发送数据到指定的线程中
  2. void send_message(CAsyncTcpServer& tcpServer, int clientId, std::string message, int message_size)
  3. {
  4. // 获取长度
  5. BYTE* buf = new BYTE(message_size + 1);
  6. memset(buf, 0, message_size + 1);
  7. for (int i = 0; i < message_size; i++)
  8. {
  9. buf[i] = message.at(i);
  10. }
  11. tcpServer.Send(clientId, buf, message_size);
  12. }

客户端完整代码如下所示,运行客户端后读者可自行使用不同的命令来接收参数返回值;

  1. #include "AsyncTcpServer.h"
  2. #include <string>
  3. #include <vector>
  4. #include <iostream>
  5. #include <boost/tokenizer.hpp>
  6. using namespace std;
  7. // 存储当前客户端的ID号
  8. std::vector<int> tcp_client_id;
  9. // 输出特定长度的行
  10. void PrintLine(int line)
  11. {
  12. for (int x = 0; x < line; x++)
  13. {
  14. printf("-");
  15. }
  16. printf("\n");
  17. }
  18. class CEventHandler : public CAsyncTcpServer::IEventHandler
  19. {
  20. public:
  21. // 客户端连接时触发
  22. virtual void ClientConnected(int clientId)
  23. {
  24. // 将登录客户端加入到容器中
  25. tcp_client_id.push_back(clientId);
  26. }
  27. // 客户端退出时触发
  28. virtual void ClientDisconnect(int clientId)
  29. {
  30. // 将登出的客户端从容器中移除
  31. vector<int>::iterator item = find(tcp_client_id.begin(), tcp_client_id.end(), clientId);
  32. if (item != tcp_client_id.cend())
  33. tcp_client_id.erase(item);
  34. }
  35. // 客户端获取数据
  36. virtual void ReceiveData(int clientId, const BYTE* data, size_t length)
  37. {
  38. std::cout << std::endl;
  39. PrintLine(80);
  40. std::cout << data << std::endl;
  41. PrintLine(80);
  42. std::cout << "[Shell] # ";
  43. }
  44. };
  45. // 同步发送数据到指定的线程中
  46. void send_message(CAsyncTcpServer& tcpServer, int clientId, std::string message, int message_size)
  47. {
  48. // 获取长度
  49. BYTE* buf = new BYTE(message_size + 1);
  50. memset(buf, 0, message_size + 1);
  51. for (int i = 0; i < message_size; i++)
  52. {
  53. buf[i] = message.at(i);
  54. }
  55. tcpServer.Send(clientId, buf, message_size);
  56. }
  57. int main(int argc, char* argv[])
  58. {
  59. CAsyncTcpServer tcpServer(10, 10000);
  60. CEventHandler eventHandler;
  61. tcpServer.AddEventHandler(&eventHandler);
  62. std::string command;
  63. while (1)
  64. {
  65. std::cout << "[Shell] # ";
  66. std::getline(std::cin, command);
  67. if (command.length() == 0)
  68. {
  69. continue;
  70. }
  71. else if (command == "help")
  72. {
  73. printf(" _ ____ _ _ \n");
  74. printf("| | _ _ / ___| ___ ___| | _____| |_ \n");
  75. printf("| | | | | | \\___ \\ / _ \\ / __| |/ / _ \\ __| \n");
  76. printf("| |__| |_| | ___) | (_) | (__| < __/ |_ \n");
  77. printf("|_____\\__, | |____/ \\___/ \\___|_|\\_\\___|\\__| \n");
  78. printf(" |___/ \n\n");
  79. printf("Usage: LySocket \t PowerBy: LyShark.com \n");
  80. printf("Optional: \n\n");
  81. printf("\t ShowSocket 输出所有Socket容器 \n");
  82. printf("\t GetCPU 获取CPU数据 \n");
  83. printf("\t GetMemory 获取内存数据 \n");
  84. printf("\t Exit 退出客户端 \n\n");
  85. }
  86. else
  87. {
  88. // 定义分词器: 定义分割符号为[逗号,空格]
  89. boost::char_separator<char> sep(", --");
  90. typedef boost::tokenizer<boost::char_separator<char>> CustonTokenizer;
  91. CustonTokenizer tok(command, sep);
  92. // 将分词结果放入vector链表
  93. std::vector<std::string> vecSegTag;
  94. for (CustonTokenizer::iterator beg = tok.begin(); beg != tok.end(); ++beg)
  95. {
  96. vecSegTag.push_back(*beg);
  97. }
  98. // 解析 [shell] # ShowSocket
  99. if (vecSegTag.size() == 1 && vecSegTag[0] == "ShowSocket")
  100. {
  101. PrintLine(80);
  102. printf("客户ID \t 客户IP地址 \t 客户端口 \n");
  103. PrintLine(80);
  104. for (int x = 0; x < tcp_client_id.size(); x++)
  105. {
  106. std::cout << tcp_client_id[x] << " \t "
  107. << tcpServer.GetRemoteAddress(tcp_client_id[x]) << " \t "
  108. << tcpServer.GetRemotePort(tcp_client_id[x]) << std::endl;
  109. }
  110. PrintLine(80);
  111. }
  112. // 解析 [shell] # GetCPU --id 100
  113. if (vecSegTag.size() == 3 && vecSegTag[0] == "GetCPU")
  114. {
  115. char *id = (char *)vecSegTag[2].c_str();
  116. send_message(tcpServer, atoi(id), "GetCPU", strlen("GetCPU"));
  117. }
  118. // 解析 [shell] # GetMemory --id 100
  119. if (vecSegTag.size() == 3 && vecSegTag[0] == "GetMemory")
  120. {
  121. char* id = (char*)vecSegTag[2].c_str();
  122. send_message(tcpServer, atoi(id), "GetMEM", strlen("GetMEM"));
  123. }
  124. // 解析 [shell] # Exit --id 100
  125. if (vecSegTag.size() == 3 && vecSegTag[0] == "Exit")
  126. {
  127. char* id = (char*)vecSegTag[2].c_str();
  128. send_message(tcpServer, atoi(id), "Exit", strlen("Exit"));
  129. }
  130. }
  131. }
  132. return 0;
  133. }

案例演示

首先运行服务端程序,接着运行多个客户端,即可实现自动上线;

当用户需要通信时,只需要指定id序号到指定的Socket套接字编号即可;

本文作者: 王瑞
本文链接: https://www.lyshark.com/post/d0805aed.html
版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

原文链接:https://www.cnblogs.com/LyShark/p/17665421.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号