一、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数据库




















