释放小七猫的分享欲

Unix 上的 UDP-Socket 编程

categories: 技术 tags: 软件
@ 17/08/2025

UDP 套接字编程基础知识

UDP(用户数据报协议)是一种无连接的、不可靠的、基于数据报的传输层通信协议。与 TCP 不同,UDP 不需要建立连接,直接发送数据报文。

套接字 API 的基本流程

  1. 创建套接字

    int socket(int domain, int type, int protocol);
    
    • domain: 协议域,如 AF_INET 表示 IPv4
    • type: 套接字类型,如 SOCK_DGRAM 表示 UDP
    • protocol: 协议,通常为 0 表示默认
  2. 服务器端操作 服务器需要执行以下操作:

    int bind(int socket, struct sockaddr* address, int addr_len);
    int recvfrom(int socket, char *buffer, int buf_len, int flags, struct sockaddr *address, int *addr_len);
    int sendto(int socket, char *message, int msg_len, int flags, struct sockaddr *address, int addr_len);
    
    • bind: 将套接字绑定到指定地址和端口
    • recvfrom: 接收数据报文并获取发送方地址
    • sendto: 向指定地址发送数据报文
  3. 客户端操作 客户端不需要建立连接,可以直接发送数据:

    int sendto(int socket, char *message, int msg_len, int flags, struct sockaddr *address, int addr_len);
    int recvfrom(int socket, char *buffer, int buf_len, int flags, struct sockaddr *address, int *addr_len);
    
    • sendto: 向指定地址发送数据报文
    • recvfrom: 接收数据报文并获取发送方地址
  4. 关闭套接字 最后需要关闭套接字以释放资源:

    int close(int socket);
    

环境准备

在 MacOS 系统下进行 UDP 套接字编程,需要使用 C++ 编译器(如 g++)和系统提供的网络编程头文件。主要包含以下头文件:

  • sys/socket.h:套接字相关函数
  • netinet/in.h:网络地址结构定义
  • arpa/inet.h:IP 地址转换函数
  • unistd.h:系统调用函数
  • stdio.h:标准输入输出函数
  • string.h:字符串处理函数

服务器端代码

UDP 服务器端程序负责接收客户端发送的数据报文,并向客户端发送响应。

#include<iostream>
#include<unistd.h>
#include<stdio.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
using namespace std;

#define SERVER_PORT  5051               //端口号
#define SERVER_IP    "127.0.0.1"        //服务器 ip
#define BUFFER_SIZE  256                //缓冲区大小

int main(int argc, char *argv[])
{
    //创建一个 UDP 套接字
    int sockSer;
    sockSer = socket(AF_INET, SOCK_DGRAM, 0);
    if(sockSer == -1){
        perror("socket");
        return -1;
    }

    //设置端口可以重用
    int yes = 1;
    if(setsockopt(sockSer, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1){
        perror("setsockopt");
        return -1;
    }

    //创建一个记录地址信息的结构体
    struct sockaddr_in addrSer, addrCli;
    addrSer.sin_family = AF_INET;              //使用 AF_INET 协议族
    addrSer.sin_port = htons(SERVER_PORT);     //设置端口号
    addrSer.sin_addr.s_addr = inet_addr(SERVER_IP);   //设置服务器 ip

    //将套接字地址与所创建的套接字号联系起来
    socklen_t addrlen = sizeof(struct sockaddr);
    int res = ::bind(sockSer, (struct sockaddr*)&addrSer, addrlen);
    if(res == -1) {
        perror("bind");
        return -1;
    }

    printf("UDP Server is running on %s:%d\n", SERVER_IP, SERVER_PORT);
    printf("Waiting for messages...\n");

    char recvbuf[BUFFER_SIZE];         //接收缓冲区
    char sendbuf[BUFFER_SIZE];         //发送缓冲区
    while(1)
    {
        // 接收来自客户端的数据
        memset(recvbuf, 0, sizeof(recvbuf));
        socklen_t client_len = sizeof(addrCli);
        int recv_len = recvfrom(sockSer, recvbuf, sizeof(recvbuf)-1, 0,
                               (struct sockaddr*)&addrCli, &client_len);
        if (recv_len > 0) {
            printf("Received from Client %s:%d: %s\n",
                   inet_ntoa(addrCli.sin_addr), ntohs(addrCli.sin_port), recvbuf);

            // 准备响应数据
            sprintf(sendbuf, "Server received: %s", recvbuf);

            // 发送响应给客户端
            if (sendto(sockSer, sendbuf, strlen(sendbuf)+1, 0,
                      (struct sockaddr*)&addrCli, client_len) <= 0) {
                printf("发送响应失败\n");
            } else {
                printf("Sent response to Client %s:%d\n",
                       inet_ntoa(addrCli.sin_addr), ntohs(addrCli.sin_port));
            }
        } else if (recv_len == 0) {
            printf("客户端已断开连接\n");
            break;
        } else {
            perror("recvfrom");
            break;
        }
    }

    close(sockSer);         //关闭套接字
    return 0;
}

客户端代码

UDP 客户端程序负责向服务器发送数据报文,并接收服务器的响应。

#include<iostream>
#include<unistd.h>
#include<stdio.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
using namespace std;

#define SERVER_PORT  5051               //端口号
#define SERVER_IP    "127.0.0.1"        //服务器 ip
#define BUFFER_SIZE  256                //缓冲区大小

int main(int argc, char *argv[])
{
    //创建一个 UDP 套接字
    int sockCli;
    sockCli = socket(AF_INET, SOCK_DGRAM, 0);
    if(sockCli == -1){
        perror("socket");
        return -1;
    }

    //创建一个记录服务器地址信息的结构体
    struct sockaddr_in addrSer;
    addrSer.sin_family = AF_INET;              //使用 AF_INET 协议族
    addrSer.sin_port = htons(SERVER_PORT);     //设置端口号
    addrSer.sin_addr.s_addr = inet_addr(SERVER_IP);   //设置服务器 ip

    printf("UDP Client is ready\n");
    printf("Enter 'quit' to exit\n");

    char sendbuf[BUFFER_SIZE];     //发送缓冲区
    char recvbuf[BUFFER_SIZE];     //接收缓冲区
    while(1)
    {
        // 显示客户端提示符,等待用户输入
        printf("Cli:>");
        // 使用 fgets 读取整行输入,包括空格
        if (fgets(sendbuf, sizeof(sendbuf), stdin) == NULL) {
            printf("读取输入失败\n");
            break;
        }
        // 移除换行符
        sendbuf[strcspn(sendbuf, "\n")] = 0;

        if(strcmp(sendbuf,"quit") == 0)    //如果客户端发送的数据为"quit",则退出。
            break;

        // 发送数据给服务器
        socklen_t server_len = sizeof(addrSer);
        if (sendto(sockCli, sendbuf, strlen(sendbuf)+1, 0,
                  (struct sockaddr*)&addrSer, server_len) <= 0) {
            printf("发送数据失败\n");
            break;
        }

        // 接收来自服务器的响应
        memset(recvbuf, 0, sizeof(recvbuf));
        int recv_len = recvfrom(sockCli, recvbuf, sizeof(recvbuf)-1, 0,
                               (struct sockaddr*)&addrSer, &server_len);
        if (recv_len > 0) {
            printf("Ser:> %s\n",recvbuf);
        } else if (recv_len == 0) {
            printf("服务器已断开连接\n");
            break;
        } else {
            perror("recvfrom");
            break;
        }
    }

    close(sockCli);       //关闭套接字
    return 0;
}

编译和运行

在 MacOS 系统下,可以使用以下命令编译这两个程序:

g++ -o UdpServer udpServer.cpp
g++ -o UdpClient udpClient.cpp

然后分别运行服务器和客户端程序:

./UdpServer
./UdpClient

UDP 与 TCP 的区别

  1. 连接性:TCP 是面向连接的,需要先建立连接;UDP 是无连接的,直接发送数据报文。
  2. 可靠性:TCP 提供可靠的数据传输,保证数据顺序和完整性;UDP 不保证数据的可靠传输。
  3. 速度:UDP 比 TCP 更快,因为没有建立连接和确认的开销。
  4. 数据边界:UDP 保持数据报文的边界;TCP 将数据看作字节流,没有边界。
  5. 应用场景:TCP 适用于对数据可靠性要求高的场景,如文件传输、网页浏览等;UDP 适用于对实时性要求高、能容忍少量数据丢失的场景,如视频通话、在线游戏等。

代码特点说明

UDP 套接字编程与 TCP 套接字编程有以下主要区别:

  1. 套接字类型:UDP 使用 SOCK_DGRAM 类型,TCP 使用 SOCK_STREAM 类型。
  2. 连接处理:UDP 不需要建立连接,直接使用 sendto 和 recvfrom 函数发送和接收数据。
  3. 地址信息:UDP 在每次发送和接收数据时都需要指定或获取地址信息。
  4. 数据传输:UDP 保持数据报文的边界,每个 sendto 调用对应一个 recvfrom 接收。

通过以上代码示例,可以了解 UDP 套接字编程的基本流程和实现方式。