Linux C语言Socket编程的连接管理
2024-04-254.3k 阅读
一、Socket 连接概述
在 Linux 环境下,使用 C 语言进行 Socket 编程时,连接管理是关键部分。Socket 是一种进程间通信(IPC)机制,它允许不同主机或同一主机上的进程进行通信。连接管理涉及到建立连接、维护连接以及关闭连接等操作。
(一)Socket 地址结构
在进行连接之前,需要了解 Socket 地址结构。在 Linux 中,常用的地址结构是 sockaddr
和 sockaddr_in
。sockaddr
是通用的地址结构,而 sockaddr_in
是专门为 IPv4 设计的地址结构。
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
};
struct sockaddr_in {
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
struct in_addr {
in_addr_t s_addr;
};
sin_family
通常设置为AF_INET
表示 IPv4 协议。sin_port
是端口号,需要使用网络字节序(可以通过htons
函数将主机字节序转换为网络字节序)。sin_addr.s_addr
是 IP 地址,同样需要使用网络字节序(可以通过inet_addr
或inet_pton
函数进行转换)。
(二)Socket 类型
在连接管理中,了解不同的 Socket 类型很重要。常见的 Socket 类型有:
- 流式 Socket(SOCK_STREAM):提供面向连接、可靠的数据传输服务,基于 TCP 协议。数据以字节流的形式传输,保证数据的顺序和完整性。
- 数据报 Socket(SOCK_DGRAM):提供无连接的数据传输服务,基于 UDP 协议。数据以独立的数据包形式传输,不保证数据的顺序和可靠性,但传输效率较高。
二、TCP 连接管理
(一)服务器端
- 创建 Socket
使用
socket
函数创建一个 Socket 描述符。
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domain
通常为AF_INET
表示 IPv4 网络。type
对于 TCP 连接,设置为SOCK_STREAM
。protocol
一般设置为 0,表示使用默认协议(对于 TCP 就是 TCP 协议)。
示例代码:
int sockfd;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
- 绑定地址 将创建的 Socket 绑定到一个特定的地址和端口。
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd
是之前创建的 Socket 描述符。addr
是一个指向sockaddr
结构的指针,通常需要将sockaddr_in
结构强制转换为sockaddr
结构。addrlen
是地址结构的长度。
示例代码:
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8080);
servaddr.sin_addr.s_addr = INADDR_ANY;
if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
perror("bind failed");
close(sockfd);
exit(EXIT_FAILURE);
}
- 监听连接 使 Socket 处于监听状态,准备接受客户端的连接请求。
#include <sys/socket.h>
int listen(int sockfd, int backlog);
sockfd
是绑定后的 Socket 描述符。backlog
表示等待连接队列的最大长度。
示例代码:
if (listen(sockfd, 5) < 0) {
perror("listen failed");
close(sockfd);
exit(EXIT_FAILURE);
}
- 接受连接 接受客户端的连接请求,并返回一个新的 Socket 描述符用于与客户端通信。
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd
是监听的 Socket 描述符。addr
用于返回客户端的地址信息(可以为NULL
)。addrlen
是addr
的长度(传入时为初始长度,返回时为实际地址长度)。
示例代码:
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
int connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &len);
if (connfd < 0) {
perror("accept failed");
close(sockfd);
exit(EXIT_FAILURE);
}
(二)客户端
- 创建 Socket
与服务器端类似,使用
socket
函数创建 Socket 描述符。
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
- 连接服务器
使用
connect
函数连接到服务器。
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd
是创建的 Socket 描述符。addr
是服务器的地址结构。addrlen
是地址结构的长度。
示例代码:
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8080);
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
if (connect(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
perror("connect failed");
close(sockfd);
exit(EXIT_FAILURE);
}
三、UDP 连接管理
(一)服务器端
- 创建 Socket
对于 UDP,同样使用
socket
函数,但type
设置为SOCK_DGRAM
。
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
- 绑定地址 与 TCP 服务器端类似,绑定到特定的地址和端口。
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8080);
servaddr.sin_addr.s_addr = INADDR_ANY;
if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
perror("bind failed");
close(sockfd);
exit(EXIT_FAILURE);
}
- 接收数据
使用
recvfrom
函数接收数据。
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
sockfd
是 Socket 描述符。buf
是接收数据的缓冲区。len
是缓冲区的长度。flags
一般设置为 0。src_addr
用于返回发送方的地址信息。addrlen
是src_addr
的长度。
示例代码:
char buffer[1024];
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
ssize_t n = recvfrom(sockfd, (char *)buffer, 1024, MSG_WAITALL,
(struct sockaddr *)&cliaddr, &len);
buffer[n] = '\0';
printf("Message from client: %s\n", buffer);
(二)客户端
- 创建 Socket
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
- 发送数据
使用
sendto
函数发送数据。
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
sockfd
是 Socket 描述符。buf
是要发送的数据缓冲区。len
是数据的长度。flags
一般设置为 0。dest_addr
是目标地址结构。addrlen
是目标地址结构的长度。
示例代码:
char *message = "Hello, server!";
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8080);
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
sendto(sockfd, (const char *)message, strlen(message), MSG_CONFIRM,
(const struct sockaddr *)&servaddr, sizeof(servaddr));
四、连接维护与异常处理
(一)心跳机制
在长时间的连接中,为了确保连接的有效性,常使用心跳机制。以 TCP 为例,服务器和客户端可以定期互相发送简单的心跳包。
- 服务器端心跳示例
// 假设已经建立连接,connfd 为连接描述符
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(connfd, &read_fds);
struct timeval tv;
tv.tv_sec = 10; // 10 秒超时
tv.tv_usec = 0;
int activity = select(connfd + 1, &read_fds, NULL, NULL, &tv);
if (activity < 0) {
perror("select error");
} else if (activity == 0) {
// 超时,可能连接已断开,可选择重新发送心跳或关闭连接
printf("Heartbeat timeout, connection may be lost\n");
} else {
// 有数据可读,可能是心跳包或其他数据
char buffer[1024];
ssize_t n = recv(connfd, buffer, 1024, 0);
buffer[n] = '\0';
if (strcmp(buffer, "heartbeat") == 0) {
// 收到心跳包,回复心跳
send(connfd, "heartbeat", strlen("heartbeat"), 0);
}
}
- 客户端心跳示例
// 假设 sockfd 为连接到服务器的 Socket 描述符
while (1) {
send(sockfd, "heartbeat", strlen("heartbeat"), 0);
char buffer[1024];
ssize_t n = recv(sockfd, buffer, 1024, 0);
buffer[n] = '\0';
if (strcmp(buffer, "heartbeat") != 0) {
// 未收到正确的心跳回复,可能连接有问题
printf("Unexpected response from server, connection may be broken\n");
break;
}
sleep(10); // 每隔 10 秒发送一次心跳
}
(二)错误处理
在连接管理过程中,可能会遇到各种错误。例如,socket
函数可能因为系统资源不足而创建失败,connect
函数可能因为服务器未启动或网络问题而连接失败。
- 通用错误处理
在每次系统调用后,通过检查返回值来判断是否发生错误。如果返回值小于 0,使用
perror
函数打印错误信息。
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
- 特定错误处理
- 连接超时:在
connect
操作中,可以通过设置SO_SNDTIMEO
和SO_RCVTIMEO
套接字选项来设置连接超时时间。
struct timeval timeout;
timeout.tv_sec = 5; // 5 秒超时
timeout.tv_usec = 0;
setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, (const char *)&timeout, sizeof(timeout));
setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (const char *)&timeout, sizeof(timeout));
if (connect(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
if (errno == EINPROGRESS) {
// 连接超时
printf("Connection timed out\n");
} else {
perror("connect failed");
}
close(sockfd);
exit(EXIT_FAILURE);
}
五、连接关闭
(一)TCP 连接关闭
- 正常关闭
- 主动关闭方:在完成数据传输后,调用
close
函数关闭连接。
- 主动关闭方:在完成数据传输后,调用
close(connfd);
- 被动关闭方:当收到主动关闭方发送的 FIN 包后,也调用
close
函数关闭连接。
- 优雅关闭
使用
shutdown
函数可以实现更优雅的关闭方式。
#include <sys/socket.h>
int shutdown(int sockfd, int how);
how
参数可以取值:SHUT_RD
:关闭读操作,不再接收数据。SHUT_WR
:关闭写操作,不再发送数据。SHUT_RDWR
:关闭读写操作。
示例代码:
// 关闭写操作,继续接收数据
shutdown(connfd, SHUT_WR);
(二)UDP 连接关闭
UDP 是无连接的协议,在完成数据传输后,直接调用 close
函数关闭 Socket 即可。
close(sockfd);
通过以上对 Linux C 语言 Socket 编程连接管理的详细介绍,从连接的建立、维护到关闭,涵盖了 TCP 和 UDP 两种常见协议的相关操作,并结合了实际的代码示例和错误处理机制,希望能帮助开发者更好地理解和应用 Socket 连接管理技术。在实际的网络编程中,还需要根据具体的需求和场景进行适当的优化和扩展。