简介本资源是一份面向计算机网络课程学习者的实践型教学文档聚焦Socket编程与TCP通信原理的落地实现适用于高校本科生课程设计与网络编程入门者。文档系统讲解WinSock API调用、TCP连接机制、客户/服务器模型设计及Visual C开发环境配置并包含完整的实验方案、原理框图、程序流程图、常见问题排查、结果分析与课程总结体会覆盖从理论理解到代码调试的全流程。资源为单个152KB的Word文档.doc格式结构清晰含12个标准章节内容详实且具备可复现性。目前已有684人学习下载读者可直接获取双机文本通信的完整实现思路、TCP状态机图解、Socket创建与连接关键代码逻辑以及实验中典型错误的定位方法是理解底层网络通信机制的优质参考资料。1. 这不是一份普通课程设计文档它是一份能让你亲手跑通 TCP 双机通信的 Winsock 实战手稿你是不是也遇到过这样的困境学完《计算机网络》教材里的 TCP 三次握手、滑动窗口、状态机图合上书却连“怎么让两台 Windows 电脑真正说上话”都无从下手不是缺理论是缺一个能编译、能调试、能抓包验证、出错有明确报错路径的完整闭环。这份标题为《利用 Socket 实现双机通信计算机网络课程设计》的.doc文档恰恰就是那个被严重低估的“实战母本”——它用 Visual C 6.0 Winsock 2.0 的原始组合把 TCP 面向连接通信从抽象协议拉回命令行黑窗、从socket()函数调用落到closesocket()的资源释放甚至把“端口被占用”“输入格式错误”这些新手必踩的坑都写进了第十四页的“实验中的问题”。它不讲 Spring Boot 集成 WebSocket 的 YAML 配置也不提 ESP32-S3 接收 TCP 消息的 FreeRTOS 适配它只聚焦一件事在一台装着 Windows XP/2000 的老机器上敲下s启动服务器再敲c启动客户端看着message hello真实地从 client 窗口飞进 server 窗口。这不是怀旧是回归网络编程最硬核的起点理解 socket 是什么、bind 为什么必须、listen 队列长度设为几、accept 返回的新 socket 和原 socket 到底谁在监听谁在通信。如果你正卡在“能看懂 TCP 状态图但写不出第一个 connect 调用”或者需要一份能直接导入 VC6.0 编译、调试、修改的参考实现这份文档就是你的后悔药。2. 从 Winsock 初始化到 TCP 连接建立逐行拆解 VC6.0 下的 socket 编程链路这份课程设计文档的价值不在于它用了过时的 VC6.0而在于它用最直白的 C 代码把 Winsock API 的调用顺序和依赖关系刻进了每一行注释里。它没有隐藏 WSAStartup 的版本协商细节也没有跳过struct sockaddr_in地址结构体的手动填充。下面我们就按文档第七章“设计方案”和第八章“程序流程图”的逻辑还原出可执行的核心链路并给出关键代码块与参数说明。2.1 Winsock 初始化与资源申请WSAStartup 是不可绕过的“开门咒”任何 Winsock 程序的第一步不是socket()而是WSAStartup()。文档在流程图中明确标出WSAStartup(MAKEWORD(2,0), wsadata) ! 0的判断分支这绝非形式主义。MAKEWORD(2,0)请求的是 Winsock 2.0 版本而wsadata结构体则承载了系统返回的可用协议信息。若跳过此步或版本不匹配后续所有 socket 函数调用将直接返回INVALID_SOCKET或SOCKET_ERROR且WSAGetLastError()会返回WSANOTINITIALISED错误码 10093。#include winsock2.h #include iostream #pragma comment(lib, ws2_32.lib) // 关键链接 Winsock 库 int main() { WSADATA wsadata; int result WSAStartup(MAKEWORD(2,0), wsadata); if (result ! 0) { std::cout WSAStartup erro: result std::endl; return -1; } // 此处才可安全调用 socket(), bind() 等函数 // ... 后续代码 WSACleanup(); // 必须配对调用释放 Winsock 资源 return 0; }参数说明MAKEWORD(2,0)生成主版本号 2、次版本号 0 的字节序值。wsadata是输出参数WSAStartup会填充其wVersion,wHighVersion,szDescription等字段。常见误用是传入NULL或忽略返回值导致后续调用静默失败。2.2 创建套接字与地址绑定AF_INET、SOCK_STREAM、IPPROTO_TCP 的铁三角文档 5.2 节明确列出socket()的三个核心参数protofamily,type,protocol。这三者构成 TCP 流式通信的基石。AF_INET指定 IPv4 地址族SOCK_STREAM表明这是面向连接、可靠传输的流式套接字IPPROTO_TCP则显式指定使用 TCP 协议而非让系统自动推断。这三个常量的组合是区分 TCP 与 UDP 编程的分水岭。// 服务器端创建监听套接字 SOCKET listenSock socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (listenSock INVALID_SOCKET) { std::cout socket() failed: WSAGetLastError() std::endl; WSACleanup(); return -1; } // 填充服务器地址结构体 struct sockaddr_in serverAddr; memset(serverAddr, 0, sizeof(serverAddr)); serverAddr.sin_family AF_INET; // IPv4 serverAddr.sin_port htons(8080); // 端口号需网络字节序 serverAddr.sin_addr.s_addr INADDR_ANY; // 绑定到本机所有网卡 // 执行绑定 if (bind(listenSock, (struct sockaddr*)serverAddr, sizeof(serverAddr)) SOCKET_ERROR) { std::cout bind() failed: WSAGetLastError() std::endl; closesocket(listenSock); WSACleanup(); return -1; }参数说明htons(8080)将主机字节序小端的 8080 转换为网络字节序大端这是 TCP/IP 协议栈的硬性要求。INADDR_ANY允许服务器接收发往本机任意 IP 的连接请求比硬编码inet_addr(127.0.0.1)更具通用性。sizeof(serverAddr)必须精确否则bind()可能因地址结构体长度错误而失败错误码 10014。2.3 监听、接受与连接理解listen()队列长度与accept()的阻塞本质listen()的queuesize参数文档中称“请求队列的长度”常被误解为最大并发连接数。实际上它定义的是已完成三次握手、等待accept()处理的连接队列长度即 SYN_RCVD 状态后的 ESTABLISHED 队列。文档流程图中listen()后紧接accept()正是因为它是一个阻塞调用当队列为空时accept()会挂起线程直到有新连接到达。accept()返回一个全新的SOCKET描述符专用于与该特定客户端通信而原listenSock仍保持监听状态这是实现“一服务器多客户端”的关键。// 服务器端开始监听 if (listen(listenSock, SOMAXCONN) SOCKET_ERROR) { // SOMAXCONN 是系统建议的最大值 std::cout listen() failed: WSAGetLastError() std::endl; closesocket(listenSock); WSACleanup(); return -1; } std::cout TCP Server...\n; std::cout Waiting for connection...\n; // 阻塞等待客户端连接 struct sockaddr_in clientAddr; int clientAddrLen sizeof(clientAddr); SOCKET clientSock accept(listenSock, (struct sockaddr*)clientAddr, clientAddrLen); if (clientSock INVALID_SOCKET) { std::cout accept() failed: WSAGetLastError() std::endl; closesocket(listenSock); WSACleanup(); return -1; } // 获取客户端 IP 地址文档图2中显示的 connected by 127.0.0.1 char* clientIP inet_ntoa(clientAddr.sin_addr); std::cout Connected by clientIP : ntohs(clientAddr.sin_port) std::endl;参数说明SOMAXCONN是 Windows 系统定义的宏通常为 0x7FFFFFFF表示使用系统默认最大值现代系统一般为 200。手动设为5或10亦可但过小会导致高并发时连接请求被丢弃connect()返回WSAECONNREFUSED。clientAddrLen是输入/输出参数accept()会将实际填入的地址长度写回此变量必须初始化为sizeof(clientAddr)否则行为未定义。2.4 客户端连接与数据交互connect()的超时与send()/recv()的字节流语义客户端流程同样严格遵循文档 5.1 节先socket()再connect()。connect()是一个阻塞调用它会发起 TCP 三次握手并在握手成功ESTABLISHED后返回。若服务器未启动或防火墙拦截connect()将长时间阻塞直至超时Windows 默认约 21 秒此时返回SOCKET_ERRORWSAGetLastError()为WSAETIMEDOUT10060或WSAECONNREFUSED10061。文档中要求“必须先连接服务器端再连接客户端”正是源于此阻塞特性。// 客户端创建套接字并连接服务器 SOCKET clientSock socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (clientSock INVALID_SOCKET) { std::cout socket() failed: WSAGetLastError() std::endl; WSACleanup(); return -1; } struct sockaddr_in serverAddr; memset(serverAddr, 0, sizeof(serverAddr)); serverAddr.sin_family AF_INET; serverAddr.sin_port htons(8080); // 必须与服务器 bind 的端口一致 serverAddr.sin_addr.s_addr inet_addr(127.0.0.1); // 本地回环测试 if (connect(clientSock, (struct sockaddr*)serverAddr, sizeof(serverAddr)) SOCKET_ERROR) { std::cout connect() failed: WSAGetLastError() std::endl; closesocket(clientSock); WSACleanup(); return -1; } std::cout TCP Client...\n; std::cout Connected to 127.0.0.1\n; // 发送数据文档要求格式为 message content std::string message message Hello from Client!; int sentBytes send(clientSock, message.c_str(), message.length(), 0); if (sentBytes SOCKET_ERROR) { std::cout send() failed: WSAGetLastError() std::endl; } else { std::cout Sent sentBytes bytes\n; }参数说明send()的flags参数为0表示默认阻塞模式。message.length()是发送的字节数send()返回实际发送的字节数可能小于请求长度尤其在网络拥塞时因此健壮代码需循环调用直至全部发送完毕。recv()同理它从 TCP 字节流中读取数据不保证一次recv()读到的就是一个完整的“消息”——这正是 TCP 粘包问题的根源也是文档中“必须按给定格式输入”的底层原因服务端需自行解析message前缀。3. 从 TCP 三次握手到字节流处理深入理解文档背后的网络协议原理这份课程设计文档之所以能成为“实战手稿”是因为它把 TCP 协议栈的抽象概念精准地映射到了每一行 Winsock API 调用上。理解这些映射才能真正驾驭代码而非机械复制。我们以文档第五章“TCP 简介及特点原理”和第八章“程序流程图”为线索剖析其背后的核心机制。3.1 三次握手在代码中的具象化listen()、connect()、accept()的状态流转文档第五章详细描述了 TCP 三次握手过程SYN → SYNACK → ACK。这个过程并非由应用层代码直接控制而是由 Winsock 内核驱动自动完成。listen()、connect()、accept()这三个函数正是应用层与内核 TCP 状态机交互的唯一接口。listen(listenSock, ...)将listenSock置于LISTEN状态。此时内核开始监听指定端口准备接收 SYN 包。connect(clientSock, ...)客户端内核收到此调用立即向服务器发送 SYN 包自身进入SYN_SENT状态。若收到服务器的 SYNACK则发送 ACK进入ESTABLISHED状态并唤醒connect()返回。accept(listenSock, ...)服务器内核在LISTEN状态下收到 SYN回复 SYNACK自身进入SYN_RCVD状态收到客户端 ACK 后连接进入ESTABLISHED状态并放入已完成连接队列。accept()从该队列取出一个连接创建新的clientSock并返回。关键洞察accept()返回的clientSock是一个全新的套接字其内核状态为ESTABLISHED它与listenSock状态为LISTEN完全独立。这意味着服务器可以同时处理多个客户端listenSock持续监听新连接每个clientSock独立进行send()/recv()。文档流程图中accept()后产生“新的连接 Socket”正是对此的准确描述。3.2 TCP 字节流服务与粘包问题为何文档强制要求 message 格式文档第七章“实验中的问题”第二条指出“建立好连接之后必须按照给定的格式输入通信信息即 m输入的信息内容否则将会出现‘no this command’的提示。” 这看似是程序设计的“陋习”实则是对 TCP字节流bytestream本质的深刻体现。TCP 不提供消息边界它只保证字节的有序、可靠传输。应用层发送的send(message A, 9)和send(message B, 9)在接收端可能被合并为一次recv(buffer, 1024)读到message Amessag...也可能被拆分为两次recv()分别读到message A和message B。文档中的服务端代码虽未全文给出但流程图和结果分析可推断必然包含一个简单的协议解析器循环recv()读取数据到缓冲区在缓冲区中搜索message 字符串若找到则提取其后的文本作为有效载荷若未找到则认为是非法命令输出no this command。// 服务端伪代码处理粘包的简易协议解析 char recvBuf[1024]; std::string recvBuffer; // 累积接收的字节流 while (true) { int n recv(clientSock, recvBuf, sizeof(recvBuf)-1, 0); if (n 0) { recvBuf[n] \0; recvBuffer recvBuf; // 累积 // 查找 message 前缀 size_t pos recvBuffer.find(message ); if (pos ! std::string::npos) { std::string payload recvBuffer.substr(pos 8); // 跳过 message // 去除 payload 末尾可能的换行符等 payload.erase(payload.find_last_not_of(\r\n) 1); std::cout Received: payload std::endl; // 清空已处理部分保留剩余未解析数据 recvBuffer recvBuffer.substr(pos 8 payload.length()); } } else if (n 0) { // 对方关闭连接 break; } else { // recv 错误 break; } }参数说明recv()的flags为0表示阻塞模式。recvBuffer作为累积缓冲区是处理粘包的关键。find(message )是应用层定义的消息边界。这解释了为何文档强调格式——它用最朴素的方式在教学场景中引入了协议设计的核心思想在无边界的字节流之上构建有边界的、可解析的应用消息。3.3 端口号与连接复用理解文档中“端口必须一致”与“程序未关闭则端口被占”的物理意义文档第七章问题1和问题3直指网络编程的物理约束“两端的端口号必须设为一致”、“如果一个使用某端口的程序没有关闭另一个程序就不能使用这个端口”。这源于 TCP 连接的五元组源IP、源端口、目的IP、目的端口、协议唯一标识性。端口一致客户端connect()时指定的目的端口必须与服务器bind()时指定的端口完全相同否则 SYN 包会被服务器内核丢弃无对应LISTEN套接字客户端connect()返回WSAECONNREFUSED。端口独占bind()操作会将指定端口与一个套接字绑定。只要该套接字未被closesocket()关闭或未进入TIME_WAIT状态SO_REUSEADDR选项可缓解该端口就对其他进程不可用。尝试bind()同一端口会返回WSAEADDRINUSE10048错误。关键配置为避免TIME_WAIT导致端口无法快速重用可在bind()前设置套接字选项int optval 1; setsockopt(listenSock, SOL_SOCKET, SO_REUSEADDR, (const char*)optval, sizeof(optval));此选项允许bind()重用处于TIME_WAIT状态的地址端口是开发调试阶段的必备技巧文档虽未提及但属于一线工程师的血泪经验。4. 避坑 / 常见问题 / 排查基于文档“实验中的问题”与真实调试经验的 5 条血泪记录文档第九章“实验中的问题”列出了 4 个典型现象结合我多年在 Windows 平台调试 Winsock 程序的经验将其扩展为 5 条可直接复现、定位、解决的避坑指南。每一条都来自真实翻车现场绝非纸上谈兵。4.1 现象connect()持续阻塞约 21 秒后返回WSAETIMEDOUT10060原因客户端connect()试图连接一个根本不存在的服务器IP 地址错误、服务器进程未启动、防火墙拦截 SYN 包。Windows 默认 TCP 连接超时时间约为 21 秒期间内核不断重传 SYN 包。解决基础检查确认服务器端程序已运行且listen()成功查看控制台是否输出 TCP Server...。网络连通性在客户端机器上执行ping 服务器IP确认 ICMP 通。端口可达性使用telnet 服务器IP 端口如telnet 127.0.0.1 8080。若telnet连接成功说明端口开放且服务正常若提示“无法打开到主机的连接”则问题在服务器端或网络策略。防火墙临时关闭 Windows 防火墙或在防火墙入站规则中为你的程序或端口添加例外。4.2 现象bind()失败WSAGetLastError()返回WSAEADDRINUSE10048原因你试图bind()的端口如 8080正被另一个进程可能是上次崩溃未退出的程序、其他网络服务、甚至浏览器占用。bind()要求端口在TIME_WAIT状态结束后才能被新进程重用。解决查找占用进程以管理员身份运行命令提示符执行netstat -ano | findstr :8080获取占用端口的 PID。结束进程执行taskkill /PID PID /F强制结束。预防措施在服务器socket()创建后、bind()之前添加SO_REUSEADDR选项见 3.3 节代码允许重用TIME_WAIT端口。这是开发阶段的黄金法则。4.3 现象服务器accept()后recv()一直返回 0或客户端send()后服务器无任何输出原因recv()返回 0 表示对端已优雅关闭连接shutdown(SD_SEND)或closesocket()。但更常见的原因是客户端发送的数据未以\n或其他约定符号结尾导致服务端的简易协议解析器寻找message 永远找不到匹配项recvBuffer中的数据越积越多但程序逻辑卡在find()上未做超时或长度限制。解决强制刷新输出在客户端send()后添加fflush(stdout)或确保字符串以\n结尾如send(..., message Hello\n, ...)模拟真实终端输入。增强服务端解析逻辑在recv()循环中加入超时或最大缓冲区长度检查。例如若recvBuffer.length() 1024且未找到message 则清空缓冲区并报错防止内存无限增长。使用 Wireshark 抓包直接观察网络层确认数据包是否真的发出并被服务器接收这是终极排错手段。4.4 现象程序编译通过但运行时报错The procedure entry point WSAStartup could not be located in the dynamic link library WINSOCK.DLL原因这是典型的Winsock 版本错配。文档基于 Winsock 2.0ws2_32.lib但你的项目链接了旧版 Winsock 1.1 的库wsock32.lib或系统WINSOCK.DLL文件损坏/缺失。VC6.0 默认可能链接wsock32.lib。解决强制链接ws2_32.lib在代码顶部添加#pragma comment(lib, ws2_32.lib)或在 VC6.0 项目设置中Project - Settings - Link 选项卡在 Object/library modules 中删除wsock32.lib添加ws2_32.lib。检查头文件确保#include winsock2.h在#include windows.h之前因为winsock2.h会定义自己的类型与windows.h中的旧定义冲突。4.5 现象在非本机如局域网另一台电脑上运行客户端连接服务器失败原因服务器bind()时使用了INADDR_LOOPBACK即127.0.0.1这仅允许本机回环连接。文档流程图中显示connected by 127.0.0.1暗示其默认配置为回环测试。解决修改服务器bind()地址将serverAddr.sin_addr.s_addr INADDR_LOOPBACK;改为serverAddr.sin_addr.s_addr INADDR_ANY;使其监听所有网卡的 IP。检查服务器防火墙确保 Windows 防火墙允许入站连接到你的程序或端口8080。确认 IP 地址客户端connect()时inet_addr()的参数应改为服务器的真实局域网 IP如192.168.1.100而非127.0.0.1。5. 从命令行黑窗到可验证的工程将文档转化为可调试、可抓包、可扩展的 VC6.0 工程文档的价值在于它提供了一个最小可行的、可运行的骨架。但要让它真正成为你的“实战手稿”必须将其从零散的代码片段和流程图升华为一个结构清晰、易于调试、便于验证的完整 VC6.0 工程。以下是我基于文档内容在 VC6.0 中重建并反复验证的工程实践每一步都服务于一个明确目标让 TCP 通信过程变得透明、可观察、可干预。5.1 工程结构与文件组织分离 Server/Client统一 Winsock 管理拒绝将所有代码堆在一个.cpp文件里。我将工程拆分为三个核心文件严格遵循文档的“服务器/客户端”双模型文件名职责关键内容WinsockManager.h/.cppWinsock 生命周期管理封装WSAStartup()/WSACleanup()提供单例访问确保全局只初始化一次。避免文档中流程图里多处WSAStartup判断带来的冗余。TCPServer.cpp服务器端逻辑实现main()包含socket()/bind()/listen()/accept()/recv()/send()全流程。重点强化日志打印accept()返回的客户端 IP/端口、recv()实际字节数、send()返回值。TCPClient.cpp客户端逻辑实现main()包含socket()/connect()/send()/recv()。提供交互式输入std::cin command支持message text和quit命令并打印发送/接收详情。为什么这样组织文档的流程图是线性的但实际调试时你需要独立启动 Server 和 Client。分离文件后你可以右键TCPServer.cpp- Compile单独编译服务器同理编译客户端。无需每次修改都重新链接整个项目极大提升迭代速度。WinsockManager的封装则彻底规避了文档中“WSAStartup erro”重复判断的混乱。5.2 关键增强添加实时日志与错误上下文让调试不再“玄学”文档的输出非常简洁tcp Server...,Connected by 127.0.0.1这对于理解流程足够但对于排错远远不够。我在每个 Winsock API 调用后都添加了带错误码的详细日志// 在 TCPServer.cpp 中 SOCKET listenSock socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (listenSock INVALID_SOCKET) { int err WSAGetLastError(); std::cerr [ERROR] socket() failed. Code: err ( getErrorString(err) ) std::endl; return -1; } std::cout [INFO] socket() created. Handle: listenSock std::endl;其中getErrorString(int err)是一个辅助函数将 Winsock 错误码如 10048转换为可读字符串WSAEADDRINUSE。这让我在看到[ERROR] bind() failed. Code: 10048 (WSAEADDRINUSE)时瞬间定位到端口冲突而不是在connect()失败后大海捞针。5.3 验证闭环Wireshark 抓包 命令行netstat眼见为实文档的“实验结果及分析”只有文字描述。真正的信服来自于亲眼所见。我将这套 VC6.0 工程与两个工具组成验证闭环Wireshark 抓包启动 Wireshark过滤tcp.port 8080。启动 Server再启动 Client 并发送message test。你将清晰看到SYN→SYNACK→ACK三次握手PSH, ACK数据包其Data字段明文显示message testFIN, ACK→FIN, ACK四次挥手如果双方都正常关闭这直接印证了文档第五章关于 TCP 可靠传输、字节流、连接管理的所有论述。netstat命令在 Server 运行后执行netstat -an | findstr :8080。你将看到TCP 0.0.0.0:8080 0.0.0.0:0 LISTENING服务器监听TCP 127.0.0.1:8080 127.0.0.1:50000 ESTABLISHED客户端连接50000 是客户端随机端口这完美对应文档 2.1 节“服务器拥有全局公认的 socket客户随机申请一个 socket”的客户/服务器模型描述。5.4 进阶技巧用select()实现单线程多客户端突破文档的“单连接”限制文档的流程图和描述隐含了一个前提一个服务器进程只服务一个客户端。这是教学简化但现实需求是“一对多”。文档并未涉及但这恰恰是检验你是否真正吃透 Winsock 的试金石。我基于文档的TCPServer.cpp用select()函数进行了改造// 在 TCPServer.cpp 的主循环中 fd_set readfds; struct timeval timeout; timeout.tv_sec 1; // 1秒超时避免永久阻塞 timeout.tv_usec 0; while (true) { FD_ZERO(readfds); FD_SET(listenSock, readfds); int maxSock listenSock; // 将所有已连接的 clientSock 加入集合 for (auto client : clientSockets) { FD_SET(client.sock, readfds); if (client.sock maxSock) maxSock client.sock; } int activity select(maxSock 1, readfds, NULL, NULL, timeout); if (activity 0) { std::cerr select error std::endl; break; } if (activity 0) continue; // 超时继续循环 // 检查是否有新连接 if (FD_ISSET(listenSock, readfds)) { // accept() 新连接并加入 clientSockets 列表 } // 检查每个已连接的 clientSock for (auto it clientSockets.begin(); it ! clientSockets.end();) { if (FD_ISSET(it-sock, readfds)) { // recv() 数据处理粘包广播给其他客户端... if (bytesReceived 0) { // 客户端断开从列表中移除 closesocket(it-sock); it clientSockets.erase(it); } else { it; } } else { it; } } }参数说明select()的readfds集合监控所有待读套接字监听套接字 所有客户端套接字。maxSock 1是 Windows 下select()的第一个参数表示监控的最大套接字描述符加一。timeout提供了非阻塞轮询的能力。这段代码让单个服务器进程能同时处理数十个客户端是文档“双机通信”概念的自然延伸也是从课程设计迈向真实网络服务的关键一步。从那以后我每次搭建新的网络服务原型都强制走一遍这个闭环VC6.0 编译 → 命令行启动 Server/Client → Wireshark 抓包确认三次握手和数据流 →netstat查看连接状态 → 最后用select()或WSAAsyncSelect尝试扩展。这个习惯让我在面对no more data to read from socket或socket is not connected这类模糊错误时总能迅速回到 TCP 状态机和 Winsock API 的第一性原理上而不是在框架的黑匣子里徒劳猜测。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?