• C++实现的Socket接口实现自定义协议通信


    资源下载地址:https://download.csdn.net/download/sheziqiong/85836435
    资源下载地址:https://download.csdn.net/download/sheziqiong/85836435

    主要仪器设备

    联网的 PC 机、Wireshark 软件

    Visual C++、gcc 等 C++ 集成开发环境。

    操作方法与实验步骤

    设计请求、指示(服务器主动发给客户端的)、响应数据包的格式,至少要考虑如下问题:

    定义两个数据包的边界如何识别。

    定义数据包的请求、指示、响应类型字段。

    定义数据包的长度字段或者结尾标记。

    定义数据包内数据字段的格式(特别是考虑客户端列表数据如何表达)。

    小组分工:1 人负责编写服务端,1 人负责编写客户端。

    客户端编写步骤(需要采用多线程模式)

    运行初始化,调用 socket(),向操作系统申请 socket 句柄。

    编写一个菜单功能,列出 7 个选项等待用户选择。

    根据用户选择,做出相应的动作(未连接时,只能选连接功能和退出功能)

    选择连接功能:请用户输入服务器 IP 和端口,然后调用 connect(),等待返回结果并打印。连接成功后设置连接状态为已连接。然后创建一个接收数据的子线程,循环调用 receive(),如果收到了一个完整的响应数据包,就通过线程间通信(如消息队列)发送给主线程,然后继续调用 receive(),直至收到主线程通知退出。

    选择断开功能:调用 close(),并设置连接状态为未连接。通知并等待子线程关闭。

    选择获取时间功能:组装请求数据包,类型设置为时间请求,然后调用 send()将数据发送给服务器,接着等待接收数据的子线程返回结果,并根据响应数据包的内容,打印时间信息。

    选择获取名字功能:组装请求数据包,类型设置为名字请求,然后调用 send()将数据发送给服务器,接着等待接收数据的子线程返回结果,并根据响应数据包的内容,打印名字信息。

    选择获取客户端列表功能:组装请求数据包,类型设置为列表请求,然后调用 send() 将数据发送给服务器,接着等待接收数据的子线程返回结果,并根据响应数据包的内容,打印客户端列表信息(编号、IP 地址、端口等)。

    选择发送消息功能(选择前需要先获得客户端列表):请用户输入客户端的列表编号和要发送的内容,然后组装请求数据包,类型设置为消息请求,然后调用 send()将数据发送给服务器,接着等待接收数据的子线程返回结果,并根据响应数据包的内容,打印消息发送结果(是否成功送达另一个客户端)。

    选择退出功能:判断连接状态是否为已连接,是则先调用断开功能,然后再退出程序。否则,直接退出程序。

    主线程除了在等待用户的输入外,还在处理子线程的消息队列,如果有消息到达,则进行处理,如果是响应消息,则打印响应消息的数据内容(比如时间、名字、客户端列表等);如果是指示消息,则打印指示消息的内容(比如服务器转发的别的客户端的消息内容、发送者编号、IP 地址、端口等)。

    服务端编写步骤(需要采用多线程模式)

    运行初始化,调用 socket(),向操作系统申请 socket 句柄

    调用 bind(),绑定监听端口(请使用学号的后 4 位作为服务器的监听端口),接着调用 listen(),设置连接等待队列长度主线程循环调用 accept(),直到返回一个有效的 socket 句柄,在客户端列表中增加一个新客户端的项目,并记录下该客户端句柄和连接状态、端口。然后创建一个子线程后继续调用 accept()。该子线程的主要步骤是(刚获得的句柄要传递给子线程,子线程内部要使用该句柄发送和接收数据):

    调用 send(),发送一个 hello 消息给客户端(可选)

    循环调用 receive(),如果收到了一个完整的请求数据包,根据请求类型做相应的动作:

    请求类型为获取时间:调用 time()获取本地时间,然后将时间数据组装进响应数据包,调用 send()发给客户端

    请求类型为获取名字:将服务器的名字组装进响应数据包,调用 send()发给客户端

    请求类型为获取客户端列表:读取客户端列表数据,将编号、IP 地址、端口等数据组装进响应数据包,调用 send()发给客户端

    请求类型为发送消息:根据编号读取客户端列表数据,如果编号不存在,将错误代码和出错描述信息组装进响应数据包,调用 send()发回源客户端;如果编号存在并且状态是已连接,则将要转发的消息组装进指示数据包。调用 send()发给接收客户端(使用接收客户端的 socket 句柄),发送成功后组装转发成功的响应数据包,调用 send()发回源客户端。

    主线程还负责检测退出指令(如用户按退出键或者收到退出信号),检测到后即通知并等待各子线程退出。最后关闭 Socket,主程序退出。

    编程结束后,双方程序运行,检查是否实现功能要求,如果有问题,查找原因,并修改,直至满足功能要求使用多个客户端同时连接服务端,检查并发性使用 Wireshark 抓取每个功能的交互数据包

    四、实验数据记录和处理

    请将以下内容和本实验报告一起打包成一个压缩文件上传:

    源代码:客户端和服务端的代码分别在一个目录

    可执行文件:可运行的.exe 文件或 Linux 可执行文件,客户端和服务端各一个

    以下实验记录均需结合屏幕截图(截取源代码或运行结果),进行文字标注(看完请删除本句)。

    描述请求数据包的格式(画图说明),请求类型的定义

    Int DES (目的地的 socket ID)
    REQ_TYPE OPT (请求的类型)
    Char[1024] MES (消息内容,固定长度)
    const int MAXMES = 1024;
    enum REQ_TYPE {DISCON, TIME, NAME, LINK, SEND};
    string REQ_STR[] = { "DISCON", "TIME", "NAME", "LINK", "SEND" };
    struct MESSAGE {
        int DES;
        REQ_TYPE OPT;
        char MES[MAXMES];
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    描述响应数据包的格式(画图说明),响应类型的定义

    Int DES (目的地的 socket ID)
    REQ_TYPE OPT (请求的类型)
    Char[1024] MES (消息内容,固定长度)

    描述指示数据包的格式(画图说明),指示类型的定义

    Int DES (目的地的 socket ID)
    REQ_TYPE OPT (请求的类型)
    Char[1024] MES (消息内容,固定长度)

    客户端初始运行后显示的菜单选项

    客户端的主线程循环关键代码截图(描述总体,省略细节部分)

    根据输入的指令,选择像服务器发放什么类型的包

    while(1) {
    // initialize the MESSAGE being sent
        MESSAGE sen ;
        sen = MESSAGE{
            -1, NAME, ""
            } ;
        int opt ;
        string Text ;
        int des_num ;
        cin >> opt ;
        switch (opt) {
        case 1:
            sen.OPT = DISCON ;
            break ;
        case 2:
            sen.OPT = TIME ;
            break ;
        case 3:
            sen.OPT = NAME ;
            break ;
        case 4:
            sen.OPT = LINK ;
            break ;
        case 5:
    // only the type of sending message to others need to be treated differently
            sen.OPT = SEND ;
    // 输入要发送的目的地和信息内容,略
        case 6:
            cout << "Quiting..." << endl ;
            return 0 ;
        default:
            cout << "Wrong option!" << endl ;
            break ;
        }
        if ( opt < 1 || opt > 6 )
            continue ;
    // sending the packet  	 	char buf[sizeof(MESSAGE)] ;  	 	memcpy(buf, &sen, sizeof(MESSAGE)) ;
        int seRet = send(client, buf, sizeof(buf), 0);
        if (seRet == -1) {
            cout << "Sending failed:" << errno << endl;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    客户端的接收数据子线程循环关键代码截图(描述总体,省略细节部分)

    void receiveT(SOCKET client) {
        char buf[sizeof(MESSAGE)];
        MESSAGE sen ;
        while ( 1 ) {
    // Message Receiving
            int reRet = recv(client, buf, sizeof(buf), 0);
            if (reRet == -1) {
                cout << "Receiving failed:"<< errno << endl;
                exit(0);
            }
            memcpy(&sen, buf, sizeof(buf)) ;
            if(sen.OPT == DISCON) {
                cout << "Disconnecting..." << endl ;
                exit(0);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    服务器初始运行后显示的界面

    服务器的主线程循环关键代码截图(描述总体,省略细节部分)

    while (true) {
        int client = accept(slisten, (sockaddr*)&sin, &addr_size);
        if (client == -1)
    // 省略
    // lambda expression used when using thread  	 	thread t([&](sockaddr_in addr, int client) {
    // 放在下一题里
        }, sin, client);
    t.detach();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    服务器的客户端处理子线程循环关键代码截图(描述总体,省略细节部分) char TMP_CLI[255];

    // Get ip from hex
    inet_ntop(AF_INET, (void*)&sin.sin_addr, TMP_CLI, 16);
    Clients[client] = sin;
    Clients_info[client].ip = TMP_CLI;
    Clients_info[client].port = sin.sin_port;
    // Initialize Flas as TRUE    Flags[client] = 1;
    while (Flags[client]) {
    // Receive the packet     char buf[sizeof(MESSAGE)];
        int reRet = recv(client, buf, sizeof(buf), 0);
        if (reRet == -1) {
            cout << "receive failed:" << errno << endl;
            break;
        }
    // interpret the packet  	 	 	 	MESSAGE rec, sen;
        memcpy(&rec, buf, sizeof(MESSAGE));
        memset(sen.MES, 0, sizeof(sen.MES));
        sen.DES = client;
        sen.OPT = rec.OPT;
    // Process it according to its type     switch (rec.OPT) {     case DISCON:
        Flags[client] = 0;
        Clients_info.erase(client);
        cout << "Disconnected link with " << client << endl;
        break;
    case TIME:
        cout << "Send time to " << client << endl;
        cc = time(0);
        tmp_time = ctime(&cc);
        strcpy(rec.MES, tmp_time.c_str());
        break;
    case NAME:
        cout << "Send name to " << client << endl;
        gethostname(name, size);
        strcpy(rec.MES, name);
        break;
    case LINK:
        cout << "Send current active links to " << client << endl;
    // 通过 map 获取现有的活跃链接,并发送
        break;
    case SEND:
        cout << "Send message from " << client << " to " << rec.DES << endl;
    // 向目标地址发送消息
        break;
    default:
        cout << "Wrong requirement type!" << endl;
    }
    // Always reply the sender after processing the packet  	 	 	 	if (rec.OPT >= DISCON && rec.OPT <= SEND) {
    // 回复发出指令的客户端
    }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    客户端选择连接功能时,客户端和服务端显示内容截图。

    Wireshark 抓取的数据包截图:

    客户端选择获取时间功能时,客户端和服务端显示内容截图。

    客户端:

    服务端:

    Wireshark 抓取的数据包截图(展开应用层数据包,标记请求、响应类型、返回的时间数据对应的位置):

    客户端选择获取名字功能时,客户端和服务端显示内容截图。

    客户端:

    服务端:

    Wireshark 抓取的数据包截图(展开应用层数据包,标记请求、响应类型、返回的名字数据对应的位置):

    相关的服务器的处理代码片段:

    gethostname(name, size);strcpy(rec.MES, name);客户端选择获取客户端列表功能时,客户端和服务端显示内容截图。

    客户端:

    服务端:

    Wireshark 抓取的数据包截图(展开应用层数据包,标记请求、响应类型、返回的客户端列表

    数据对应的位置):

    相关的服务器的处理代码片段:

    tmp_link = "\tID\tIP\tPort\n";
    for (auto i : Clients_info)
        tmp_link = tmp_link + "\t" + to_string(i.first) + "\t" + i.second.ip + "\t"
                   + to_string(i.second.port) + "\n";
    strcpy(rec.MES, tmp_link.c_str());
    
    • 1
    • 2
    • 3
    • 4
    • 5

    客户端选择发送消息功能时,客户端和服务端显示内容截图。

    发送消息的客户端:

    服务器:

    接收消息的客户端:

    Wireshark 抓取的数据包截图(发送和接收分别标记):

    发送:

    接受:

    相关的服务器的处理代码片段:

    cout << "Send message from " << client << " to " << rec.DES << endl;
    tmp_text = rec.MES;
    tmp_text = "message from [" + to_string(client) + "]: \n" + tmp_text;
    memset(sen.MES, 0, sizeof(sen.MES));
    strcpy(sen.MES, tmp_text.c_str());
    sen.DES = rec.DES;
    cout << "Start sending..." << endl;
    ret = send(sen.DES, (char*)&sen, sizeof(sen), 0);
    if (ret < 0) {
        cout << "send failed" << endl;
        strcpy(rec.MES, "Sending Failed");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    相关的客户端(发送和接收消息)处理代码片段:发送:

    sen.OPT = SEND ;
    cout << "To. " ;
    cin >> des_num ;
    sen.DES = des_num ;
    cout << "Input the text need to send:\n" ;
    cin >> Text ;
    strcpy(sen.MES, Text.c_str()) ;
    接受就是前面接受服务器消息的方法。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    拔掉客户端的网线,然后退出客户端程序。观察客户端的 TCP 连接状态,并使用 Wireshark观察客户端是否发出了 TCP 连接释放的消息。同时观察服务端的 TCP 连接状态在较长时间内分钟以上)是否发生变化。直接用关闭窗口的方式关掉了,有释放连接。

    再次连上客户端的网线,重新运行客户端程序。选择连接功能,连上后选择获取客户端列表功能,查看之前异常退出的连接是否还在。选择给这个之前异常退出的客户端连接发送消息,出现了什么情况?

    没有了该线程对应的 while 循环中会在链接断开的时候立即得到链接失败的信息。

    断开前:

    断开后:

    修改获取时间功能,改为用户选择 1 次,程序内自动发送 100 次请求。服务器是否正常处理了次请求,截取客户端收到的响应(通过程序计数一下是否有 100 个响应回来),并使用Wireshark 抓取数据包,观察实际发出的数据包个数。

    开始:

    结束:

    开始:

    结束:

    多个客户端同时连接服务器,同时发送时间请求(程序内自动连续调用 100 次 send),服务器和客户端的运行截图

    资源下载地址:https://download.csdn.net/download/sheziqiong/85836435
    资源下载地址:https://download.csdn.net/download/sheziqiong/85836435

  • 相关阅读:
    Python3 - Linux 下安装 LibreOffice 以及使用
    服务网格和CI/CD集成:讨论服务网格在持续集成和持续交付中的应用。
    浅析TSINGSEE智能边缘网关的人体检测技术及应用场景
    【蓝桥杯选拔赛真题04】C++计算24数字游戏 青少年组蓝桥杯C++选拔赛真题 STEMA比赛真题解析
    java基础10题
    Apache的安装与目录结构详细解说
    求解plc相关问题下载程序
    Java JVM——10.对象实例化、内存布局、访问定位和直接内存
    关于JAVA中字节码文件版本号、产品版本号及开发版本号的关系
    C++之保存编译全部中间文件(二百一十五)
  • 原文地址:https://blog.csdn.net/newlw/article/details/125540827