使用UDP套接字编程详解【C语言】

UDP(User Datagram Protocol,用户数据报协议)是一种面向无连接的传输层协议,用于在计算机网络上发送数据。它与 TCP(Transmission Control Protocol,传输控制协议)相比具有轻量、高效的特点,但牺牲了可靠性和顺序传输的保证。udp的通信过程默认也是阻塞的。

  • 无连接:UDP 是无连接的,不会维护连接状态,每次发送数据都是独立的。
  • 不可靠性:UDP 不保证数据的可靠传输,数据报文可能丢失、重复或顺序错乱。
  • 数据包大小:UDP 数据报文大小受限制,超过网络的最大传输单元(MTU)会被分片,可能导致性能下降。
  • 顺序保证:UDP 不保证数据报文的顺序交付,接收方收到数据的顺序可能与发送方不同。
  • 适用场景:适合实时数据传输,如实时音视频流、游戏数据更新等,对实时性要求高而对数据完整性要求较低的场景。

1.UDP通信流程

  • 创建套接字

    • 首先,服务器端和客户端分别创建 UDP 套接字。套接字是应用程序与网络之间的接口,用于发送和接收数据。
  • 绑定地址和端口(可选):

    • 在服务器端,可以选择将套接字绑定到特定的 IP 地址和端口上,以便客户端能够发送数据到指定的地址。客户端通常不需要绑定,而是直接向服务器发送数据。
  • 发送数据报文

    • 客户端通过 sendto 函数将数据报文发送到服务器端的指定地址和端口。UDP 不需要建立连接,因此每个数据报文都是独立发送的,不会进行握手或状态管理。
  • 接收数据报文

    • 服务器端使用 recvfrom 函数接收从客户端发送过来的数据报文。这个函数能够获取发送方的地址信息,以便服务器端可以知道从哪个客户端接收到数据。
  • 处理数据

    • 服务器端和客户端在接收到数据后,可以根据协议和应用程序的需求对数据进行处理,例如解析、验证、响应等操作。
  • 关闭套接字

    • 当通信结束或者程序不再需要使用套接字时,服务器端和客户端分别使用 close 函数关闭套接字,释放资源。

2.相关操作函数

2.1 socket 函数

cpp 复制代码
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
功能:创建一个套接字。
参数:
    domain:指定协议族,常用的有 AF_INET(IPv4 地址)和 AF_INET6(IPv6 地址)。
    type:指定套接字类型,常用的有 SOCK_DGRAM(数据报套接字,UDP)和 SOCK_STREAM(流式套接字,TCP)。
    protocol:通常设置为 0,表示使用默认协议。
返回值:返回一个非负的文件描述符(套接字描述符),表示套接字创建成功;返回 -1 表示创建失败。

2.2 bind 函数

cpp 复制代码
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:将一个本地地址绑定到套接字。
参数:
    sockfd:套接字描述符,即 socket 函数返回的文件描述符。
    addr:指向 sockaddr 结构体的指针,包含要绑定的 IP 地址和端口信息。
    addrlen:addr 结构体的长度。
返回值:成功返回 0,失败返回 -1。
注意事项:
服务器端通常需要绑定一个固定的 IP 地址和端口,以便客户端能够发送数据到指定的地址。
客户端一般不需要绑定,而是直接发送数据到服务器端。

2.3 sendto 函数

cpp 复制代码
#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:套接字描述符。
    buf:指向要发送数据的缓冲区。
    len:要发送数据的长度。
    flags:通常设置为 0,表示默认行为。
    dest_addr:指向 sockaddr 结构体的指针,包含目标地址(IP 地址和端口)信息。IP和端口都存储在这里边, 是大端存储的
    addrlen:dest_addr 结构体的长度。
返回值:成功返回发送的字节数,失败返回 -1。
注意事项:
UDP 是无连接的,每次发送数据时都要指定目标地址。
数据报文的大小应小于网络的最大传输单元(MTU),以免发生分片,影响性能。

2.4 recvfrom 函数

cpp 复制代码
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
功能:从指定的源地址接收数据报文。
参数:
    sockfd:套接字描述符。
    buf:指向存放接收数据的缓冲区。
    len:缓冲区的大小。
    flags:通常设置为 0,表示默认行为。
    src_addr:指向 sockaddr 结构体的指针,用于存放发送方的地址信息。IP和端口都存储在这里边, 是大端存储的
    addrlen:src_addr 结构体的长度,在调用时需传入 src_addr 的实际长度。
返回值:成功返回接收到的字节数,失败返回 -1。
注意事项:
recvfrom 函数会阻塞直到接收到数据或发生错误。
可以通过 src_addr 参数获取发送方的 IP 地址和端口信息,用于处理接收到的数据。

2.5 close 函数

cpp 复制代码
#include <unistd.h>
int close(int sockfd);
功能:关闭套接字。
参数:
    sockfd:要关闭的套接字描述符。
返回值:成功返回 0,失败返回 -1。
注意事项:
在不再需要使用套接字时应及时关闭,释放系统资源。
关闭套接字后,相关的文件描述符将不再可用,不能再进行数据发送和接收操作。

3.示例代码

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define PORT 8888
#define MAXLINE 1024

int main() {
    int sockfd;
    struct sockaddr_in server_addr, client_addr;
    char buffer[MAXLINE];
    socklen_t client_len;
    ssize_t n;

    // 创建UDP套接字
    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    memset(&server_addr, 0, sizeof(server_addr));
    memset(&client_addr, 0, sizeof(client_addr));

    // 设置服务器地址结构
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(PORT);

    // 绑定服务器地址到套接字
    if (bind(sockfd, (const struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    printf("Server listening on port %d...\n", PORT);

    while (1) {
        client_len = sizeof(client_addr);

        // 接收来自客户端的消息
        n = recvfrom(sockfd, (char *)buffer, MAXLINE, MSG_WAITALL, (struct sockaddr *)&client_addr, &client_len);
        buffer[n] = '\0';

        // 打印客户端信息
        char client_ip[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN);
        printf("Received message from %s:%d - %s\n", client_ip, ntohs(client_addr.sin_port), buffer);

        // 发送响应消息给客户端
        strcpy(buffer, "Hello from server");
        sendto(sockfd, (const char *)buffer, strlen(buffer), MSG_CONFIRM, (const struct sockaddr *)&client_addr, client_len);
        printf("Server : Hello message sent\n");
    }

    close(sockfd);
    return 0;
}

作为数据接收端,服务器端通过bind()函数绑定了固定的端口,然后基于这个固定的端口通过recvfrom()函数接收客户端发送的数据,同时通过这个函数也得到了数据发送端的地址信息(recvfrom的第三个参数),这样就可以通过得到的地址信息通过sendto()函数给客户端回复数据了。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define PORT 8888
#define MAXLINE 1024

int main() {
    int sockfd;
    struct sockaddr_in server_addr;
    char buffer[MAXLINE];
    socklen_t server_len;
    ssize_t n;

    // 创建UDP套接字
    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    memset(&server_addr, 0, sizeof(server_addr));

    // 设置服务器地址结构
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    while (1) {
        printf("Enter message to send : ");
        fgets(buffer, MAXLINE, stdin);

        // 发送消息给服务器
        sendto(sockfd, (const char *)buffer, strlen(buffer), MSG_CONFIRM, (const struct sockaddr *)&server_addr, sizeof(server_addr));
        printf("Message sent to server.\n");

        // 接收服务器的响应消息
        n = recvfrom(sockfd, (char *)buffer, MAXLINE, MSG_WAITALL, (struct sockaddr *)&server_addr, &server_len);
        buffer[n] = '\0';
        printf("Server : %s\n", buffer);
    }

    close(sockfd);
    return 0;
}

作为数据发送端,客户端不需要绑定固定端口,客户端使用的端口是随机绑定的(也可以调用bind()函数手动进行绑定)。客户端在接收服务器端回复的数据的时候需要调用recvfrom()函数,因为客户端在发送数据之前就已经知道服务器绑定的固定的IP和端口信息了,所以接收服务器数据的时候就可以不保存服务器端的地址信息,直接将函数的最后两个参数指定为NULL即可。

相关推荐
学Linux的语莫9 分钟前
ansible变量
linux·运维·服务器·ansible
北京迅为11 分钟前
【北京迅为】iTOP-4412全能版使用手册-第十二章 Linux系统编程简介
linux·嵌入式硬件·4412开发板
Dearrrrrrrr14 分钟前
H3C OSPF 多区域实验
网络·计算机网络·智能路由器
清源妙木真菌19 分钟前
Linux:进程控制
linux
爱吃喵的鲤鱼21 分钟前
Linux——文件系统清尾、动静态库
linux·运维·服务器
fpcc27 分钟前
c++应用网络编程之十五Nagle算法
网络·c++
疯狂吧小飞牛37 分钟前
C语言解析命令行参数
c语言·开发语言
最数据41 分钟前
Linux或者Docker中时区查询和修改(差8小时问题)
linux·运维·服务器·docker·.net
皓月盈江1 小时前
Linux宝塔部署wordpress网站更换服务器IP后无法访问管理后台和打开网站页面显示错乱
linux·服务器·wordpress·无法访问wordpess后台·打开网站页面错乱·linux宝塔面板·wordpress更换服务器
网络安全King1 小时前
[网络安全系列面试题] GET 和 POST 的区别在哪里?
网络·安全·web安全