前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >使用socket实现TCP和UDP传输

使用socket实现TCP和UDP传输

作者头像
战神伽罗
发布2019-09-03 20:06:21
1.5K0
发布2019-09-03 20:06:21
举报

转载自:https://blog.csdn.net/timmiy/article/details/51946093

https://blog.csdn.net/timmiy/article/details/52016946

socket由IP地址和端口号组成,可以通过TCP,UDP,IP协议实现不同虚拟机或不同计算机之间的通信,效率较高。

(一)运用TCP协议进行socket通信

TCP是面向连接的,它在进行通信之前,需要双方先进行沟通,然后才能进行通信。而且TCP是以数据流的方式进行数据传递,会自动的进行拆包和组包的过程。所以TCP的连接是比较可靠的,但是它的传输速度也因此相对较慢。接下来分别介绍服务端和客户端,看下如何在windows系统中用C++语言实现TCP通信。

在windows中,要想进行socket网络操作,必须包含一个名叫做WinSock2.h(或者WinSock.h),如果包含的是WinSock2.h则必须在windows.h之前,否则会产生一些重定义的编译错误。包含完头文件之后,还要链接一个库文件ws2_32.lib,完成之后,我们就可以开始进行TCP服务端和客户端的编写了。(如果使用Visual Studio编译器运行,VS会自动生成.h文件,不需要自己手动包含和链接库文件。)

1.1 服务端

首先给出服务端的实现思路:

1.初始化socket环境 -> 2.创建服务器socket -> 3.初始化端口和ip地址调用bind进行绑定 -> 4.调用listen进行监听 -> 5.调用accept接收客户端的请求 -> 6.调用recv和send与客户端进行通信 -> 7.调用WSACleanup及closesocket关闭网络环境和socket

下面是具体的实现示例程序:

代码语言:javascript
复制
  1. #include <stdio.h>
  2. #include <winsock2.h> // 必须包含windwos.h之前
  3. #include <Windows.h>
  4. #pragma comment(lib,"ws2_32.lib")
  5. #define PORT 6000
  6. DWORD WINAPI clientProc(LPARAM lparam) //通信接收和发送数据过程函数(recv、send)
  7. {
  8. SOCKET sockClient = (SOCKET)lparam;
  9. char buf[1024];
  10. while (TRUE)
  11. {
  12. memset(buf, 0, sizeof(buf));
  13. // 接收客户端的一条数据
  14. int ret = recv(sockClient, buf, sizeof(buf), 0);
  15. //检查是否接收失败
  16. if (SOCKET_ERROR == ret)
  17. {
  18. printf("socket recv failed\n");
  19. closesocket(sockClient);
  20. return -1;
  21. }
  22. else
  23. {
  24. printf("%s\r\n", buf);
  25. }
  26. // 0 代表客户端主动断开连接
  27. if (ret == 0)
  28. {
  29. printf("client close connection\n");
  30. closesocket(sockClient);
  31. return -1;
  32. }
  33. // 发送数据
  34. char *p = "hello client";
  35. ret = send(sockClient, p, strlen(p), 0);
  36. //检查是否发送失败
  37. if (SOCKET_ERROR == ret)
  38. {
  39. printf("socket send failed\n");
  40. closesocket(sockClient);
  41. return -1;
  42. }
  43. }
  44. closesocket(sockClient);
  45. return 0;
  46. }
  47. bool InitNetEnv() //网络环境初始化函数
  48. {
  49. // 进行网络环境的初始化操作
  50. WSADATA wsa;
  51. if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
  52. {
  53. printf("WSAStartup failed\n");
  54. return false;
  55. }
  56. return true;
  57. }
  58. int main(int argc, char * argv[])
  59. {
  60. if (!InitNetEnv()) //Step1:初始化网络环境
  61. {
  62. return -1;
  63. }
  64. // Step2:初始化完成,创建一个TCP的socket
  65. //socket(协议域,指定socket类型,指定协议)
  66. SOCKET sServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  67. //检查是否创建失败
  68. if (sServer == INVALID_SOCKET)
  69. {
  70. printf("socket failed\n");
  71. return -1;
  72. }
  73. printf("Create socket OK\n");
  74. //Step3:进行绑定操作(bind)
  75. SOCKADDR_IN addrServ;
  76. addrServ.sin_family = AF_INET; // 协议簇为IPV4的
  77. addrServ.sin_port = htons(PORT); // 端口 因为本机是小端模式,网络是大端模式,调用htons把本机字节序转为网络字节序
  78. addrServ.sin_addr.S_un.S_addr = INADDR_ANY; // ip地址,INADDR_ANY表示绑定电脑上所有网卡IP
  79. //完成绑定操作
  80. //bind(socket描述字, 绑定给listenfd的协议地址,地址长度)
  81. int ret = bind(sServer, (sockaddr *)&addrServ, sizeof(sockaddr));
  82. //检查绑定是否成功
  83. if (SOCKET_ERROR == ret)
  84. {
  85. printf("socket bind failed\n");
  86. WSACleanup(); // 释放网络环境
  87. closesocket(sServer); // 关闭网络连接
  88. return -1;
  89. }
  90. printf("socket bind OK\n");
  91. // Stpe4:绑定成功,进行监听(listen)
  92. //listen(socket描述字, socket可以排队的最大连接个数)
  93. ret = listen(sServer, 10);
  94. //检查是否监听成功
  95. if (SOCKET_ERROR == ret)
  96. {
  97. printf("socket listen failed\n");
  98. WSACleanup();
  99. closesocket(sServer);
  100. return -1;
  101. }
  102. printf("socket listen OK\n");
  103. // 监听成功
  104. sockaddr_in addrClient; // 用于保存客户端的网络节点的信息
  105. int addrClientLen = sizeof(sockaddr_in);
  106. while (TRUE)
  107. {
  108. //新建一个socket,用于客户端
  109. SOCKET *sClient = new SOCKET;
  110. //Step5:等待客户端的连接(accept)
  111. //accept(服务器的描述字,指向客户端的协议地址, 协议地址的长度)
  112. *sClient = accept(sServer, (sockaddr*)&addrClient, &addrClientLen);
  113. if (INVALID_SOCKET == *sClient)
  114. {
  115. printf("socket accept failed\n");
  116. WSACleanup();
  117. closesocket(sServer);
  118. delete sClient;
  119. return -1;
  120. }
  121. //Step6:创建线程为客户端做数据收发
  122. CreateThread(0, 0, (LPTHREAD_START_ROUTINE)clientProc, (LPVOID)*sClient, 0, 0);
  123. }
  124. closesocket(sServer); //关闭网络环境和socket
  125. WSACleanup();
  126. return 0;
  127. }

下面对代码中的实现函数进行说明。

1)首先,要进行网络操作,我们先要进行一下网络环境的初始化。WSAStartup函数就是用来初始化网络环境的。其声明如下:

代码语言:javascript
复制
  1. int WSAStartup(
  2. WORD wVersionRequested, //版本号,一般使用2.2版本
  3. LPWSADATA lpWSAData <span style="white-space:pre"> </span>//WSAData地址
  4. );

函数的第二个参数,接收一个WSAData结构的指针,该结构里边包含了版本号,我们传递的版本号会对该结构里边的版本号进行初始化。

2)初始化完成之后,我们需要创建一个socket(套接字),这个套接字相当于管道,用于客户端和服务端的连接。调用socket函数我们可以创建一个套接字,声明如下:

代码语言:javascript
复制
  1. SOCKET socket(
  2. int af, //IP协议簇
  3. int type, //套接字类型,TCP应该用SOCK_STREAM
  4. int protocol<span style="white-space:pre"> </span> //协议
  5. );

其实,socket也是一个内核对象,但是它没有内核对象所拥有的明显标志,安全属性。

3)创建好套接字后呢,我们需要告诉操作系统需要在哪个地址和端口上进行网络操作,相当于管道通信中绑定到标准输入输出口上。绑定的时候,需要有一个SOCKADDR_IN这个结构体,声明如下:

代码语言:javascript
复制
  1. struct sockaddr_in{
  2. short sin_family;//协议簇
  3. unsigned short sin_port;//端口
  4. struct in_addr sin_addr;//ip地址
  5. char sin_zero[8];//为了设置和SOCKADDR结构等长的补充字节
  6. };

还有一个SOCKADDR结构和上面这个的功能完全一样,但是SOCKADDR这个结构里边只有两个成员,一个是协议簇,一个是14个字节的char数组,为了让我们更好的编写代码,于是将char数组拆解成SOCKADDR_IN 中后三个成员。

初始化完端口,地址等信息后,需要调用bind函数,来完成绑定操作,声明如下:

代码语言:javascript
复制
  1. int bind(
  2. SOCKET s, //我们创建的那个socket
  3. const struct sockaddr FAR *name, //sockaddr结构指针
  4. int namelen //sockaddr长度
  5. );

4)绑定之后,我们还需要调用listen函数来进行监听操作,这个操作呢,就相当于门卫一样了,如果有人来,就告诉你一声,这就是监听。该函数声明如下:

代码语言:javascript
复制
  1. int listen(
  2. SOCKET s, //我们创建的socket
  3. int backlog //最大连接的队列长度
  4. );

第二个参数backlog呢,我们一般不要给的太大,这就好比你去交电费,还要进行排队等候,如果排队的人多了,这就会给你留下不好的体验,因此随便给个10,100的就行了。

5)监听完成之后,我们就可以进行接收客户端的连接了,我们需要调用accept这个函数来进行接客。声明如下:

代码语言:javascript
复制
  1. SOCKET accept(
  2. SOCKET s, //我们监听的那个socket
  3. struct sockaddr FAR *addr, //我们需要传递一个sockaddr的地址,用于保存客户端的地址
  4. int FAR *addrlen //sockaddr的长度指针
  5. );

6)接完客之后,我们就可以进行通信了,需要调用recv和send两个函数来进行收发数据,它们的声明如下:

代码语言:javascript
复制
  1. int recv(
  2. SOCKET s, //客户端的socket
  3. char FAR *buf, //接收的缓冲区
  4. int len, //缓冲区的大小
  5. int flags //标志位,一般为0
  6. );
  7. int send(
  8. SOCKET s, //客户端的socket
  9. const char FAR *buf, //发送数据的缓冲区
  10. int len, //缓冲区的大小
  11. int flags //标志位,一般为0
  12. );

7)当我们传输完数据后,应该调用WSACleanup和closesocket来进行关闭网络环境和套接字。声明如下:

代码语言:javascript
复制
  1. int WSACleanup (void);
  2. int closesocket(
  3. SOCKET s //要关闭的套接字
  4. );

1.2 客户端

首先给出使用TCP协议在客户端的思路:

1.初始化socket环境 -> 2.创建客户端socket -> 3.调用connect连接指定的服务器 -> 4.调用recv和send与服务端进行通信 -> 5.调用WSACleanup及closesocket关闭网络环境和socket

下面是具体的实现程序:

代码语言:javascript
复制
  1. #include <stdio.h>
  2. #include <winsock2.h>
  3. #include <Windows.h>
  4. #pragma comment(lib,"ws2_32.lib")
  5. #define PORT 6000
  6. int main(int argc, char * argv[])
  7. {
  8. //Step1:初始化网络环境
  9. WSADATA wsa;
  10. if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
  11. {
  12. printf("WSAStartup failed\n");
  13. return -1;
  14. }
  15. // Step2:初始化完成,创建一个TCP的socket
  16. //socket(协议域,指定socket类型,指定协议)
  17. SOCKET sServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  18. if (sServer == INVALID_SOCKET)
  19. {
  20. printf("socket failed\n");
  21. return -1;
  22. }
  23. //Step3:指定连接的服务端信息(bind)
  24. SOCKADDR_IN addrServ;
  25. addrServ.sin_family = AF_INET;
  26. addrServ.sin_port = htons(PORT);
  27. //客户端只需要连接指定的服务器地址,127.0.0.1是本机的回环地址
  28. addrServ.sin_addr.S_un.S_addr = inet_addr("10.170.54.98");
  29. // 服务器Bind 客户端是进行连接
  30. //connect(客户端的socket描述字, 服务器的socket地址, 服务器地址长度)
  31. int ret = connect(sServer, (SOCKADDR*)&addrServ, sizeof(SOCKADDR));//开始连接
  32. if (SOCKET_ERROR == ret)
  33. {
  34. printf("socket connect failed\n");
  35. WSACleanup();
  36. closesocket(sServer);
  37. return -1;
  38. }
  39. //Step4:连接成功后,就可以进行通信了(send,recv)
  40. char szBuf[1024];
  41. memset(szBuf, 0, sizeof(szBuf));
  42. sprintf_s(szBuf, sizeof(szBuf), "Hello server");
  43. //当服务端是recv的时候,客户端就需要send,若两端同时进行收发则会卡在这里,因为recv和send都是阻塞的
  44. ret = send(sServer, szBuf, strlen(szBuf), 0);
  45. if (SOCKET_ERROR == ret)
  46. {
  47. printf("socket send failed\n");
  48. closesocket(sServer);
  49. return -1;
  50. }
  51. ret = recv(sServer, szBuf, sizeof(szBuf), 0);
  52. if (SOCKET_ERROR == ret)
  53. {
  54. printf("socket recv failed\n");
  55. closesocket(sServer);
  56. return -1;
  57. }
  58. printf("%s\n", szBuf);
  59. closesocket(sServer); //Step5:关闭已连接socket描述字
  60. WSACleanup();
  61. system("pause");
  62. return 0;
  63. }

下面对代码中的函数进行解释。

(1-2)客户端比较简单,前面的部分和服务端都基本相同(初始化、建立socket)

(3)在绑定操作上会有所差别。服务端绑定的IP地址是本机所有网卡的IP,而客户端只需要绑定一个即可,因为对客户端来说,我们只需连接指定的服务器。赋值完SOCKADDR_IN结构之后,服务端会调用bind函数,而客户端呢,需要调用connect函数,其声明如下:

代码语言:javascript
复制
  1. int connect(
  2. SOCKET s, //要进行连接的socket
  3. const struct sockaddr FAR *name, //SOCKADDR结构地址
  4. int namelen //SOKADDR大小
  5. );

(4)连接成功后,就可以和服务端进行通信了,调用recv和send来进行收发数据。需要注意的是,如果服务端程序先进行recv操作,则我们应该在客户端先进行send操作,若两个同时进行相同的操作的话,则会卡在当前的位置,因为recv和send都是阻塞型的函数。

(5)当通信完之后,就可以关闭连接了。文章开头讲过,当客户端和服务端刚开始连接的时候呢,两者会先进行沟通,这个沟通需要3个步骤来完成,我们称之为3次握手,同样的关闭连接的时候,需要进行4个步骤来完成,我们称之为4次握手。如果你是粗暴型的,直接拔网线呢,它也会完成其中的两次步骤,作为应用层开发,并不需要深究其中的原理,若感兴趣,可自行查找资料。

在两个Visual Studio中依次运行服务端及客户端程序,得到socket通信结果如下:

(二)运用UDP协议进行socket通信

相比TCP来说,UDP相对比较简单,刚开始的时候,和TCP一样都需要先进行网络环境的初始化,即调用WSAStartup函数。然后呢,我们也需要创建一个socket,这个socket和TCP的那个socket不同,上篇提过TCP创建一个socket调用socket函数时,第二个参数为SOCK_STREAM,而UDP则需要给定一个SOCK_DGRAM,然后在第三个参数上给一个IPPROTO_UDP,这样我们就创建好了一个UDP的socket。

接下来,也和TCP一样,指定SOCKADDR_IN的地址信息(端口,ip),指定完之后呢,若是客户端,则可以直接就进行通信了,若是服务端,则还需要增加一步bind操作,当我们调用bind函数,进行绑定后,服务端就可以和客户端进行通信了。而TCP的服务端还有两个步骤,一个是listen,一个是accept,UDP省略了这两个步骤。

2.1 服务端

首先给出使用UDP协议实现socket通信的服务端的实现思路:

1.初始化socket环境 -> 2.创建服务器socket -> 3.初始化端口和ip地址调用bind进行绑定 -> 4.调用recvfrom和sendto与客户端进行通信 -> 5.调用WSACleanup及closesocket关闭网络环境和socket

下面是具体的实现代码:

代码语言:javascript
复制
  1. #include <stdio.h>
  2. #include <winsock2.h>
  3. #include <Windows.h>
  4. #pragma comment(lib,"ws2_32.lib")
  5. #define PORT 6000
  6. int main(int argc, char* argv[])
  7. {
  8. //Step1:初始化网络环境
  9. WSADATA wsa;
  10. if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
  11. {
  12. printf("WSAStartup failed\n");
  13. return -1;
  14. }
  15. //Step2:建立一个UDP的socket
  16. //建立socket参数:socket(协议域,指定socket类型,指定协议)(和TCP协议后两个参数不同,都为IP协议族)
  17. SOCKET sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
  18. if (sock == SOCKET_ERROR)
  19. {
  20. printf("create socket failed\n");
  21. return -1;
  22. }
  23. //Step3:绑定地址信息
  24. sockaddr_in serverAddr;
  25. serverAddr.sin_family = AF_INET; // 协议簇为IPV4的
  26. serverAddr.sin_port = htons(PORT); // 端口 因为本机是小端模式,网络是大端模式,调用htons把本机字节序转为网络字节序
  27. serverAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY); // ip地址,INADDR_ANY表示绑定电脑上所有网卡IP
  28. //bind(socket描述字, 绑定给listenfd的协议地址,地址长度)
  29. bind(sock, (sockaddr*)&serverAddr, sizeof(sockaddr));
  30. //Step5:与客户端进行通信
  31. char buf[512];
  32. while (TRUE)
  33. {
  34. memset(buf, 0, 512);
  35. // 网络节点的信息,用来保存客户端的网络信息
  36. sockaddr_in clientAddr;
  37. memset(&clientAddr, 0, sizeof(sockaddr_in));
  38. int clientAddrLen = sizeof(sockaddr);
  39. //接收客户端发来的数据
  40. //recvfrom参数:socket名称,接收数据的缓冲区,缓冲区大小,标志位(调用操作方式),sockaddr结构地址,sockaddr结构大小地址
  41. //sockaddr地址用来保存从哪里发来,和发送到哪里的地址信息
  42. int ret = recvfrom(sock, buf, 512, 0, (sockaddr*)&clientAddr, &clientAddrLen);
  43. //inet_ntoa函数转化为ip,ntohs函数转化为端口号
  44. printf("Recv msg:%s from IP:[%s] Port:[%d]\n", buf, inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));
  45. // 发一个数据包返回给客户
  46. //sendto参数:socket名称,发送数据的缓冲区,缓冲区大小,标志位(调用操作方式),sockaddr结构地址,sockaddr结构大小地址
  47. sendto(sock, "Hello World!", strlen("Hello World!"), 0, (sockaddr*)&clientAddr, clientAddrLen);
  48. printf("Send msg back to IP:[%s] Port:[%d]\n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));
  49. }
  50. return 0;
  51. }

前面提到TCP进行数据的收发是通过recv和send两个API来进行数据的收发的。而UDP也需要两个函数,叫做recvfrom和sendto,这两个和TCP那两个有点不同,其声明如下:

代码语言:javascript
复制
  1. int recvfrom(
  2. SOCKET s, //socket
  3. char FAR* buf, <span style="white-space:pre"> </span>//接收数据的缓冲区
  4. int len, //缓冲区的大小
  5. int flags, //标志位,调用操作方式
  6. struct sockaddr FAR *from, //sockaddr结构地址
  7. int FAR *fromlen //sockaddr结构大小地址
  8. );
  9. int sendto(
  10. SOCKET s, //socket
  11. const char FAR *buf, //发送数据的缓冲区
  12. int len, //缓冲区大小
  13. int flags, //标志位,调用操作方式
  14. const struct sockaddr FAR *to, //sockaddr结构地址
  15. int tolen //sockaddr结构大小地址
  16. );

注意,这两个函数里边有一个sockaddr结构地址,它是用来保存该数据发送者的信息的。上篇提过,TCP是面向连接的,它在通信之前需要进行三次握手来确定双方是否已经准备好了。因此,双方很清楚数据是从哪里来的。而UDP是面向数据包的,因此就好像寄快递一样,你必须在快递上写一张纸条,上面填好姓名,地址等信息,填好之后,接收者才知道该东西是由谁寄过来的。因此,上面两个函数提供了sockaddr结构的地址,用于保存从哪里发来的和发送到哪里的地址信息。

2.2 客户端

给出使用UDP协议实现socket通信的客户端的示例代码:

1.初始化socket环境 -> 2.创建客户端socket -> 3.调用recvfrom和sendto与服务端进行通信 -> 4.WSACleanup及closesocket关闭网络环境和socket

下面是具体的实现程序:

代码语言:javascript
复制
  1. #include <stdio.h>
  2. #include <winsock2.h>
  3. #include <Windows.h>
  4. #pragma comment(lib,"ws2_32.lib")
  5. #define PORT 6000
  6. int main(int argc, char* argv[])
  7. {
  8. //Step1:初始化网络环境
  9. WSADATA wsa;
  10. if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
  11. {
  12. printf("WSAStartup failed\n");
  13. return -1;
  14. }
  15. //Step2:建立一个UDP的socket
  16. //建立socket参数:socket(协议域,指定socket类型,指定协议)(和TCP协议后两个参数不同,都为IP协议族)
  17. SOCKET sockClient = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
  18. if (sockClient == INVALID_SOCKET)
  19. {
  20. printf("create socket failed\n");
  21. return -1;
  22. }
  23. // 申明一个网络地址信息的结构体,保存服务器的地址信息
  24. sockaddr_in addr = { 0 };
  25. addr.sin_family = AF_INET; // 协议簇为IPV4的
  26. addr.sin_port = htons(PORT); // 端口 因为本机是小端模式,网络是大端模式,调用htons把本机字节序转为网络字节序
  27. addr.sin_addr.S_un.S_addr = inet_addr("10.170.54.98"); // 服务器的ip地址
  28. //Step3:与服务端进行通信
  29. char buf[] = "client test!";
  30. //发送数据
  31. //sendto参数:socket名称,接收数据的缓冲区,缓冲区大小,标志位(调用操作方式),sockaddr结构地址,sockaddr结构大小地址
  32. int dwSent = sendto(sockClient, buf, strlen(buf), 0, (SOCKADDR *)&addr, sizeof(SOCKADDR));
  33. if (dwSent == 0)
  34. {
  35. printf("send %s failed\n", buf);
  36. return -1;
  37. }
  38. printf("send msg:%s\n", buf);
  39. char recvBuf[512];
  40. memset(recvBuf, 0, 512);
  41. sockaddr_in addrSever = { 0 };
  42. int nServerAddrLen = sizeof(sockaddr_in);
  43. // 接收数据
  44. //recvfrom参数:socket名称,接收数据的缓冲区,缓冲区大小,标志位(调用操作方式),sockaddr结构地址,sockaddr结构大小地址
  45. int dwRecv = recvfrom(sockClient, recvBuf, 512, 0, (SOCKADDR *)&addrSever, &nServerAddrLen);
  46. printf("Recv msg from server : %s\n", recvBuf);
  47. //Step4:关闭SOCKET连接
  48. closesocket(sockClient);
  49. //清理网络环境
  50. WSACleanup();
  51. system("pause");
  52. return 0;
  53. }

在两个Visual Studio中依次运行服务端及客户端程序,得到socket通信结果如下:

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • (一)运用TCP协议进行socket通信
    • 1.1 服务端
      • 1.2 客户端
      • (二)运用UDP协议进行socket通信
        • 2.1 服务端
          • 2.2 客户端
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档