小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
1 UDP网络编程
1.1 流程
服务器:
- 创建套接字 socket()
- 填充服务器网络信息结构体 struct sockaddr_in
- 将套接字与服务器网络信息结构体绑定 bind()
- 进行通信 recvfrom()/sendto()
客户端:
- 创建套接字 socket()
- 填充服务器网络信息结构体 struct sockaddr_in
- 进行通信 recvfrom()/sendto()
1.2 recvfrom()/sendto()
1 -- recvfrom()
- 头文件:
#include <sys/types.h>#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);- 功能:接收数据
- 参数:
-
sockfd:文件描述符
- 服务器:accept的返回值
- 客户端:socket的返回值
-
buf:保存接收到的数据
-
len:理论要接收的字节数
-
flags:标志位
-
0 阻塞
-
MSG_DONTWAIT 非阻塞
-
-
src_addr:源的地址,接收谁的数据,他的信息会自动填充到这个参数
-
addrlen:src_addr的大小
-
- 返回值:
- 成功:实际接收的字节数
- 失败:-1
2 -- sendto()
- 头文件:
#include <sys/types.h>#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);- 功能:发送数据
- 参数:
-
sockfd:文件描述符
- 服务器:accept的返回值
- 客户端:socket的返回值
-
buf:要发送的数据
-
len:理论要发送的字节数
-
flags:标志位
-
0 阻塞
-
MSG_DONTWAIT 非阻塞
-
-
dest_addr:目标地址,需要自己指定
-
addrlen:dest_addr的大小
-
- 返回值:
- 成功:发送的字节数
- 失败:-1
1.3 代码(两个demo)
1.3.1 服务器
//UDP网络编程之服务器
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <string.h>
#define N 128
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s - %s - %d\n", __FILE__, __func__, __LINE__);\
exit(1);\
}while(0)
int main(int argc, char const *argv[])
{
if(argc < 3)
{
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd;
struct sockaddr_in serveraddr, clientaddr;
socklen_t addrlen = sizeof(serveraddr);
char buf[N] = {0};
//第一步:创建套接字
if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
{
ERRLOG("socket error");
}
//第二步:填充服务器网络信息结构体
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
//第三步:将套接字与服务器网络信息结构体绑定
if(bind(sockfd, (struct sockaddr *)&serveraddr, addrlen) == -1)
{
ERRLOG("bind error");
}
//进行通信
while(1)
{
NEXT:
if(recvfrom(sockfd, buf, N, 0, (struct sockaddr *)&clientaddr, &addrlen) == -1)
{
ERRLOG("recvfrom error");
}
if(strcmp(buf, "quit") == 0)
{
printf("客户端%s-%d退出了\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
goto NEXT;
}
printf("%s-%d:%s\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port), buf);
strcat(buf, "^_^");
if(sendto(sockfd, buf, N, 0, (struct sockaddr *)&clientaddr, addrlen) == -1)
{
ERRLOG("sendto error");
}
}
return 0;
}
1.3.2 客户端
//UDP网络编程之客户端
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <string.h>
#define N 128
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s - %s - %d\n", __FILE__, __func__, __LINE__);\
exit(1);\
}while(0)
int main(int argc, char const *argv[])
{
if(argc < 3)
{
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd;
struct sockaddr_in serveraddr;
socklen_t addrlen = sizeof(serveraddr);
//第一步:创建套接字
if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
{
ERRLOG("socket error");
}
//第二步:填充服务器网络信息结构体
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
//进行通信
char buf[N] = {0};
while(1)
{
fgets(buf, N, stdin);
buf[strlen(buf) - 1] = '\0';
if(sendto(sockfd, buf, N, 0, (struct sockaddr *)&serveraddr, addrlen) == -1)
{
ERRLOG("sendto error");
}
if(strcmp(buf, "quit") == 0)
{
printf("客户端退出了\n");
exit(0);
}
memset(buf, 0, N);
if(recvfrom(sockfd, buf, N, 0, (struct sockaddr *)&serveraddr, &addrlen) == -1)
{
ERRLOG("recvfrom error");
}
printf("服务器:%s\n", buf);
}
return 0;
}
2 基于UDP的TFTP协议
2.1 基本概念
TFTP:==简单文件传送协议==
最初用于引导无盘系统,被设计用来传输小文件
特点:
- 基于UDP协议实现
- 不进行用户有效性认证
数据传输模式:
- octet:二进制模式
- netascii:文本模式
- mail:已经不再支持
2.2 客户端下载文件通信过程
首先客户端要给服务器发一个信息,告知服务我要要进行下载,第一条发送的就是请求指令,注意固定==端口号就是69==,当服务器接收到请求数据之后,之后所有与客户端的通信会开启临时的端口进行通信,为了防止出现问题。
服务器向客户端回一个数据包,[有编号,有大小,有内容(512)(默认)]
客户端拿到数据包之后接受数据包,把512文件的内容写到指定的文件中,接受完数据客户端向服务器发送应答包,有编号与就收到的数据包编号一致;
接着继续这样通信;
直到发送的数据包不足512,
TFTP通信过程总结(无选项)
- 服务器在69号端口等待客户端的请求
- 服务器若批准此请求,则使用临时端口与客户端进行通信
- 每个数据包的编号都有变化(从1开始)
- 每个数据包都要得到ACK的确认,如果出现超时,则需要重新发送最后的包(数据或ACK)
- 数据的长度以512Byte传输
- 小于512Byte的数据意味着传输结束
2.3 协议分析
注意客户端下载文件是读,服务器通过操作码来区分不同的请求
读写请求:例如:操作码为1表示下载+文件名+0间隔符+模式(eg:octet)+0表示结尾,选项先不管;
错误码:
- 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.
2.4 TFTP客户端下载功能组包和接收数据并解析(demo)
最多下载32M,65535*
//UDP网络编程之客户端
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <string.h>
#define N 128
#define ERRLOG(errmsg) do{\
perror(errmsg);\
printf("%s - %s - %d\n", __FILE__, __func__, __LINE__);\
exit(1);\
}while(0)
int main(int argc, char const *argv[])
{
if(argc < 3)
{
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd;
struct sockaddr_in serveraddr;
socklen_t addrlen = sizeof(serveraddr);
//第一步:创建套接字
if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
{
ERRLOG("socket error");
}
//第二步:填充服务器网络信息结构体
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
serveraddr.sin_port = htons(atoi(argv[2]));
char filename[32] = {0};
printf("请输入要下载的文件名:");
scanf("%s", filename);
//TFTP客户端实现下载功能
//组包将数据发送给服务器并告知执行下载操作
//如果定义结构体的话,文件名是不确定的,长短不一定
unsigned char buf[600] = {0};//数据包
int n;//组包的字节个数
unsigned short code;//差错码
unsigned short num;//块编号
unsigned char text[600] = {0};//存放内容
ssize_t bytes;//接收返回的字节数,最大516
//操作码的赋值
//方法1:通过强制类型转换得到两个字节的空间并赋值网络字节序的值
//*(unsigned short *)buf = htons(1);
//方法2:将数据拆分成以一个字节为单位,就不需要考虑字节序了
//buf[0] = 0;
//buf[1] = 1;
//组包:
n = sprintf(buf, "%c%c%s%c%s%c", 0, 1, filename, 0, "octet", 0);
//发送数据给服务器
if(sendto(sockfd, buf, n, 0, (struct sockaddr *)&serveraddr, addrlen) == -1)
{
ERRLOG("sendto error");
}
//接收数据
memset(buf, 0, 600);
if((bytes = recvfrom(sockfd, buf, 600, 0, (struct sockaddr *)&serveraddr, &addrlen)) == -1)
{
ERRLOG("recvfrom error");
}
//解析数据包
//buf是整个数据,接下来分段解析
//操作码
code = ntohs(*(unsigned short *)buf);
//块编号或者差错码
num = ntohs(*(unsigned short *)(buf+2));
//文件内容或者差错信息
strncpy(text, buf+4, bytes-4);//截取512大小的数据包
printf("code:%d, num:%d\n", code, num);
printf("%s\n", text);
return 0;
}