目录
基于UDP的TFTP文件上传
代码:
#include<head.h>
//实现下载功能
int do_download(int cfd, struct sockaddr_in sin)
{
//定义变量存储下载请求包
char buf[516] = "";
//定义变量存储文件名
char fileName[40] = "";
printf("请输入文件名:");
scanf("%s", fileName);
getchar();
//组装请求包
short *p1 = (short *)buf;
*p1 = htons(1); //表明要下载
char *p2 = buf+2; //文件名段
strcpy(p2, fileName);
char *p4 = p2+strlen(p2)+1; //模式段
strcpy(p4, "octet");
int size = 4 + strlen(p2) + strlen(p4); //要发送数据的大小
//向服务器发送下载请求
if(sendto(cfd, buf, size, 0, (struct sockaddr*)&sin, sizeof(sin)) == -1)
{
perror("sendto error");
return -1;
}
printf("请求成功\n");
//循环接收回复服务器发来的消息
int flag = 0;
int fd=-1 ;
//循环接收数据包
int res=0;
unsigned short num = 1; //块编号
socklen_t addrlen = sizeof(sin);
while(1)
{
bzero(buf, sizeof(buf));
res = recvfrom(cfd, buf, sizeof(buf), 0, (struct sockaddr*)&sin, &addrlen);
if(res < 0)
{
perror("recvfrom error");
return -1;
}
if(3 == buf[1]) //数据包
{
if(0 == flag) //防止文件重复打开
{
//创建并打开filename文件
fd = open(fileName, O_WRONLY|O_CREAT|O_TRUNC, 0664);
if(fd ==-1 )
{
perror("open error");
return -1;
}
flag = 1;
}
//判断当前的块编号
if(htons(num) == *(unsigned short*)(buf+2)) //防止数据包重复到达
{
//提取数据,写入到文件中
if(write(fd, buf+4, res-4) < 0)
{
break;
}
//回复ACK包将数据包的操作码修改成4,发送数据包的前四个字节。
buf[1] = 4;
sendto(cfd, buf, 4, 0, (struct sockaddr*)&sin, sizeof(sin)) ;
//判断数据包的大小是否小于516
if(res < 516)
{
printf("文件下载完成\n");
break;
}
num++; //块编号加1
}
}
else if(5 == buf[1]) //错误包
{
//打印错误信息
printf("ERROR:%s\n", buf+4);
break;
}
}
close(fd);
return 0;
}
//实现上传功能
int do_upload(int cfd, struct sockaddr_in sin)
{
//定义变量存储文件名
char fileName[40] = "";
printf("请输入要上传的文件名:");
scanf("%s", fileName);
getchar();
int fd=-1;
fd=open(fileName,O_RDONLY);
if(fd==-1)
{
perror("open error");
return -1;
}
//定义变量存储上传请求包
char buf[516] = "";
//组装请求包
short *p1 = (short *)buf;
*p1 = htons(2); //表明要上传
char *p2 = buf+2; //文件名段
strcpy(p2, fileName);
char *p4 = p2+strlen(p2)+1; //模式段
strcpy(p4, "octet");
int size = 4 + strlen(p2) + strlen(p4); //要发送数据的大小
//向服务器发送上传请求
if(sendto(cfd, buf, size, 0, (struct sockaddr*)&sin, sizeof(sin)) == -1)
{
perror("sendto error");
return -1;
}
printf("请求成功\n");
//循环接收回复服务器发来的消息
//循环发送数据包
int res=0;
unsigned short num = 0; //块编号
socklen_t addrlen = sizeof(sin);
while(1)
{
bzero(buf, sizeof(buf));
res = recvfrom(cfd, buf, sizeof(buf), 0, (struct sockaddr*)&sin, &addrlen);
if(res < 0)
{
perror("recvfrom error");
return -1;
}
if(4 == buf[1]) //判断是否是ACK
{
//判断当前数据包的编号是否等于应答包的编号
if(num == ntohs(*(unsigned short*)(buf+2)))
{
//修改操作码为数据包
buf[1] = 3;
//填充块编号
num++;
*(unsigned short*)(buf+2) = htons(num);
//发送数据
res =read(fd,buf+4,512);
if(res< 0)
{
perror("read error");
return -1;
}else if(res==0)
{
printf("上传完成\n");
break;
}
sendto(cfd, buf, res+4, 0, (struct sockaddr*)&sin, sizeof(sin)) ;
} else
{
break;
}
}
else if(5 == buf[1]) //错误包
{
//打印错误信息
printf("ERROR:%s\n", buf+4);
break;
}
}
close(fd);
return 0;
}
/*******************************主函数*********************/
int main(int argc, const char *argv[])
{
if(argc != 2)
{
printf("input error\n");
printf("usage:./a.out ip\n");
return -1;
}
//1、创建套接字
int cfd = socket(AF_INET, SOCK_DGRAM, 0);
if(cfd == -1)
{
perror("socket error");
return -1;
}
//2、填充服务器地址信息结构体
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(69);
sin.sin_addr.s_addr = inet_addr(argv[1]);
int mune = -1;
while(1)
{
system("clear"); //清屏
printf("\t\t======1、下载=======\n");
printf("\t\t======2、上传=======\n");
printf("\t\t======0、退出=======\n");
printf("请输入功能:");
scanf("%d", &mune);
getchar();
//多分支选择
switch(mune)
{
case 1:
{
do_download(cfd, sin);
}
break;
case 2:
{
do_upload(cfd,sin);
}
break;
case 0:
goto POS;
default:printf("输入功能有误,请重新输入\n");
}
//阻塞
printf("输入任意键,按回车清空:");
while(getchar() != '\n');
}
POS:
//关闭套接字
close(cfd);
return 0;
}
效果图:
什么是网络字节序,为什么引入?
网络字节序是数据在不同主机之间传输的一种格式,采用大端存储。不同主机在存储多字节整数时,由于cpu内核架构不同,分为大端存储和小端存储,主机间通信时,由于大小端存储不同,收到的数据也可能不是想要的数据,对此,引入网络字节序。无论发送端是大端存储还是小端存储,数据发送到网络上时统一按网络字节序传输。接收端接收消息后,再根据自己的存储方式将网络字节序转换为本机字节序。
TCP服务器端通信流程的详细描述
1.创建套接字用于和客户端进行连接(socket)。
2.填充服务器地址信息结构体(地址族、本地地址和端口号),绑定套接字(bind)。
3.设置被动监听状态(listen)。
4.定义用于接收客户端地址信息的结构体变量,服务器接受连接请求(accept)。
5.和客户端进行数据传输(send/recv)。
6.关闭套接字(close)。
TCP客户端通信流程的详细描述
1.创建套接字用于和服务器进行连接和通信(socket)。
2.填充客户端地址信息结构体(地址族、本地地址和端口号),绑定套接字(bind)。(非必须)
3.填充服务器地址信息结构体,连接服务器。(connect)
4.和服务器进行数据传输(sendto/recvfrom)。
5.关闭套接字(close)。
什么是Nagle算法?
当应用程序产生少量的数据(例如1个字节),并且这些数据以数据包的形式发送到远端服务器时,就可能导致网络由于太多的数据包而过载。这种情况下,虽然只有1个字节的有效数据,但每个数据包却需要额外的40个字节的包头(IP头20字节+TCP头20字节)。如果发送端多次发送包含少量字符的数据包,Nagle算法会尝试将这些小包累积起来,直到它们达到一定的大小或者一定的时间后再一起发送,以减少网络拥塞的可能性。