
简介本资源是东南大学自动化学院《信息通信网络概论》课程第四次实验的完整报告面向计算机网络初学者、高校相关专业学生及网络编程实践者聚焦TCP/IP与UDP/IP双协议栈下的网络通信应用开发。报告详述基于WinSock接口的客户机/服务器程序设计原理、实现流程与界面交互逻辑涵盖连接建立、消息收发、主机名与时间戳获取、自定义字符画如/n牛、/p电话及系统调用/r打开资源管理器等扩展功能并附关键代码片段与实验记录截图。资源为单个PDF文件共1页大小仅20KB轻量便携内容结构清晰含实验目的、原理、步骤、设备配置、详细记录、总结及思考题六大模块。目前已有88人学习下载适合用于课程复习、实验复现、Socket编程入门参考及教学案例拓展。1. 东南大学计算机网络第四次实验报告一份被低估的 WinSock 实战手稿藏着 TCP/UDP 双协议调试的完整链路这不是一份躺在课程归档目录里吃灰的 PDF而是一份 2016 年实打实跑在 Windows XP/7 环境下的 MFC WinSock 双协议通信工程实录。它没用 Python 的socket库封装没调asyncio异步抽象而是用原始WSAStartup()、socket()、bind()、listen()、accept()、send()、recv()、sendto()、recvfrom()一行行敲出来的客户端-服务器双向交互系统——所有逻辑都压在CProject2Dlg和CUDPprojectDlg这两个对话框类里。它解决的不是“怎么连上服务器”而是“怎么让两个进程在局域网里真正‘说上话’还能画牛、弹窗、开资源管理器、导出聊天记录”。适合刚学完《计算机网络》运输层章节、正卡在“理论懂了但代码写不出来”阶段的学生也适合想快速复现一个可调试、可扩展、带 UI 的 WinSock 入门项目的嵌入式/工控工程师——毕竟工业现场的 Modbus TCP、Profinet 封装底层绕不开的就是这套 socket 生命周期管理。你不需要重装 VS2008只要一台能跑 MFC 的 Windows 机器就能把这份报告里的代码抠出来编译、断点、改参数、加日志亲眼看到accept()返回新 socket 的那一刻recvfrom()收到 UDP 包时fromlen被自动更新的瞬间。2. TCP 通信模块拆解从套接字生命周期到自定义协议指令解析2.1 套接字初始化与连接建立为什么WSAStartup()必须在最前且版本不能错TCP 模块的起点不是socket()而是WSAStartup()。报告中虽未显式写出该函数调用但从其使用SOCKET类型、SOCK_STREAM参数及后续connect()行为可反推——这是 WinSock 1.1 或 2.2 的典型用法。正确初始化是后续一切操作的前提// 必须在任何 socket 操作前调用 WORD wVersionRequested MAKEWORD(2, 2); // 请求 WinSock 2.2 WSADATA wsaData; int err WSAStartup(wVersionRequested, wsaData); if (err ! 0) { AfxMessageBox(_T(WSAStartup failed: ) CString(err)); return FALSE; } // 验证版本是否匹配关键 if (LOBYTE(wsaData.wVersion) ! 2 || HIBYTE(wsaData.wVersion) ! 2) { WSACleanup(); AfxMessageBox(_T(WinSock version mismatch)); return FALSE; }提示MAKEWORD(2,2)生成的是低位为 2、高位为 2 的字即 0x0202对应 WinSock 2.2。若误写为MAKEWORD(1,1)在较新系统上可能返回WSASYSNOTREADY若系统只装了 WinSock 1.1WSAStartup()会成功但后续IPPROTO_TCP可能不被识别。这是很多初学者编译通过却运行报错的第一道坎。2.2 服务器端监听循环accept()返回的新 socket 才是通信主体报告中“服务器端流程”第 3 步明确指出“接受请求返回一个新的对应于此次连接的套接字”。这句极关键——它揭示了 TCP 并发模型的本质主 socketlistenfd只负责accept()真正的收发必须用accept()返回的clientfd。常见错误是直接在listenfd上recv()结果永远阻塞或返回错误。// 服务器核心监听循环简化版对应报告步骤 3-5 SOCKET listenfd socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); struct sockaddr_in servaddr, cliaddr; memset(servaddr, 0, sizeof(servaddr)); servaddr.sin_family AF_INET; servaddr.sin_addr.s_addr htonl(INADDR_ANY); servaddr.sin_port htons(8080); bind(listenfd, (struct sockaddr*)servaddr, sizeof(servaddr)); listen(listenfd, 5); // 5 是等待连接队列长度 while (TRUE) { int clilen sizeof(cliaddr); SOCKET clientfd accept(listenfd, (struct sockaddr*)cliaddr, clilen); if (clientfd INVALID_SOCKET) { AfxMessageBox(_T(accept failed)); continue; } // ✅ 关键所有通信操作都在 clientfd 上进行 char recvbuf[1024]; int n recv(clientfd, recvbuf, sizeof(recvbuf)-1, 0); if (n 0) { recvbuf[n] \0; ProcessCommand(clientfd, recvbuf); // 解析 /n /p /l 等指令 } closesocket(clientfd); // 单次会话后关闭 clientfd非 listenfd } closesocket(listenfd);ProcessCommand()是报告附录中隐含的核心函数它接收recv()到的字符串按/分割并匹配首字符如n→DrawNiu()。这个设计将网络层数据与应用层语义解耦是构建可扩展协议的基础。2.3 客户端连接与消息发送connect()的阻塞特性与超时控制客户端流程第一步是connect()它会触发三次握手。报告中未提超时但在真实环境中若服务器宕机或防火墙拦截connect()会阻塞长达 20–30 秒Windows 默认导致 UI 冻结。必须手动设置非阻塞或超时// 方案一设置 socket 为非阻塞再 connect推荐用于 GUI u_long nonBlocking 1; ioctlsocket(sockfd, FIONBIO, nonBlocking); int ret connect(sockfd, (struct sockaddr*)servaddr, sizeof(servaddr)); if (ret SOCKET_ERROR) { int err WSAGetLastError(); if (err WSAEWOULDBLOCK) { // 进入轮询用 select() 检测连接是否就绪 fd_set writefds; FD_ZERO(writefds); FD_SET(sockfd, writefds); struct timeval timeout {5, 0}; // 5秒超时 int selret select(0, NULL, writefds, NULL, timeout); if (selret 0 FD_ISSET(sockfd, writefds)) { // 连接成功检查 SO_ERROR 获取最终状态 int so_error; int len sizeof(so_error); getsockopt(sockfd, SOL_SOCKET, SO_ERROR, (char*)so_error, len); if (so_error 0) { // 真正连上了 } } } }注意MFC 对话框中若直接connect()阻塞整个窗口会无响应。必须用非阻塞select()或另启线程否则用户点击“连接”按钮后只能干等体验极差。2.4 自定义协议指令集从/n画牛到/r启动资源管理器的底层实现报告最亮眼的不是 socket 本身而是其上构建的轻量级应用协议。所有/x指令均通过字符串匹配触发本地系统调用本质是将网络输入映射为 Windows API指令功能关键 Win32 API报告页码依据/n绘制 ASCII 牛CProject2Dlg::DrawNiu()附录代码P5 图“输入 /n 画一个牛”/p绘制 ASCII 电话CProject2Dlg::DrawPhone()附录代码P5 图“输入 /p 画一个电话”/l弹出 MessageBoxAfxMessageBox(开心(*^ ω ^*))P6 图“输入 /l 弹出一个对话框”/r打开资源管理器ShellExecute(NULL,explore,C:/windows/media,...)P5/P19 多处提及这些函数并非网络协议标准而是学生自主设计的“本地扩展指令”。其价值在于它强制开发者思考“收到数据后该做什么”而非止步于printf(recv: %s, buf)。例如/r的实现void CProject2Dlg::OnSend() { CString strInput; GetDlgItemText(IDC_EDIT_INMSG, strInput); if (strInput.Left(2) _T(/r)) { // ✅ 安全路径处理避免注入 CString path _T(C:/windows/media); ShellExecute(m_hWnd, _T(explore), path, NULL, NULL, SW_SHOWNORMAL); return; } // ... 其他指令处理 }注意ShellExecute第二个参数explore是关键它指定以资源管理器方式打开若用open则会尝试用默认程序打开文件夹可能失败。路径硬编码虽不灵活但符合教学实验定位——重点是验证指令通路而非工程健壮性。3. UDP 通信模块剖析无连接模型下的地址管理与状态维护3.1 UDP 套接字创建与绑定为何服务器无需listen()但必须bind()UDP 是无连接协议服务器端流程只有三步socket()→bind()→recvfrom()。报告中明确指出“服务器1. 创建套接字并将套接字绑定到一个本地地址和端口上2. 等待接收数据3. 关闭套接字”。这里bind()不仅是为了占用端口更是为了recvfrom()能获取发送方地址SOCKET sockfd socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); struct sockaddr_in servaddr; memset(servaddr, 0, sizeof(servaddr)); servaddr.sin_family AF_INET; servaddr.sin_addr.s_addr htonl(INADDR_ANY); servaddr.sin_port htons(8080); bind(sockfd, (struct sockaddr*)servaddr, sizeof(servaddr)); // ✅ recvfrom 的 from 参数会填入发送方 IP 和端口 struct sockaddr_in cliaddr; int clilen sizeof(cliaddr); char recvbuf[1024]; int n recvfrom(sockfd, recvbuf, sizeof(recvbuf)-1, 0, (struct sockaddr*)cliaddr, clilen); if (n 0) { recvbuf[n] \0; CString ip inet_ntoa(cliaddr.sin_addr); // 获取发送方 IP int port ntohs(cliaddr.sin_port); // 获取发送方端口 // 显示[192.168.1.100:54321] Hello }客户端则只需sendto()无需connect()但可选用于简化后续send()/recv()调用。报告中客户端流程为“创建套接字 → 向服务器发送数据 → 关闭套接字”完全符合 UDP 无状态特性。3.2 发送方地址动态获取recvfrom()如何解决 UDP 的“无连接”悖论TCP 通过三次握手建立连接双方地址在accept()时已知UDP 没有连接服务器如何知道该向谁回包答案就在recvfrom()的from参数——它由内核填充记录下每个数据包的源地址。报告 P18 “客户机与服务器对话”图注明确写道“服务器并没有填写客户机的 IP 地址能够接收到客户机发送的消息并且获得发送方 IP”。这正是recvfrom()的核心价值。// 服务器收到消息后用同一 socket from 地址回包 void SendResponse(SOCKET sockfd, const char* msg, const struct sockaddr* to, socklen_t tolen) { sendto(sockfd, msg, strlen(msg), 0, to, tolen); } // 在 recvfrom 后立即调用 SendResponse(sockfd, Server: OK, (struct sockaddr*)cliaddr, clilen);提示sendto()的to参数必须是recvfrom()填充的cliaddr不能是servaddr那是本机地址。初学者常在此处混淆导致消息发回给自己或丢弃。3.3 UDP 的“伪连接”管理如何在无状态协议上模拟会话严格来说 UDP 无会话概念但报告中服务器需“填写客户机 IP”才能回包这实则是人工维护了一个单点会话状态。其逻辑是首次recvfrom()获取cliaddr后将其缓存为m_cliaddr后续sendto()均用此地址。这虽违背 UDP 无状态本质却是 GUI 应用的实用妥协——用户需要点击“开始聊天”才激活回包能力。// 服务器类成员变量 struct sockaddr_in m_cliaddr; bool m_bClientAddrSet; // OnReceive 函数中 int n recvfrom(m_sockfd, recvbuf, sizeof(recvbuf)-1, 0, (struct sockaddr*)m_cliaddr, m_clilen); if (n 0 !m_bClientAddrSet) { m_bClientAddrSet TRUE; SetDlgItemText(IDC_STATIC_STATE, _T(已获取客户端地址可发送消息)); }这种设计暴露了 UDP 在交互式应用中的短板缺乏内置会话标识。生产环境常用方案是引入应用层 Session ID如 HTTP Cookie或改用 TCP。但对教学而言手动缓存sockaddr_in是理解“地址即身份”的最佳入口。3.4 UDP 消息导出功能fopen()/fwrite()实现聊天记录持久化报告 P22 明确要求“导出消息记录”并在附录给出fopen()/fwrite()代码。这是将内存数据落地为文件的关键一步也是调试与审计的基础// 导出当前列表所有消息到 txt 文件 void CUDPprojectDlg::OnExportLog() { CString strPath; CFileDialog dlg(FALSE, _T(txt), _T(chat_log.txt), OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT, _T(Text Files (*.txt)|*.txt|All Files (*.*)|*.*||)); if (dlg.DoModal() IDOK) { strPath dlg.GetPathName(); CEdit* pEdit (CEdit*)GetDlgItem(IDC_EDIT_OUTMSG); DWORD len pEdit-GetWindowTextLength(); if (len 0) return; CString strContent; pEdit-GetWindowText(strContent.GetBuffer(len1), len1); strContent.ReleaseBuffer(); FILE* fp _tfopen(strPath, _T(a)); // 追加模式安全 if (fp) { _ftprintf(fp, _T(\n Exported at %s \n), COleDateTime::GetCurrentTime().Format()); _fputts(strContent, fp); fclose(fp); AfxMessageBox(_T(Log exported successfully!)); } } }注意_tfopen是 Unicode 安全的文件打开函数a模式确保不覆盖历史记录_ftprintf格式化时间戳增强可读性。若用w模式每次导出都会清空旧日志这是新手易踩的坑。4. MFC 界面与系统集成从控件背景透明到 ShellExecute 指令执行4.1 控件背景透明化OnCtlColor()与TRANSPARENT模式的精确控制报告 P6/P22 多次提到“添加背景图片、改变控件背景色、字体”其核心技术是重载OnCtlColor()。该函数在每次绘制控件时被调用返回画刷决定背景色。要实现文本框IDC_EDIT_OUTMSG白色背景、静态文本IDC_STATIC_STATE透明必须精准匹配控件 ID 和nCtlColorHBRUSH CProject2Dlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) { HBRUSH hbr CDialog::OnCtlColor(pDC, pWnd, nCtlColor); // ✅ 精确匹配编辑框 ID if (pWnd-GetDlgCtrlID() IDC_EDIT_OUTMSG) { pDC-SetBkColor(RGB(255,255,255)); // 白色背景 pDC-SetTextColor(RGB(0,0,0)); // 黑色文字 return (HBRUSH)::GetStockObject(WHITE_BRUSH); } // ✅ 静态文本设为透明关键必须同时设 TRANSPARENT NULL_BRUSH if (nCtlColor CTLCOLOR_STATIC) { pDC-SetBkMode(TRANSPARENT); // 文字背景透明 return (HBRUSH)::GetStockObject(NULL_BRUSH); // 控件背景透明 } return hbr; }避坑若只设SetBkMode(TRANSPARENT)而不返回NULL_BRUSH静态文本背景仍为灰色若对编辑框也返回NULL_BRUSH则输入光标会消失。必须按控件类型分治。4.2ShellExecute指令安全执行路径白名单与参数校验报告中/r、/e、/t等指令均调用ShellExecute打开系统资源。若直接拼接用户输入极易引发命令注入如输入/r C:\Windows\System32\cmd.exe /c calc。教学代码虽未做校验但生产环境必须加固void CProject2Dlg::DoAction(char cmd) { CString path; switch(cmd) { case r: path _T(C:/windows/media); break; case e: path _T(https://www.seu.edu.cn); break; case t: path _T(F:/); break; default: return; } // ✅ 白名单路径 URL 编码对 URL if (cmd e) { // 对 URL 中特殊字符编码简化版 path.Replace(_T( ), _T(%20)); path.Replace(_T(), _T(%26)); } ShellExecute(m_hWnd, _T(open), path, NULL, NULL, SW_SHOWNORMAL); }提示ShellExecute的lpOperation参数open适用于文件/URLexplore专用于文件夹print用于打印。用错会导致静默失败。4.3 时间与主机名注入time()/localtime()与gethostname()的跨平台陷阱报告 P6 要求“添加发送时间”和“获取对方主机名”代码片段显示使用time()/localtime()和gethostname()。这两者在 Windows 下工作良好但存在潜在陷阱// ✅ 正确的时间格式化避免 localtime() 返回指针失效 time_t t1 time(NULL); struct tm* tm_info localtime(t1); // 注意t1非 t1 CString timestr; timestr.Format(_T(%04d/%02d/%02d %02d:%02d:%02d), tm_info-tm_year 1900, tm_info-tm_mon 1, tm_info-tm_mday, tm_info-tm_hour, tm_info-tm_min, tm_info-tm_sec); // ✅ 主机名获取需预分配足够缓冲区 char hostname[256]; gethostname(hostname, sizeof(hostname)-1); hostname[sizeof(hostname)-1] \0; // 确保结尾 \0避坑localtime()返回struct tm*指向静态内存多次调用会覆盖必须立即拷贝内容。gethostname()若缓冲区不足会截断且不保证\0结尾必须手动补\0。4.4 字符画与表情符号ASCII 艺术的内存布局与 Unicode 兼容性报告 P6 的“自定义字符画”用字符串数组存储小象/x指令循环输出。而表情符号如( ╥ ﹏╥ )使用 Unicode 字符。在 MFC Unicode 工程中CString默认支持 UTF-16但需确保源文件保存为 UTF-8 with BOM 或 UTF-16// 字符画数组UTF-16 安全 const wchar_t* TP_xin_str[] { L ___________, L / \\, L / \\, L| |, L| O O |, L| V |, L \\__ __/, L \\_______/ }; int TP_xin_int 8; // 输出时用 SetWindowTextW 或 AfxMessageBoxW for(int i0; iTP_xin_int; i) { m_strOutput TP_xin_str[i]; m_strOutput L\r\n; } GetDlgItem(IDC_EDIT_OUTMSG)-SetWindowTextW(m_strOutput);注意若工程设为多字节MBCSUnicode 表情会显示为乱码。必须确认项目属性 → 常规 → 字符集 “使用 Unicode 字符集”。5. 避坑指南TCP/UDP 开发中 5 个血泪经验总结5.1 现象connect()成功但send()返回WSAECONNRESET原因服务器端accept()后未及时recv()客户端发送数据时服务器 socket 已关闭或未进入recv()状态TCP 层发送 RST 包。解决服务器accept()后必须立即进入recv()循环或客户端send()前先recv()确认连接就绪如服务器发 Welcome 消息。5.2 现象UDP 服务器recvfrom()收不到数据WSAGetLastError()返回WSAEMSGSIZE原因接收缓冲区太小数据包被截断。报告中recvbuf[1024]足够但若客户端发大于 1024 字节的包内核丢弃剩余部分并置错。解决增大缓冲区至65535UDP 最大包长或检查n返回值若n sizeof(recvbuf)-1且末尾非\0说明可能被截断。5.3 现象MFC 对话框中ShellExecute打开网址失败返回值为 31SE_ERR_NOASSOC原因ShellExecute的lpFile参数传入了 URL 字符串如https://baidu.com但未指定lpOperationopen系统无法关联默认浏览器。解决必须显式传open作为第二参数ShellExecute(NULL, open, https://baidu.com, NULL, NULL, SW_SHOWNORMAL)。5.4 现象OnCtlColor()设置后静态文本背景变黑或闪烁原因未正确处理CTLCOLOR_BTN或CTLCOLOR_DLG消息导致父窗口背景被错误覆盖或返回了错误的画刷如对CTLCOLOR_STATIC返回WHITE_BRUSH。解决只对目标控件 ID 和nCtlColor组合返回定制画刷其余一律调用基类CDialog::OnCtlColor()对CTLCOLOR_STATIC必须返回NULL_BRUSH并设SetBkMode(TRANSPARENT)。5.5 现象gethostname()返回主机名但gethostbyname()查不到对应 IP原因gethostname()返回的是 NetBIOS 名如DESKTOP-ABC123而gethostbyname()需要 DNS 可解析的域名或 hosts 文件条目。局域网内常无 DNS 解析。解决直接用INADDR_ANY绑定或让用户输入 IP若需解析应调用getaddrinfo()现代替代并指定AI_PASSIVE。6. 进阶技巧从实验报告到可部署服务的四步加固6.1 将单次会话升级为多客户端并发select()模型实战报告中的 TCP 服务器是单线程阻塞模型一次只能服务一个客户端。要支持多用户必须用select()监听多个 socket。核心是维护一个fd_set将listenfd和所有clientfd加入其中// 初始化 fd_set readfds; SOCKET maxfd listenfd; FD_ZERO(readfds); FD_SET(listenfd, readfds); // 主循环 while (TRUE) { fd_set tmpfds readfds; // 每次 select 前重置 int nready select(maxfd 1, tmpfds, NULL, NULL, NULL); if (FD_ISSET(listenfd, tmpfds)) { // 有新连接 SOCKET clientfd accept(listenfd, NULL, NULL); FD_SET(clientfd, readfds); if (clientfd maxfd) maxfd clientfd; } // 遍历所有 clientfd 检查可读 for (SOCKET i listenfd 1; i maxfd; i) { if (FD_ISSET(i, tmpfds)) { char buf[1024]; int n recv(i, buf, sizeof(buf)-1, 0); if (n 0) { buf[n] \0; BroadcastToAllClients(i, buf); // 广播给其他客户端 } else { // 客户端断开 FD_CLR(i, readfds); closesocket(i); } } } }提示select()的maxfd必须是最大 socket 描述符 1FD_ISSET()检查前必须复制fd_set因为select()会修改原集合。6.2 UDP 服务器增加心跳机制用setsockopt()设置SO_RCVTIMEOUDP 无连接服务器无法感知客户端是否存活。报告中“填写客户机 IP”是手动操作生产环境需自动维护。方案是客户端定期发心跳包如/ping服务器用setsockopt()设置接收超时超时未收则清理地址// 服务器端设置 5 秒超时 int timeout 5000; // 5000 ms setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (char*)timeout, sizeof(timeout)); // 接收循环中 while (TRUE) { int n recvfrom(sockfd, buf, sizeof(buf)-1, 0, (struct sockaddr*)cliaddr, clilen); if (n SOCKET_ERROR) { int err WSAGetLastError(); if (err WSAETIMEDOUT) { // 5秒内无数据可认为客户端离线 ClearClientFromList(cliaddr); continue; } } UpdateClientLastActive(cliaddr); // 更新最后活跃时间 }6.3 日志系统升级用CStdioFile替代fopen()实现线程安全写入报告中的fopen()/fwrite()在多客户端场景下会因文件竞争导致日志混乱。MFC 的CStdioFile提供更安全的封装// 线程安全的日志写入需加临界区 CCriticalSection m_logLock; void LogMessage(const CString msg) { m_logLock.Lock(); CStdioFile file; if (file.Open(_T(server.log), CFile::modeCreate | CFile::modeNoTruncate | CFile::modeWrite)) { file.SeekToEnd(); file.WriteString(_T([) COleDateTime::GetCurrentTime().Format() _T(] ) msg _T(\r\n)); file.Close(); } m_logLock.Unlock(); }6.4 配置外置化用CWinApp::GetProfileString()读取 INI 文件硬编码端口8080、路径C:/windows/media不利于部署。应提取到seu_net.ini[Network] Port8080 [Paths] MediaPathC:/windows/media// 读取配置 CString portStr AfxGetApp()-GetProfileString(_T(Network), _T(Port), _T(8080)); int port _ttoi(portStr); CString mediaPath AfxGetApp()-GetProfileString(_T(Paths), _T(MediaPath), _T(C:/windows/media));从那以后我每次复现实验报告都强制走一遍WSAStartup()版本校验、select()多路复用、CStdioFile日志、INI 配置这四步——它们不增加功能但让代码从“能跑”变成“可维护、可监控、可部署”。这份 2016 年的东南大学实验报告表面是 TCP/UDP 教学内里却是一份 Win32 网络编程的生存手册。希望帮到你。本文还有配套的精品资源点击获取