Socket 套接字编程-UDP

Socket 套接字编程-UDP

七月 28, 2025 次阅读

Socket 函数详解

Socket(套接字)是网络编程的核心接口,用于实现不同主机之间的进程通信(IPC,Inter-Process Communication)。它提供了一种标准的 API,使应用程序能够通过 TCP/IP、UDP 或其他协议进行网络数据传输。


Socket 的基本概念

Socket 可以看作是两个进程(客户端和服务器)之间的通信端点(Endpoint),它封装了 IP 地址和端口号,使得数据可以在网络上传输。

  • IP 地址:标识网络上的主机(如 192.168.1.1google.com)。
  • 端口号(Port):标识主机上的具体服务(如 HTTP:80、SSH:22)。

Socket 通常用于:

  • TCP(可靠传输):如 HTTP、FTP、SSH。
  • UDP(无连接传输):如 DNS、视频流、在线游戏。

Socket 的核心函数

(1) socket() - 创建套接字

#include <sys/socket.h>

int socket(int domain, int type, int protocol);
  • 功能:创建一个 Socket 文件描述符(fd)。
  • 参数
    • domain:协议族(AF_INET 表示 IPv4,AF_INET6 表示 IPv6)。
    • type:通信类型:
      • SOCK_STREAM(TCP,可靠连接)
      • SOCK_DGRAM(UDP,无连接)
    • protocol:通常设为 0(自动选择)。
  • 返回值
    • 成功:返回 Socket 文件描述符(int)。
    • 失败:返回 -1,并设置 errno

示例

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
    perror("socket creation failed");
    exit(EXIT_FAILURE);
}

(2) bind() - 绑定 IP 和端口

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 功能:将 Socket 绑定到特定的 IP 地址和端口(服务器端使用)。
  • 参数
    • sockfd:Socket 文件描述符。
    • addr:指向 sockaddr 结构的指针(存储 IP 和端口)。
    • addrlensockaddr 结构的大小。
  • 返回值
    • 成功:0
    • 失败:-1,并设置 errno

示例

struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;  // 绑定所有网卡
server_addr.sin_port = htons(8080);        // 绑定 8080 端口

if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
    perror("bind failed");
    close(sockfd);
    exit(EXIT_FAILURE);
}

(3) listen() - 监听连接(TCP 服务器)

int listen(int sockfd, int backlog);
  • 功能:使 Socket 进入监听状态,等待客户端连接(仅用于 TCP)。
  • 参数
    • sockfd:Socket 文件描述符。
    • backlog:等待连接队列的最大长度。
  • 返回值
    • 成功:0
    • 失败:-1,并设置 errno

示例

if (listen(sockfd, 5) == -1) {
    perror("listen failed");
    close(sockfd);
    exit(EXIT_FAILURE);
}

(4) accept() - 接受连接(TCP 服务器)

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 功能:接受客户端的连接请求,返回一个新的 Socket 用于通信。
  • 参数
    • sockfd:监听 Socket。
    • addr:存储客户端地址信息(可设为 NULL)。
    • addrlen:客户端地址结构的大小(可设为 NULL)。
  • 返回值
    • 成功:返回一个新的 Socket 文件描述符(用于数据传输)。
    • 失败:-1,并设置 errno

示例

struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_sock = accept(sockfd, (struct sockaddr*)&client_addr, &client_len);
if (client_sock == -1) {
    perror("accept failed");
    close(sockfd);
    exit(EXIT_FAILURE);
}

(5) connect() - 连接服务器(TCP 客户端)

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 功能:客户端连接服务器(仅用于 TCP)。
  • 参数
    • sockfd:Socket 文件描述符。
    • addr:服务器地址信息。
    • addrlen:地址结构大小。
  • 返回值
    • 成功:0
    • 失败:-1,并设置 errno

示例

struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);  // 连接本地 8080 端口

if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
    perror("connect failed");
    close(sockfd);
    exit(EXIT_FAILURE);
}

(6) send() / recv() - TCP 数据收发

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  • 功能:TCP 方式发送/接收数据。
  • 参数
    • sockfd:Socket 文件描述符。
    • buf:数据缓冲区。
    • len:数据长度。
    • flags:通常设为 0(阻塞模式)。
  • 返回值
    • 成功:返回实际发送/接收的字节数。
    • 失败:-1,并设置 errno

示例

char buffer[1024];
ssize_t bytes_received = recv(client_sock, buffer, sizeof(buffer), 0);
if (bytes_received == -1) {
    perror("recv failed");
    close(client_sock);
    exit(EXIT_FAILURE);
}
buffer[bytes_received] = '\0';  // 确保字符串终止
printf("Received: %s\n", buffer);

(7) sendto() / recvfrom() - UDP 数据收发

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);
  • 功能:UDP 方式发送/接收数据(不需要 connect)。
  • 参数
    • dest_addr / src_addr:目标/源地址。
  • 示例
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);

char message[] = "Hello UDP Server!";
sendto(sockfd, message, strlen(message), 0,
       (struct sockaddr*)&server_addr, sizeof(server_addr));

Socket 工作流程

TCP 服务器

  1. socket() → 创建 Socket
  2. bind() → 绑定 IP 和端口
  3. listen() → 开始监听
  4. accept() → 接受客户端连接
  5. recv() / send() → 数据通信
  6. close() → 关闭 Socket

TCP 客户端

  1. socket() → 创建 Socket
  2. connect() → 连接服务器
  3. send() / recv() → 数据通信
  4. close() → 关闭 Socket

UDP 服务器/客户端

  1. socket() → 创建 Socket
  2. bind()(客户端可选,但一般是不需要的)
  3. sendto() / recvfrom() → 直接发送/接收数据
  4. close() → 关闭 Socket

总结

函数 用途 适用协议
socket() 创建 Socket TCP/UDP
bind() 绑定 IP 和端口 TCP/UDP(服务器)
listen() 监听连接 TCP(服务器)
accept() 接受连接 TCP(服务器)
connect() 连接服务器 TCP(客户端)
send() / recv() TCP 数据收发 TCP
sendto() / recvfrom() UDP 数据收发 UDP
close() 关闭 Socket TCP/UDP

下面是对 UDP 收发数据接口的详细说明

UDP 收发数据接口详解

由于 UDP(User Datagram Protocol)是无连接的协议,每次发送和接收数据时都需要明确指定或获取对方的地址信息。因此,UDP 通信主要使用以下两个接口:

  1. recvfrom —— 接收数据,并获取发送方的地址信息。
  2. sendto —— 发送数据,并指定目标地址信息。

1. recvfrom —— 接收 UDP 数据

函数原型

#include <sys/socket.h>

ssize_t recvfrom(
    int sockfd,                   // UDP 套接字描述符
    void *buf,                    // 接收数据的缓冲区
    size_t len,                  // 缓冲区大小
    int flags,                   // 控制选项(通常设为 0)
    struct sockaddr *src_addr,    // 保存发送方的地址信息
    socklen_t *addrlen           // 地址结构体的长度(输入输出参数)
);

参数说明

  • sockfd:UDP 套接字描述符(由 socket(AF_INET, SOCK_DGRAM, 0) 创建)。
  • buf:存放接收数据的缓冲区。
  • len:缓冲区的最大容量。
  • flags:控制选项(如 MSG_WAITALLMSG_PEEK,通常设为 0)。
  • src_addr:用于保存发送方的地址(struct sockaddr_instruct sockaddr)。
  • addrlen:输入时为 src_addr 的大小,输出时为实际接收到的地址长度。

返回值

  • 成功:返回接收到的字节数(>0)。
  • 失败:返回 -1,并设置 errno(如 EAGAINECONNREFUSED)。

示例代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

int main() {
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket");
        exit(1);
    }

    struct sockaddr_in server_addr = {0};
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);
    server_addr.sin_addr.s_addr = INADDR_ANY;

    if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) {
        perror("bind");
        exit(1);
    }

    char buffer[1024];
    struct sockaddr_in client_addr;
    socklen_t addr_len = sizeof(client_addr);

    ssize_t recv_len = recvfrom(
        sockfd, buffer, sizeof(buffer), 0,
        (struct sockaddr *)&client_addr, &addr_len
    );

    if (recv_len < 0) {
        perror("recvfrom");
        exit(1);
    }

    printf("Received %zd bytes from %s:%d\n", 
           recv_len, 
           inet_ntoa(client_addr.sin_addr), 
           ntohs(client_addr.sin_port));

    close(sockfd);
    return 0;
}

2. sendto —— 发送 UDP 数据

函数原型

#include <sys/socket.h>

ssize_t sendto(
    int sockfd,                   // UDP 套接字描述符
    const void *buf,             // 要发送的数据
    size_t len,                  // 数据长度
    int flags,                   // 控制选项(通常设为 0)
    const struct sockaddr *dest_addr,  // 目标地址
    socklen_t addrlen            // 目标地址长度
);

参数说明

  • sockfd:UDP 套接字描述符。
  • buf:要发送的数据缓冲区。
  • len:数据长度。
  • flags:控制选项(通常设为 0)。
  • dest_addr:目标地址(struct sockaddr_instruct sockaddr)。
  • addrlen:目标地址的长度。

返回值

  • 成功:返回发送的字节数(>=0)。
  • 失败:返回 -1,并设置 errno(如 EMSGSIZEENOBUFS)。

示例代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

int main() {
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket");
        exit(1);
    }

    struct sockaddr_in server_addr = {0};
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    const char *msg = "Hello, UDP Server!";
    ssize_t sent_len = sendto(
        sockfd, msg, strlen(msg), 0,
        (struct sockaddr *)&server_addr, sizeof(server_addr)
    );

    if (sent_len < 0) {
        perror("sendto");
        exit(1);
    }

    printf("Sent %zd bytes to server\n", sent_len);
    close(sockfd);
    return 0;
}

为什么 UDP 需要 recvfromsendto

由于 UDP 是无连接的协议,每次通信时都需要明确:

  1. 接收数据时:要知道数据是谁发来的(recvfrom 返回 src_addr)。
  2. 发送数据时:要指定数据发给谁(sendto 需要 dest_addr)。

对比 TCP

特性 TCP UDP
连接方式 面向连接(connect/accept 无连接
收发接口 send/recv sendto/recvfrom
地址管理 连接建立后自动维护 每次通信需手动指定

  • recvfrom:用于接收 UDP 数据,并获取发送方的地址。
  • sendto:用于发送 UDP 数据,并指定目标地址。
  • UDP 是无连接的,因此每次通信都需要显式处理地址信息,而 TCP 是面向连接的,地址信息在建立连接后自动维护。

UDP 通信服务实现

以下是对这段 UDP 封装代码的详细介绍和注释说明:


udp.hpp

这段代码实现了一个简单的 UDP 网络通信封装

#ifndef _UDP_HPP_
#define _UDP_HPP_ 1  // 头文件保护宏

#include <iostream>
#include <sys/socket.h>  // socket相关函数
#include <netinet/in.h>   // sockaddr_in结构体
#include <arpa/inet.h>    // inet_ntop等转换函数
#include <cstring>        // bzero函数

// 错误码枚举
enum {
    socket_error = 1,  // socket创建失败
    bind_error,        // 绑定端口失败
};

inline char addr_buffer[1024];  // 全局缓冲区(用于地址转换)

namespace udp {
    /**
     * 创建UDP Socket
     * @return 成功返回socket文件描述符,失败退出程序
     */
    int Socket() {
        // 创建IPv4 UDP Socket
        int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (sockfd == -1) {
            perror("socket");      // 打印错误信息
            exit(socket_error);    // 退出程序
        }
        return sockfd;
    }

    /**
     * 绑定Socket到指定端口
     * @param sockfd  Socket文件描述符
     * @param port    要绑定的端口号
     */
    void Bind(int sockfd, int port) {
        struct sockaddr_in local;
        bzero(&local, sizeof(local));  // 清空结构体
        
        // 设置地址族和端口
        local.sin_family = AF_INET;           // IPv4
        local.sin_addr.s_addr = INADDR_ANY;   // 监听所有网卡
        local.sin_port = htons(port);         // 端口转为网络字节序
        
        // 执行绑定
        int n = bind(sockfd, (struct sockaddr*)(&local), sizeof(local));
        if (n != 0) {
            perror("bind");
            exit(bind_error);
        }
    }

    /**
     * 从sockaddr_in结构体解析IP和端口
     * @param addr_in  输入的网络地址结构体
     * @param addr     输出的IP字符串
     * @param port     输出的端口号(主机字节序)
     */
    void GetAddrAndPort(struct sockaddr_in &addr_in, std::string &addr, uint16_t &port) {
        port = ntohs(addr_in.sin_port);  // 端口转为主机字节序
        
        // 将二进制IP转为字符串
        inet_ntop(AF_INET, &addr_in.sin_addr, addr_buffer, sizeof(addr_buffer) - 1);
        addr = addr_buffer;  // 存储结果
    }
};

#endif

  1. Socket() 函数

    • 使用 socket(AF_INET, SOCK_DGRAM, 0) 创建 UDP Socket
    • 失败时通过 perror 输出错误信息并退出
  2. Bind() 函数

    • INADDR_ANY 表示监听所有网卡(包括公网和本地)
    • htons(port) 将端口转为网络字节序(大端)
    • bind() 将Socket与地址绑定
  3. GetAddrAndPort() 函数

    • ntohs() 将网络字节序端口转为主机字节序
    • inet_ntop() 将二进制IP地址转为可读字符串
  4. 全局缓冲区

    • addr_buffer 用于临时存储IP字符串
    • 注意:多线程环境下建议改用局部变量

以下是对这段 UDP 服务端代码的详细注释和解释:


udp_server.hpp

该代码实现了一个简单的 UDP 服务端

#ifndef _UDP_SERVER_HPP_
#define _UDP_SERVER_HPP_ 1  // 头文件保护宏

#include <iostream>
#include <unistd.h>      // close函数
#include "udp.hpp"       // UDP基础封装
#include "log.hpp"       // 日志模块

const int defaultport = 8080;  // 默认监听端口
const int default_size = 1024; // 接收缓冲区大小

class UdpServer {
public:
    /**
     * 构造函数
     * @param port 监听端口(默认8080)
     */
    UdpServer(uint16_t port = defaultport) 
        : port_(port) {}  // 初始化端口

    /**
     * 初始化Socket和绑定
     */
    void Init() {
        // 1. 创建UDP Socket
        sockfd_ = udp::Socket();
        lg(Info, "socket success, sockfd: %d", sockfd_);

        // 2. 绑定端口
        udp::Bind(sockfd_, port_);
        lg(Info, "socket bind success");
    }

    /**
     * 启动服务主循环
     */
    void Start() {
        char buffer[default_size];  // 接收缓冲区

        while (true) {  // 持续监听
            struct sockaddr_in peer;  // 客户端地址
            socklen_t len = sizeof(peer);

            // 3. 接收数据(阻塞式)
            ssize_t n = recvfrom(sockfd_, buffer, default_size - 1, 0, 
                                (struct sockaddr*)&peer, &len);

            if (n > 0) {  // 接收成功
                buffer[n] = '\0';  // 确保字符串终止

                // 4. 解析客户端地址
                std::string addr;
                uint16_t port;
                udp::GetAddrAndPort(peer, addr, port);

                // 5. 记录日志
                lg(Info, "UDP Server Get a message from [%s: %d]: %s", 
                    addr.c_str(), port, buffer);

                // 6. 回显数据
                sendto(sockfd_, buffer, strlen(buffer), 0, 
                      (struct sockaddr*)&peer, len);
            }
        }
    }

    /**
     * 析构函数(关闭Socket)
     */
    ~UdpServer() {
        close(sockfd_);
    }

private:
    uint16_t port_;  // 监听端口
    int sockfd_;     // Socket文件描述符
};

#endif

关键代码解析

代码段 功能说明
udp::Socket() 创建UDP Socket(SOCK_DGRAM
udp::Bind() 绑定到INADDR_ANY(所有网卡)和指定端口
recvfrom() 接收客户端数据,同时获取客户端地址
udp::GetAddrAndPort() sockaddr_in解析IP和端口
sendto() 将数据回发给原客户端
lg(Info,...) 记录通信日志(依赖外部日志模块)

工作流程

  1. 初始化阶段

       sequenceDiagram
        UdpServer->>+udp::Socket: 创建Socket
        UdpServer->>+udp::Bind: 绑定端口
  2. 运行阶段

       sequenceDiagram
        Client->>UdpServer: 发送UDP数据包
        UdpServer->>UdpServer: 记录客户端地址和消息
        UdpServer->>Client: 回显相同数据

udp_client.cc

以下是一个简单的UDP客户端示例代码,用于与服务器进行交互:

#include <iostream>
#include <cstring>
#include <unistd.h>
#include "udp.hpp"  // 引入自定义UDP封装
using namespace std;

int main(int argc, char *argv[]) {
    // 1. 参数检查
    if (argc != 3) {
        cerr << "Usage: " << argv[0] << " server_ip server_port" << endl;
        return 1;
    }

    // 2. 解析命令行参数
    string server_ip = argv[1];          // 服务器IP地址
    uint16_t server_port = stoi(argv[2]);// 服务器端口号

    // 3. 创建UDP Socket
    int sockfd = udp::Socket();          // 调用封装函数创建Socket

    // 4. 配置服务器地址结构
    struct sockaddr_in server;
    bzero(&server, sizeof(server));      // 清空结构体
    server.sin_port = htons(server_port); // 设置端口(转为网络字节序)
    server.sin_family = AF_INET;         // IPv4地址族
    server.sin_addr.s_addr = inet_addr(server_ip.c_str()); // 转换IP为二进制格式

    // 5. 主交互循环
    while (true) {
        // 5.1 获取用户输入
        string inbuffer;
        cout << "Please Enter# ";
        getline(cin, inbuffer);

        // 5.2 发送数据到服务器
        ssize_t n = sendto(sockfd, inbuffer.c_str(), inbuffer.size(), 0,
                          (struct sockaddr*)&server, sizeof(server));
        
        if (n > 0) {  // 发送成功
            // 5.3 准备接收回显
            char buffer[1024];
            struct sockaddr_in temp;    // 临时存储回复方地址(未使用)
            socklen_t len = sizeof(temp);

            // 5.4 接收服务器回复
            ssize_t m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0,
                               (struct sockaddr*)&temp, &len);
            
            if (m > 0) {  // 接收成功
                buffer[m] = '\0';  // 添加字符串终止符
                cout << "server echo# " << buffer << endl;
            }
        } else {  // 发送失败则退出
            break;
        }
    }

    // 6. 关闭Socket
    close(sockfd);
    return 0;
}

核心功能

  • 实现一个 UDP 客户端,能够:
    • 通过命令行参数指定服务器地址和端口
    • 交互式发送用户输入的消息
    • 接收并显示服务器的回显(Echo)

关键步骤

步骤 关键函数/操作 说明
参数检查 argc != 3 确保输入格式正确
创建Socket udp::Socket() 创建UDP套接字
地址配置 inet_addr() + htons() 将字符串IP和端口转为网络格式
数据发送 sendto() 发送数据到指定服务器
数据接收 recvfrom() 接收服务器回复(不验证来源)
资源清理 close() 关闭Socket

工作流程图示

sequenceDiagram
    participant User
    participant Client
    participant Server

    User->>Client: 输入消息
    Client->>Server: sendto(message)
    Server->>Client: recvfrom(echo)
    Client->>User: 打印回显

通过主函数挂起服务端:

#include <iostream>
#include "udp_server.hpp"

using namespace std;

int main(int argc, char *argv[])
{
    int port;
    if(argc == 1)
    {
        port = 8080;
    }else if(argc == 2)
    {
        port = stoi(argv[1]);
    }else{
        cerr << "Usage: " << argv[0] << "(default port: 8080)" << endl << 
        "OR" << endl << argv[0] << " port" << endl;
        return 1;
    }
    UdpServer us(port);
    us.Init();
    us.Start();
    return 0;
}

而后我们进行通信,可以看到通信正常:

# 客户端
╭─ljx@VM-16-15-debian ~/linux_review/udp
╰─➤  ./udp_client.o 82.156.255.140 8888
Please Enter# 1234
server echo# 1234
Please Enter# 12345
server echo# 12345
Please Enter# Liu Jiaxuan say: Hello!
server echo# Liu Jiaxuan say: Hello!
# 服务端
╭─ljx@VM-16-15-debian ~/linux_review/udp  
╰─➤  ./main.o 8888
[Info][2025-7-28 23:43:22] socket success, sockfd: 3
[Info][2025-7-28 23:43:22] socket bind success
[Info][2025-7-28 23:43:30] UDP Server Get a message from [82.156.255.140: 55472]: 1234
[Info][2025-7-28 23:43:32] UDP Server Get a message from [82.156.255.140: 55472]: 12345
[Info][2025-7-28 23:43:56] UDP Server Get a message from [82.156.255.140: 55472]: Liu Jiaxuan say: Hello!