connect系统调用及示例

我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 connect 函数,它是 TCP 客户端用来向服务器发起连接请求的核心系统调用。

1. 函数介绍

connect 是一个 Linux 系统调用,主要用于TCP 客户端(使用 SOCK_STREAM 套接字)来主动建立与服务器的连接。它也可以用于UDP 客户端(使用 SOCK_DGRAM 套接字)来设置默认的目标地址。

data-ad-format="fluid" data-ad-layout-key="-7k+ex-4a-9w+4a">

你可以把 connect 想象成拨打电话:

你先有了一个电话听筒(通过 socket 创建了套接字)。

你知道你要打给谁(知道服务器的 IP 地址和端口号)。

你按下拨打键(调用 connect)。

电话那头的服务器响铃,接听后,你们之间的通话线路就建立了。

对于 TCP 来说,connect 会触发 TCP 的**三次握手 **(Three-way Handshake) 过程,这是 TCP 协议用来建立可靠连接的标准步骤。

2. 函数原型

1
2
3
4
#include <sys/socket.h> // 必需

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

3. 功能

  • **建立连接 **(TCP) 对于 TCP 套接字 (SOCK_STREAM),connect 发起一个连接请求到由 addr 参数指定的服务器地址。它会执行 TCP 三次握手,直到连接成功建立或失败。

  • **设置默认目标 **(UDP) 对于 UDP 套接字 (SOCK_DGRAM),connect 不会发送任何数据包或执行握手。它只是在内核中为该套接字记录下目标地址。之后对该套接字的 write/send 调用将默认发送到这个地址,read/recv 只会接收来自这个地址的数据。这简化了 UDP 客户端的编程,使其行为更像 TCP。

4. 参数

  • int sockfd: 这是之前通过 socket() 系统调用成功创建的套接字文件描述符。

const struct sockaddr *addr: 这是一个指向套接字地址结构的指针,该结构包含了要连接的服务器的地址信息(IP 地址和端口号)。

  • 对于 IPv4,通常使用 struct sockaddr_in。

  • 对于 IPv6,通常使用 struct sockaddr_in6。

  • 在调用时,通常会将具体的地址结构(如 sockaddr_in)强制类型转换为 (struct sockaddr *) 传入。

socklen_t addrlen: 这是 addr 指向的地址结构的大小(以字节为单位)。

  • 对于 struct sockaddr_in,这个值通常是 sizeof(struct sockaddr_in)。

  • 对于 struct sockaddr_in6,这个值通常是 sizeof(struct sockaddr_in6)。

5. 返回值

  • **成功时 **(TCP) 对于 TCP 套接字,连接成功建立后,返回 0。此时,套接字 sockfd 已准备好进行数据传输(read/write)。

  • **成功时 **(UDP) 对于 UDP 套接字,总是立即返回 0,因为它只是设置了默认地址,并不真正“连接”。

  • 失败时: 返回 -1,并设置全局变量 errno 来指示具体的错误原因(例如 ECONNREFUSED 远程主机拒绝连接,ETIMEDOUT 连接超时,EHOSTUNREACH 主机不可达,EADDRINUSE 本地地址已被使用,EINVAL 套接字状态无效等)。

阻塞与非阻塞:

  • 阻塞套接字(默认):调用 connect 会阻塞(挂起)当前进程,直到连接成功建立或发生错误。对于 TCP,这意味着等待三次握手完成。

非阻塞套接字(通过 SOCK_NONBLOCK 或 fcntl 设置):调用 connect 会立即返回。

  • 如果连接不能立即建立,connect 返回 -1,并将 errno 设置为 EINPROGRESS。

  • 程序需要使用 select、poll 或 epoll 来检查套接字何时变为可写(表示连接完成),然后使用 getsockopt 检查 SO_ERROR 选项来确定连接最终是成功还是失败。

6. 相似函数,或关联函数

  • socket: 用于创建套接字,是 connect 的前置步骤。

  • bind: (服务器端)将套接字绑定到本地地址。客户端通常不需要显式调用 bind。

  • listen / accept: (服务器端)用于监听和接受客户端的连接请求。

  • getpeername: 连接建立后,用于获取对方(peer)的地址信息。

  • getsockname: 用于获取本地套接字的地址信息。

  • close: 关闭套接字,对于 TCP 连接,这会发起断开连接的四次挥手过程。

7. 示例代码

示例 1:基本的 TCP 客户端 connect

这个例子演示了一个典型的 TCP 客户端如何使用 connect 连接到服务器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// tcp_client.c
#include <sys/socket.h> // socket, connect
#include <netinet/in.h> // sockaddr_in
#include <arpa/inet.h> // inet_pton
#include <unistd.h> // close, write, read
#include <stdio.h> // perror, printf, fprintf
#include <stdlib.h> // exit
#include <string.h> // strlen, memset

#define PORT 8080
#define SERVER_IP "127.0.0.1" // 可替换为实际服务器 IP

int main() {
int sock = 0;
struct sockaddr_in serv_addr;
char *hello = "Hello from TCP client";
char buffer&#91;1024] = {0};
int valread;

// 1. 创建 TCP 套接字
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
printf("TCP client socket created (fd: %d)\n", sock);

// 2. 配置服务器地址
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);

// 将 IPv4 地址从文本转换为二进制
if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {
fprintf(stderr, "Invalid address/ Address not supported: %s\n", SERVER_IP);
close(sock);
exit(EXIT_FAILURE);
}

// 3. 发起连接 (阻塞调用)
printf("Connecting to %s:%d...\n", SERVER_IP, PORT);
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
perror("connection failed");
close(sock);
exit(EXIT_FAILURE);
}
printf("Connected to server successfully.\n");

// 4. 发送数据
printf("Sending message: %s\n", hello);
if (write(sock, hello, strlen(hello)) != (ssize_t)strlen(hello)) {
perror("write failed");
// 注意:write 返回 ssize_t,strlen 返回 size_t
// 比较时最好类型一致或强制转换
} else {
printf("Message sent successfully.\n");
}

// 5. 接收响应
printf("Waiting for server response...\n");
valread = read(sock, buffer, sizeof(buffer) - 1);
if (valread > 0) {
buffer&#91;valread] = '\0'; // 确保字符串结束
printf("Received from server: %s\n", buffer);
} else if (valread == 0) {
printf("Server closed the connection.\n");
} else {
perror("read failed");
}

// 6. 关闭套接字
close(sock);
printf("Client socket closed.\n");

return 0;
}

代码解释:

使用 socket(AF_INET, SOCK_STREAM, 0) 创建一个 IPv4 TCP 套接字。

初始化 struct sockaddr_in 结构体 serv_addr,填入服务器的 IP 地址和端口号。

  • 使用 inet_pton(AF_INET, …) 将点分十进制的 IP 字符串转换为网络二进制格式。

  • 使用 htons(PORT) 将端口号从主机字节序转换为网络字节序。

关键步骤: 调用 connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr))。

  • 这是一个阻塞调用。程序会在此处暂停,直到连接建立(三次握手完成)或失败。

  • 如果服务器没有运行或无法访问,connect 会失败并返回 -1,同时设置 errno。

连接成功后,使用 write 发送数据到服务器。

使用 read 从服务器接收数据。

通信结束后,调用 close 关闭套接字。

示例 2:UDP 客户端使用 connect 简化通信

这个例子展示了如何在 UDP 客户端中使用 connect 来设置默认目标地址,从而可以使用 read/write 而不是 sendto/recvfrom。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// udp_client_with_connect.c
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define PORT 8081
#define SERVER_IP "127.0.0.1"

int main() {
int sock;
struct sockaddr_in serv_addr;
char *message = "Hello UDP server via connect!";
char buffer&#91;1024];
ssize_t bytes_sent, bytes_received;

// 1. 创建 UDP 套接字
sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
printf("UDP client socket created (fd: %d)\n", sock);

// 2. 配置服务器地址
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {
fprintf(stderr, "Invalid address/ Address not supported\n");
close(sock);
exit(EXIT_FAILURE);
}

// 3. 使用 connect 设置默认目标地址 (UDP 的 connect 不发送数据包)
printf("Connecting UDP socket to %s:%d (sets default destination)...\n", SERVER_IP, PORT);
if (connect(sock, (const struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
perror("connect failed");
close(sock);
exit(EXIT_FAILURE);
}
printf("UDP socket 'connected' (default destination set).\n");

// 4. 发送数据 (无需指定地址)
// write/send 都可以,因为目标地址已通过 connect 设置
bytes_sent = write(sock, message, strlen(message));
if (bytes_sent < 0) {
perror("write failed");
} else {
printf("Sent %zd bytes to server: %s\n", bytes_sent, message);
}

// 5. 接收数据 (只接收来自已连接地址的数据)
// read/recv 都可以
bytes_received = read(sock, buffer, sizeof(buffer) - 1);
if (bytes_received < 0) {
perror("read failed");
} else if (bytes_received == 0) {
printf("Server closed the (logical) connection.\n");
} else {
buffer&#91;bytes_received] = '\0';
printf("Received %zd bytes from server: %s\n", bytes_received, buffer);
}

// 6. 关闭套接字
close(sock);
printf("UDP client socket closed.\n");

return 0;
}

代码解释:

创建一个 SOCK_DGRAM (UDP) 套接字。

配置服务器地址 serv_addr。

关键步骤: 调用 connect(sock, …). 对于 UDP,这不会发送任何网络数据包。

它只是告诉内核:“对于这个套接字 sock,如果没有特别指定,以后发送的数据就发到 serv_addr,接收的数据也只接受来自 serv_addr 的。”

连接后,可以使用 write/read 或 send/recv 进行数据传输,无需再指定目标地址(不像 sendto/recvfrom 那样)。

这简化了 UDP 客户端的代码,使其用法更接近 TCP。

示例 3:非阻塞 connect (高级用法)

这个例子演示了如何对非阻塞套接字使用 connect,并使用 select 来等待连接完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
// nonblocking_connect.c
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h> // fcntl
#include <sys/select.h> // select
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h> // errno

#define PORT 8080
#define SERVER_IP "127.0.0.1"
#define TIMEOUT_SEC 5

int main() {
int sock;
struct sockaddr_in serv_addr;
fd_set write_fds;
struct timeval timeout;
int error;
socklen_t len = sizeof(error);

// 1. 创建 TCP 套接字
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}

// 2. 将套接字设置为非阻塞模式
int flags = fcntl(sock, F_GETFL, 0);
if (flags < 0) {
perror("fcntl F_GETFL failed");
close(sock);
exit(EXIT_FAILURE);
}
if (fcntl(sock, F_SETFL, flags | O_NONBLOCK) < 0) {
perror("fcntl F_SETFL failed");
close(sock);
exit(EXIT_FAILURE);
}
printf("Socket set to non-blocking mode.\n");

// 3. 配置服务器地址
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {
fprintf(stderr, "Invalid address\n");
close(sock);
exit(EXIT_FAILURE);
}

// 4. 发起连接 (非阻塞调用)
printf("Initiating non-blocking connect to %s:%d...\n", SERVER_IP, PORT);
int conn_result = connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr));

if (conn_result < 0) {
if (errno != EINPROGRESS) {
perror("connect failed with unexpected error");
close(sock);
exit(EXIT_FAILURE);
}
// 如果 errno == EINPROGRESS, 连接正在进行中
printf("Connect in progress...\n");
} else {
// 立即成功 (罕见)
printf("Connect succeeded immediately.\n");
close(sock);
return 0;
}

// 5. 使用 select 等待套接字变为可写 (连接完成或失败)
FD_ZERO(&write_fds);
FD_SET(sock, &write_fds);
timeout.tv_sec = TIMEOUT_SEC;
timeout.tv_usec = 0;

printf("Waiting up to %d seconds for connection to complete...\n", TIMEOUT_SEC);
int select_result = select(sock + 1, NULL, &write_fds, NULL, &timeout);

if (select_result < 0) {
perror("select failed");
close(sock);
exit(EXIT_FAILURE);
} else if (select_result == 0) {
printf("Connection timed out after %d seconds.\n", TIMEOUT_SEC);
close(sock);
exit(EXIT_FAILURE);
} else {
// select 返回 > 0, 表示至少有一个 fd 就绪
// 我们只监视了 sock 的可写事件
if (FD_ISSET(sock, &write_fds)) {
// 套接字可写,连接过程完成(成功或失败)
// 需要通过 getsockopt 检查 SO_ERROR 来确定最终结果
if (getsockopt(sock, SOL_SOCKET, SO_ERROR, &error, &len) < 0) {
perror("getsockopt failed");
close(sock);
exit(EXIT_FAILURE);
}

if (error != 0) {
// error 变量包含了 connect 失败时的 errno 值
errno = error;
perror("connect failed asynchronously");
close(sock);
exit(EXIT_FAILURE);
} else {
printf("Asynchronous connect succeeded!\n");
}
}
}

// 6. 连接成功,可以进行通信了
printf("Ready to send/receive data on the connected socket.\n");

// ... 这里可以进行 read/write 操作 ...

close(sock);
printf("Socket closed.\n");
return 0;
}

代码解释:

创建 TCP 套接字。

使用 fcntl 将套接字设置为非阻塞模式 (O_NONBLOCK)。

配置服务器地址。

调用 connect。因为套接字是非阻塞的:

  • 如果连接能立即建立,connect 返回 0(罕见)。

  • 如果连接不能立即建立(通常是这种情况),connect 返回 -1,并将 errno 设置为 EINPROGRESS。这表明连接正在后台进行。

关键: 使用 select 来等待连接完成。

  • 监视套接字的可写 (write_fds) 事件。对于非阻塞 connect,当连接尝试完成(无论成功还是失败)时,套接字会变为可写。

  • 设置一个超时时间,避免无限期等待。

select 返回后,检查是超时还是套接字就绪。

如果套接字就绪,调用 getsockopt(sock, SOL_SOCKET, SO_ERROR, …) 来获取连接的最终状态。

  • 如果 SO_ERROR 的值为 0,表示连接成功。

  • 如果 SO_ERROR 的值非 0,该值就是连接失败时的错误码,将其赋给 errno 并打印错误信息。

连接成功后,套接字就可以像平常一样用于 read/write 了。

重要提示与注意事项:

TCP 三次握手: 对于 TCP 套接字,connect 的核心作用是启动并等待三次握手完成。

UDP 的特殊性: 对于 UDP,connect 不涉及网络交互,仅在内核中设置默认地址。

阻塞 vs 非阻塞: 理解阻塞和非阻塞 connect 的行为差异对于编写高性能或响应式网络程序至关重要。

错误处理: connect 失败的错误码 (errno) 提供了丰富的信息,如 ECONNREFUSED (端口未监听), ETIMEDOUT (超时), ENETUNREACH (网络不可达) 等。

客户端通常不 bind: 客户端程序通常不需要调用 bind 来绑定本地地址,操作系统会自动分配一个临时端口。

getpeername: 连接建立后,可以使用 getpeername 来确认连接的对端地址。

总结:

connect 是 TCP 客户端发起网络连接的关键函数,它对于 UDP 客户端则提供了一种简化地址管理的方法。掌握其在阻塞和非阻塞模式下的行为对于进行有效的网络编程非常重要。

data-ad-format="auto" data-full-width-responsive="true">