【深大计算机网络】实验一 流式视频传输 实验报告 附代码

目录

一、实验目的与要求

二、实验过程

1.实验环境搭建:

2.补充代码,实现单文件视频传输:

3.补充代码,实现流式视频传输

三、实验结果

四、实验分析

五、实验总结

六、思考题

尾注:

附:本实验所用代码


一、实验目的与要求

实验目的:

1.理解TCP套接字的定义

2.掌握基本的TCP套接字编程方法

3.学习简单网络应用的编程思路

4.学习网络编程相关的一些库

实验要求:

1.了解网络编程的相关库

2.掌握编写简单网络应用的技能

3.依照步骤完成实验内容

4.对实验结果截图

5.撰写实验报告

二、实验过程

1.实验环境搭建:

由于本地环境上已经有edge浏览器,所以编译环境方面只需下载devcppPortable,在课程平台上下载所需文件压缩包后,解压后打开devcppPortable.exe即可直接使用。

图:解压后文件夹中的内容

2.补充代码,实现单文件视频传输:

        本步骤需要:阅读给出的实验所需代码框架,理解相关变量、函数的作用;根据实验要求,在理解代码的前提下正确补充所需代码,完成单个视频的传输。

        ①下载源代码框架:

在实验正式开始之前,还需要下载实验所需的代码框架和传输所用的视频文件。最后在代码框架的同一目录下新建一个/download文件夹,用于存储传输后的数据(视频)。

        ②阅读代码并理解Socket连接的建立过程:

        本过程具体流程同实验文档指引,通过建立Socket 连接,不同进程之间可以进行数据交互(双方都可以互通数据)。在具体的使用过程中。服务器端使用 Socket 绑定到一个特定的网络地址和端口,监听来自客户端的连接请求。当客户端也通过 Socket 连接到服务器的网络地址和端口时,就可以进行数据的传输。

        ③通过编写代码,在服务端向客户端发送视频文件:

        实验中给出的代码框架中已经包含了以下步骤:

        创建发送缓冲区char buffer[BUFFER_SIZE] = {0};

        打开文件、发送文件大小给client;

        接下来,我们需要使用send()函数,将缓冲区的数据发送至客户端。但是,在原始代码框架中,原文件中的数据并未存入缓冲区中,所以如果没注意到这一点,直接发送缓冲区文件,相当于直接发送了空文件,最终将会导致视频无法打开。因此,在发送之前,需要使用fread函数读取文件fp中的数据,写入到缓冲区buffer中。最终代码如图:

代码:在服务端向客户端发送视频文件

        需要注意的是:buffer的大小往往远小于单个文件的大小,所以需要循环读取和发送,记录每一次循环发送的文件量,直到发送完毕。

        最后,原始代码也提供了发送视频文件结束符,关闭文件及socket连接的功能。

        ④通过编写代码,在客户端从服务端接收视频文件:

        此步骤与上一步骤相似,原始代码包含了相关的预备步骤以及结尾步骤。但在需要编写的部分中,存在更多需要注意的点:首先,要理解到文件并不是存入缓冲区buffer就完成了,还需要将buffer中的数据循环写入用于保存文件的临时变量video_segement中,此处我尝试过使用strcat()函数,但是并不能达成要求,后来改为使用memcpy()函数则可。

代码:将buffer中内容写入video_segement

        除此之外,与文件的发送相似,也需要对接收到的数据进行计数,直到接收到整个视频文件。但是,由于最后一个片段的大小可能小于buffer的大小,所以接收的时候如果还是选择接收BUFFER_SIZE这么多的数据的话,可能会连带接下来的视频结束符也一起被写入buffer,造成接下来的真正接收结束符的函数接收不到数据而无法继续运行。因此,每次接收的时候需要判断剩余的数据量是否足够填满整个buffer,如果可以,接收的数据大小位BUFFER_SIZE,如果不能,就直接接收剩余量的数据。

代码:在客户端从服务端接收视频文件

经过这四个步骤,即可完成单视频文件的传输,先运行服务端,再运行客户端即可完成文件的传输。运行后的结果如图所示:

 

                          图:客户端运行窗口输出结果                                图:download文件夹结果

3.补充代码,实现流式视频传输

        本步骤需要:在客户端实现连续请求一系列视频文件,实现流式视频传输;调用函数,实现流式视频的在线播放。

        ①调用播放器库的函数,实现对视频段文件的播放:

        在添加函数之前,需要下载实验1所需的文件(包含所需函数对应的头文件以及视频播放器),将其解压到原始代码框架所在的文件夹,并添加头文件:#include "player.h"

        在客户端添加代码:

        首先需要在main()函数最开始的部分添加StartStreamingServer()函数,准备播放器相关程序。

        接着在每个视频段接收完毕后(文件写入后)添加ReceiveSegment()函数,其中需要包含视频段数据、视频段名称以及视频段数据大小的参数。

代码:将函数添加到文件写入后

        最后在main()函数的末尾添加WaitEnd()函数即可,作用是等待播放器播放完视频并释放资源。

        经过此步骤,可以在网页播放器中看到实验一中传输的视频文件。

        ②补充代码,实现完整的流式视频传输:

        任务1过程中,有一步是在客户端中通过发送视频文件名给服务端,请求相应视频文件,因此,在了解视频文件名的规律后,通过循环遍历某个清晰度下的所有视频名,循环将其发送给服务端,即可完成任务。

代码:循环请求视频文件

       此处使用的是memcpy()函数拼接视频文件名,除此之外,也可以使用strcat()函数。

        ③在服务端接收client端发送的请求,并发送指定视频文件:

        通过阅读代码可知,服务端允许一直接收客户端的请求,并发送相应文件,故此部分不需要额外添加代码即可直接实现。

三、实验结果

在完成所有实验步骤之后运行代码,客户端的部分输出如图:

图:客户端运行窗口输出结果

在download文件夹中可看到59个视频段落文件,如图所示:

图:download文件夹中的部分文件

并可看到整个视频文件的流式播放,最终结果如图所示:

图:网页播放器效果

四、实验分析

        实验结果分析:本次实验圆满完成任务一和任务二的要求内容,实现了单个视频的传输与保存(保存到download文件夹中)、流式视频的传输与播放。整体上符合预期

        实验中遇到的问题:

        1.在实验早期,遇到过代码编译不通过的问题,经过后来的更新实验环境得到了解决。

        2.实验中遇到过多次视频无法播放的问题,后来分析可知是由于视频文件传输中出现了错误,服务端的文件写入与传输、客户端的文件接收与写入,这其中任何一个步骤出现错误都会导致视频文件无法播放,需要认真检查代码,找到问题,并给出解决方案。

五、实验总结

本实验通过使用devcppPortable软件,在computer-networks环境下,在实验文档提供的代码框架的基础上进行了代码编写。分别实现了单个视频和多个视频的传输、保存与播放。

本实验中,需要花时间阅读代码、编写代码,最困难的阶段是对代码进行检查与纠错(如上文提到的视频无法播放的问题),经过发现问题到解决问题的整个流程,我得到的经验有:阅读代码的过程中要充分理解代码,最好能理解每个变量的作用,并做好注释、遇到问题先想办法自己解决,如果很长一段时间都没有进展,就寻求老师或助教的帮助、纠错与调试代码的过程可能困难而漫长,一定要有耐心。

六、思考题

思考题一:

1. 请根据任务2中进一步完善代码:server和client如何确定所有视频的传输已经完成,并分别结束视频的接收和发送,然后关闭连接?请在代码中实现,并解释和说明;

        答:①在接收到最后一个结束符后,即可在client客户端中退出循环,结束视频的接收。而由于退出循环,也不会再向server请求文件,结束了视频的发送。最后调用closesocket()函数即可关闭链接。

代码:客户端断开链接

代码:服务端断开链接

2. 请分别结合任务1和任务2所实现的代码,解释并说明在server向client传输单个视频时,使用视频文件长度和文件结束终止符号的作用是什么?如果去掉相应的判断会出现什么现象?还有什么方法能够解决出现的问题?

        答:①文件长度是为了告诉客户端应该接收的文件量,如果去掉会导致客户端不知道何时停止接收文件;②结束符号是为了告诉客户端文件传输的结束,可以开始请求下一个文件,或者可以进行文件的存储与播放,如果去掉则也可能导致客户端不知道何时停止接收文件。③我认为可以使用一个计时器来解决:如果一段时间内没有收到数据,就停止文件接收,但是这种方法可能会降低文件传输的效率,所以加上结束符和文件大小可能更好。

3. 在任务2中,我们将一个长的视频段切分为小的视频段进行传输,那么在TCP协议和IP协议中,又是如何将一个完整的文件切分为独立的数据包进行传输的?其数据包中的数据单元大小限制是多少?如果超出了限制,协议中是如何处理的?那么,在TCP/IP协议中,如何传输超大型文件(大于2G),其中存在的问题是什么,如何解决?

        答:①数据在传输时会被切分成数据包进行发送。每个数据包的最大长度是65535字节。②数据的大小超过这个限制时,IP可以将一个大的数据包切分成小的数据包发送,每个数据包包含一个表示原始数据位置的偏移字段,用于重组成原始的数据。③传输超大文件可能出现:拼接成的大文件数据错、发生错误难以修复等问题;解决方式:可以使用增加校验码的方式解决,使用校验码验证文件对错的同时对错误的数据进行修正。

4. 请自行调查和学习TCP/IP协议中,数据分片的相关资料,同时结合上述几个问题的内容,谈谈你的理解;

        答:

        在TCP/IP协议中,数据分片是一个十分重要的过程,是因为物理网络层对于一个单独的包能够传输的大小有限制。

        传输一个大型文件时,需要把这个大类型文件分割成一个个更小的、可以在网络上传输的数据片段。这个过程就叫做数据分片。

        在IP层,每一个分片都被封装成一个独立的IP数据包进行传输。每个数据包包含了原始数据的一个片段,以及一个标识这个片段在原始数据中位置的偏移量。通过这种方式,接收方可以根据收到的多个包中的偏移量,把这些分片重新组合起来,恢复成原始的大型文件。

        TCP协议会通过一系列复杂的手段来确保数据的可靠传输。这其中的一种方式就是所谓的"滑动窗口"机制,这个机制可以动态的调整每次可以发送的数据量,以适应网络的变化情况。

        总的来说,数据分片是网络传输中的一个基本过程,它使我们能够在有限的网络资源中传输足够大的数据。但如何处理大文件的传输等问题也仍需要更多的解决方案。

尾注:

        本实验是本课程的第一次实验,初次阅读网络编程相关代码可能会遇到比较大的困难,但其中部分语句与本实验需要编写的部分关系不大,可以适当忽略,专注于实验的核心部分。

        本实验是24年新设置的创新实验,与往年的实验不同,不同班级也会有不同的实验安排,且最新的实验要求不一定与此相同,请注意分辨!

        如有疑问欢迎讨论,如有好的建议与意见欢迎提出,如有发现错误则恳请指正!

附:本实验所用代码

客户端:

#include <winsock2.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <malloc.h>
#include <string.h>
#include "player.h"

#define DOWNLOAD_PATH ".\\download\\"

#define INT_SIZE sizeof(int)
#define REQUEST_SIZE 35
#define PORT 7788
#define BUFFER_SIZE 1024
#define STOP_BYTE 0xFF
#define VIDEO_LEN 60 // 视频总时长为60s

int main()
{
    /***初始化阶段***/
    StartStreamingServer();
    WSADATA wsaData;
    WSAStartup(MAKEWORD(2, 2), &wsaData);

    int sock = 0;//套接字描述符
    struct sockaddr_in serv_addr;//存储地址信息
    char buffer[BUFFER_SIZE] = {0};//发送缓冲区,存储待发送的数据

    //创建一个套接字,并返回套接字描述符
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
        //perror("Socket creation error");
        return -1;
    }
    else
        printf("Client Create Socket Success. \n");

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);
    serv_addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");

    //向服务端发送连接请求
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
    {
        perror("Connection Failed");
        return -1;
    }
    else
        printf("Client Connect Server Success. \n");

    /*****************************************************************/
    /********** 任务1: 如何向server循环请求连续的视频文件?**********/
    /*****************************************************************/

    // 发送下载请求
    int bytes_sent = 0;
    unsigned char s_stop_byte = 0xFF;

    // 视频文件名
   	/******************************************************************/
    /***************** 任务2:如何按顺序选择视频文件?*****************/
    /******************************************************************/
	for (int req_idx = 0; req_idx <= 59; req_idx++) {
		char req[REQUEST_SIZE] = "ocean-1080p-8000k-";//req用于保存文件名
		
		//接下来在req中写入完整的视频名
		int len_req = strlen(req);
		if (req_idx < 10) {
			req[len_req] = req_idx + 48;
			req[len_req + 1] = '\0';
			memcpy(req + len_req + 1, ".ts", 4);
		}
		else {
			req[len_req] = (req_idx / 10) + 48;
			req[len_req + 1] = (req_idx % 10) + 48;
			req[len_req + 2] = '\0';
			memcpy(req + len_req + 2, ".ts", 4);
		}
		
	
	//请求下载文件:发送文件名、结束符
    bytes_sent = send(sock, req, REQUEST_SIZE, 0);
    if (bytes_sent < 0)
        printf("ERROR in send\n");
    bytes_sent = send(sock, &s_stop_byte, sizeof(s_stop_byte), 0);
    if (bytes_sent < 0)
        printf("ERROR in send\n");
    printf("send req: %s\n", req);

    // 接收文件的大小
    int file_size;
    unsigned long file_size_buf;
    int bytes_recv = 0;
    bytes_recv = recv(sock, (char *)&file_size_buf, INT_SIZE, 0);
    file_size = ntohl(file_size_buf);
    printf("file_size %d \n", file_size);

    // 接收视频片段
    char *video_segement = malloc(file_size);
    if (video_segement == NULL)
    {
        perror("malloc failed");
        // 处理内存分配失败的情况,可能需要退出程序
        return -1;
    }
    int recv_count = 0;
    while (recv_count < file_size)
    {
        int bytes;
        if(file_size - recv_count >= BUFFER_SIZE)//判断剩余文件大小
        	bytes = recv(sock, buffer, BUFFER_SIZE, 0);
        else
        	bytes = recv(sock, buffer, file_size - recv_count, 0);
        	
        // 将 buffer 中接收到的数据复制到 video_segement 的相应位置
    	memcpy(video_segement + recv_count, buffer, bytes);
    	recv_count += bytes;
    }
	printf("video recieve complete\n");



    unsigned char r_stop_byte;
    
    if (recv(sock, &r_stop_byte, 1, 0) != 1 || r_stop_byte != STOP_BYTE){
    	printf("ERROR in receiving stop byte 0x%02X \n", r_stop_byte); // 检查文件结束符
	}
	//printf("stop byte:0x%x\n",r_stop_byte);
	
    //strcat(video_segement, (char *)&r_stop_byte);//存入结束符
	
	r_stop_byte = 'e';                                                 // 重置



/*输出video_segement中的数据,用于判断接收是否出现问题
	for(int i=0;i<file_size;i+=50000){
		for(int j=i;j<i+50;j++)
			printf("j:%d  video_segement:%d  ", j, video_segement[j]);
		printf("\n");
	}
	printf("\n");
*/
	
    // 写入文件
    char file_path[40] = DOWNLOAD_PATH;
    strcat(file_path, req);
    printf("file_path %s \n", file_path);
    FILE *fp = fopen(file_path, "wb"); // 以二进制模式打开文件,并返回文件指针
    if (fp == NULL)
    {
        perror("fopen");
        exit(EXIT_FAILURE);
    }
    fwrite(video_segement, 1, file_size, fp);
    ReceiveSegment(video_segement, req, file_size);
	/***数据接收完成阶段***/

    // 释放内存
    free(video_segement);
    video_segement = NULL;

	}

    /*************************************************************************************/
    /*********任务2(扩展):如何在视频流传输完成后,通知server结束视频传输?*************/
    /*************************************************************************************/

    /***结束阶段***/
    closesocket(sock);
    WSACleanup();

	WaitEnd();
    return 0;
}

服务端:

#include <winsock2.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define INT_SIZE sizeof(unsigned long)
#define REQUEST_SIZE 35
#define PORT 7788
#define BUFFER_SIZE 1024
#define STOP_BYTE 0xFF
#define VIDEO_PATH "./video/"
#define END_REQUEST "END_REQUEST"
#define VIDEO_LEN 60 // 视频总时长为60s

// 定义一个获取文件大小的函数,参数是一个文件指针
long get_file_size(FILE *fp)
{
    long file_size = -1;
    fpos_t cur_pos;
    fgetpos(fp, &cur_pos);
    fseek(fp, 0, SEEK_END);
    file_size = ftell(fp);
    fsetpos(fp, &cur_pos);
    return file_size;
}

int main()
{
    /***初始化阶段***/
    WSADATA wsaData;
    WSAStartup(MAKEWORD(2, 2), &wsaData);
    int server_fd, new_socket;
    struct sockaddr_in server_address;//存储地址信息
    int opt = 1;
    int addrlen = sizeof(server_address);
    char buffer[BUFFER_SIZE] = { 0 };//发送缓冲区,存储待发送的数据

    // 创建套接字server_fd
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0)
    {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }
    else
        printf("Create Server Socket Success.\n");

    // 设置套接字选项
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = INADDR_ANY;
    server_address.sin_port = htons(PORT);

    // 使用bind()函数将之前创建的套接字server_fd与sockaddr_in结构体变量绑定,关联套接字和服务器的IP地址及端口号
    if (bind(server_fd, (struct sockaddr *)&server_address, sizeof(server_address)) < 0)
    {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }
    else
        printf("Server Bind Port Success. \n");

    // 监听套接字:使套接字server_fd进入监听端口,准备接收来自客户端的连接请求。
    if (listen(server_fd, 3) < 0)
    {
        perror("listen");
        exit(EXIT_FAILURE);
    }
    else
        printf("Server Listening......\n");

    /*
    当服务端监听到端口上的连接请求时,使用accept() 函数接收连接并返回一个新的连接SOCKET:new_socket。
    这个新SOCKET用于服务器和客户端之间的后续通信。
    */
    if ((new_socket = accept(server_fd, (struct sockaddr *)&server_address, &addrlen)) < 0)
    {
        perror("accept");
        exit(EXIT_FAILURE);
    }
    else
        printf("Server Accept Success. \n");

    while (1)
    {
        // 接收客户端下载请求
        int bytes_recv = 0;
        char req[REQUEST_SIZE] = "";//文件名
        bytes_recv = recv(new_socket, req, REQUEST_SIZE, 0);//接收文件名
        if (bytes_recv <= 0)
            printf("ERROR in recv\n");
        printf("req: %s\n", req);
        unsigned char r_stop_byte;
        if (recv(new_socket, &r_stop_byte, 1, 0) != 1 || r_stop_byte != STOP_BYTE)//接收并判断结束符
            printf("ERROR in receiving stop byte 0x%02X", r_stop_byte);
        r_stop_byte = 'e'; // 重置

        /****************************************************************************/
        /********** 任务2(扩展):如何根据client信号,终止传输并退出循环?**********/
        /****************************************************************************/

        // 找到文件
        char *file_name = req;
        char file_path[35] = VIDEO_PATH;
        strcat(file_path, file_name);
        FILE *fp = fopen(file_path, "rb"); // 以二进制模式打开文件,并返回文件指针
        if (fp == NULL)
        {
            perror("File open failed");
            exit(EXIT_FAILURE);
        }

        unsigned long file_size = (unsigned long)get_file_size(fp);//文件大小
        printf("%s %d \n", file_path, file_size);//输出文件的路径以及大小

        // 发送文件的大小到客户端
        unsigned long file_size_buf = htonl(file_size);
        int bytes_sent = 0;
        int reqLen = sizeof(req);
        bytes_sent = send(new_socket, (char *)&file_size_buf, INT_SIZE, 0);
        if (bytes_sent < 0)
            printf("ERROR in send\n");

        // 发送视频片段
        int send_count = 0;
        while (send_count < file_size){                        
            // 读取文件
    		int read_size = fread(buffer, 1, BUFFER_SIZE, fp);
    		// 发送数据
    		send_count += send(new_socket, buffer, read_size, 0);
        }
		
        // 发送文件结束符
        unsigned char s_stop_byte = 0xFF;
        Sleep(5);
        bytes_sent = send(new_socket, &s_stop_byte, sizeof(s_stop_byte), 0);
        if (bytes_sent < 0)
            printf("ERROR in send\n");

        // 关闭文件
        fclose(fp);
        printf("close file \n");
    }

    /***结束阶段***/
    closesocket(server_fd);
    closesocket(new_socket);
    WSACleanup();

    return 0;
}

  • 10
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值