UDP编程

发布时间 2023-09-16 17:15:24作者: 常羲和

UDP编程

1. 字节序

1.1 字节序概述

字节序概念:是指多字节数据的存储顺序

分类:

  • 小端格式:将低位字节数据存储在低地址
  • 大端格式:将高位字节数据存储在低地址

大端:高字节数据存放低地址image-20230916095503766

小端:低字节数据存放低地址image-20230916095545486

1.2 确认主机的字节序

编写一个共用体,内存大小为2个字节。为short赋值,通过char取值进行验证

typedef union
{
    unsigned short num;
    unsigned char buf[2];
}Data;

1.3 网络通信中字节序的变化

网络数据必须大端

字节序的特点

  • 网络协议指定了通信字节序—大端
  • 只有在多字节数据处理时才需要考虑字节序
  • 运行在同一台计算机上的进程相互通信时,一般不用考虑字节序
  • 异构计算机之间通讯,需要转换自己的字节序为网络字节序

【注】在需要字节序转换的时候一般调用特定字节序转换函数

image-20230916100245448

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编程架构

image-20230916103218734

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通信过程(不带选项)

image-20230916143920243

TFTP通信过程总结(无选项)

  • 服务器在69号端口等待客户端的请求
  • 服务器若批准此请求,则使用临时端口与客户端进行通信
  • 每个数据包的编号都有变化(从1开始)
  • 每个数据包都要得到ACK的确认,如果出现超时,则需要重新发送最后的包(数据或ACK)
  • 数据的长度以512Byte传输
  • 小于512Byte的数据意味着传输结束

4.3 TFTP协议分析

image-20230916144254917

注意:

  • 以上的0代表都是'\0'
  • 不同的差错码对应不同的错误信息

image-20230916144407442

错误码:

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 协议分析-带选项

报文格式:

image-20230916144739969

如果发送带选项的读写请求:

image-20230916144808304

可用选项:

  • tsize选项:当读操作时,tsize选项的参数必须为“0”,服务器会返回待读取的文件的大小;当写操作时,tsize选项参数应为待写入文件的大小,服务器会回显该选项
  • blksize选项:修改传输文件时使用的数据块的大小(范围:8~65464)
  • timeout选项:修改默认的数据传输超时时间(单位:秒)

image-20230916145250475

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 广播与单播的对比

单播:

image-20230916165343990

广播:

image-20230916165405166

5.5 套接口选项

int setsockopt(int sockfd,int level,int optname,const void *optval,socklen_t optlen);

成功0,失败-1

image-20230916165623809

【注意】SO_BROADCAST选项值是0或1,1表示真,即允许;0表示假,即是单播。

6. UDP多播

6.1 多播的概述

多播:数据的收发仅仅在同一分组中进行

多播的特点:

1.多播地址表示一组接口

2.多播可以用于广域网使用

3.在IPV4中,多播是可选的

image-20230916170001001

6.2 多播地址

IPV4的D类地址是多播地址

十进制:224.0.0.1~239.255.255.254

十六进制:E0.00.00.01~EF.FF.FF.FE

多播地址向以太网MAC地址的映射:

image-20230916170143798

6.3 UDP多播工作过程

image-20230916170218698

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

image-20230916170556519