UDP编程
1. 字节序
1.1 字节序概述
字节序概念:是指多字节数据的存储顺序
分类:
- 小端格式:将低位字节数据存储在低地址
- 大端格式:将高位字节数据存储在低地址
大端:高字节数据存放低地址
小端:低字节数据存放低地址
1.2 确认主机的字节序
编写一个共用体,内存大小为2个字节。为short赋值,通过char取值进行验证
typedef union
{
unsigned short num;
unsigned char buf[2];
}Data;
1.3 网络通信中字节序的变化
网络数据必须大端
字节序的特点
- 网络协议指定了通信字节序—大端
- 只有在多字节数据处理时才需要考虑字节序
- 运行在同一台计算机上的进程相互通信时,一般不用考虑字节序
- 异构计算机之间通讯,需要转换自己的字节序为网络字节序
【注】在需要字节序转换的时候一般调用特定字节序转换函数
1.4 字节序的转换函数
include <arpa/inet.h>
1.4.1 发送者调用的函数
//将32位的主机字节序 转换成 网络字节序
uint32_t htonl(uint32_t hostlong); //转IP
//将16位主机字节序 转换成 网络字节序
uint16_t htons(uint16_t hostshort); //转端口
1.4.2 接受者调用函数
//将32位的网络字节序 转换成 主机字节序
uint32_t ntohl(uint32_t netlong); //转IP
//将16位的网络字节序转换成主机字节序
uint16_t ntohs(uint16_t netshort); //转端口
1.5 IP地址转换函数
include <arpa/inet.h>
1.5.1 inet_pton
函数
功能:字符串IP地址转整型数据,即将点分十进制数串转换成32位无符号整数
int inet_pton(int af,const char *src,void *dst);
参数:
- af:转换的协议
- AF_INET(IPV4)
- AF_INET6(IPV6)
- src:点分十进制数串的首元素地址
- dst:4字节的IP地址
返回值:成功1,失败-1
1.5.2 inet_ntop
函数
功能:整型数据转字符串格式ip地址
const char *inet_ntop(int af,const void*src,char *dst,socklen_t size);
参数:
- af:转换的协议,如AF_INET(IPV4) AF_INET6(IPV6)
- src:4字节的IP地址的起始地址
- dst:存放点分十进制数串的起始地址
- size:点分十进制数串的最大长度
-
define INET_ADDRSTRLEN 16 //for ipv4
-
define INET_ADDRSTRLEN 46//for ipv6
-
返回值:成功则返回字符串的首地址;失败则返回NULL
2. 编程流程
应用层通信三大要素:协议、IP地址、端口
2.1 UDP概述
面向无连接的用户数据报协议,在传输数据前不需要先建立连接
目的主机的传输层收到UDP报文后,不需要给出任何确认。
UDP特点
- 相比TCP速度稍快些
- 简单的请求/应答应用程序可以使用UDP
- 对于海量数据传输不应该使用UDP
- 广播和多播应用必须使用UDP
UDP使用场景:DNS(域名解析)、NFS(网络文件系统)、RTP(流媒体)等
2.2 网络编程接口-socket
网络通信要解决的是不同主机进程间的通信
socket作用:提供不同主机上的进程之间的通信
socket特点:
- socket也称套接字
- 是一种文件描述符,代表了一个通信管道的一个端点(全双工)
- 类似对文件的操作一样,可以使用read、write、close等函数对socket套接字进行网络数据的收取和发送等操作
- 得到socket套接字(描述符)的方法调用socket
2.3 UDP的C/S编程架构
3. UDP编程的API
include <sys/socket.h>
3.1 创建UDP套接字
创建一个用于网络通信的socket套接字(描述符)
int socket(int family,int type,int protocol);
参数:
- family:协议族(AF_INET,AF_INET6、PF_PACKET等)
- type:套接字类(SOCK_STREAM、SOCK_DRGAM、SOCK_RAW等),UDP的为SOCK_DGRAM。SOCK_STREAM为TCP的
- protocol:协议类别(0、IPPROTO_TCP、IPPROTO_UDP等),一般采用0
返回值:失败-1
特点:创建套接字时,系统不会分配端口
【扩展】:使用完套接字之后,可以通过close()关闭
3.2 地址结构
include <netinet/in.h>
3.2.1 IPV4地址结构struct sockaddr_in
struct sockaddr_in
{
sa_family_t sin_family;//2字节 A_INET AF_INET6
in_port_t sin_port;//2字节 端口
struct in_addr sin_addr;//4字节 IP地址
char sin_zero[8];//8字节
};
struct in_addr
{
in_addr_t s_addr;//4字节
};
3.2.2 通用套接字地址机构
这个结构体用于地址类型转换的,不是用来存数据
struct sockaddr
{
sa_family_t sa_family;//2字节
char sa_data[14];//14字节
};
为了使不同格式地址能被传入套接字函数,地址须要强制转换成通用套接字地址结构
3.2.3 两种地址结构使用场合
在定义源地址和目的地址结构的时候,选用struct sockaddr_in;
当调用编程接口函数,且该函数需要传入地址结构时需要用struct sockaddr
进行强制转换
3.3 发送数据sendto
函数
需要知道对方的IP和端口号
ssize_t sendto(int sockfd,const void *buf,size_t nbytes,int flags,const struct sockaddr *to,socklen_t addrlen);
功能:向to结构体指针中指定的ip,发送UDP数据报
参数:
- sockfd:套接字
- buf:发送数据缓冲区
- nbytes:发送数据缓冲区的大小
- flags:一般为0
- to:指向目的主机地址结构体的指针
- addrlen:to所指向内容的长度
注意:
- 通过to和addrlen确定目的地址
- udp可以发送0长度的UDP数据包
返回值:成功发送数据的字符数,失败-1
3.4 bind函数
bind函数在UDP服务端进程中使用
让套接字有一个固定的port以及IP地址。只能绑定自己主机的ip信息
int bind(int sockfd,const struct sockaddr *myaddr,socklen_t addrlen);
功能:将本地协议地址与sockfd绑定
参数:
- sockfd:socket套接字
- myaddr:指向特定协议的地址结构指针
- addrlen:该地址结构的长度
返回值:成功0,失败非0
3.5 接收数据recvfrom
函数
ssize_t recvfrom(int sockfd,void *buf,size_t nbytes,int flags,struct sockaddr *from,socklen_t *addrlen);
功能:接收UDP数据,并将源地址信息保存在from指向的结构中
参数:
- sockfd:套接字
- buf:接收数据缓冲区
- nbytes:接收数据缓冲区的大小
- flags:套接字标志(常为0)
- from:源地址结构体指针,用来保存数据的来源
- addrlen:from所指内容的长度
【注意】通过from和addrlen参数存放数据来源信息,from和addrlen可以为NULL,表示不保存数据来源
返回值:成功,接收到的字节数;失败-1
3.6 UDP编程注意点
3.6.1 UDP客户端注意点
1.本地IP、本地端口
2.目的IP、目的端口
3.在客户端的代码中,我们只设置了目的IP、目的端口
客户端的本地ip和port是我们调用sendto的时候linux系统底层自动给客户端分配的;
分配端口的方式为随机分配,即每次运行系统给的port不一样
3.6.2 UDP服务器注意点
1.服务器之所以要bind是因为它的本地port需要固定,而不是随机的
2.服务器也可以主动地给客户端发送数据
3.客户端也可以用bind,这样客户端的本地端口就是固定的了,但一般不这样做
3.7 综合小练习:聊天
整体应用即可以发送端,也可以接收端
启动程序时,从命令行中读取一个端口号,作为当前UDP服务绑定的端口
程序中应该采用多任务并行的方式,一个线程接收数据,一个线程发送数据
#include <stdio.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
void *recv_task(void *arg);//接收数据的线程任务函数
void *send_task(void *arg);//发送数据的线程任务函数
int main(int argc,char const *argv[])
{
if(argc != 2)
{
printf("format error!success format is ./a.out 8000\n");
return 1;
}
int sfd = socket(AF_INET,SOCK_DGRAM,0);
if(sfd<0)
{
prror("socket");
return -1;
}
//绑定IP和端口(UDP服务)
struct sockaddr_in bind_addr;
bzero(&bind_addr,sizeof(bind_addr));
bind_addr.sin_family = AF_INET;
bind_addr.sin_port = htons(argv[1]);
bind_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(sfd,(struct sockaddr*)&bind_addr,sizeof(bind_addr))!=0)
{
perror("bind");
close(sfd);
return -1;
}
pthread_t tid1,tid2;
pthread_create(&tid1,NULL,recv_task,&sfd);
pthread_create(&tid2,NULL,send_task,&sfd);
pthread_join(tid2,NULL);
pthread_cancel(tid1);
pthread_join(tid1,NULL);
close(sfd);
return 0;
}
void *recv_task(void *arg)
{
int sock_fd = *((int*)arg);
printf("启动recv_task %d\n",sock_fd);
char buf[128] = "";
struct sockaddr_in src_addr;
socklen_t sock_len;
while(1)
{
bzero(buf,128);
bzero(&src_addr,sizeof(src_addr));
ssize_t len=recvfrom(sock_fd,buf,128,0,(struct sockaddr*)&src_addr,&sock_len);
if(len<0)
{
perror("recvfrom");
}
char ip[16]="";
int port = ntohs(src_addr.sin_addr);
inet_ntop(AF_INET,&src_addr.sin_addr.s_addr,ip,INET_ADDRSTRLEN);
printf("%s:%d 说:%s/n",ip,port,buf);
}
}
void *send_task(void *arg)
{
int sock_fd = *((int*)arg);
char ip[16] = "";
int port = 0;
struct sockaddr_in dst_addr;
while(1)
{
char buf[128]="";
fgets(buf,128,stdin);
buf[strlen(buf)-1]=0;
if(buf[0]=='@')
{
if(buf[1]==':')
{
strcpy(ip,"127.0.0.1");
port = atoi(buf+2);
}
else
{
strcpy(ip,strtok(buf+1,":"));
port = atoi(strtok(NULL,":"));
}
printf("ip:port is %s:%d\n",ip,port);
bzero(&dst_addr,sizeof(dst_addr));
dst_addr.sin_family=AF_INET;
dst_addr.sin_family=htons(port);
inet_pton(sock_fd,ip,&dst_addr.sin_addr.s_addr);
}
els
{
if(strncmp(buf,"exit",4)==0)
{
break;
}
if(port==0)
{
printf("请先确认发送的目标IP和端口号\n";)
countine;
}
if(sendto(sock_fd,buf,strlen(buf),0,(struct sockaddr*)&dst_addr,sizeof(dst_addr))<0)
{
perror("sendto");
}
}
}
}
4. TFTP协议
4.1 TFTP概述
TFTP:简单文件传送协议
最初用于引导无盘系统,被设计用来传输小文件
特点:
- 基于UDP协议实现
- 不进行用户有效性认证
数据传输模式:
octet
:二进制模式netascii
:文本模式
4.2 TFTP通信过程(不带选项)
TFTP通信过程总结(无选项)
- 服务器在69号端口等待客户端的请求
- 服务器若批准此请求,则使用临时端口与客户端进行通信
- 每个数据包的编号都有变化(从1开始)
- 每个数据包都要得到ACK的确认,如果出现超时,则需要重新发送最后的包(数据或ACK)
- 数据的长度以512Byte传输
- 小于512Byte的数据意味着传输结束
4.3 TFTP协议分析
注意:
- 以上的0代表都是'\0'
- 不同的差错码对应不同的错误信息
错误码:
0 未定义,参见错误信息
1 File not found
2 Accss violation
3 Disk full or addocation exceeded
4 illegal TFTP operation
5 Unknown transfer ID
6 File already exists
7 No such user
8 Unsupported option(s) requested
4.4 协议分析-带选项
报文格式:
如果发送带选项的读写请求:
可用选项:
- tsize选项:当读操作时,tsize选项的参数必须为“0”,服务器会返回待读取的文件的大小;当写操作时,tsize选项参数应为待写入文件的大小,服务器会回显该选项
- blksize选项:修改传输文件时使用的数据块的大小(范围:8~65464)
- timeout选项:修改默认的数据传输超时时间(单位:秒)
TFTP通信过程总结(带选项)
- 可以通过发送带选项的读、写请求给server
- 如果server允许修改选项则发送选项修改确认包
- server发送的数据、选项修改确认包都是临时port
- server通过timeout来对丢失数据包的重新发送
4.5 练习-TFTP客户端
要求:使用TFTP协议,下载server上的文件到本地
实现思路:
- 构造请求报文,送至服务器(69号端口)
- 等待服务器回应
- 分析服务器回应
- 接收数据,直到接收到的数据包小于规定数据长度
#include <stdio.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
int main(int argc,char const *argv[])
{
if(argc<3)
{
printf("format:%s server_ip filename\n",argv[0]);
return -1;
}
int sock_fd =socket(AF_INET,SOCK_DGRAM,0);
if(sock_fd<0)
{
perror("socket");
return -1;
}
//生成请求数据包
char request[64];
int request_size = sprintf(request,"%c%c%s%c%s%c",0,1,argv[2],0,"octet",0);
//发送请求
struct sockaddr_in server_addr;
bzero(&server_addr,sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(69);
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
sendto(sock_fd,request,request_size,0,(struct sockaddr*)&server_addr.sizeof(server_addr));
char buf[516]="";
struct sockaddr_in data_addr;
socklen_t data_addr_len=sizeof(data_addr);
bzero(&data_addr,data_addr_len);
ssize_t len=recvfrom(sock_fd,buf,516,0,(struct sockaddr*)&data_addr,&data_addr_len);
//验证接收的数据是否ok
if(buf[1]==3)
{
//创建本地文件的描述符(打开或创建文件)
int fd = open(argc[2],O_CREAT|O_WRONLY,0666);
while(1)
{
//收到的是数据包
write(fd,buf+4,len-4);
//回ACK
buf[1]=4;
sendto(sock_fd,buf,4,0,(struct sockaddr*)&data_addr,sizeof(data_addr));
if(len<516)
{
printf("数据接收完成\n");
break;
}
bzero(&data_addr,data_addr_len);
bzero(buf,sizeof(buf));
len = recvfrom(sock_fd,buf,516,0,(struct sockaddr*)&data_addr,&data_addr_len);
}
close (fd);
}
else if(buf[1]==5)
{
//收到的错误信息包
printf("error:%s\n",buf+2);
}
else if(buf[1]==6)
{
//收到的OACK包,包含请求选项回传的值
}
close(sock_fd);
return 0;
}
5. UDP广播
广播:由一台主机向该主机所在子网内的所有主机发送数据的方式
广播只能用UDP或原始IP实现,不能用TCP
5.1 广播的用途
单个服务器与多个客户主机通信时减少分组流通
以下协议都用到广播
- 地址解析协议(ARP)
- 动态主机配置协议(DHCP)
- 网络时间协议(NTP)
5.2 UDP广播的特点
1.处于同一子网的所有主机都必须处理数据
2.UDP数据包会沿协议栈向上一直到UDP层
3.运行音视频等较高速率工作的应用,会带来大负
4.局限于局域网内使用
5.3 UDP广播地址
(网络ID,主机ID)
- 网络ID表示由子网掩码中1覆盖的连续位
- 主机ID表示由子网掩码中0覆盖的连续位
定向广播地址:主机ID全1
- 例:对于192.168.220.0/24,其定向广播地址为192.168.220.225
- 通常路由器不转发该广播
受限广播地址:255.255.255.255
- 路由器从不转发该广播
5.4 广播与单播的对比
单播:
广播:
5.5 套接口选项
int setsockopt(int sockfd,int level,int optname,const void *optval,socklen_t optlen);
成功0,失败-1
【注意】SO_BROADCAST
选项值是0或1,1表示真,即允许;0表示假,即是单播。
6. UDP多播
6.1 多播的概述
多播:数据的收发仅仅在同一分组中进行
多播的特点:
1.多播地址表示一组接口
2.多播可以用于广域网使用
3.在IPV4中,多播是可选的
6.2 多播地址
IPV4的D类地址是多播地址
十进制:224.0.0.1~239.255.255.254
十六进制:E0.00.00.01~EF.FF.FF.FE
多播地址向以太网MAC地址的映射:
6.3 UDP多播工作过程
6.4 多播地址结构体
在IPV4(AF_INET)中,多播地址结构体用如下结构体ip_mreq
表示
struct in_addr
{
in_addr_t s_addr;
};
struct ip_mreq
{
struct in_addr imr_multiaddr;//多播组IP
struct in_addr imr_interface;//将要添加到多播组的IP
};
6.5 多播套接口选项
int setsockopt(int sockfd,int level,int optname,const void*optval,socklen_t optlen);
成功0,失败-1
标签:多播,UDP,字节,int,编程,地址,include From: https://www.cnblogs.com/dijun666/p/17706981.html