Linux网络编程【3】(UDP网络编程)

319 阅读6分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

1 UDP网络编程

1.1 流程

服务器:

  1. 创建套接字 socket()
  2. 填充服务器网络信息结构体 struct sockaddr_in
  3. 将套接字与服务器网络信息结构体绑定 bind()
  4. 进行通信 recvfrom()/sendto()

客户端:

  1. 创建套接字 socket()
  2. 填充服务器网络信息结构体 struct sockaddr_in
  3. 进行通信 recvfrom()/sendto()

图片.png

1.2 recvfrom()/sendto()

1 -- recvfrom()

  1. 头文件:
    1. #include <sys/types.h>
    2. #include <sys/socket.h>
  2. ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
  3. 功能:接收数据
  4. 参数:
    1. sockfd:文件描述符

      1. 服务器:accept的返回值
      2. 客户端:socket的返回值
    2. buf:保存接收到的数据

    3. len:理论要接收的字节数

    4. flags:标志位

      1. 0 阻塞

      2. MSG_DONTWAIT 非阻塞

    5. src_addr:源的地址,接收谁的数据,他的信息会自动填充到这个参数

    6. addrlen:src_addr的大小

  5. 返回值:
    1. 成功:实际接收的字节数
    2. 失败:-1

2 -- sendto()

  1. 头文件:
    1. #include <sys/types.h>
    2. #include <sys/socket.h>
  2. ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
  3. 功能:发送数据
  4. 参数:
    1. sockfd:文件描述符

      1. 服务器:accept的返回值
      2. 客户端:socket的返回值
    2. buf:要发送的数据

    3. len:理论要发送的字节数

    4. flags:标志位

      1. 0 阻塞

      2. MSG_DONTWAIT 非阻塞

    5. dest_addr:目标地址,需要自己指定

    6. addrlen:dest_addr的大小

  5. 返回值:
    1. 成功:发送的字节数
    2. 失败:-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:==简单文件传送协议==

最初用于引导无盘系统,被设计用来传输小文件

特点

  1. 基于UDP协议实现
  2. 不进行用户有效性认证

数据传输模式

  1. octet:二进制模式
  2. netascii:文本模式
  3. mail:已经不再支持

2.2 客户端下载文件通信过程

图片.png

首先客户端要给服务器发一个信息,告知服务我要要进行下载,第一条发送的就是请求指令,注意固定==端口号就是69==,当服务器接收到请求数据之后,之后所有与客户端的通信会开启临时的端口进行通信,为了防止出现问题。

服务器向客户端回一个数据包,[有编号,有大小,有内容(512)(默认)]

客户端拿到数据包之后接受数据包,把512文件的内容写到指定的文件中,接受完数据客户端向服务器发送应答包,有编号与就收到的数据包编号一致

接着继续这样通信;

直到发送的数据包不足512,

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

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

2.3 协议分析

注意客户端下载文件是读,服务器通过操作码来区分不同的请求

读写请求:例如:操作码为1表示下载+文件名+0间隔符+模式(eg:octet)+0表示结尾,选项先不管;

图片.png

错误码:

  • 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;
}