[C高手编程] Socket编程、套接字选项与多路复用:深入探索网络通信

在这里插入图片描述

????????⚡️⚡️专栏:C高手编程-面试宝典/技术手册/高手进阶⚡️⚡️????????
「C高手编程」专栏融合了作者十多年的C语言开发经验,汇集了从基础到进阶的关键知识点,是不可多得的知识宝典。如果你是即将毕业的学生,面临C语言的求职面试,本专栏将帮助你扎实地掌握核心概念,轻松应对笔试与面试;如果你已有两三年的工作经验,专栏中的内容将补充你在实践中可能忽略的新技术和技巧;而对于资深的C语言程序员,这里也将是一本实用的技术备查手册,提供全面的知识回顾与更新。无论处在哪个阶段,「C高手编程」都能助你一臂之力,成为C语言领域的行家里手。

概述

本章将详细介绍C语言中的socket编程,包括socket的创建、配置、连接和数据传输等关键概念。通过本章的学习,读者将能够掌握socket编程的基础知识,并能够在实际项目中应用这些知识构建可靠的网络应用程序。

1. Socket编程基础

1.1 Socket定义

  • 定义:Socket(套接字)是用于网络通信的一种抽象接口,它允许一个进程与另一个进程进行通信,无论这两个进程是否在同一台机器上。

1.2 Socket地址结构

  • 地址结构:不同的地址族(如AF_INET和AF_INET6)使用不同的地址结构。
    • struct sockaddr_in:IPv4地址结构。
    • struct sockaddr_in6:IPv6地址结构。

1.3 Socket类型

  • 类型:SOCK_STREAM(流式套接字)、SOCK_DGRAM(数据报套接字)。

在这里插入图片描述

2. Socket创建与配置

2.1 创建Socket

2.1.1 socket函数
  • 函数原型int socket(int domain, int type, int protocol);
  • 头文件<sys/socket.h>
  • 参数
    • domain:地址家族(AF_INET, AF_INET6等)。
    • type:套接字类型(SOCK_STREAM, SOCK_DGRAM等)。
    • protocol:协议(通常为0,表示使用默认协议)。
  • 返回值:成功返回一个整数(文件描述符),失败返回-1。
  • 描述:此函数用于创建一个新的套接字。

2.2 配置Socket

2.2.1 bind函数
  • 函数原型int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 头文件<sys/socket.h>
  • 参数
    • sockfd:套接字文件描述符。
    • addr:指向struct sockaddr的指针。
    • addrlen:地址结构的长度。
  • 返回值:成功返回0,失败返回-1。
  • 描述:此函数用于将套接字与本地地址绑定。

2.3 监听连接

2.3.1 listen函数
  • 函数原型int listen(int sockfd, int backlog);
  • 头文件<sys/socket.h>
  • 参数
    • sockfd:套接字文件描述符。
    • backlog:监听队列的最大长度。
  • 返回值:成功返回0,失败返回-1。
  • 描述:此函数用于将套接字设置为监听模式。

3. 客户端连接

3.1 连接服务器

3.1.1 connect函数
  • 函数原型int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 头文件<sys/socket.h>
  • 参数
    • sockfd:套接字文件描述符。
    • addr:指向struct sockaddr的指针。
    • addrlen:地址结构的长度。
  • 返回值:成功返回0,失败返回-1。
  • 描述:此函数用于建立与服务器的连接。

4. 接收客户端连接

4.1 接受连接

4.1.1 accept函数
  • 函数原型int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 头文件<sys/socket.h>
  • 参数
    • sockfd:监听套接字文件描述符。
    • addr:指向struct sockaddr的指针。
    • addrlen:地址结构的长度。
  • 返回值:成功返回新的套接字文件描述符,失败返回-1。
  • 描述:此函数用于接受客户端的连接请求。

5. 数据传输

5.1 发送数据

5.1.1 send函数
  • 函数原型ssize_t send(int sockfd, const void *buf, size_t len, int flags);
  • 头文件<sys/socket.h>
  • 参数
    • sockfd:套接字文件描述符。
    • buf:指向要发送的数据的指针。
    • len:要发送的数据长度。
    • flags:标志位。
  • 返回值:成功返回发送的字节数,失败返回-1。
  • 描述:此函数用于发送数据。

5.2 接收数据

5.2.1 recv函数
  • 函数原型ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  • 头文件<sys/socket.h>
  • 参数
    • sockfd:套接字文件描述符。
    • buf:指向接收数据的指针。
    • len:接收数据的最大长度。
    • flags:标志位。
  • 返回值:成功返回接收的字节数,失败返回-1。
  • 描述:此函数用于接收数据。

在这里插入图片描述

6. 完整示例:服务器端

下面是一个完整的服务器端示例,该示例展示了如何使用上述函数来创建一个简单的TCP服务器,它监听一个端口并处理来自客户端的连接请求。

#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>

#define PORT 8080
#define BUFFER_SIZE 100

int main() {
    // 创建socket
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("Socket creation failed");
        return 1;
    }

    // 配置socket
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = INADDR_ANY;

    if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("Bind failed");
        return 1;
    }

    // 开始监听
    if (listen(sockfd, 5) == -1) {
        perror("Listen failed");
        return 1;
    }

    printf("Server listening on port %d\n", PORT);

    while (1) {
        // 接受连接
        socklen_t client_len = sizeof(struct sockaddr_in);
        int client_fd = accept(sockfd, (struct sockaddr *)&server_addr, &client_len);
        if (client_fd == -1) {
            perror("Accept failed");
            continue;
        }

        // 接收数据
        char buffer[BUFFER_SIZE];
        ssize_t bytes_received = recv(client_fd, buffer, BUFFER_SIZE, 0);
        if (bytes_received == -1) {
            perror("Receive failed");
            continue;
        }
        buffer[bytes_received] = '\0'; // Null-terminate the string
        printf("Received from %s:%d: %s\n",
               inet_ntoa(server_addr.sin_addr), ntohs(server_addr.sin_port), buffer);

        // 发送响应
        const char *response = "Hello, client!";
        ssize_t bytes_sent = send(client_fd, response, strlen(response), 0);
        if (bytes_sent == -1) {
            perror("Send failed");
            continue;
        }
        printf("Sent %zd bytes\n", bytes_sent);

        // 关闭连接
        close(client_fd);
    }

    // 关闭socket
    close(sockfd);
    return 0;
}

7. 完整示例:客户端端

接下来是一个客户端示例,它展示了如何使用上述函数来创建一个简单的TCP客户端,该客户端连接到服务器并发送消息。

#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>

#define SERVER_PORT 8080
#define BUFFER_SIZE 100

int main() {
    // 创建socket
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("Socket creation failed");
        return 1;
    }

    // 配置server address
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);

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

    // 发送数据
    const char *message = "Hello, server!";
    ssize_t bytes_sent = send(sockfd, message, strlen(message), 0);
    if (bytes_sent == -1) {
        perror("Send failed");
        return 1;
    }
    printf("Sent %zd bytes\n", bytes_sent);

    // 接收数据
    char buffer[BUFFER_SIZE];
    ssize_t bytes_received = recv(sockfd, buffer, BUFFER_SIZE, 0);
    if (bytes_received == -1) {
        perror("Receive failed");
        return 1;
    }
    buffer[bytes_received] = '\0'; // Null-terminate the string
    printf("Received %zd bytes: %s\n", bytes_received, buffer);

    // 关闭socket
    close(sockfd);
    return 0;
}

在这里插入图片描述

8. 套接字选项

8.1 设置套接字选项

8.1.1 setsockopt函数
  • 函数原型int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
  • 头文件<sys/socket.h>
  • 参数
    • sockfd:套接字文件描述符。
    • level:选项级别。
    • optname:选项名称。
    • optval:指向选项值的指针。
    • optlen:选项值的长度。
  • 返回值:成功返回0,失败返回-1。
  • 描述:此函数用于设置套接字选项。

8.2 获取套接字选项

8.2.1 getsockopt函数
  • 函数原型int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
  • 头文件<sys/socket.h>
  • 参数
    • sockfd:套接字文件描述符。
    • level:选项级别。
    • optname:选项名称。
    • optval:指向选项值的指针。
    • optlen:选项值的长度。
  • 返回值:成功返回0,失败返回-1。
  • 描述:此函数用于获取套接字选项。

8.3 常见套接字选项

  • SO_REUSEADDR:允许重新绑定到仍在TIME_WAIT状态的地址。
  • SO_KEEPALIVE:保持连接活动状态。
  • SO_LINGER:控制关闭套接字时的行为。
  • SO_RCVBUFSO_SNDBUF:设置接收和发送缓冲区大小。
8.3.1 示例
#include <sys/socket.h>
#include <stdio.h>

int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("Socket creation failed");
        return 1;
    }

    int optval = 1;
    if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) == -1) {
        perror("Setsockopt failed");
        return 1;
    }

    return 0;
}

在这里插入图片描述

9. 多路复用

9.1 使用select函数

9.1.1 select函数
  • 函数原型int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
  • 头文件<sys/select.h>
  • 参数
    • nfds:文件描述符的最大值加1。
    • readfds:读事件的文件描述符集合。
    • writefds:写事件的文件描述符集合。
    • exceptfds:异常事件的文件描述符集合。
    • timeout:超时时间。
  • 返回值:成功返回就绪的文件描述符数量,超时返回0,失败返回-1。
  • 描述:此函数用于监控多个文件描述符的就绪状态,包括读、写和异常事件。

9.2 使用poll函数

9.2.1 poll函数
  • 函数原型int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  • 头文件<sys/poll.h>
  • 参数
    • fds:文件描述符集合。
    • nfds:文件描述符的数量。
    • timeout:超时时间。
  • 返回值:成功返回就绪的文件描述符数量,超时返回0,失败返回-1。
  • 描述:此函数用于监控多个文件描述符的就绪状态,与select类似但更为高效。

9.3 使用epoll函数

9.3.1 epoll_create函数
  • 函数原型int epoll_create(int size);
  • 头文件<sys/epoll.h>
  • 参数
    • size:初始事件表的大小。
  • 返回值:成功返回一个整数(文件描述符),失败返回-1。
  • 描述:此函数用于创建一个epoll实例。
9.3.2 epoll_ctl函数
  • 函数原型int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • 头文件<sys/epoll.h>
  • 参数
    • epfd:epoll文件描述符。
    • op:操作类型(EPOLL_CTL_ADD, EPOLL_CTL_MOD, EPOLL_CTL_DEL)。
    • fd:文件描述符。
    • event:指向struct epoll_event的指针。
  • 返回值:成功返回0,失败返回-1。
  • 描述:此函数用于添加、修改或删除监控的文件描述符。
9.3.3 epoll_wait函数
  • 函数原型int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
  • 头文件<sys/epoll.h>
  • 参数
    • epfd:epoll文件描述符。
    • events:事件数组。
    • maxevents:数组的最大大小。
    • timeout:超时时间。
  • 返回值:成功返回就绪的文件描述符数量,超时返回0,失败返回-1。
  • 描述:此函数用于等待文件描述符就绪。
9.3.4 示例
#include <sys/socket.h>
#include <sys/epoll.h>
#include <stdio.h>
#include <unistd.h>

#define MAX_EVENTS 10
#define MAX_FDS 10

int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("Socket creation failed");
        return 1;
    }

    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);
    server_addr.sin_addr.s_addr = INADDR_ANY;

    if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("Bind failed");
        return 1;
    }

    if (listen(sockfd, 5) == -1) {
        perror("Listen failed");
        return 1;
    }

    int epoll_fd = epoll_create(MAX_FDS);
    if (epoll_fd == -1) {
        perror("Epoll create failed");
        return 1;
    }

    struct epoll_event ev;
    ev.events = EPOLLIN | EPOLLET;
    ev.data.fd = sockfd;
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sockfd, &ev) == -1) {
        perror("Epoll add failed");
        return 1;
    }

    while (1) {
        struct epoll_event events[MAX_EVENTS];
        int num_events = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        if (num_events == -1) {
            perror("Epoll wait failed");
            break;
        }

        for (int i = 0; i < num_events; i++) {
            if (events[i].data.fd == sockfd) {
                // 接受连接
                socklen_t client_len = sizeof(struct sockaddr_in);
                int client_fd = accept(sockfd, (struct sockaddr *)&server_addr, &client_len);
                if (client_fd == -1) {
                    perror("Accept failed");
                    continue;
                }

                // 添加客户端到epoll
                ev.data.fd = client_fd;
                ev.events = EPOLLIN | EPOLLET;
                if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev) == -1) {
                    perror("Epoll add failed");
                    close(client_fd);
                    continue;
                }
            } else {
                // 接收数据
                char buffer[100];
                ssize_t bytes_received = recv(events[i].data.fd, buffer, sizeof(buffer), 0);
                if (bytes_received == -1) {
                    perror("Receive failed");
                    close(events[i].data.fd);
                    continue;
                }
                buffer[bytes_received] = '\0'; // Null-terminate the string
                printf("Received %zd bytes: %s\n", bytes_received, buffer);
            }
        }
    }

    close(sockfd);
    close(epoll_fd);
    return 0;
}

10. 结论

本章详细介绍了C语言中的socket编程,涵盖了socket的创建、配置、连接、数据传输、套接字选项设置以及多路复用等多个方面。通过本章的学习,读者可以掌握socket编程的核心概念和技术,并能够在实际开发中构建可靠的网络应用程序。

  1. Socket创建与配置

    • 使用socket函数创建套接字。
    • 使用bind函数绑定套接字到特定地址。
    • 使用listen函数使套接字进入监听状态。
  2. 客户端连接与接收

    • 使用connect函数建立客户端与服务器之间的连接。
    • 使用accept函数接收客户端连接请求。
  3. 数据传输

    • 使用send函数从服务器发送数据给客户端。
    • 使用recv函数从客户端接收数据。
  4. 套接字选项

    • 使用setsockopt函数设置套接字选项以优化网络通信。
    • 使用getsockopt函数获取套接字选项的状态。
  5. 多路复用

    • 使用select函数实现非阻塞式的I/O多路复用,提高程序效率。
    • select函数监控多个文件描述符的就绪状态,包括读、写和异常事件。
上一篇:docker对nginx.conf进行修改后页面无变化或页面报错


下一篇:Redis数据结构:List类型全面解析