connect系统调用及示例
好的,我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 connect
函数,它是 TCP 客户端用来向服务器发起连接请求的核心系统调用。
1. 函数介绍
connect
是一个 Linux 系统调用,主要用于TCP 客户端(使用 SOCK_STREAM
套接字)来主动建立与服务器的连接。它也可以用于UDP 客户端(使用 SOCK_DGRAM
套接字)来设置默认的目标地址。
你可以把 connect
想象成拨打电话:
- 你先有了一个电话听筒(通过
socket
创建了套接字)。 - 你知道你要打给谁(知道服务器的 IP 地址和端口号)。
- 你按下拨打键(调用
connect
)。 - 电话那头的服务器响铃,接听后,你们之间的通话线路就建立了。
对于 TCP 来说,connect
会触发 TCP 的**三次握手 **(Three-way Handshake) 过程,这是 TCP 协议用来建立可靠连接的标准步骤。
2. 函数原型
#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 *)
传入。
- 对于 IPv4,通常使用
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
连接到服务器。
// 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" // 可替换为实际服务器 IPint main() {int sock = 0;struct sockaddr_in serv_addr;char *hello = "Hello from TCP client";char buffer[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[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
。
// 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[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[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
来等待连接完成。
// 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 5int 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 客户端则提供了一种简化地址管理的方法。掌握其在阻塞和非阻塞模式下的行为对于进行有效的网络编程非常重要。