TCP/IP网络编程 第六章 UDP传输只需要一个socket

56 阅读5分钟

上一个章节学完如何实现一个带有数据边界的TCP,这个章节就要学习如何写一个UDP。

UDP

本次实验的效果

image.png

UDP网络编程的最大特点

一台电脑只需要一个UDP socket 就可以实现和其他的任何电脑交互。理论上UDP是不存在server和client的区别,你只要对外发送消息你就是server, 你只要读取消息你就是client.

有一个for循环,他会接收UDP 包, UDP 在接收信息时是以数据报(包)为单位进行接收的

sleep 期间的情况:

  • 由于 recvfrom() 没有被调用,操作系统会将接收到的数据存储在 socket 的接收缓冲区 中。
  • 如果缓冲区足够大,3 条数据都会被存储在缓冲区中,等待 recvfrom() 逐条读取。
  • 如果缓冲区已满,则超出缓冲区大小的任何新数据会被丢弃(数据报丢失)。

数据包的长度限制

  1. UDP 数据包的最大长度

    • UDP 协议中,数据包的长度限制是 64 KB(65535 字节),这是由 UDP 报文头中的 长度字段(Length) 决定的。

    • UDP 报文结构

      • UDP 头部长度固定为 8 字节。

      • UDP 数据部分(有效载荷,Payload)的最大长度为 65507 字节

        65535(UDP 数据包总长度) - 8(UDP 头部) - 20(IP 头部) = 65507 字节
        
      • 因此,UDP 数据包的有效数据部分最大只能有 65507 字节

  2. 实际传输中的限制

    • 在真实网络中,UDP 数据包的长度还会受到底层协议(如以太网)的 MTU(最大传输单元) 限制。

      • 以太网的默认 MTU 为 1500 字节,减去 IP 头部(20 字节)和 UDP 头部(8 字节),单个 UDP 数据包的有效数据部分最大为 1472 字节
      • 如果数据包超过 MTU,大多数网络设备会对数据进行 IP 分片(将数据包拆分成多个片段进行传输)。
  3. 分片的影响

    • UDP 本身不处理重组或分片问题。如果数据包被分片,分片的重组由 IP 层完成。
    • 如果网络丢失了某个片段,整个 UDP 数据包都会被丢弃(因为 UDP 没有重传机制)。

程序中的实际数据长度

在您的程序中,接收数据的缓冲区大小由 BUF_SIZE 定义:

#define BUF_SIZE 30

这表示程序一次最多可以接收 30 字节的 UDP 数据(包括 '\0' 终止符)

  • 如果接收的数据包大于缓冲区大小(BUF_SIZE),超出部分会被 截断
  • recvfrom() 的返回值(str_len)会告知实际接收到的数据长度。

server代码

下面是server.c代码

#include <stdio.h>    // For standard input and output
#include <stdlib.h>   // For exit() and atoi()
#include <string.h>   // For memset()
#include <arpa/inet.h> // For sockaddr_in, htons(), htonl(), etc.
#include <unistd.h>   // For close(), sleep()

#define BUF_SIZE 30 // Define buffer size for messages

// Function declaration for error handling
void error_handling(char *message);

int main(int argc, char *argv[]) {
    int sock;                          // Socket file descriptor
    char message[BUF_SIZE];            // Buffer to store incoming messages
    struct sockaddr_in my_adr, your_adr; // Structures for socket address information
    socklen_t adr_sz;                  // Variable to hold the size of the address structure
    int str_len, i;                    // Variables for string length and loop counter

    // Check if the program is run with the correct number of arguments
    if (argc != 2) {
        printf("Usage: %s <port>\n", argv[0]); // Display usage instructions
        exit(1);                              // Exit the program if arguments are invalid
    }

    // Create a UDP socket
    sock = socket(PF_INET, SOCK_DGRAM, 0); // PF_INET for IPv4, SOCK_DGRAM for UDP
    if (sock == -1)                        // Check if socket creation failed
        error_handling("socket() error");

    // Initialize the my_adr structure with zeros
    memset(&my_adr, 0, sizeof(my_adr));
    my_adr.sin_family = AF_INET;          // Set address family to IPv4
    my_adr.sin_addr.s_addr = htonl(INADDR_ANY); // Accept connections from any IP address
    my_adr.sin_port = htons(atoi(argv[1]));    // Convert port number from string to network byte order

    // Bind the socket to the specified IP address and port
    if (bind(sock, (struct sockaddr *)&my_adr, sizeof(my_adr)) == -1) {
        error_handling("bind() error");   // Handle bind error
    }

    // Loop to receive messages
    for (i = 0; i < 3; i++) {
        sleep(5); // Delay 5 seconds between receiving messages

        // Initialize the size of the sender's address structure
        adr_sz = sizeof(your_adr);

        // Receive a message from a client
        str_len = recvfrom(sock, message, BUF_SIZE, 0, 
                           (struct sockaddr *)&your_adr, &adr_sz);

        // Print the received message
        printf("Message %d: %s \n", i + 1, message);
    }

    // Close the socket after finishing
    close(sock);

    return 0; // Indicate successful program termination
}

// Error handling function
void error_handling(char *message) {
    // Print the error message and exit the program
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

client代码

发送3个数据报给server

下面是client.c 代码

#include <stdio.h>    // For standard input/output
#include <stdlib.h>   // For exit() and atoi()
#include <string.h>   // For memset()
#include <arpa/inet.h> // For networking functions (inet_addr, htons, etc.)
#include <unistd.h>   // For close()

#define BUF_SIZE 30 // Define the buffer size for messages

// Error handling function declaration
void error_handling(char *message);

int main(int argc, char *argv[]) {
    int sock; // Socket file descriptor

    // Messages to send
    char msg1[] = "Hi!";
    char msg2[] = "I'm another UDP host!";
    char msg3[] = "Nice to meet you";

    struct sockaddr_in your_adr; // Structure for the recipient's address
    socklen_t your_adr_sz;       // Size of the recipient's address structure

    // Check for correct number of arguments
    if (argc != 3) {
        printf("Usage: %s <IP> <port>\n", argv[0]); // Display usage instructions
        exit(1);                                    // Exit if arguments are incorrect
    }

    // Create a UDP socket
    sock = socket(PF_INET, SOCK_DGRAM, 0); // PF_INET for IPv4, SOCK_DGRAM for UDP
    if (sock == -1)                        // Check if socket creation failed
        error_handling("socket() error");

    // Initialize the recipient's address structure
    memset(&your_adr, 0, sizeof(your_adr));       // Zero out the structure
    your_adr.sin_family = AF_INET;               // Use IPv4
    your_adr.sin_addr.s_addr = inet_addr(argv[1]); // Set the recipient's IP address
    your_adr.sin_port = htons(atoi(argv[2]));      // Set the recipient's port

    // Send messages to the specified recipient
    sendto(sock, msg1, sizeof(msg1), 0, (struct sockaddr *)&your_adr, sizeof(your_adr));
    sendto(sock, msg2, sizeof(msg2), 0, (struct sockaddr *)&your_adr, sizeof(your_adr));
    sendto(sock, msg3, sizeof(msg3), 0, (struct sockaddr *)&your_adr, sizeof(your_adr));

    // Close the socket after sending
    close(sock);

    return 0; // Exit successfully
}

// Error handling function
void error_handling(char *message) {
    // Print the error message and exit
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

结果如图

server 先启动监听socket image.png

client 发送3条消息就关闭

image.png

需要注意的点

UDP在传输信息的时候也要转换成网络字节序传输,到本地再转换成本地字节序;但是这次实验消息是string类型,不需要考虑字节序问题。因为,字符串的每个字符本质上是一个字节(8 位),不涉及主机字节序和网络字节序的差异。