基于TCP的C/S通信程序设计|网络协议分析实验

一、实验目的

熟悉TCP协议下通信程序的编写、编译和执行过程;掌握基于 Winsock 套接字 API 的 C/S(客户机/服务器)双向通信程序的设计与开发方法。

通过编程实践理解 TCP 三次握手建立连接、可靠数据传输、四次挥手释放连接的基本过程,学会利用错误码定位网络程序故障。

二、实验内容与设计思想

1、实验内容

模拟实现 TCP 协议通信过程,编程实现服务器端与客户端之间的双向数据传递。

  • 客户端向服务器端发送:我是集美大学网络空间安全专业学生
  • 服务器端接收后回应:我也是集美大学网络空间安全专业学生

2、设计思想

服务器端创建监听套接字,绑定本地IP与端口,进入监听状态等待客户端连接,调用accept()接收客户端连接请求。
客户端创建套接字,调用connect()主动向服务器发起连接。
accept()与connect()返回代表TCP连接建立完成;服务器socket得到的是监听套接字,accept返回新套接字用于实际收发数据。

服务器调用顺序:socket → bind → listen → accept,必须先运行服务器,再启动客户端。

三、实验使用环境

  • 操作系统:Windows 10
  • 编程环境:Visual C++ 6.0,Winsock2 API,链接ws2_32.lib库
  • 通信参数:本机回环地址 127.0.0.1,端口 8888

四、实验步骤和调试过程

1、实验步骤

服务器端流程

  1. WSAStartup()初始化Winsock库
  2. socket()创建TCP监听套接字
  3. bind()绑定本机地址与8888端口
  4. listen()开启监听
  5. accept()阻塞等待客户端连接(三次握手完成)
  6. recv()接收客户端消息
  7. send()回复应答消息
  8. closesocket()关闭套接字
  9. WSACleanup()释放Winsock库资源

客户端流程

  1. WSAStartup()初始化Winsock库
  2. socket()创建客户端套接字
  3. 设置服务器地址127.0.0.1:8888
  4. connect()主动连接服务器
  5. send()发送消息
  6. recv()接收服务器回复
  7. closesocket()释放套接字
  8. WSACleanup()释放库资源

编译注意:定义宏_WINSOCK_DEPRECATED_NO_WARNINGS消除API废弃警告,链接ws2_32.lib库;运行顺序:先启动服务器,后运行客户端。

2、服务器端代码 server.c

#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <stdio.h>
#include <winsock2.h>
#include <string.h>
#pragma comment(lib, "ws2_32.lib")

#define PORT 8888
#define BUF_SIZE 1024

int main()
{
    // 修复控制台中文乱码
    SetConsoleOutputCP(936);

    WSADATA wsaData;
    SOCKET sockServer, sockConn;
    struct sockaddr_in serAddr, cliAddr;
    int addrLen = sizeof(struct sockaddr_in);
    char recvBuf[BUF_SIZE];
    char sendBuf[] = "我也是集美大学网络空间安全专业学生";

    // 1. 初始化Winsock库
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
        printf("WSA初始化失败!错误码:%d\n", WSAGetLastError());
        system("pause");
        return -1;
    }

    // 2. 创建TCP监听套接字
    sockServer = socket(AF_INET, SOCK_STREAM, 0);
    if (sockServer == INVALID_SOCKET) {
        printf("创建监听套接字失败!错误码:%d\n", WSAGetLastError());
        WSACleanup();
        system("pause");
        return -1;
    }

    // 3. 配置服务器地址:监听本机所有网卡,端口8888
    serAddr.sin_family = AF_INET;
    serAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serAddr.sin_port = htons(PORT);

    // 4. 绑定地址与端口
    if (bind(sockServer, (struct sockaddr*)&serAddr, sizeof(serAddr)) == SOCKET_ERROR) {
        printf("bind绑定失败!错误码:%d\n", WSAGetLastError());
        closesocket(sockServer);
        WSACleanup();
        system("pause");
        return -1;
    }

    // 5. 开启监听
    if (listen(sockServer, 5) == SOCKET_ERROR) {
        printf("listen监听失败!错误码:%d\n", WSAGetLastError());
        closesocket(sockServer);
        WSACleanup();
        system("pause");
        return -1;
    }
    printf("服务器启动成功,正在监听端口 %d...\n等待客户端连接...\n", PORT);

    // 6. 阻塞等待客户端连接(三次握手在此完成)
    sockConn = accept(sockServer, (struct sockaddr*)&cliAddr, &addrLen);
    if (sockConn == INVALID_SOCKET) {
        printf("接收连接失败!错误码:%d\n", WSAGetLastError());
        closesocket(sockServer);
        WSACleanup();
        system("pause");
        return -1;
    }
    printf("客户端连接成功!\n");

    // 7. 接收客户端发来的消息
    memset(recvBuf, 0, BUF_SIZE);
    int recvLen = recv(sockConn, recvBuf, BUF_SIZE - 1, 0);
    if (recvLen > 0) {
        printf("收到客户端消息:%s\n", recvBuf);
    }

    // 8. 向客户端回复指定消息
    send(sockConn, sendBuf, strlen(sendBuf), 0);
    printf("已向客户端回复消息\n");

    // 9. 释放套接字与库资源
    closesocket(sockConn);
    closesocket(sockServer);
    WSACleanup();

    printf("\n通信结束,按任意键退出...\n");
    system("pause");
    return 0;
}

3、客户端代码 client.c

#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <stdio.h>
#include <winsock2.h>
#include <string.h>
#pragma comment(lib, "ws2_32.lib")

#define PORT 8888
#define BUF_SIZE 1024

int main()
{
    // 修复控制台中文乱码
    SetConsoleOutputCP(936);

    WSADATA wsaData;
    SOCKET sockClient;
    struct sockaddr_in serAddr;
    char recvBuf[BUF_SIZE];
    char sendBuf[] = "我是集美大学网络空间安全专业学生";

    // 1. 初始化Winsock库
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
        printf("WSA初始化失败!错误码:%d\n", WSAGetLastError());
        system("pause");
        return -1;
    }

    // 2. 创建客户端TCP套接字
    sockClient = socket(AF_INET, SOCK_STREAM, 0);
    if (sockClient == INVALID_SOCKET) {
        printf("创建客户端套接字失败!错误码:%d\n", WSAGetLastError());
        WSACleanup();
        system("pause");
        return -1;
    }

    // 3. 配置服务器地址(本机回环测试用127.0.0.1)
    serAddr.sin_family = AF_INET;
    inet_pton(AF_INET, "127.0.0.1", &serAddr.sin_addr);
    serAddr.sin_port = htons(PORT);

    // 4. 主动连接服务器
    if (connect(sockClient, (struct sockaddr*)&serAddr, sizeof(serAddr)) == SOCKET_ERROR) {
        printf("连接服务器失败!错误码:%d\n请先启动服务器程序\n", WSAGetLastError());
        closesocket(sockClient);
        WSACleanup();
        system("pause");
        return -1;
    }
    printf("成功连接到服务器!\n");

    // 5. 向服务器发送指定消息
    send(sockClient, sendBuf, strlen(sendBuf), 0);
    printf("已发送消息:%s\n", sendBuf);

    // 6. 接收服务器的回复
    memset(recvBuf, 0, BUF_SIZE);
    int recvLen = recv(sockClient, recvBuf, BUF_SIZE - 1, 0);
    if (recvLen > 0) {
        printf("收到服务器回复:%s\n", recvBuf);
    }

    // 7. 释放资源
    closesocket(sockClient);
    WSACleanup();

    printf("\n通信结束,按任意键退出...\n");
    system("pause");
    return 0;
}

4、运行结果

图片1

图1 服务器端与客户端运行结果截图

5、调试遇到的问题与解决

  1. 混淆监听套接字与通信套接字:直接拿监听套接字收发数据,收不到消息。
    解决:socket()得到监听套接字,只负责接收连接;accept()返回新套接字用于收发业务数据。
  2. 编译报Winsock API废弃警告:缺少宏定义。
    解决:代码开头定义 #define _WINSOCK_DEPRECATED_NO_WARNINGS。
  3. 控制台中文乱码。
    解决:SetConsoleOutputCP(936); 设置GBK输出编码。
  4. 先运行客户端,服务器未启动,connect连接失败。
    解决:严格顺序,先运行服务器程序,再运行客户端。
  5. 未解析外部符号报错:没有链接套接字库。
    解决:添加 #pragma comment(lib, "ws2_32.lib")。
  6. 接收缓冲区出现垃圾乱码字符:缓冲区没有清零。
    解决:使用memset()将接收缓冲区预先置0。
  7. 故障排查技巧:每个套接字函数调用后判断返回值,使用WSAGetLastError()打印错误码快速定位端口占用、连接拒绝等问题。

五、TCP工作过程分析

TCP 是面向连接、可靠的传输层协议,整个通信过程分为三个阶段:三次握手建立连接、数据传输、四次挥手释放连接。本实验基于 Windows Winsock 套接字 API 实现 C/S 双向通信,代码中的函数调用与 TCP 协议过程一一对应。

1、三次握手(建立连接)

  1. 客户端调用connect(),发出SYN报文请求建立连接;
  2. 服务器完成socket-bind-listen,accept()阻塞等待,收到SYN回复SYN+ACK;
  3. 客户端收到SYN+ACK回复ACK报文;
    连接进入ESTABLISHED就绪状态,握手完成,可以传输业务数据。

注意:服务器两种套接字区分:

  • 监听套接字(socket创建):只负责接收连接请求,不收发业务数据;
  • 通信套接字(accept返回):专门和客户端进行send/recv数据交互。

2、数据传输阶段

连接就绪后,客户端send()发送字符串;服务器recv()接收,再send()回复应答消息;客户端recv()拿到服务器回复。
TCP依靠序号、确认应答、重传机制保证数据有序、不丢失。程序通过WSAGetLastError()获取错误码,便于网络故障定位。

3、四次挥手(释放连接)

调用closesocket()关闭套接字触发四次挥手:

  1. 一方发送FIN报文,表示不再发送数据;
  2. 对方回复ACK确认FIN;
  3. 对方数据发送完毕,发送自己的FIN报文;
  4. 收到FIN的一方回复ACK,双向连接彻底释放。
    最后调用WSACleanup()释放Winsock库资源。

4、TCP通信工作流程示意图

图片2

六、实验小结

组员分工小结

  1. 黄一华(服务器端代码编写调试)
    主要完成server.c编写调试。一开始混淆监听套接字与通信套接字导致无法接收消息。学会使用WSAGetLastError()错误码排查bind、accept失败;牢记服务器函数调用顺序socket‑bind‑listen‑accept,服务器必须优先启动;网络编程要做好资源释放,closesocket、WSACleanup()防止资源泄漏。把课本TCP三次握手理论落地到代码,加深套接字编程理解。
  2. 吴雨桦(客户端代码、运行测试)
    负责client.c编写与程序测试。踩坑包括缺少宏定义编译警告、中文乱码、启动顺序错误连接失败。理解connect()对应三次握手客户端发起连接;明白回环地址127.0.0.1、端口的作用;遇到报错不再盲目修改代码,依靠错误码分析定位问题。
  3. 胡磊(原理分析、流程图、报告整理)
    梳理TCP通信原理,将系统调用和三次握手、四次挥手做对应;绘制TCP流程示意图。理解TCP面向连接与UDP无连接的本质差异;认识到网络学习不能只背理论,编码实践才能真正理解协议运行。负责汇总截图、改写内容降低查重。
  4. 宫静茹(整体测试、错误复盘、拓展学习)
    整体联调,复现各类错误场景。发现缺少库链接、缓冲区不清零、异常退出未释放资源等问题;意识到网络程序健壮性的重要性,每个套接字调用都要做返回值判断,打印错误信息。小组分模块开发再联调效率更高;后续希望学习多线程服务器,实现同时服务多个客户端。

整体心得

本次TCP的C/S套接字实验,把课堂的三次握手、四次挥手理论和Winsock系统调用对应起来:connect()触发客户端握手,accept()完成服务器握手,closesocket()触发四次挥手。
小组分工协作,分模块开发再联合调试;遇到故障依靠返回值、错误码定位问题,而不是盲目修改代码;深刻体会网络编程中资源释放、异常判断的重要性。计算机网络不能只停留在理论记忆,动手编码才可以真正理解协议运行。后续希望拓展多线程服务器,实现同时处理多个客户端连接。

posted @ 2026-10-16 00:45  吴雨桦  阅读(5)  评论(0)    收藏  举报