{T}

UDP连接模式:已连接UDP套接字

在基础篇中,我们已经了解了 UDP(User Datagram Protocol)数据报协议的相关知识,明确了"UDP 是无连接协议"这一基本特性。然而,UDP 套接字实际上支持通过 connect() 系统调用建立与对端地址的关联关系,这就是所谓的"已连接 UDP 套接字"(Connected UDP Socket)。本文将深入分析已连接 UDP 套接字的工作原理、应用场景及性能影响。

已连接与未连接 UDP 套接字对比

UDP 套接字分为两种工作模式:未连接模式(Unconnected)和已连接模式(Connected)。二者的核心区别在于是否与特定的对端地址建立了映射关系。

图表渲染中…

从一个示例程序开始

以下是一个客户端示例程序,该程序在 UDP 套接字上调用 connect() 函数,之后通过 sendto()recvfrom() 向服务器端发送和接收报文。

plaintext
#include "lib/common.h"
# define    MAXLINE     4096
 
int main(int argc, char **argv) {
    if (argc != 2) {
        error(1, 0, "usage: udpclient1 <IPaddress>");
    }
 
    int socket_fd;
    socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
 
    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERV_PORT);
    inet_pton(AF_INET, argv[1], &server_addr.sin_addr);
 
    socklen_t server_len = sizeof(server_addr);
 
    if (connect(socket_fd, (struct sockaddr *) &server_addr, server_len)) {
        error(1, errno, "connect failed");
    }
 
    struct sockaddr *reply_addr;
    reply_addr = malloc(server_len);
 
    char send_line[MAXLINE], recv_line[MAXLINE + 1];
    socklen_t len;
    int n;
 
    while (fgets(send_line, MAXLINE, stdin) != NULL) {
        int i = strlen(send_line);
        if (send_line[i - 1] == '\n') {
            send_line[i - 1] = 0;
        }
 
        printf("now sending %s\n", send_line);
        size_t rt = sendto(socket_fd, send_line, strlen(send_line), 0, (struct sockaddr *) &server_addr, server_len);
        if (rt < 0) {
            error(1, errno, "sendto failed");
        }
        printf("send bytes: %zu \n", rt);
 
        len = 0;
        recv_line[0] = 0;
        n = recvfrom(socket_fd, recv_line, MAXLINE, 0, reply_addr, &len);
        if (n < 0)
            error(1, errno, "recvfrom failed");
        recv_line[n] = 0;
        fputs(recv_line, stdout);
        fputs("\n", stdout);
    }
 
    exit(0);
}
 

程序关键步骤说明:

  • 第 9-10 行:创建 UDP 套接字(SOCK_DGRAM);
  • 第 12-16 行:构造 IPv4 地址结构,绑定目标端口和 IP;
  • 第 20-22 行:调用 connect() 将 UDP 套接字与对端地址进行关联。此处 connect 的语义更接近 setpeername——它不会触发网络交互,仅在内核中建立映射关系
  • 第 31-55 行:程序主体,读取标准输入后调用 sendto() 发送给对端,然后调用 recvfrom() 等待对端响应。

在未启动服务端的情况下运行该程序:

plaintext
$ ./udpconnectclient 127.0.0.1
g1
now sending g1
send bytes: 2
recvfrom failed: Connection refused (111)
 

此处的关键现象是:UDP 本是无连接协议,但程序却收到了 "Connection refused" 错误。这正是 connect() 调用产生的效果。

UDP connect 的作用

与 TCP 的 connect() 触发三次握手不同,UDP 的 connect() 调用不会引起任何网络交互,不会发送"握手"报文。

UDP connect() 的核心作用是:使应用程序能够接收异步错误(Asynchronous Error)信息

当不调用 connect() 时,在服务器端未启动的情况下,客户端程序不会报错,只会阻塞在 recvfrom() 上等待返回。

调用 connect() 后,操作系统内核建立了"UDP 套接字——目的地址+端口"之间的映射关系。其工作机制如下:

图表渲染中…
  1. 应用程序调用 sendto()send() 发送报文后,操作系统内核接管该报文并尝试发送到目标地址;
  2. 如果目标地址不可达,网络中会返回一个 ICMP(Internet Control Message Protocol)报文,其中包含目的地址和端口信息;
  3. 若未进行 connect() 操作,内核无法将 ICMP 不可达信息与特定 UDP 套接字关联,因此无法通知应用程序;
  4. 若已进行 connect() 操作,内核从映射表中定位到对应的 UDP 套接字,当应用程序再次调用 recvfrom()recv() 时,即可收到 "Connection refused" 错误信息。

收发函数的选择

对 UDP 套接字调用 connect() 后,关于收发函数的使用,不同 UNIX 实现的行为存在差异:

  • 推荐做法:使用 send()/write() 发送数据,使用 recv()/read() 接收数据;若使用 sendto(),需将目标地址信息置零;若使用 recvfrom(),需将源地址信息置零。
  • Linux 行为(Linux 4.4+,含 Linux 7.0):使用 sendto()recvfrom() 时,系统会自动忽略 to/from 地址信息。
  • macOS 行为:需严格遵守上述推荐做法,否则可能产生异常结果。

出于跨平台兼容性考虑,推荐遵循常规做法。

服务器端 connect 示例

一般情况下,服务器端不会主动调用 connect(),因为一旦连接,该套接字将只能与一个客户端通信。但在特定场景下(如服务器仅需服务唯一客户端),服务器端也可以使用 connect()

服务器端程序示例:

plaintext
#include "lib/common.h"
 
static int count;
 
static void recvfrom_int(int signo) {
    printf("\nreceived %d datagrams\n", count);
    exit(0);
}
 
int main(int argc, char **argv) {
    int socket_fd;
    socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
 
    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(SERV_PORT);
 
    bind(socket_fd, (struct sockaddr *) &server_addr, sizeof(server_addr));
 
    socklen_t client_len;
    char message[MAXLINE];
    message[0] = 0;
    count = 0;
 
    signal(SIGINT, recvfrom_int);
 
    struct sockaddr_in client_addr;
    client_len = sizeof(client_addr);
 
    int n = recvfrom(socket_fd, message, MAXLINE, 0, (struct sockaddr *) &client_addr, &client_len);
    if (n < 0) {
        error(1, errno, "recvfrom failed");
    }
    message[n] = 0;
    printf("received %d bytes: %s\n", n, message);
 
    if (connect(socket_fd, (struct sockaddr *) &client_addr, client_len)) {
        error(1, errno, "connect failed");
    }
 
    while (strncmp(message, "goodbye", 7) != 0) {
        char send_line[MAXLINE];
        sprintf(send_line, "Hi, %s", message);
 
        size_t rt = send(socket_fd, send_line, strlen(send_line), 0);
        if (rt < 0) {
            error(1, errno, "send failed ");
        }
        printf("send bytes: %zu \n", rt);
 
        size_t rc = recv(socket_fd, message, MAXLINE, 0);
        if (rc < 0) {
            error(1, errno, "recv failed");
        }
 
        count++;
    }
 
    exit(0);
}
 

程序关键步骤说明:

  • 第 11-12 行:创建 UDP 套接字;
  • 第 14-18 行:构造 IPv4 地址,绑定通配地址 INADDR_ANY 和指定端口;
  • 第 20 行:绑定 UDP 套接字与 IPv4 地址;
  • 第 27 行:注册信号处理函数,响应 Ctrl+C 信号;
  • 第 32-37 行:调用 recvfrom() 等待客户端报文到达,并将客户端信息保存到 client_addr 中;
  • 第 39-41 行:调用 connect() 操作,将 UDP 套接字与客户端地址进行绑定
  • 第 43-59 行:程序主体,对接收的信息添加 "Hi" 前缀后发送给客户端,并持续从客户端接收报文,直到客户端发送 "goodbye" 为止。

注意:此处所有收发函数均使用了 send()recv()

对应的客户端程序:

plaintext
#include "lib/common.h"
# define    MAXLINE     4096
 
int main(int argc, char **argv) {
    if (argc != 2) {
        error(1, 0, "usage: udpclient3 <IPaddress>");
    }
 
    int socket_fd;
    socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
 
    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERV_PORT);
    inet_pton(AF_INET, argv[1], &server_addr.sin_addr);
 
    socklen_t server_len = sizeof(server_addr);
 
    if (connect(socket_fd, (struct sockaddr *) &server_addr, server_len)) {
        error(1, errno, "connect failed");
    }
 
    char send_line[MAXLINE], recv_line[MAXLINE + 1];
    int n;
 
    while (fgets(send_line, MAXLINE, stdin) != NULL) {
        int i = strlen(send_line);
        if (send_line[i - 1] == '\n') {
            send_line[i - 1] = 0;
        }
 
        printf("now sending %s\n", send_line);
        size_t rt = send(socket_fd, send_line, strlen(send_line), 0);
        if (rt < 0) {
            error(1, errno, "send failed ");
        }
        printf("send bytes: %zu \n", rt);
 
        recv_line[0] = 0;
        n = recv(socket_fd, recv_line, MAXLINE, 0);
        if (n < 0)
            error(1, errno, "recv failed");
        recv_line[n] = 0;
        fputs(recv_line, stdout);
        fputs("\n", stdout);
    }
 
    exit(0);
}
 

程序关键步骤说明:

  • 第 9-10 行:创建 UDP 套接字;
  • 第 12-16 行:构造 IPv4 地址,绑定指定端口和 IP;
  • 第 20-22 行:调用 connect() 将 UDP 套接字与服务器地址进行关联
  • 第 27-46 行:程序主体,读取标准输入后调用 send() 发送给对端,然后调用 recv() 等待对端响应。

运行测试:先启动服务器端程序,然后依次启动客户端 1 和客户端 2,让客户端 1 先发送 UDP 报文。

服务器端输出:

plaintext
$ ./udpconnectserver
received 2 bytes: g1
send bytes: 6
 

客户端 1 输出:

plaintext
$ ./udpconnectclient2 127.0.0.1
g1
now sending g1
send bytes: 2
Hi, g1
 

客户端 2 输出:

plaintext
$ ./udpconnectclient2 127.0.0.1
g2
now sending g2
send bytes: 2
recv failed: Connection refused (111)
 

客户端 1 先发送报文,服务器端通过 connect() 与客户端 1 绑定后,客户端 2 的报文被服务器拒绝,客户端 2 从内核收到 ICMP 错误,recv() 返回 "Connection refused"。

性能考虑

对 UDP 客户端而言,connect() 还能带来一定程度的性能提升。

未使用 connect() 时,每次发送报文的过程为:

plaintext
连接套接字 → 发送报文 → 断开套接字连接 → 连接套接字 → 发送报文 → 断开套接字连接 → ……

使用 connect() 后,过程变为:

plaintext
连接套接字 → 发送报文 → 发送报文 → …… → 最后断开套接字连接

每次"连接套接字"操作需要查找路由表信息等内核开销,因此 connect() 减少了重复的连接建立和断开操作,可以获得一定的性能提升。

内核版本注记:Linux 3.9+ 引入了 recvmmsg()sendmmsg() 系统调用,支持批量收发 UDP 报文,进一步提升了 UDP 的性能表现。Linux 4.18+ 中 sendmmsg() 的性能得到持续优化。在 Linux 7.0 中,UDP 的 GRO(Generic Receive Offload)和 GSO(Generic Segmentation Offload)支持更加完善,配合 connect() 使用效果更佳。

总结

本文深入分析了 UDP 套接字调用 connect() 方法的原理与应用。对 UDP 使用 connect() 的核心目的是:

  1. 接收异步错误通知:通过建立套接字与对端地址的映射关系,使应用程序能够感知 ICMP 不可达等异步错误;
  2. 性能提升:避免每次发送报文时重复进行路由查找等内核操作;
  3. 代码简化:使用 send()/recv() 替代 sendto()/recvfrom(),减少参数传递。

思考题

  1. 是否可以对一个 UDP 套接字进行多次 connect() 操作?请动手验证。
  2. 如果需要使用多播(Multicast)或广播(Broadcast),应如何使用 connect()

版本信息

  • 更新日期:2026-06-09
  • 目标内核:Linux 7.0