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

Linux C语言socket编程接口详解

2024-10-195.6k 阅读

一、socket 编程基础概念

在深入探讨 Linux C 语言 socket 编程接口之前,我们先来了解一些基本概念。Socket(套接字)最初是由加利福尼亚大学伯克利分校为 UNIX 操作系统开发的一种进程间通信(IPC)机制,它提供了一种在不同主机或同一主机上的不同进程之间进行通信的方式。Socket 可以看作是应用层与传输层之间的桥梁,使得应用程序能够方便地使用网络协议进行数据传输。

在网络通信中,有两个重要的概念:地址族(Address Family)和套接字类型(Socket Type)。

1.1 地址族

地址族定义了网络地址的类型和格式。常见的地址族有 AF_INET(IPv4 地址族)和 AF_INET6(IPv6 地址族)。AF_INET 用于 IPv4 网络,它使用 32 位的 IP 地址和 16 位的端口号来标识网络中的一个进程。AF_INET6 则用于 IPv6 网络,它使用 128 位的 IP 地址,提供了更大的地址空间和更好的路由效率。

此外,还有 AF_UNIX 地址族,它用于本地进程间通信,使用文件系统路径作为地址。

1.2 套接字类型

套接字类型决定了通信的特性,常见的套接字类型有:

  • SOCK_STREAM:流式套接字,提供面向连接的、可靠的字节流服务。这种类型使用传输控制协议(TCP),在通信前需要先建立连接,数据传输过程中保证数据的顺序和完整性,适用于对数据准确性要求较高的应用,如文件传输、远程登录等。
  • SOCK_DGRAM:数据报套接字,提供无连接的、不可靠的数据报服务。它使用用户数据报协议(UDP),通信时不需要建立连接,直接发送数据报,但不保证数据的顺序和可靠性,适用于对实时性要求较高但对数据准确性要求相对较低的应用,如音频、视频流传输等。
  • SOCK_RAW:原始套接字,允许应用程序直接访问底层网络协议,绕过传输层协议(如 TCP 和 UDP)。它主要用于开发网络协议分析工具、自定义网络协议等。

二、Linux C 语言 socket 编程接口函数

接下来,我们详细介绍 Linux C 语言中用于 socket 编程的主要接口函数。

2.1 socket 函数

#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
  • 参数说明

    • domain:指定地址族,如 AF_INET、AF_INET6 或 AF_UNIX。
    • type:指定套接字类型,如 SOCK_STREAM、SOCK_DGRAM 或 SOCK_RAW。
    • protocol:通常设置为 0,表示使用默认协议。对于 SOCK_STREAM 类型,默认协议是 TCP;对于 SOCK_DGRAM 类型,默认协议是 UDP。
  • 返回值:成功时返回一个非负整数,即套接字描述符(socket descriptor),类似于文件描述符,后续的 socket 操作都将使用这个描述符。失败时返回 -1,并设置 errno 以指示错误原因。

2.2 bind 函数

#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 参数说明

    • sockfd:由 socket 函数返回的套接字描述符。
    • addr:指向一个 struct sockaddr 类型的结构体指针,该结构体包含了要绑定的地址信息。对于 AF_INET 地址族,实际使用的是 struct sockaddr_in 结构体,它包含了 IP 地址和端口号等信息。
    • addrlenaddr 结构体的长度。
  • 返回值:成功时返回 0,失败时返回 -1,并设置 errno 指示错误原因。

2.3 listen 函数

#include <sys/socket.h>
int listen(int sockfd, int backlog);
  • 参数说明

    • sockfd:套接字描述符。
    • backlog:指定等待连接队列的最大长度。当有多个客户端同时请求连接时,未处理的连接请求将被放入这个队列中。
  • 返回值:成功时返回 0,失败时返回 -1,并设置 errno 指示错误原因。此函数仅用于面向连接的套接字(如 SOCK_STREAM),在调用 accept 函数之前需要先调用 listen 函数。

2.4 accept 函数

#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 参数说明

    • sockfd:监听套接字描述符,即调用 listen 函数后的套接字。
    • addr:可选参数,指向一个 struct sockaddr 类型的结构体,用于存放客户端的地址信息。
    • addrlen:一个指向 socklen_t 类型的变量的指针,用于传入 addr 结构体的初始长度,并在函数返回时更新为实际接收到的地址长度。
  • 返回值:成功时返回一个新的套接字描述符,用于与客户端进行通信。失败时返回 -1,并设置 errno 指示错误原因。

2.5 connect 函数

#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 参数说明

    • sockfd:套接字描述符。
    • addr:指向一个 struct sockaddr 类型的结构体指针,包含了要连接的服务器地址信息。
    • addrlenaddr 结构体的长度。
  • 返回值:成功时返回 0,失败时返回 -1,并设置 errno 指示错误原因。此函数用于客户端连接到服务器,在面向连接的套接字(如 SOCK_STREAM)中使用。

2.6 send 和 recv 函数

#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  • 参数说明

    • sockfd:套接字描述符。
    • buf:对于 send 函数,是指向要发送数据的缓冲区指针;对于 recv 函数,是指向接收数据的缓冲区指针。
    • len:要发送或接收的数据长度。
    • flags:通常设置为 0,用于指定一些额外的选项,如 MSG_DONTROUTE 表示不使用路由表查找等。
  • 返回值send 函数成功时返回实际发送的字节数,失败时返回 -1,并设置 errno 指示错误原因。recv 函数成功时返回实际接收到的字节数,当连接关闭时返回 0,失败时返回 -1,并设置 errno 指示错误原因。

2.7 sendto 和 recvfrom 函数

#include <sys/types.h>
#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);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);
  • 参数说明

    • sockfd:套接字描述符。
    • buf:与 sendrecv 函数类似,分别指向发送和接收数据的缓冲区。
    • len:要发送或接收的数据长度。
    • flags:通常设置为 0,用于指定额外选项。
    • dest_addrsendto 函数):指向目标地址的 struct sockaddr 结构体指针。
    • src_addrrecvfrom 函数):指向源地址的 struct sockaddr 结构体指针,用于存放发送方的地址信息。
    • addrlen:对于 sendto 函数,是 dest_addr 结构体的长度;对于 recvfrom 函数,是一个指向 socklen_t 类型变量的指针,用于传入 src_addr 结构体的初始长度,并在函数返回时更新为实际接收到的地址长度。
  • 返回值:与 sendrecv 函数类似,sendto 成功时返回实际发送的字节数,失败时返回 -1;recvfrom 成功时返回实际接收到的字节数,连接关闭时返回 0,失败时返回 -1。这两个函数主要用于无连接的套接字(如 SOCK_DUDP),也可用于面向连接的套接字。

2.8 close 函数

#include <unistd.h>
int close(int fd);
  • 参数说明fd 是要关闭的套接字描述符。

  • 返回值:成功时返回 0,失败时返回 -1,并设置 errno 指示错误原因。此函数用于关闭套接字,释放相关资源。

三、基于 TCP 的 socket 编程示例

下面我们通过一个简单的示例来演示基于 TCP 的 socket 编程,包括服务器端和客户端代码。

3.1 服务器端代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>

#define PORT 8080
#define MAX_CLIENTS 10
#define BUFFER_SIZE 1024

void error_handling(const char *message) {
    perror(message);
    exit(1);
}

int main() {
    int server_socket, client_socket;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    char buffer[BUFFER_SIZE];

    // 创建套接字
    server_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (server_socket == -1) {
        error_handling("socket creation failed");
    }

    // 初始化服务器地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    server_addr.sin_port = htons(PORT);

    // 绑定地址
    if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        error_handling("bind failed");
    }

    // 监听连接
    if (listen(server_socket, MAX_CLIENTS) == -1) {
        error_handling("listen failed");
    }
    printf("Server is listening on port %d...\n", PORT);

    // 接受客户端连接
    client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &client_addr_len);
    if (client_socket == -1) {
        error_handling("accept failed");
    }

    // 接收数据
    memset(buffer, 0, BUFFER_SIZE);
    int bytes_received = recv(client_socket, buffer, BUFFER_SIZE - 1, 0);
    if (bytes_received == -1) {
        error_handling("recv failed");
    } else if (bytes_received == 0) {
        printf("Connection closed by client\n");
    } else {
        buffer[bytes_received] = '\0';
        printf("Received from client: %s\n", buffer);
    }

    // 发送数据
    const char *response = "Message received successfully!";
    if (send(client_socket, response, strlen(response), 0) == -1) {
        error_handling("send failed");
    }

    // 关闭套接字
    close(client_socket);
    close(server_socket);

    return 0;
}

3.2 客户端代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>

#define PORT 8080
#define SERVER_IP "127.0.0.1"
#define BUFFER_SIZE 1024

void error_handling(const char *message) {
    perror(message);
    exit(1);
}

int main() {
    int client_socket;
    struct sockaddr_in server_addr;
    char buffer[BUFFER_SIZE];

    // 创建套接字
    client_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (client_socket == -1) {
        error_handling("socket creation failed");
    }

    // 初始化服务器地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
    server_addr.sin_port = htons(PORT);

    // 连接服务器
    if (connect(client_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        error_handling("connect failed");
    }

    // 发送数据
    const char *message = "Hello, server!";
    if (send(client_socket, message, strlen(message), 0) == -1) {
        error_handling("send failed");
    }

    // 接收数据
    memset(buffer, 0, BUFFER_SIZE);
    int bytes_received = recv(client_socket, buffer, BUFFER_SIZE - 1, 0);
    if (bytes_received == -1) {
        error_handling("recv failed");
    } else if (bytes_received == 0) {
        printf("Connection closed by server\n");
    } else {
        buffer[bytes_received] = '\0';
        printf("Received from server: %s\n", buffer);
    }

    // 关闭套接字
    close(client_socket);

    return 0;
}

在上述示例中,服务器端首先创建一个 TCP 套接字,绑定到指定的 IP 地址和端口,然后监听客户端连接。当有客户端连接时,接受连接并接收客户端发送的数据,然后向客户端发送响应。客户端则创建套接字并连接到服务器,发送数据后等待接收服务器的响应。

四、基于 UDP 的 socket 编程示例

接下来我们看一个基于 UDP 的 socket 编程示例,同样包括服务器端和客户端代码。

4.1 服务器端代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>

#define PORT 8080
#define BUFFER_SIZE 1024

void error_handling(const char *message) {
    perror(message);
    exit(1);
}

int main() {
    int server_socket;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    char buffer[BUFFER_SIZE];

    // 创建套接字
    server_socket = socket(AF_INET, SOCK_DGRAM, 0);
    if (server_socket == -1) {
        error_handling("socket creation failed");
    }

    // 初始化服务器地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);

    // 绑定地址
    if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        error_handling("bind failed");
    }

    // 接收数据
    memset(buffer, 0, BUFFER_SIZE);
    int bytes_received = recvfrom(server_socket, buffer, BUFFER_SIZE - 1, 0,
                                  (struct sockaddr *)&client_addr, &client_addr_len);
    if (bytes_received == -1) {
        error_handling("recvfrom failed");
    } else {
        buffer[bytes_received] = '\0';
        printf("Received from client: %s\n", buffer);
    }

    // 发送数据
    const char *response = "Message received successfully!";
    if (sendto(server_socket, response, strlen(response), 0,
               (struct sockaddr *)&client_addr, client_addr_len) == -1) {
        error_handling("sendto failed");
    }

    // 关闭套接字
    close(server_socket);

    return 0;
}

4.2 客户端代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>

#define PORT 8080
#define SERVER_IP "127.0.0.1"
#define BUFFER_SIZE 1024

void error_handling(const char *message) {
    perror(message);
    exit(1);
}

int main() {
    int client_socket;
    struct sockaddr_in server_addr;
    char buffer[BUFFER_SIZE];

    // 创建套接字
    client_socket = socket(AF_INET, SOCK_DUDP, 0);
    if (client_socket == -1) {
        error_handling("socket creation failed");
    }

    // 初始化服务器地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
    server_addr.sin_port = htons(PORT);

    // 发送数据
    const char *message = "Hello, server!";
    if (sendto(client_socket, message, strlen(message), 0,
               (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        error_handling("sendto failed");
    }

    // 接收数据
    socklen_t server_addr_len = sizeof(server_addr);
    memset(buffer, 0, BUFFER_SIZE);
    int bytes_received = recvfrom(client_socket, buffer, BUFFER_SIZE - 1, 0,
                                  (struct sockaddr *)&server_addr, &server_addr_len);
    if (bytes_received == -1) {
        error_handling("recvfrom failed");
    } else {
        buffer[bytes_received] = '\0';
        printf("Received from server: %s\n", buffer);
    }

    // 关闭套接字
    close(client_socket);

    return 0;
}

在这个 UDP 示例中,服务器端和客户端都创建 UDP 套接字。客户端直接向服务器发送数据报,服务器接收数据报后进行处理并返回响应。与 TCP 不同,UDP 不需要建立连接,数据的发送和接收更加简单直接,但不保证数据的可靠性和顺序性。

五、socket 编程中的错误处理

在 socket 编程中,错误处理是非常重要的。每个 socket 函数在失败时都会设置 errno 变量,通过检查 errno 可以确定错误的具体原因。常见的错误有:

  • EACCES:权限不足,例如试图绑定到一个受保护的端口。
  • EADDRINUSE:地址已被使用,当试图绑定到一个已经被其他进程占用的地址和端口时会出现此错误。
  • ECONNREFUSED:连接被拒绝,通常是因为服务器没有在指定的端口监听。
  • EFAULT:无效的指针,例如传递给函数的地址指针无效。
  • ENETUNREACH:网络不可达,可能是网络配置问题或目标主机不可达。

在编写 socket 程序时,应该在每个可能出错的函数调用后检查返回值,并根据 errno 进行相应的错误处理,以提高程序的稳定性和健壮性。

六、高级 socket 编程主题

6.1 多线程 socket 编程

在实际应用中,服务器可能需要同时处理多个客户端的连接。使用多线程可以有效地实现这一点。每个客户端连接可以由一个单独的线程来处理,这样服务器可以在不阻塞的情况下同时与多个客户端进行通信。

6.2 非阻塞 I/O

默认情况下,socket I/O 操作是阻塞的,即当调用 recvsend 等函数时,线程会被阻塞,直到数据准备好或操作完成。非阻塞 I/O 允许在数据未准备好时立即返回,这样可以提高程序的并发性能。可以通过 fcntl 函数设置套接字为非阻塞模式。

6.3 套接字选项

通过 setsockoptgetsockopt 函数,可以设置和获取套接字的各种选项,如 SO_REUSEADDR(允许重用本地地址)、SO_RCVBUF(接收缓冲区大小)、SO_SNDBUF(发送缓冲区大小)等。这些选项可以根据应用的需求进行调整,以优化网络性能。

通过以上对 Linux C 语言 socket 编程接口的详细介绍、示例代码以及高级主题的探讨,希望读者能够对 socket 编程有更深入的理解,并能够在实际项目中灵活运用这些知识。