目录
一、实验目的与要求
实验目的:
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;
}