1)tftp协议概述
简单文件传输协议,适用于在网络上进行文件传输的一套标准协议,使用UDP传输
特点:
是应用层协议
基于UDP协议实现
数据传输模式
octet:二进制模式(常用)
mail:已经不再支持
2)tftp下载模型
TFTP通信过程总结
- 服务器在69号端口等待客户端的请求
- 服务器若批准此请求,则使用 临时端口 与客户端进行通信。
- 每个数据包的编号都有变化(从1开始)
- 每个数据包都要得到ACK的确认,如果出现超时,则需要重新发送最后的数据包或ACK包
- 数据长度以512Byte传输的,小于512Byte的数据意味着数据传输结束。
3)tftp协议分析
差错码:
0 未定义,差错错误信息
1 File not found.
2 Access violation.
3 Disk full or allocation exceeded.
4 illegal TFTP operation.
5 Unknown transfer ID.
6 File already exists.
7 No such user.
8 Unsupported option(s) requested.
#include <head.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define PORT 69 //1024~49151
#define IP "192.168.122.49" //
int do_download(int sfd, struct sockaddr_in sin);
int do_upload(int sfd, struct sockaddr_in sin);
int main(int argc, const char *argv[])
{
//创建报式套接字
int sfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sfd < 0)
{
ERR_MSG("socket");
return -1;
}
printf("socket create success sfd=%d __%d__\n", sfd, __LINE__);
//绑定--->非必须绑定
//如果不绑定,操作系统会自动绑定本机IP,从49152~65535范围内随机一个未被使用的端口号
//填充服务器的地址信息结构体
//供给下面的sendto使用,
struct sockaddr_in sin;
sin.sin_family = AF_INET; //必须填AF_INET;
sin.sin_port = htons(PORT); //服务器绑定的端口,网络字节序
sin.sin_addr.s_addr = inet_addr(IP); //服务器绑定的IP,本机IP ifconfig
char choose = 0;
while(1)
{
printf("----------------------------\n");
printf("----------1. 下载-----------\n");
printf("----------2. 上传-----------\n");
printf("----------3. 退出-----------\n");
printf("----------------------------\n");
printf("请输入>>> ");
choose = getchar();
while(getchar()!=10);
switch(choose)
{
case '1':
do_download(sfd, sin);
break;
case '2':
do_upload(sfd, sin);
break;
case '3':
goto END;
break;
default:
printf("输入错误,请重新输入\n");
break;
}
}
END:
//关闭所有文件描述符
close(sfd);
return 0;
}
int do_upload(int sfd, struct sockaddr_in sin)
{
int ret_val = 0;
char filename[20] = "";
printf("请输入要上传的文件名>>> ");
scanf("%s", filename);
while(getchar()!=10);
//判断文件存不存在
int fd = open(filename, O_RDONLY);
if(fd < 0)
{
ERR_MSG("open");
return -1;
}
//发送上传请求
//组上传请求包
char buf[516] = "";
char *ptr = buf;
unsigned short *p1 = (unsigned short*)buf;
*p1 = htons(2);
char *p2 = buf + 2;
strcpy(p2,filename);
char *p3 = p2 + strlen(p2)+1;
strcpy(p3,"octet");
//计算大小
int size =2+strlen(filename)+7;
//sendto
if(sendto(sfd, buf, size, 0, (struct sockaddr*)&sin, sizeof(sin)) < 0)
{
ERR_MSG("sendto");
return -1;
}
socklen_t addrlen = sizeof(sin);
ssize_t res = 0;
unsigned short num = 0; //本地记录的快编号
while(1)
{
bzero(buf, sizeof(buf));
//接收服务器的应答
res = recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&sin, &addrlen);
if(res < 0)
{
ERR_MSG("recvfrom");
ret_val = -1;
break;
}
//printf("res = %ld __%d__\n", res, __LINE__);
//printf("0:%d 1:%d 2:%d 3:%d\n", buf[0], buf[1], buf[2], buf[3]);
if(4 == buf[1])
{
if(htons(num) == *(unsigned short*)(buf+2))
{
//组数据包给服务器
num++;
*(unsigned short*)buf = htons(3);
*(unsigned short*)(buf+2) = htons(num);
res = read(fd, buf+4, 512);
if(res < 0)
{
ret_val = -1;
break;
}
else if(0 == res)
{
printf("文件:%s 上传完毕\n", filename);
break;
}
//发送数据包
if(sendto(sfd, buf, res+4, 0, (struct sockaddr*)&sin, sizeof(sin)) < 0)
{
ERR_MSG("sendto");
return -1;
}
}
}
else if(5 == buf[1])//错误包
{
printf("MSG_ERR: code[%d] msg[%s] __%d__\n", \
ntohs(*(unsigned short*)(buf+2)), buf+4, __LINE__);
ret_val = -1;
break;
}
}
close(fd);
return ret_val;
}
int do_download(int sfd, struct sockaddr_in sin)
{
int ret_val = 0;
char buf[516] = "";
char filename[20] = "";
printf("请输入要下载的文件名>>> ");
scanf("%s", filename);
while(getchar()!=10);
//发送下载请求
//组协议包
char *ptr = buf;
unsigned short *p1 = (unsigned short*)buf;
*p1 = htons(1);
char *p2 = buf + 2;
strcpy(p2,filename);
char *p3 = p2 + strlen(p2)+1;
strcpy(p3,"octet");
//计算大小
int size =2+strlen(filename)+7;
//sendto
if(sendto(sfd, buf, size, 0, (struct sockaddr*)&sin, sizeof(sin)) < 0)
{
ERR_MSG("sendto");
return -1;
}
//本地创建并打开要下载的文件
int fd = -1; //必须初始化一个无意义的文件描述符,否则下面的close
socklen_t addrlen = sizeof(sin);
ssize_t res = 0;
unsigned short num = 0; //本地记录的快编号
//循环接收数据包,回复ACk
while(1)
{
bzero(buf, sizeof(buf));
//接收数据包
res = recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&sin, &addrlen);
if(res < 0)
{
ERR_MSG("recvfrom");
ret_val = -1;
break;
}
//printf("res = %ld __%d__\n", res, __LINE__);
//printf("0:%d 1:%d 2:%d 3:%d\n", buf[0], buf[1], buf[2], buf[3]);
//if(ntohs(*(unsigned short*)buf) == 3)
//由于操作码占两个字节的无符号整数,所以传输的时候以大端传输
//所有有效差错码,会存储在高地址上,即存储在buf[1]位置,buf[0]中存储的是0
if(3 == buf[1]) //数据包
{
//UDP可能会数据重复,为了防止重复收包。
//则在本地记录了一下服务器回过来的快编号,每次处理前,先判断快编号是否正确
if(htons(num+1) == *(unsigned short*)(buf+2))
{
num++;
if(-1 == fd)
{
fd = open(filename, O_WRONLY|O_CREAT|O_TRUNC, 0664);
if(fd < 0)
{
ERR_MSG("open");
ret_val = -3;
break;
}
}
//将获取到的数据,写入到文件中
if(write(fd, buf+4, res-4) < 0)
{
ERR_MSG("write");
ret_val = -3;
break;
}
//回复ACK,数据包前四个字节与ACK包基本一致
//只有操作码不一致,有效操作码在buf[1]的位置,直接将buf[1]从3,修改成4即可。
buf[1] = 4;
if(sendto(sfd, buf, 4, 0, (struct sockaddr*)&sin, sizeof(sin)) < 0)
{
ERR_MSG("sendto");
ret_val = -1;
break;
}
if(res-4 < 512)
{
printf("======= 文件下载完毕 =======\n");
break;
}
}
}
else if(5 == buf[1])//错误包
{
printf("MSG_ERR: code[%d] msg[%s] __%d__\n", \
ntohs(*(unsigned short*)(buf+2)), buf+4, __LINE__);
ret_val = -1;
break;
}
}
close(fd);
return ret_val;
}
head.h文件
#ifndef __HEAD_H__
#define __HEAD_H__
#define ERR_MSG(msg) do{\
fprintf(stderr,"__%d__:",__LINE__);\
perror(msg);\
}while(0)
#include <dirent.h> // 目录项
#include <fcntl.h> // 文件控制
#include <fnmatch.h> // 文件名匹配类型
#include <glob.h> // 路径名模式匹配类型
#include <grp.h> // 组文件
#include <netdb.h> // 网络数据库操作
#include <pwd.h> // 口令文件
#include <regex.h> // 正则表达式
#include <tar.h> // TAR归档值
#include <termios.h> // 终端I/O
#include <unistd.h> // 符号常量
#include <utime.h> // 文件时间
#include <wordexp.h> // 字符扩展类型
#include <arpa/inet.h> // INTERNET定义
#include <net/if.h> // 套接字本地接口
#include <netinet/in.h> // INTERNET地址族
#include <netinet/tcp.h> // 传输控制协议定义
#include <sys/mman.h> // 内存管理声明
#include <sys/select.h> // Select函数
#include <sys/socket.h> // 套接字借口
#include <sys/stat.h> // 文件状态
#include <sys/times.h> // 进程时间
#include <sys/types.h> // 基本系统数据类型
#include <sys/un.h> // UNIX域套接字定义
#include <sys/utsname.h> // 系统名
#include <sys/wait.h> // 进程控制
#include <cpio.h> // cpio归档值
#include <dlfcn.h> // 动态链接
#include <fmtmsg.h> // 消息显示结构
#include <ftw.h> // 文件树漫游
#include <iconv.h> // 代码集转换使用程序
#include <langinfo.h> // 语言信息常量
#include <libgen.h> // 模式匹配函数定义
#include <monetary.h> // 货币类型
//#include <ndbm.h> // 数据库操作
#include <nl_types.h> // 消息类别
#include <poll.h> // 轮询函数
#include <search.h> // 搜索表
#include <strings.h> // 字符串操作
#include <syslog.h> // 系统出错日志记录
#include <ucontext.h> // 用户上下文
#include <ulimit.h> // 用户限制
#include <utmpx.h> // 用户帐户数据库
#include <sys/ipc.h> // IPC(命名管道)
#include <sys/msg.h> // 消息队列
#include <sys/resource.h> // 资源操作
#include <sys/sem.h> // 信号量
#include <sys/shm.h> // 共享存储
#include <sys/statvfs.h> // 文件系统信息
#include <sys/time.h> // 时间类型
#include <sys/timeb.h> // 附加的日期和时间定义
#include <sys/uio.h> // 矢量I/O操作
#include <aio.h> // 异步I/O
#include <mqueue.h> // 消息队列
#include <pthread.h> // 线程
#include <sched.h> // 执行调度
#include <semaphore.h> // 信号量
#include <spawn.h> // 实时spawn接口
#include <stropts.h> // XSI STREAMS接口
//#include <trace.h> // 事件跟踪
#include <assert.h> //设定插入点
#include <ctype.h> //字符处理
#include <errno.h> //定义错误码
#include <float.h> //浮点数处理
#include <iso646.h> //对应各种运算符的宏
#include <limits.h> //定义各种数据类型最值的常量
#include <locale.h> //定义本地化C函数
#include <math.h> //定义数学函数
#include <setjmp.h> //异常处理支持
#include <signal.h> //信号机制支持
#include <stdarg.h> //不定参数列表支持
#include <stddef.h> //常用常量
#include <stdio.h> //定义输入/输出函数
#include <stdlib.h> //定义杂项函数及内存分配函数
#include <string.h> //字符串处理
#include <time.h> //定义关于时间的函数
#include <wchar.h> //宽字符处理及输入/输出
#include <wctype.h> //宽字符分类
#endif
#include <head.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>
#include <pthread.h>
#define PORT 6666 //1024~49151
#define IP "192.168.122.15" //IP地址,本机IP ifconfig
struct cli_msg
{
int newfd;
struct sockaddr_in cin;
};
void* deal_cli_msg(void* arg);
int main(int argc, const char *argv[])
{
//创建流式套接字
int sfd = socket(AF_INET, SOCK_STREAM, 0);
if(sfd < 0)
{
ERR_MSG("socket");
return -1;
}
printf("socket create success sfd = %d\n", sfd);
//设置允许端口快速被重用
int resue = 1;
if(setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &resue, sizeof(resue)) < 0)
{
ERR_MSG("setsockopt");
return -1;
}
//填充服务器的地址信息结构体
//真实的地址信息结构体根据地址族执行,AF_INET: man 7 ip
struct sockaddr_in sin;
sin.sin_family = AF_INET; //必须填AF_INET;
sin.sin_port = htons(PORT); //端口号的网络字节序,1024~49151
sin.sin_addr.s_addr = inet_addr(IP); //IP地址的网络字节序,ifconfig查看
//绑定---必须绑定
if(bind(sfd, (struct sockaddr*)&sin, sizeof(sin)) < 0)
{
ERR_MSG("bind");
return -1;
}
printf("bind success ");
//将套接字设置为被动监听状态
if(listen(sfd, 128) < 0)
{
ERR_MSG("listen");
return -1;
}
printf("listen success ");
//功能:阻塞函数,阻塞等待客户端连接成功。
//当客户端连接成功后,会从已完成连接的队列头中获取一个客户端信息,
//并生成一个新的文件描述符;新的文件描述符才是与客户端通信的文件描述符
struct sockaddr_in cin; //存储连接成功的客户端的地址信息
socklen_t addrlen = sizeof(cin);
int newfd = -1;
pthread_t tid;
struct cli_msg info;
while(1)
{
//主线程负责连接
//accept函数阻塞之前,会先预选一个没有被使用过的文件描述符
//当解除阻塞后,会判断预选的文件描述符是否被使用
//如果被使用了,则重新遍历一个没有被用过的
//如果没有被使用,则直接返回预先的文件描述符;
newfd = accept(sfd, (struct sockaddr*)&cin, &addrlen);
if(newfd < 0)
{
ERR_MSG("accept");
return -1;
}
printf("[%s : %d] newfd=%d 客户端连接成功\n", \
inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), newfd);
info.newfd = newfd;
info.cin = cin;
//能运行到当前位置,则代表有客户端连接成功
//则需要创建一个分支线程用来,与客户端交互
if(pthread_create(&tid, NULL, deal_cli_msg, &info) != 0)
{
fprintf(stderr, "pthread_create failed __%d__\n", __LINE__);
return -1;
}
pthread_detach(tid); //分离线程
}
//关闭所有套接字文件描述符
close(sfd);
return 0;
}
//线程执行体
void* deal_cli_msg(void* arg) //void* arg = (void*)&info
{
//---------问题处------
int newfd = ((struct cli_msg*)arg)->newfd;
struct sockaddr_in cin = ((struct cli_msg*)arg)->cin;
char buf[128] = "";
ssize_t res = -1;
while(1)
{
bzero(buf, sizeof(buf));
//接收
res = recv(newfd, buf, sizeof(buf), 0);
if(res < 0)
{
ERR_MSG("recv");
break;
}
else if(0 == res)
{
fprintf(stderr, "[%s : %d] newfd=%d 客户端下线\n", \
inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), newfd);
break;
}
printf("[%s : %d] newfd=%d : %s\n", \
inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), newfd, buf);
//发送 -- 将数据拼接一个 *_* 发送回去
strcat(buf, "*_*");
if(send(newfd, buf, sizeof(buf), 0) < 0)
{
ERR_MSG("send");
break;
}
printf("send success\n");
}
close(newfd);
pthread_exit(NULL);
}
上述程序中
1. 多线程中的newfd,能否修改成全局,不行,为什么?
2. 多线程中分支线程的newfd能否不另存,直接用指针间接访问主线程中的newfd,不行,为什么?
//必须要另存,因为同一个进程下的线程共享其附属进程的所有资源
//如果使用全局,则会导致每次连接客户端后, newfd和cin会被覆盖
//如果使用指针间接访问外部成员变量,也会导致,成员变量被覆盖。