UDP连接模式:已连接UDP套接字
在基础篇中,我们已经了解了 UDP(User Datagram Protocol)数据报协议的相关知识,明确了"UDP 是无连接协议"这一基本特性。然而,UDP 套接字实际上支持通过 connect() 系统调用建立与对端地址的关联关系,这就是所谓的"已连接 UDP 套接字"(Connected UDP Socket)。本文将深入分析已连接 UDP 套接字的工作原理、应用场景及性能影响。
已连接与未连接 UDP 套接字对比
UDP 套接字分为两种工作模式:未连接模式(Unconnected)和已连接模式(Connected)。二者的核心区别在于是否与特定的对端地址建立了映射关系。
从一个示例程序开始
以下是一个客户端示例程序,该程序在 UDP 套接字上调用 connect() 函数,之后通过 sendto() 和 recvfrom() 向服务器端发送和接收报文。
#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()等待对端响应。
在未启动服务端的情况下运行该程序:
$ ./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 套接字——目的地址+端口"之间的映射关系。其工作机制如下:
- 应用程序调用
sendto()或send()发送报文后,操作系统内核接管该报文并尝试发送到目标地址; - 如果目标地址不可达,网络中会返回一个 ICMP(Internet Control Message Protocol)报文,其中包含目的地址和端口信息;
- 若未进行
connect()操作,内核无法将 ICMP 不可达信息与特定 UDP 套接字关联,因此无法通知应用程序; - 若已进行
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()。
服务器端程序示例:
#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()。
对应的客户端程序:
#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 报文。
服务器端输出:
$ ./udpconnectserver
received 2 bytes: g1
send bytes: 6
客户端 1 输出:
$ ./udpconnectclient2 127.0.0.1
g1
now sending g1
send bytes: 2
Hi, g1
客户端 2 输出:
$ ./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() 时,每次发送报文的过程为:
连接套接字 → 发送报文 → 断开套接字连接 → 连接套接字 → 发送报文 → 断开套接字连接 → ……使用 connect() 后,过程变为:
连接套接字 → 发送报文 → 发送报文 → …… → 最后断开套接字连接每次"连接套接字"操作需要查找路由表信息等内核开销,因此 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() 的核心目的是:
- 接收异步错误通知:通过建立套接字与对端地址的映射关系,使应用程序能够感知 ICMP 不可达等异步错误;
- 性能提升:避免每次发送报文时重复进行路由查找等内核操作;
- 代码简化:使用
send()/recv()替代sendto()/recvfrom(),减少参数传递。
思考题
- 是否可以对一个 UDP 套接字进行多次
connect()操作?请动手验证。 - 如果需要使用多播(Multicast)或广播(Broadcast),应如何使用
connect()?
版本信息
- 更新日期:2026-06-09
- 目标内核:Linux 7.0