
简介一份聚焦计算机网络课程设计的Socket双机通信完整报告面向需要完成同类课题或学习TCP网络编程的高校学生。文档基于WinSock API采用TCP面向连接方式实现文本数据交换详细展开了Socket“套接字”原理、WinSocket通信机制、TCP协议特点与状态机等关键知识点。包体为单份doc文档整体大小152KB内容结构完整依次涵盖设计任务、WinSocket与TCP原理、Visual C开发环境、设计方案、系统原理框图和程序流程图、实验中的问题、结果分析及总结体会。该资源已有684人学习下载适合作为课程设计参考模板可帮助读者快速理解双机通信实现思路、掌握TCP编程要点并借鉴规范的报告撰写与排错思路。1. 一份Socket双机通信课程设计文档把TCP从书里搬到屏幕上Socket双机通信是计算机网络课程设计里出现频率最高的题目之一这份文档的价值在于它把“理论课上的TCP三次握手”和“能在Windows上实际跑起来的代码”串成了一条完整链路先用文字讲清WinSock是什么、TCP为什么可靠再给出服务器端和客户端的完整调用顺序最后连实验中的端口冲突、连接顺序问题都直接写在文档里。对正在做计算机网络课设的在校学生来说这是一份能照着改、照着写报告的参照物对刚接触socket网络编程的从业者来说这份文档也足够让你在半小时内理解socket编程的基本骨架——先启服务器再启客户端通过127.0.0.1把文本消息从一端送到另一端然后把这个过程换成真实IP就是双机通信。文档基于Visual C 6.0平台代码风格是传统的C语言WinSock API调用不依赖MFC框架学习成本比想象中低。2. 从WinSock到TCP状态机为什么这份文档把原理讲在前面2.1 WinSock的本质它不是协议而是一层API文档在前半部分花了不小篇幅解释socket是什么这个铺垫不是废话。很多人第一次接触socket编程容易把“socket”和“TCP/IP协议”混为一谈。socket实际是TCP/IP网络暴露给应用程序的API接口在Windows平台上这套接口被封装成WinSock。文档里有一句很关键的描述socket是一种文件描述符网络的socket数据传输是一种特殊的I/O。这句话理解透了后续看socket()、send()、recv()这些函数时思路就顺了——它们的调用方式和文件操作高度相似核心区别只是数据不是落在磁盘而是流向网络。WinSock在Windows下由两部分组成开发组件和运行组件。开发组件是给程序员用的包括WINSOCK.H头文件、导入库和文档运行组件是WINSOCK.DLL动态链接库程序运行时由系统加载。这种拆分在实际使用中的意义是你写代码时只需要引入头文件并链接对应的库不需要关心WINSOCK.DLL怎么装的Windows系统自带了它。另外WinSock是面向客户/服务器模型设计的服务器需要有一个全局公认的socket客户端的socket则可以随机申请。文档用打电话来打比方没有人知道对方的电话号码通话就无从谈起。这个比喻很朴素但准确说明了“服务器端口固定、客户端端口随机”这个机制。常用的socket类型有两种流式socketSOCK_STREAM和数据报式socketSOCK_DGRAM。前者面向连接对应TCP服务后者无连接对应UDP服务。这个选择直接影响后面所有代码的写法——如果选错了连接建立的逻辑完全不通。2.2 TCP为什么可靠从三次握手到滑动窗口TCP被选为这个课设的传输层协议原因是设计任务明确要求“采用TCP面向连接方式实现文本数据的交换”同时要求理解TCP状态机图。那TCP靠什么做到可靠传输文档梳理了这样一条完整链路应用层的字节流被TCP分割成报文段每个字节分配一个序号接收端收到后回传ACK确认发送端启动定时器如果在合理往返时延内没收到确认就判定报文丢失并重传每个报文段都带校验和接收端发现校验失败就丢弃该报文段且不回复确认逼发送端超时重发。在这之上还有滑动窗口做流量控制慢启动算法做拥塞控制。这些机制单独拎出来都能写一篇长文但对做课设的人来说最需要记住的是三次握手的过程因为程序里connect()和accept()的成功返回本质就是三次握手完成的标志。第一次握手客户端发送SYN包序号为x进入SYN_SEND状态第二次握手服务器收到SYN后回复ACKx1同时自己也发送一个SYN包序号为y即SYNACK包服务器进入SYN_RECV状态第三次握手客户端收到服务器的SYNACK再回复一个ACKy1之后双方进入Established状态。状态机图上最核心的三个状态——SYN_SEND、SYN_RECV、Established正好对应客户端connect()调用后、服务器accept()返回后、以及双方正式开始收发数据时。后面你在程序里看到的connect()返回0在底层的含义就是三次握手走完了。2.3 为什么选Visual C 6.0一个课程设计场景下的现实考虑文档专门用一章介绍Visual C这段内容很多人会跳过但实际上它解释了整套代码的编译环境。文档把Visual C拆成三块Developer Studio集成开发环境负责编辑和工程管理本身不编译、MFC类库可以不用、Platform SDK含C/C编译器CL、NMAKE等命令行工具才是真正编译代码的部分。这个拆解给了一个明确信号用Visual C 6.0写WinSock程序可以完全不碰MFC直接用Win32 API。这也是这份文档的代码风格——纯C语言调用socket API没有类封装没有消息映射。从实际环境看虽然Visual C 6.0是很老的IDE但直到今天许多学校的课程设计仍然指定或默认使用它。原因不外乎几点对WinSock的支持非常成熟winsock.h就在系统SDK里工程配置简单新建Win32 Console Application就能跑代码兼容性好拿到的参考代码基本都能直接编译。如果你手头只有新版Visual Studio用Visual Studio 2015以上版本新建一个控制台空项目同样可以编译这份代码只需要在预处理定义里加上_WINSOCK_DEPRECATED_NO_WARNINGS来消除旧API的警告。这不影响程序行为只是编译器对旧式API的提示不同。3. 把服务器和客户端跑起来WinSock调用顺序与函数参数明细3.1 服务器端五个调用socket、bind、listen、accept、recv文档的程序设计部分给出了服务器端通信的完整步骤建立socket、绑定端口、进入监听、接受连接、收发数据、关闭socket。在WinSock API下这个过程对应五个核心函数调用下面是服务器端的骨架代码#include winsock2.h #include stdio.h #pragma comment(lib, ws2_32.lib) int main() { WSADATA wsaData; SOCKET listenSock, acceptSock; struct sockaddr_in addr, clientAddr; char recvBuf[256]; int clientAddrLen sizeof(clientAddr); // 1. 初始化WinSock请求2.0以上版本 if (WSAStartup(MAKEWORD(2, 0), wsaData) ! 0) { printf(WSAStartup error\n); return -1; } // 2. 创建流式socket对应TCP listenSock socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); if (listenSock INVALID_SOCKET) { printf(socket error: %d\n, WSAGetLastError()); WSACleanup(); return -1; } // 3. 绑定IP和端口端口要和客户端一致 addr.sin_family AF_INET; addr.sin_addr.s_addr htonl(INADDR_ANY); // 监听所有本地地址 addr.sin_port htons(8888); // 端口号务必与客户端相同 if (bind(listenSock, (struct sockaddr*)addr, sizeof(addr)) SOCKET_ERROR) { printf(bind error: %d\n, WSAGetLastError()); closesocket(listenSock); WSACleanup(); return -1; } // 4. 开始监听等待客户端连接 if (listen(listenSock, 5) SOCKET_ERROR) { printf(listen error\n); closesocket(listenSock); WSACleanup(); return -1; } printf(tcp server...\n); // 5. 接受客户端连接返回新的socket用于通信 acceptSock accept(listenSock, (struct sockaddr*)clientAddr, clientAddrLen); if (acceptSock INVALID_SOCKET) { printf(accept error\n); closesocket(listenSock); WSACleanup(); return -1; } printf(connected by %s\n, inet_ntoa(clientAddr.sin_addr)); // 6. 接收客户端发来的文本消息 recv(acceptSock, recvBuf, sizeof(recvBuf), 0); printf(received: %s\n, recvBuf); // 7. 释放资源 closesocket(acceptSock); closesocket(listenSock); WSACleanup(); return 0; }这段代码有几个关键参数需要说明。WSAStartup(MAKEWORD(2, 0), wsaData)的第一个参数是请求的socket版本号MAKEWORD(2,0)表示请求WinSock 2.0这是绝大多数Windows系统都支持的版本socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)中PF_INET表示IPv4协议族SOCK_STREAM是流式socketIPPROTO_TCP明确指定TCP协议三个参数缺一不可bind()里的htonl(INADDR_ANY)表示监听本机所有网络接口这样无论连接来自127.0.0.1还是局域网IP都能接收到listen(listenSock, 5)的第二个参数是请求队列长度表示服务器正在处理当前连接时最多还能排队的连接请求数量。注意accept()的返回值——它返回的是一个全新的socket描述符真正的数据收发走的是这个新socket而listenSock继续留在监听状态。这是很多初学者最容易误解的地方以为accept()返回的还是原来的监听socket。文档在“WinSocket通信的步骤”里明确写了“服务器会产生一个新的连接Socket来处理用户的请求而原来的Socket仍然会进行监听操作”这个机制也是TCP并发处理多个客户端的基础。3.2 客户端四个调用socket、connect、send、closesocket客户端相对简单核心是socket()创建套接字connect()发起连接send()发送数据。文档里的客户端流程是建立socket、从命令行获取服务器IP和端口、发送连接请求、等待回馈、交互数据、关闭连接。在单机演示场景下服务器IP直接写127.0.0.1即可。客户端骨架代码#include winsock2.h #include stdio.h #pragma comment(lib, ws2_32.lib) int main() { WSADATA wsaData; SOCKET clientSock; struct sockaddr_in serverAddr; char sendBuf[256]; // 1. 初始化WinSock if (WSAStartup(MAKEWORD(2, 0), wsaData) ! 0) { printf(WSAStartup error\n); return -1; } // 2. 创建socket参数与服务器端保持一致 clientSock socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); if (clientSock INVALID_SOCKET) { printf(socket error\n); WSACleanup(); return -1; } // 3. 指定服务器地址和端口 serverAddr.sin_family AF_INET; serverAddr.sin_addr.s_addr inet_addr(127.0.0.1); // 本机测试用 serverAddr.sin_port htons(8888); // 必须与服务器bind的端口一致 if (connect(clientSock, (struct sockaddr*)serverAddr, sizeof(serverAddr)) SOCKET_ERROR) { printf(connect error: %d\n, WSAGetLastError()); closesocket(clientSock); WSACleanup(); return -1; } printf(tcp client... connected to 127.0.0.1\n); // 4. 发送文本消息按文档要求以m开头 sprintf(sendBuf, m hello from client); send(clientSock, sendBuf, strlen(sendBuf), 0); // 5. 关闭连接释放资源 closesocket(clientSock); WSACleanup(); return 0; }connect()的三个参数分别是客户端socket描述符、服务器地址结构指针和地址结构长度。地址结构里的sin_addr.s_addr是服务器IP的二进制表示这里用inet_addr(127.0.0.1)把点分十进制字符串转成网络字节序的整数sin_port用htons(8888)把主机字节序端口转成网络字节序。这两个转换函数很容易被忽略但写错的话表现就是connect()返回10049这类错误后面会展开讲。send()的第四个参数flags一般填0文档里的解释是“包含了请求特殊选项的位做系统调试用”实际开发中极少用到非0值。从调用顺序上看服务器端的bind→listen→accept和客户端的connect是两个并行的流程connect()发出后要等服务器的accept()响应三次握手完成两端才都进入Established状态。所以运行顺序必须是先启动服务器再启动客户端。文档的“实验中的问题”第4条专门强调了这一点“必须先连接服务器端再连接客户端否则不能预期将服务器端和客户端连通。”3.3 代码跑通之后用netstat对照TCP状态机代码能编译、能运行不代表你理解了TCP。一个很实用的验证方法是在程序运行的不同阶段打开命令行执行netstat -an查看本机的TCP连接状态。启动服务器并调用listen()后你会看到本地8888端口处于LISTENING状态客户端connect()发起后客户端一侧会出现SYN_SENT状态整个连接建立成功后两端都会显示出ESTABLISHED状态。把这些状态变化和三次握手的文字描述对照起来看比你背十遍状态机图都管用。提示netstat是Windows自带的网络状态查看工具查看到的是本机TCP/UDP端口的实时状态这个命令只做本地排障不涉及任何外部网络资源。这个习惯的价值在双机通信场景下更明显当两台电脑连不上时你分别在两边跑netstat一眼就能看出是服务器没进入LISTENING还是客户端卡在SYN_SENT排查范围立刻缩小一半。文档提供了系统的原理框图和程序流程图仔细看会发现主程序先用InputType()读取用户输入输入“s”走服务器流程输入“c”走客户端流程其他字符输出“no this command”。这段流程对应到代码里就是一个简单的分支判断但这样的设计让一份代码同时承担服务器和客户端两个角色适合课设报告展示。4. 双机通信常见问题排查端口、顺序、收包格式的四个坑4.1 文档里已经写出来的坑第一手经验比教程更值得抄文档“实验中的问题”一章只有四条但每一条都是实际运行中遇到的真实故障。第一条是“建立Socket连接时两端的端口号必须设为一致否则无法建立连接”。这个坑在代码层面表现为服务器bind了8888端口客户端connect的却是8889connect()会直接返回失败因为目标端口上根本没有人监听。排查时先看两端的htons()参数是否一致再看有没有被中间设备改写端口。第二条是“建立好连接之后必须按照给定的格式输入通信信息即m输入的信息内容否则会提示no this command”。这说明文档提供的程序里有一个输入格式校验逻辑服务端收到的消息如果不以m开头会被判定为非法命令。遇到这类问题时不要想着绕过校验而是按照程序约定好的格式发送数据这也提醒你接手别人代码时先找到消息格式定义再动手写发送逻辑。第三条是“如果一个使用某端口的程序没有关闭另一个程序就不能使用这个端口”。这是端口占用问题Windows下的典型报错是Windows socket error: 通常每个套接字地址(协议/网络地址/端口)只允许使用一次错误码是10048。原因通常是上次运行的程序没有正常关闭进程还在后台占用着8888端口。解决方法是打开任务管理器结束残留进程或者用netstat -ano查出占用端口的进程PID再在任务管理器里对应结束。第四条是“必须先连接服务器端再连接客户端”。这个顺序问题在代码层面表现为客户端connect()请求发出后迟迟得不到响应程序卡在connect()调用上。因为服务器还没启动SYN包发出去没人应答客户端会一直等待直到超时。这不是bug是TCP三次握手的正常行为。后面排查的时候看到connect()卡住第一反应应该是查服务器端程序有没有启动、有没有进入listen状态。4.2 实测中容易复现的三个新坑除了文档里写的四条实际动手跑这份代码时还有几个高频翻车点。第一个是连接成功后服务器收不到消息或者收到的消息乱码。现象是客户端显示send成功服务器端recv返回后打印的内容不完整或带有乱码。原因是忽视了TCP是字节流协议底层不保证一次recv()就能收全所有字节。文档里明确写过“如果一方的应用程序先传10字节又传20字节再传50字节连接的另一方将无法了解发方每次发送了多少字节。”所以接收方要做循环接收直到缓冲区收满或者约定好的消息终止符出现。课设代码里通常只recv一次能跑通是因为数据量小但你要知道这个写法是偷懒的换到真实场景会翻车。第二个是两台真实电脑之间连不通但同一台机器上127.0.0.1完全正常。现象是双机模式下connect()返回10060连接超时。原因分两头查如果代码里服务器IP写死为127.0.0.1客户端连的其实是自己服务器那台机器根本没收到请求如果IP已经改成对方地址那大概率是Windows防火墙拦截了入站连接。解决方法是把客户端代码里的inet_addr(127.0.0.1)换成服务器的实际局域网IP并在服务器那台机器上给防火墙添加入站规则放行TCP 8888端口。第三个坑和缓冲区有关客户端连续发送多条消息服务器端只能收到第一条。现象是服务器打印出第一条后进程就结束了。原因不是网络问题而是课设代码里recv()只调用了一次程序收到第一条消息后走完流程直接closesocket()退出了。解决方法是把recv()放进循环或者改成收到消息后继续等待下一条。从学习的角度看建议改成下面这样while (recv(acceptSock, recvBuf, sizeof(recvBuf), 0) 0) { printf(received: %s\n, recvBuf); memset(recvBuf, 0, sizeof(recvBuf)); }这段循环接收代码在逻辑上更符合TCP字节流的特点。recv()的返回值是实际接收到的字节数大于0说明收到了数据等于0说明对端正常关闭了连接小于0说明出错。把memset放在printf后面清空缓冲区可以防止上一次的残留数据干扰下一次显示。参数上recvBuf直接传入数组名即可sizeof(recvBuf)得到整个缓冲区大小防止越界写入。注意最后一个坑很容易被误判为“发送端没发出来”。实际排障时你可以在客户端send()之后立刻调用closesocket()这会触发TCP的FIN包服务器端recv()可能返回0而收不到数据。正确做法是发送后先不关socket等服务器回一条确认消息或等待几十毫秒再关闭。5. 从127.0.0.1到真实局域网把文档代码改成双机通信文档的实验结果是基于单机完成的服务器端输出“connected by 127.0.0.1”客户端输出“connected to 127.0.0.1”两台“机器”其实跑在同一台电脑上。对课设验收来说这已经能说明原理但要做到真正的双机通信只需要改两个地方并且补充一步验证操作。第一个改动是IP地址。客户端代码里的inet_addr(127.0.0.1)要改成服务器那台电脑的局域网IP比如inet_addr(192.168.1.100)。服务器端不需要改因为bind()里用的是INADDR_ANY它已经监听了本机所有网卡地址。第二个改动是防火墙。服务器端的Windows防火墙默认会拦掉入站的TCP连接需要手动放行8888端口或者允许当前程序通过防火墙。在控制面板的“允许应用通过防火墙”里把程序添加进去即可。如果你想直接用命令行添加规则管理员权限下执行netsh advfirewall firewall add rule nametcp8888 dirin actionallow protocolTCP localport8888这条命令的含义是在高级防火墙配置中新建一条入站规则规则名叫tcp8888动作是放行协议是TCP端口是本机8888。执行成功后另一台电脑上的客户端就能正常连上了。完成代码修改后强烈建议按这样一个顺序验证先保持127.0.0.1跑一遍确认代码逻辑没问题再改成Linux局域网IP跑一遍同时在两台机器上分别执行netstat -an观察ESTABLISHED状态最后用wireshark抓一次三次握手的包截图放进课设报告里。这一步是把“会执行”提升到“懂原理”的关键答辩时老师问TCP状态机你直接说“我们抓包看到客户端先发SYN服务器回SYNACK客户端再回ACK然后状态就变成ESTABLISHED了”这比背课本上的定义有说服力得多。我最初带学生做这个课设时很多人在单机跑通后就觉得万事大吉结果答辩被问“为什么两台电脑之间连不上”就哑口无言。后来我给自己定了个习惯凡是socket通信的代码不管多简单都要在真实双机环境下完整跑一遍连接、收发、断开全过程把netstat的状态变化记录在报告附录里。从那以后凡是按这个流程走完的学生答辩时基本都能顶住追问。希望你也能把这份文档当跳板跑通只是起点把三次握手和状态机刻在脑子里才是这门课设真正要交的答卷希望帮到你。本文还有配套的精品资源点击获取