MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Linux C语言Socket编程的连接管理

2024-04-254.3k 阅读

一、Socket 连接概述

在 Linux 环境下,使用 C 语言进行 Socket 编程时,连接管理是关键部分。Socket 是一种进程间通信(IPC)机制,它允许不同主机或同一主机上的进程进行通信。连接管理涉及到建立连接、维护连接以及关闭连接等操作。

(一)Socket 地址结构

在进行连接之前,需要了解 Socket 地址结构。在 Linux 中,常用的地址结构是 sockaddrsockaddr_insockaddr 是通用的地址结构,而 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_addrinet_pton 函数进行转换)。

(二)Socket 类型

在连接管理中,了解不同的 Socket 类型很重要。常见的 Socket 类型有:

  1. 流式 Socket(SOCK_STREAM):提供面向连接、可靠的数据传输服务,基于 TCP 协议。数据以字节流的形式传输,保证数据的顺序和完整性。
  2. 数据报 Socket(SOCK_DGRAM):提供无连接的数据传输服务,基于 UDP 协议。数据以独立的数据包形式传输,不保证数据的顺序和可靠性,但传输效率较高。

二、TCP 连接管理

(一)服务器端

  1. 创建 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);
}
  1. 绑定地址 将创建的 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);
}
  1. 监听连接 使 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);
}
  1. 接受连接 接受客户端的连接请求,并返回一个新的 Socket 描述符用于与客户端通信。
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • sockfd 是监听的 Socket 描述符。
  • addr 用于返回客户端的地址信息(可以为 NULL)。
  • addrlenaddr 的长度(传入时为初始长度,返回时为实际地址长度)。

示例代码:

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);
}

(二)客户端

  1. 创建 Socket 与服务器端类似,使用 socket 函数创建 Socket 描述符。
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
    perror("socket creation failed");
    exit(EXIT_FAILURE);
}
  1. 连接服务器 使用 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 连接管理

(一)服务器端

  1. 创建 Socket 对于 UDP,同样使用 socket 函数,但 type 设置为 SOCK_DGRAM
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
    perror("socket creation failed");
    exit(EXIT_FAILURE);
}
  1. 绑定地址 与 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);
}
  1. 接收数据 使用 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 用于返回发送方的地址信息。
  • addrlensrc_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);

(二)客户端

  1. 创建 Socket
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
    perror("socket creation failed");
    exit(EXIT_FAILURE);
}
  1. 发送数据 使用 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 为例,服务器和客户端可以定期互相发送简单的心跳包。

  1. 服务器端心跳示例
// 假设已经建立连接,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);
    }
}
  1. 客户端心跳示例
// 假设 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 函数可能因为服务器未启动或网络问题而连接失败。

  1. 通用错误处理 在每次系统调用后,通过检查返回值来判断是否发生错误。如果返回值小于 0,使用 perror 函数打印错误信息。
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
    perror("socket creation failed");
    exit(EXIT_FAILURE);
}
  1. 特定错误处理
  • 连接超时:在 connect 操作中,可以通过设置 SO_SNDTIMEOSO_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 连接关闭

  1. 正常关闭
    • 主动关闭方:在完成数据传输后,调用 close 函数关闭连接。
close(connfd);
  • 被动关闭方:当收到主动关闭方发送的 FIN 包后,也调用 close 函数关闭连接。
  1. 优雅关闭 使用 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 连接管理技术。在实际的网络编程中,还需要根据具体的需求和场景进行适当的优化和扩展。