一、tftp客户端下载
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<myhead.h>
#define SER_PORT 69 //服务器端口号
#define SER_IP "192.168.10.101" //服务器IP地址
int main(int argc, const char *argv[])
{
//1、创建用于通信的客户端 套接字文件描述符
int cfd = socket(AF_INET,SOCK_DGRAM,0);
if(cfd == -1)
{
perror("socket error");
return -1;
}
printf("socket success\n");
//2、向服务器发送下载请求
//2.1 填充服务器地址
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(SER_PORT);
inet_pton(AF_INET,SER_IP,&sin.sin_addr);
//终端输入操作的文件名
char destname[50] = "";
printf("请输入您需要操作的文件名:");
fgets(destname,sizeof(destname),stdin);
//将回车替换成0
destname[strlen(destname)-1] = '\0';
//2.2 创建请求数据包
char buf[516] = "";
short *op = (short *)buf;
*op = htons(1);
char *filename = buf + 2;
strcpy(filename, destname);
char *zero1 = filename + strlen(filename);
*zero1 = 0;
char *mode = zero1 + 1;
strcpy(mode, "octet");
char *zero2 = mode + strlen(mode);
*zero2 = 0;
size_t size = 2 + strlen(filename) + strlen(mode) + 2;
printf("%s\n",buf);
//2.3发送请求数据包
sendto(cfd,buf,size,0,(struct sockaddr*)&sin,sizeof(sin));
//出错判断
uint16_t err_op = htons(5);
//打开下载位置的文件描述符
int newfd = open("5.png",O_APPEND|O_CREAT|O_WRONLY|O_TRUNC,0664);
while(1)
{
//2.4 接收数据
socklen_t addrlen = sizeof(sin);
bzero(buf, sizeof(buf));
int res = 0;
//2.5 解析数据
res = recvfrom(cfd,buf,sizeof(buf),0,(struct sockaddr*)&sin,&addrlen);
//判断出错
if (res < 4)
{
printf("接收到的数据包太小,无法解析");
break;
}
//接收操作码
uint16_t opcode = ntohs(*(uint16_t *)buf);
if(opcode == 5)
{
//错误码
char err_code[2] = "";
strncpy(err_code, buf + 2, 2);
printf("ERROR[%d:%s]\n", ntohs(*(uint16_t*)err_code), buf + 4);
break;
}
else if(opcode == 3)
{
if(res < 516)
{
write(newfd,buf+4,res-4);
printf("下载完成\n");
break;
}
//将接收到的数据写入当前目录下的5.png文件中
write(newfd,buf+4,res-4);
//向服务器回复确认数据包ACK
*op = htons(4);
sendto(cfd,buf,4,0,(struct sockaddr*)&sin,sizeof(sin));
}
else
{
printf("未知操作码: %d", opcode);
break;
}
}
//4、关闭文件描述符
close(newfd);
close(cfd);
return 0;
}
二、基于UDP的网络聊天室
项目需求:
- 如果有用户登录,其他用户可以收到这个人的登录信息
- 如果有人发送信息,其他用户可以收到这个人的群聊信息
- 如果有人下线,其他用户可以收到这个人的下线信息
- 服务器可以发送系统信息
效果展示
源码
ChatRoom.h
#ifndef CHATROOM_H
#define CHATROOM_H
#include <myhead.h>
#define MAX_CLIENTS 10
#define BUFFER_SIZE 1024
//客户端发送消息的三种模式
#define LOGIN 1
#define CHAT 2
#define QUIT 3
//服务器端存储用户信息结构体
typedef struct node
{
struct sockaddr_in cin; //用户地址结构体
struct node* next; //指针域
} Client;
//客户端用户发送信息结构体
typedef struct
{
int type;
char usrName[50];
char msgText[1024];
}msgTyp;
//信号处理
typedef void (*sighandler_t)(int);
//函数声明
int ser_recv(int sfd, Client* head);
int ser_login(int sfd, Client* head, msgTyp rcv_msg, struct sockaddr_in cin);
int ser_quit(int sfd, Client* head, msgTyp rcv_msg, struct sockaddr_in cin);
int ser_chat(int sfd, Client* head, msgTyp rcv_msg, struct sockaddr_in cin);
int ser_system(int sfd, struct sockaddr_in sin);
#endif
ChatSer.c
#include"ChatRoom.h"
int main(int argc, const char *argv[])
{
//终端输入服务器IP和端口号
//判断终端输入
if(argc != 3)
{
printf("输入错误\n");
printf("usage:./a.out IP PORT");
return -1;
}
//1、创建用于通信的服务器套接字文件描述符
int sfd = socket(AF_INET,SOCK_DGRAM,0);
if(sfd == -1)
{
perror("socket error");
return -1;
}
printf("sockeyt success\n");
//端口号快速重用
int reuse =-1;
if(setsockopt(sfd,SOL_SOCKET,SO_REUSEADDR,&reuse,sizeof(reuse)) == -1)
{
perror("setsockopt error");
return -1;
}
printf("setsockopt success\n");
//2、为套接字绑定IP地址和端口号
//2.1、填充地址信息结构体
struct sockaddr_in sin;
sin.sin_family = AF_INET; //通信域
sin.sin_port = htons(atoi(argv[2])); //端口号
sin.sin_addr.s_addr = inet_addr(argv[1]); //IP地址
//2.2、绑定
if(bind(sfd,(struct sockaddr*)&sin,sizeof(sin)) == -1)
{
perror("bind error");
return -1;
}
printf("bind success\n");
//创建进程
int pid = 0;
pid = fork();
if(pid > 0)
{
//父进程, 接收消息
Client* head = (Client*)malloc(sizeof(Client));
head->next = NULL;
ser_recv(sfd , head);
}
else if(pid==0)
{
//子进程,发送系统消息
ser_system(sfd, sin);
}
//4、关闭文件描述符
close(sfd);
return 0;
}
//系统提示消息
int ser_system(int sfd, struct sockaddr_in sin)
{
msgTyp sys_msg = {htonl(CHAT), "**system**"};
while(1)
{
bzero(sys_msg.msgText, sizeof(sys_msg.msgText));
fgets(sys_msg.msgText, sizeof(sys_msg.msgText), stdin);
sys_msg.msgText[strlen(sys_msg.msgText)-1] = 0;
//将当前进程当做客户端,父进程当做服务器,发送信息;
if(sendto(sfd, &sys_msg, sizeof(sys_msg), 0, (void*)&sin, sizeof(sin)) < 0)
{
perror("sendto");
return -1;
}
}
printf("系统消息发送成功");
return 0;
}
//登录上线函数
int ser_login(int sfd, Client* head, msgTyp rcv_msg, struct sockaddr_in cin)
{
//打印登录成功
printf("%s [%s:%d]登录成功\n", rcv_msg.usrName, (char*)inet_ntoa(cin.sin_addr), ntohs(cin.sin_port));
sprintf(rcv_msg.msgText, "-----%s登录成功-----", rcv_msg.usrName);
while(head->next != NULL)
{
head = head->next;
if(sendto(sfd, &rcv_msg, sizeof(rcv_msg), 0, (void*)&(head->cin), sizeof(head->cin)) < 0)
{
perror("sendto");
return -1;
}
}
//将登录成功的客户端信息添加到链表中;
Client *temp = (Client*)malloc(sizeof(Client));
temp->cin = cin;
temp->next = NULL;
head->next = temp;
return 0;
}
//接收函数
int ser_recv(int sfd, Client* head)
{
msgTyp rcv_msg;
int recv_len = 0;
struct sockaddr_in cin;
socklen_t clen = sizeof(cin);
while(1)
{
//接收消息
recv_len = recvfrom(sfd, &rcv_msg, sizeof(rcv_msg), 0, (void*)&cin, &clen);
if(recv_len < 0)
{
perror("recvfrom");
return -1;
}
//提取出协议
//将网络字节序转换为主机字节序
int type = ntohl(rcv_msg.type);
switch(type)
{
case LOGIN:
ser_login(sfd, head, rcv_msg, cin);
break;
case CHAT:
ser_chat(sfd, head, rcv_msg, cin);
break;
case QUIT:
ser_quit(sfd, head, rcv_msg, cin);
break;
}
}
}
//聊天发送函数
int ser_chat(int sfd, Client* head, msgTyp rcv_msg, struct sockaddr_in cin)
{
printf("%s [%s:%d]chat成功\n", rcv_msg.usrName, (char*)inet_ntoa(cin.sin_addr), ntohs(cin.sin_port));
//重新拼接群聊消息: 名字+消息
char buf[258] = "";
sprintf(buf, "%s:%s", rcv_msg.usrName, rcv_msg.msgText);
strcpy(rcv_msg.msgText, buf);
//循环发送,除了自己以外的ip地址
while(head->next != NULL)
{
head = head->next;
if(memcmp(&cin, &head->cin, sizeof(cin)) != 0)
{
if(sendto(sfd, &rcv_msg, sizeof(rcv_msg), 0, (void*)&(head->cin), sizeof(head->cin)) < 0)
{
perror("sendto");
return -1;
}
}
}
return 0;
}
//退出函数
int ser_quit(int sfd, Client* head, msgTyp rcv_msg, struct sockaddr_in cin)
{
printf("%s [%s:%d]已下线\n", rcv_msg.usrName, (char*)inet_ntoa(cin.sin_addr), ntohs(cin.sin_port));
sprintf(rcv_msg.msgText, "-----%s 已下线-----\n", rcv_msg.usrName);
//循环遍历链表,发送"xxx一下线"的信息
//发送给当前客户端外的其余客户端
while(head->next != NULL)
{
if(memcmp(&cin, &head->next->cin, sizeof(cin)) == 0)
{
//从链表中删除该客户端信息
//free
Client* temp = head->next;
head->next = temp->next;
free(temp);
}
else
{
head = head->next;
if(sendto(sfd, &rcv_msg, sizeof(rcv_msg), 0, (void*)&head->cin, sizeof(head->cin)) < 0)
{
perror("sendto");
return -1;
}
}
}
return 0;
}
ChatCli.c
#include"ChatRoom.h"
//信号处理函数
void handler(int sig)
{
//回收子进程资源并退出
while(waitpid(-1,NULL, WNOHANG)>0);
exit(0);
}
int main(int argc, const char *argv[])
{
//终端输入服务器IP和端口号
//判断终端输入
if(argc != 3)
{
printf("输入错误\n");
printf("usage:./a.out IP PORT");
return -1;
}
//注册信号处理函数,让子进程退出后,父进程回收子进程资源并退出
sighandler_t h = signal(SIGCHLD, handler);
if(h == SIG_ERR)
{
perror("signal error");
return -1;
}
//1、创建用于通信的客户端 套接字文件描述符
int cfd = socket(AF_INET,SOCK_DGRAM,0);
if(cfd == -1)
{
perror("socket error");
return -1;
}
printf("sockeyt success\n");
//端口号快速重用
int reuse =-1;
if(setsockopt(cfd,SOL_SOCKET,SO_REUSEADDR,&reuse,sizeof(reuse)) == -1)
{
perror("setsockopt error");
return -1;
}
printf("setsockopt success\n");
//2、为套接字绑定IP地址和端口号
//2.1、填充地址信息结构体
//3、数据收发
char buf[128] = "";
//填充服务器地址信息
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(atoi(argv[2]));
sin.sin_addr.s_addr = inet_addr(argv[1]);
//用户登录
msgTyp msg;
msg.type = htonl(LOGIN);
printf("请输入姓名:");
fgets(msg.usrName,sizeof(msg.usrName),stdin);
msg.usrName[strlen(msg.usrName) - 1] = '\0';
//将登录信息发送给服务器
if(sendto(cfd, &msg, sizeof(msg), 0, (struct sockaddr*)&sin, sizeof(sin)) == -1)
{
perror("发送登录信息失败");
return -1;
}
printf("登录信息发送成功\n");
//父进程用于接收服务器消息,子进程用于向服务器发送消息
pid_t pid = fork();
if (pid < 0)
{
perror("创建子进程失败");
return -1;
}
else if (pid == 0)
{
// 子进程:向服务器发送消息
while (1)
{
//终端输入聊天信息
bzero(msg.msgText, sizeof(msg.msgText));
fgets(msg.msgText, sizeof(msg.msgText), stdin);
msg.msgText[strlen(msg.msgText) - 1] = '\0';
if (strcmp(msg.msgText, "quit") == 0)
{
msg.type = htonl(QUIT);
}
else
{
msg.type = htonl(CHAT);
}
//发送给服务器
if (sendto(cfd, &msg, sizeof(msg), 0, (struct sockaddr*)&sin, sizeof(sin)) == -1)
{
perror("发送消息失败");
exit(-1);
}
if(msg.type == htonl(QUIT))
{
exit(0); //退出子进程;
}
}
}
else
{
// 父进程:接收服务器消息
msgTyp recvMsg;
while (1)
{
if (recvfrom(cfd, &recvMsg, sizeof(recvMsg), 0, NULL,NULL) == -1)
{
perror("接收消息失败");
break;
}
printf("%s\n",recvMsg.msgText);
}
}
//4、关闭套接字
close(cfd);
return 0;
}
附录
myhead.h
#include<stdio.h>
#include<errno.h> //错误码的头文件
#include <sys/types.h>
#include <sys/stat.h>
#include<dirent.h> //文件夹操作头文件
#include <fcntl.h> //open的头文件
#include<unistd.h> //close\sleep的头文件
#include<string.h> //str系列函数头文件
#include<math.h> //数学头文件
#include<stdlib.h> //标准库文件
#include<time.h> //时间头文件
#include <dirent.h> //关于目录操作的头文件
#include <sys/wait.h> //关于资源回收的头文件
#include <pthread.h> //线程相关函数头文件
#include <semaphore.h> //无名信号量的头文件
#include <signal.h> //信号绑定函数对应的头文件
#include <sys/ipc.h> //进程间通信的头文件
#include <sys/msg.h> //消息队列的头文件
#include <sys/shm.h> //共享内存的头文件
#include <sys/user.h> //获取用户信息的头文件
#include <sys/sem.h> //信号灯集的头文件
#include <netinet/in.h> //点分十进制地址转换成网络字节序
#include <sys/socket.h> //套接字头文件
#include <arpa/inet.h> //字节序转换函数所在头文件
#include <linux/input.h> //读取键盘输入
#include <sys/un.h> //域套接字
#include <sys/select.h> //IO多路复用select函数
#include <poll.h> //IO多路复用poll函数
#include<sqlite3.h> //sqlite数据库