ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C语言网络编程必知:winsock2.h头文件与ws2_32.lib链接完全指南

C语言网络编程必知:winsock2.h头文件与ws2_32.lib链接完全指南 简介本资源为Windows平台网络编程核心头文件 WINSOCK2.H 的标准C语言头文件面向C/C初学者、嵌入式开发人员及Windows系统级程序员用于支持TCP/IP socket编程、网络通信初始化与底层套接字操作。资源包仅含1个.h头文件体积精简16KB可直接集成至VS或MinGW等开发环境无需额外依赖适用于网络客户端/服务器开发、协议栈调试及Winsock API学习实践。目前已有1215人下载学习体现了其在基础网络编程教学与工程实践中的高频使用价值。读者获取后即可立即引用该头文件调用WSAStartup、socket、bind、connect等关键Winsock函数结合MSDN文档快速开展UDP/TCP通信实验掌握Windows下网络编程的初始化流程、错误处理机制及跨版本兼容要点。1. 为什么C语言网络编程绕不开WINSOCK2这个头文件很多C语言初学者一路从变量、指针、结构体学过来自我感觉基本功还算扎实等到某天想写个TCP通信DEMO或者做一个局域网传输工具时一上手就看到一个压根没见过的头文件——winsock2.h。不少人的第一反应是百度搜一下复制一段代码然后把#include winsock2.h放上去编译结果要么是报一堆LNK2019找不到符号要么是一堆宏重定义。踩完这些坑才意识到这个头文件并不是简单包含进来就能用的。WINSOCK2是Windows平台下C/C进行网络编程的标准套接字接口全称是Windows Sockets 2。它本质上是Windows对Berkeley Socket模型的一套实现但又不完全是照搬而是扩展了很多Windows特有的能力。你在Linux下写网络程序用sys/socket.h在Windows下绝大多数情况用的就是winsock2.h。很多教材在讲Socket编程的时候默认拿Linux环境做演示导致换到Windows环境来实践的时候很多同学直接被头文件和链接库配置整懵了。这篇内容就是专门来把这个环节梳理清楚的。这个头文件适合谁看主要两类人一类是C语言基础已经学完、想往网络编程方向走的学生另一类是工作里需要在Windows平台快速写网络工具、但又不想每次都被编译环境折磨的开发者。看完之后你至少能做到知道winsock2.h是什么、什么时候该包含它、为什么包含它还不够、报错出现时怎么快速定位。2. 引入winsock2.h的正确姿势include顺序与宏定义2.1 为什么它必须在windows.h之前包含如果你去翻老的Win32网络程序源码经常会看到这样一段include组合#include winsock2.h #include windows.h这个顺序是有讲究的不是随便排的。因为早期版本的windows.h内部会间接包含winsock.h也就是Winsock 1.1版本的头文件而winsock2.h是Winsock 2.0的头文件。如果编译器先处理了windows.h再处理winsock2.h两个头文件里面都定义了SOCKADDR_IN这样的结构体、socket()这样的函数声明、一堆网络相关的宏预处理器就会喷出一大堆重定义错误。为了绕开这个坑最稳妥的做法是把winsock2.h放在所有Windows头文件的前面让它先被包含之后windows.h在间接引入winsock.h时会因为WIN32_LEAN_AND_MEAN或者_WINSOCKAPI_这类宏已经被定义而跳过Winsock 1.1的部分。这也是我在自己的项目里一直坚持的写法。还有一个偷懒但也非常可靠的方案是在include之前定义一个宏#define WIN32_LEAN_AND_MEAN #include windows.h #include winsock2.hWIN32_LEAN_AND_MEAN会告诉windows.h不要包含那些不常用的Windows API头文件其中就包括了老版的winsock.h间接规避了冲突。不过建议不要因为这个宏就放松对include顺序的重视两者的配合理解会让你在解决别人留下的烂摊子时更有底气。2.2 只include远远不够ws2_32.lib链接问题这是新手最容易卡住的地方之一。很多人写了个最简单的样子#include stdio.h #include winsock2.h int main() { WSADATA wsaData; WSAStartup(MAKEWORD(2, 2), wsaData); return 0; }编译能过链接却报error LNK2019: 无法解析的外部符号 __imp__WSAStartup8。原因很简单头文件只负责声明函数长什么样也就是编译阶段的“约定”真正让程序能找到函数实现需要告诉链接器去哪个.lib文件里找。WINSOCK2的实现代码在系统的ws2_32.dll里而链接器需要的是对应的导入库ws2_32.lib。解决方式有下面这几种方式做法适用的编辑器/IDE说明代码指定#pragma comment(lib, ws2_32.lib)VS、VSCodeMSVC、MinGW部分支持最推荐源码自带说明换环境不丢VS工程配置项目属性 - 链接器 - 输入 - 附加依赖项里加ws2_32.libVisual Studio直观但换工程要重新配命令行参数gcc -o test.exe test.c -lws2_32MinGW-w64、CygwinLinux习惯平移过来的方式我自己写小工具时一般直接在代码顶部加上#pragma comment(lib, ws2_32.lib)这样就算把源码发给同事或者拷到另一台电脑上重新编译也不会因为忘记配置工程而白折腾半小时。注意如果你用的是MinGW-w64环境#pragma comment虽然在部分版本也能识别但更保险的方式还是编译时加-lws2_32参数。我实测在MSYS2的MinGW-w64 GCC 12.2版本下#pragma comment(lib, ws2_32.lib)是可以正常工作的但在一些老版本或者TDM-GCC上可能会被忽略所以命令行编译时习惯性带上-lws2_32就万无一失了。2.3 一个最小可用模板把上面两小节的内容合起来一个不会踩坑的最小模板是这样#define WIN32_LEAN_AND_MEAN #include winsock2.h #include windows.h #include stdio.h #pragma comment(lib, ws2_32.lib) int main() { WSADATA wsa; int ret WSAStartup(MAKEWORD(2, 2), wsa); if (ret ! 0) { printf(WSAStartup failed: %d\n, ret); return 1; } printf(Winsock initialized. version: %d.%d\n, LOBYTE(wsa.wVersion), HIBYTE(wsa.wVersion)); WSACleanup(); return 0; }这个模板是我在写任何Windows网络程序时的标配开头。先把框架跑起来再往里填充Socket逻辑。有些新人喜欢一开始就堆一大堆代码然后报错了都不知道是哪一行引起的还是建议像这样先小步验证再逐步扩展。3. 从WSAStartup到sendWINSOCK2核心API的完整实操流程3.1 第一步用WSAStartup完成协议栈初始化WSAStartup可以说是所有Winsock程序的第一道大门。它的作用是在进程内部加载Winsock协议栈并协商版本号。这个函数是必须最先调用的其他任何Socket相关API都要在它之后才能使用哪怕你只是创建一个最基础的UDP透传Socket。int WSAStartup( WORD wVersionRequested, // 高位为主版本号低位为次版本号 LPWSADATA lpWSAData // 返回协议栈详细信息 );这里的参数用MAKEWORD(2, 2)表示请求Winsock 2.2版本。为什么要协商版本因为Windows系统本身可能支持不同版本的Winsock程序请求一个版本系统返回它能提供的版本如果两者不兼容WSAStartup会失败。返回值0代表成功非零值对应具体的错误码。有个小细节值得留意WSAStartup是进程级的初始化不是每次创建Socket前都要调一次。正常流程是程序启动时初始化一次程序退出前调用WSACleanup释放资源。如果你在一个消息循环里反复调WSAStartup和WSACleanup性能损耗非常明显还可能导致一些内部状态错乱。另外WSAStartup并不只是“走个过场”它背后会做Winsock协议的动态加载和引用计数管理。也就是说多次调用会让引用计数递增每调一次WSAStartup就得对应一次WSACleanup不然资源释放不干净。写DLL的时候这个坑尤其容易踩因为DLL的入口和出口都会涉及到初始化和清理的逻辑。3.2 创建Socket到建立连接bind、listen、accept、connect如果说WSAStartup是打开一扇门那后面的socket、bind、listen、accept、connect这一串函数才是真正的跑道。创建SocketSOCKET s socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (s INVALID_SOCKET) { int err WSAGetLastError(); // 处理错误 }三个参数分别代表地址族IPv4还是IPv6、套接字类型流式还是数据报、协议。TCP用SOCK_STREAMUDP用SOCK_DGRAM。不需要死记硬背理解就好AF_INET是IPv4对应struct sockaddr_inAF_INET6是IPv6对应struct sockaddr_in6。这里提一句很多老代码会写PF_INET那是老BSD风格的遗留实测在Windows上AF_INET和PF_INET值一样但语义上AF_更贴近“地址族”建议直接用AF_INET。绑定地址服务端要接受连接必须先绑定一个IP和端口struct sockaddr_in addr; addr.sin_family AF_INET; addr.sin_port htons(8888); addr.sin_addr.s_addr htonl(INADDR_ANY); // 监听所有网卡 bind(listen_sock, (struct sockaddr*)addr, sizeof(addr));这段代码里有两处很容易让C语言新手紧张htons和htonl是干嘛的以及为什么bind要强制转换成struct sockaddr*。后者是因为bind这个函数是通用接口设计出来就是为了兼容IPv4和IPv6所以第二参数用了一个更“原始”的结构体指针来统一传参。这就类似于C语言里的void*你在调用时手动转成具体类型。前者htons的作用我在后面专门讲字节序的时候会细说这里先记住端口和IP地址都要转成网络字节序。监听与接受连接listen(listen_sock, SOMAXCONN); SOCKET client_sock accept(listen_sock, NULL, NULL);listen的第二个参数表示等待队列的最大长度SOMAXCONN是系统默认的最大值。accept会阻塞当前线程直到有客户端连接进来。很多人第一次写服务端程序时在这里会疑惑既然accept会阻塞那我要同时处理多个客户端怎么办这就涉及到多线程、select或者IOCP等模型了属于下一步要进阶的内容。初学者前期可以先用单线程阻塞模式把整个流程跑通再慢慢升级。客户端发起连接struct sockaddr_in server_addr; server_addr.sin_family AF_INET; server_addr.sin_port htons(8888); inet_pton(AF_INET, 127.0.0.1, server_addr.sin_addr); connect(client_sock, (struct sockaddr*)server_addr, sizeof(server_addr));客户端不需要bind因为操作系统会为它自动分配一个临时端口。注意这里的inet_pton在主流的Win7及以上系统都支持如果你的目标环境是上古Windows XP那可能得用inet_addr替代。我在实际开发中统一用inet_pton因为支持IPv6文本格式转换方便以后扩展。3.3 网络字节序为什么端口和IP要转来转去字节序是C语言网络编程里绕不开的经典话题同时也是很多自学者容易忽略的细节。简单来说不同的CPU架构在内存里存放多字节整数的方式不同分为大端和小端。x86架构是典型的小端而网络协议规定数据在网络上传输时统一使用大端字节序。这就导致本地整数和网络传输字节序大概率不一致所以需要转换函数来对接。实际操作中你不需要手动去判断CPU是大端还是小端直接用API就行htons将16位整数从主机字节序转换为网络字节序通常用来转端口号。htonl将32位整数从主机字节序转换为网络字节序通常用来转IP地址。ntohs和ntohl作用相反从网络字节序转回主机字节序通常在accept之后解析对方端口和IP时使用。还是以端口8888为例。8888的十六进制是0x22B8在小端机器上存为B8 22而网络传输要求22 B8htons(8888)就是做这个转换的。如果你忘了转换直接把8888赋给sin_port那么实际监听的端口可能是0xB822换算出来的值也就是47138客户端连8888端口自然会失败。这类问题的排查过程很痛苦因为代码看起来哪里都没错。顺带提一下字符串形式的IP地址转成32位整数的函数不止inet_addr和inet_pton两个还有个inet_aton在Linux下更常用。Windows下inet_pton已经够用了用法也比较现代、安全。3.4 收尾与错误处理WSACleanup和WSAGetLastError有朋友在实习时写过一段网络程序功能是完成了但每次关闭程序时任务管理器里都能看到残留的进程句柄后来发现是没调closesocket和WSACleanup。这种资源泄漏在开发调试阶段问题不大但如果你的程序是长期运行的服务器进程日积月累会让句柄耗尽最终导致系统拒绝创建新的Socket。一个健壮的收尾流程是这样的closesocket(client_sock); closesocket(listen_sock); WSACleanup();closesocket关闭Socket句柄WSACleanup释放Winsock初始化的资源。顺序不能反先关Socket再清理Winsock环境。多说一句closesocket和C标准库的close、fclose不是一回事Windows专门为Socket提供了这个API不要混用。错误处理方面WSAGetLastError()等价于Windows API里的GetLastError()专门拿Socket调用失败的错误码。错误码非常多但常见的有这些错误码含义常见诱因WSAETIMEDOUT (10060)连接超时对方防火墙拦截、IP错误WSAECONNREFUSED (10061)连接被拒绝端口没监听、服务端没启动WSAECONNRESET (10054)连接被重置对方异常退出、发送数据违背协议WSAEHOSTUNREACH (10065)主机不可达网络不通、IP段错误调试时我习惯写一个错误输出函数把错误码和对应的文本一起打印出来这样比盯着一个数字猜半天效率高很多。别嫌麻烦写工具程序时多这十几行代码排查问题的时间能省下一大截。3.5 一个完整的TCP回射Server代码示例把前面几个小节串起来写一个最简单的TCP回射服务端#define WIN32_LEAN_AND_MEAN #include winsock2.h #include windows.h #include stdio.h #pragma comment(lib, ws2_32.lib) int main() { WSADATA wsa; WSAStartup(MAKEWORD(2, 2), wsa); SOCKET listen_sock socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (listen_sock INVALID_SOCKET) { printf(socket failed: %d\n, WSAGetLastError()); WSACleanup(); return 1; } struct sockaddr_in addr; addr.sin_family AF_INET; addr.sin_port htons(8888); addr.sin_addr.s_addr htonl(INADDR_ANY); if (bind(listen_sock, (struct sockaddr*)addr, sizeof(addr)) SOCKET_ERROR) { printf(bind failed: %d\n, WSAGetLastError()); closesocket(listen_sock); WSACleanup(); return 1; } if (listen(listen_sock, SOMAXCONN) SOCKET_ERROR) { printf(listen failed: %d\n, WSAGetLastError()); closesocket(listen_sock); WSACleanup(); return 1; } printf(server listening on 8888...\n); SOCKET client accept(listen_sock, NULL, NULL); if (client INVALID_SOCKET) { printf(accept failed: %d\n, WSAGetLastError()); closesocket(listen_sock); WSACleanup(); return 1; } char buf[1024]; int recv_len recv(client, buf, sizeof(buf) - 1, 0); if (recv_len 0) { buf[recv_len] \0; printf(recv: %s\n, buf); send(client, buf, recv_len, 0); } closesocket(client); closesocket(listen_sock); WSACleanup(); return 0; }这段代码算是一个新手友好的最小可运行示例包含了初始化、建Socket、绑定、监听、接受连接、收发数据、资源释放的全部环节。把这段跑通之后再往多线程、非阻塞、select模型的方向扩展就会顺很多。4. 常见报错与排查实录链接失败、红色波浪线、预编译头文件4.1 VSCode里winsock2.h找不到或红色波浪线VSCode这几年成了C语言学习的主流编辑器配置简单、扩展丰富。但是很多人在VSCode里写入了#include winsock2.h之后发现编辑器画了一根红色波浪线提示找不到该头文件。这个问题的本质不是代码错了而是IntelliSense不知道去哪找Windows SDK的头文件。解决步骤其实不复杂我按实际操作顺序整理一下安装C/C扩展ms-vscode.cpptools这是微软官方扩展支持语法高亮、补全和调试。按CtrlShiftP打开命令面板输入C/C: Edit Configurations (UI)。在“Include path”这一项中添加Windows SDK的Include目录。具体路径因系统而异常见的是C:\Program Files (x86)\Windows Kits\10\Include\版本号\ucrtC:\Program Files (x86)\Windows Kits\10\Include\版本号\umC:\Program Files (x86)\Windows Kits\10\Include\版本号\shared其中winsock2.h就位于um目录下。你可以在资源管理器里实际看一眼确认一下目录名称是不是对的。配置完成后重启VSCode或者重新加载窗口红色波浪线应该就能消失。还有一种情况是编译本身没问题但Ctrl点击跳转不到头文件定义。这个通常是IntelliSense的includePath没有生效或者.vscode/c_cpp_properties.json文件路径配置出了问题。直接打开这个JSON文件检查一下includePath数组确保有Windows Kits相关路径即可。顺带提一句如果你在项目里用了“万能头文件”#include bits/stdc.h那是GCC环境的专利在MSVC环境下是不存在的别把这个混到Windows项目里来。4.2 链接错误LNK2019/LNK1120的根源与处理在我帮人排查过的网络程序编译问题里这类错误是出现频率最高的。典型报错是error LNK2019: 无法解析的外部符号 __imp__WSAStartup8函数 _main 中引用了该符号 error LNK1120: 1 个无法解析的外部命令这个信息已经非常明确了代码里调用了WSAStartup但链接器找不到它的实现。解决办法就是前面反复强调的链接ws2_32.lib。用Visual Studio的图形界面配置路径是右键项目 - 属性 - 链接器 - 输入 - 附加依赖项在列表中添加ws2_32.lib我自己更推荐的是在源码开头加#pragma comment(lib, ws2_32.lib)。原因很简单配置文件跟源码是分开的两个东西源码发到别人手上配置就丢了而#pragma直接写在源码里属于自描述文件谁拿到都能直接编译。如果你用的是GCC命令行那对应的是gcc -o server.exe server.c -lws2_32忘记加-lws2_32就会出现同样的问题。这个坑普遍到什么程度很多初学者在VSCode里配好了.vscode/tasks.json里面负责编译的参数往往只有gcc -g -o xxx xxx.c根本没带-lws2_32所以头文件能解析编译能通过链接必报错。还有一类LNK2019不是Winsock的问题。如果你在Release x64版本下看到类似“无法打开预编译头文件: x64\Release\xxx.pch”的报错大概率是预编译头PCH设置出了问题。这通常发生在你把一个原来用VS创建的项目拷贝到另一台机器上路径变了但.vcxproj里缓存的中间目录还指向旧路径。解决办法是在Visual Studio里选择“项目”-“重新扫描解决方案”或者直接清理解决方案再重新生成让编译器重新生成PCH文件。如果还不行可以在项目属性里把“预编译头”选项改成“不使用预编译头”代价是编译速度会略微下降但能保证项目正常编译。4.3 winsock2.h和windows.h的宏冲突这个主题我前面已经提过两次了但实际遇到问题时报错形态可能会让人摸不着头脑。最常见的报错是error C2011: sockaddr : struct type redefinition error C2011: timeval : struct type redefinition看到这种“结构体重定义”的错误第一反应不应该是怀疑自己的代码而是检查include顺序。绝大多数情况下是因为windows.h被包含在了winsock2.h的前面。还有一种隐蔽的冲突场景有些第三方库比如某些图像库、串口库它们的头文件内部会自己去包含windows.h你没办法控制它们的位置。这时候可以在#include winsock2.h前先定义#define _WINSOCKAPI_ #include winsock2.h这个宏的作用是阻止windows.h内部再去包含winsock.h。我实测有效但要注意这个宏必须在任何windows.h包含之前定义才生效。还有一种做法是直接用#include winsock2.h替换掉原来的#include windows.h前提是你确认项目里没有用到windows.h独有的一些API。另外timeval结构体重定义在MinGW环境下也比较常见。如果你不想动include顺序可以考虑在包含头文件之前定义_TIMEVAL_DEFINED来跳过timeval的重复定义但这属于绕过本质上还是会掩盖问题建议还是把头文件顺序理清楚。4.4 一个容易忽略的陷阱socket编程里的字符串和缓冲区说到C语言字符串处理是基本功。很多人在写网络程序时会在缓冲区处理上翻车最典型的就是用strlen去算发送长度char buf[] hello, server; send(client, buf, strlen(buf), 0);这里有个隐藏问题strlen返回的是字符串长度不包含末尾的\0。如果是文本协议这倒没什么问题但如果你传输的数据里可能包含\0字节比如结构体或者二进制文件必须改用sizeof或者显式指定长度。另一处常见隐患是recv的返回值。recv返回的是实际接收到的字节数这个值可能小于缓冲区大小也可能为0代表对端关闭连接也可能是SOCKET_ERROR三种情况必须分开处理不能想当然认为一次recv就能收完整个数据包。在C语言的内存管理语境下缓冲区越界是隐蔽炸弹。比如char buf[64]; recv(client, buf, 1024, 0); // buf只有64字节但recv试图写1024字节这种错误在调试的时候未必立刻崩溃但可能在某次运行中被系统检测到栈破坏然后弹出一个莫名奇妙的断言窗口。我自己在写网络程序时尽量用固定大小缓冲区然后在recv之前把剩余空间算清楚收完后手动补\0把缓冲区当作有边界的内存区域来管理。5. 环境配置冷知识从VSCode到命令行5.1 Visual Studio vs MinGW-w64同样是C语言写Windows网络程序不同的工具链对winsock2.h的支持程度大同小异但一些小差异还是值得注意。Visual Studio使用MSVC编译器Windows SDK是自带的只要安装了“使用C的桌面开发”工作负载winsock2.h、ws2_32.lib就都在了不需要额外配置。MinGW-w64则不同它用的是GCC编译器需要你指定头文件和库文件的搜索路径。如果你是从MSYS2或者WinLibs安装的MinGW一般已经自带了Windows SDK相关的头文件和导入库用下面的命令就可以编译gcc -o server.exe server.c -lws2_32如果提示找不到winsock2.h先检查MinGW安装目录下的include文件夹里有没有这个文件没有的话就要检查一下是不是装的是裸GCC而没有带Windows API头文件。有些精简版的GCC套件确实不含Win32 API头文件建议直接换成完整版WinLibs。5.2 在VSCode里配置tasks.json实现一键编译VSCode环境下写C语言项目运行编译任务是最常用的调试方式之一。我参考网上常见的配置整理了一个针对Winsock项目的tasks.json{ version: 2.0.0, tasks: [ { label: build winsock, type: cppbuild, command: gcc, args: [ -g, main.c, -o, main.exe, -lws2_32 ], group: { kind: build, isDefault: true }, problemMatcher: [ $gcc ] } ] }注意args里的-lws2_32少了这个参数你的程序链接时百分百报错。如果你的环境是MSVC用cl.exe编译的话对应的命令是cl main.c /Fe:main.exe /link ws2_32.libMSVC里头文件路径不需要手动指定装了Windows SDK就默认能找到但链接参数ws2_32.lib不能省。5.3 为新手准备的万能模板库我习惯把自己常用的Win32网络程序模板存到一个固定目录里每次新建项目直接拷贝一份改改。模板里面做了三件事版本宏定义、包含正确的头文件顺序、链接库声明。这样哪怕某天我必须用一台全新的电脑、空白的环境拉下来代码也能快速编译。在这段模板里我还会顺手定义一个打印错误码的工具函数static void print_win_error(const char* tag) { int err WSAGetLastError(); printf([%s] error code: %d\n, tag, err); }调试时在关键Socket调用后面加上这个加上system(pause)就能在控制台窗口一闪而过之前把错误信息留住。很多网上教程里没有提到这个小技巧但实际上对新手来说控制台窗口一闪而过真的是最让人崩溃的事情之一。6. 写在最后的实操心得WINSOCK2这个东西单独拿出来看并不难难的是它和C语言本身的特性以及Windows平台的编译体系纠缠在一起导致很多人在搞懂之前就已经被各种报错劝退了。我自己刚学的时候也经历过那种绝望感明明代码就是照着书敲的为什么编译出来全是错。后来总结了三条属于自己的经验。第一永远先确认环境再怀疑代码。遇到编译报错先看路径、看include顺序、看有没有链接ws2_32.lib这三样没问题再研究业务逻辑。第二不要一次写太多代码。每次只验证一小块功能比如先跑通WSAStartup再跑通socket创建再往下走。万金油式的一大坨代码是最难排查的。第三错误码一定要打印出来。很多问题你盯着代码看半天看不出来但把WSAGetLastError()的返回值一对照文档原因就摆在眼前了。最后再分享一个小技巧如果你在调试服务器程序时发现端口一直显示被占用多半是上一次运行的程序没有正常关闭Socket。Windows下可以用netstat -ano | findstr 8888查到占用进程的PID再用taskkill /PID pid /F杀进程。这个操作我几个月前帮学弟排查问题时用过一次他当时已经卡了整整一下午结果两分钟搞定整个人都愣住了。网络编程这条路从winsock2.h这个头文件开始后面还有一整个精彩的世界在等着你。先把地基打好后面的路会越走越顺。本文还有配套的精品资源点击获取
返回列表