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

C 语言网络编程结合多线程实践

2022-04-074.0k 阅读

C 语言网络编程基础

网络编程概述

网络编程是指编写程序使计算机能够通过网络进行数据交换和通信。在 C 语言中,网络编程主要基于套接字(Socket)接口。套接字是一种抽象层,它提供了应用程序与网络协议栈之间的接口,使得开发者能够方便地进行网络通信。

套接字类型

  1. 流套接字(SOCK_STREAM)
    • 提供面向连接、可靠的字节流服务。它使用传输控制协议(TCP),保证数据的有序传输和完整性。例如,在文件传输、远程登录等场景中,流套接字是常见的选择。
    • 特点:数据传输可靠,无差错、无重复且按顺序到达;需要在通信双方建立连接,连接建立过程使用三次握手。
  2. 数据报套接字(SOCK_DGRAM)
    • 提供无连接、不可靠的数据报服务。它使用用户数据报协议(UDP),数据以独立的数据报形式传输,不保证数据的可靠交付和顺序。常用于实时性要求较高但对数据准确性要求相对较低的场景,如视频流、音频流传输。
    • 特点:不需要建立连接,传输速度快,但可能会出现数据丢失、重复或乱序的情况。

创建套接字

在 C 语言中,使用 socket 函数来创建套接字。该函数定义在 <sys/socket.h> 头文件中,其原型如下:

#include <sys/socket.h>
int socket(int domain, int type, int protocol);
  1. 参数说明
    • domain:指定套接字的协议族,常见的有 AF_INET(IPv4 协议)、AF_INET6(IPv6 协议)等。
    • type:指定套接字的类型,如 SOCK_STREAMSOCK_DGRAM
    • protocol:通常设置为 0,由系统根据 domaintype 自动选择合适的协议。对于 AF_INETSOCK_STREAM,系统会选择 TCP 协议;对于 AF_INETSOCK_DGRAM,系统会选择 UDP 协议。
  2. 返回值
    • 成功时返回一个非负整数,即套接字描述符(socket descriptor),后续的网络操作将使用这个描述符。
    • 失败时返回 -1,并设置 errno 变量以指示错误类型。

绑定套接字

创建套接字后,通常需要将其绑定到一个特定的地址和端口上,以便其他程序能够找到并与之通信。使用 bind 函数来完成绑定操作,其原型如下:

#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  1. 参数说明
    • sockfd:要绑定的套接字描述符。
    • addr:指向一个 struct sockaddr 结构体的指针,该结构体包含了要绑定的地址和端口信息。在 IPv4 中,实际使用的是 struct sockaddr_in 结构体,它与 struct sockaddr 可以相互转换。struct sockaddr_in 定义如下:
#include <netinet/in.h>
struct sockaddr_in {
    sa_family_t    sin_family; /* 地址族,通常为 AF_INET */
    in_port_t      sin_port;   /* 端口号,使用网络字节序 */
    struct in_addr sin_addr;   /* IPv4 地址,使用网络字节序 */
};
struct in_addr {
    in_addr_t s_addr; /* IPv4 地址,使用网络字节序 */
};
- `addrlen`:`addr` 结构体的长度。

2. 返回值 - 成功时返回 0。 - 失败时返回 -1,并设置 errno 变量以指示错误类型。

监听套接字(仅适用于 TCP 服务器)

对于 TCP 服务器,在绑定套接字后,需要使用 listen 函数将套接字设置为监听状态,准备接受客户端的连接请求。其原型如下:

#include <sys/socket.h>
int listen(int sockfd, int backlog);
  1. 参数说明
    • sockfd:要监听的套接字描述符。
    • backlog:指定等待连接队列的最大长度。当有多个客户端同时请求连接时,未被接受的连接请求会在这个队列中等待。
  2. 返回值
    • 成功时返回 0。
    • 失败时返回 -1,并设置 errno 变量以指示错误类型。

接受连接(仅适用于 TCP 服务器)

TCP 服务器在监听状态下,使用 accept 函数来接受客户端的连接请求。其原型如下:

#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  1. 参数说明
    • sockfd:监听套接字描述符。
    • addr:用于存储客户端地址信息的 struct sockaddr 结构体指针(通常为 struct sockaddr_in)。
    • addrlen:一个指向 socklen_t 类型变量的指针,用于指定 addr 结构体的长度,并在函数返回时更新为实际接收到的客户端地址长度。
  2. 返回值
    • 成功时返回一个新的套接字描述符,用于与客户端进行通信。
    • 失败时返回 -1,并设置 errno 变量以指示错误类型。

连接服务器(仅适用于 TCP 客户端)

TCP 客户端使用 connect 函数来连接到服务器。其原型如下:

#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  1. 参数说明
    • sockfd:客户端套接字描述符。
    • addr:指向服务器地址信息的 struct sockaddr 结构体指针(通常为 struct sockaddr_in)。
    • addrlenaddr 结构体的长度。
  2. 返回值
    • 成功时返回 0。
    • 失败时返回 -1,并设置 errno 变量以指示错误类型。

数据传输

  1. TCP 数据传输
    • 在 TCP 连接建立后,使用 sendrecv 函数进行数据传输。
    • send 函数原型:
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
    - `sockfd`:套接字描述符。
    - `buf`:指向要发送数据的缓冲区指针。
    - `len`:要发送的数据长度。
    - `flags`:通常设置为 0。
    - 返回值:成功时返回实际发送的字节数,失败时返回 -1,并设置 `errno` 变量。
- `recv` 函数原型:
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
    - `sockfd`:套接字描述符。
    - `buf`:用于接收数据的缓冲区指针。
    - `len`:缓冲区的长度。
    - `flags`:通常设置为 0。
    - 返回值:成功时返回实际接收的字节数,0 表示连接关闭,失败时返回 -1,并设置 `errno` 变量。

2. UDP 数据传输 - UDP 使用 sendtorecvfrom 函数进行数据传输,因为 UDP 是无连接的,所以每次发送和接收数据时都需要指定目标地址和端口。 - 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`:套接字描述符。
    - `buf`:指向要发送数据的缓冲区指针。
    - `len`:要发送的数据长度。
    - `flags`:通常设置为 0。
    - `dest_addr`:指向目标地址的 `struct sockaddr` 结构体指针(通常为 `struct sockaddr_in`)。
    - `addrlen`:`dest_addr` 结构体的长度。
    - 返回值:成功时返回实际发送的字节数,失败时返回 -1,并设置 `errno` 变量。
- `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`:套接字描述符。
    - `buf`:用于接收数据的缓冲区指针。
    - `len`:缓冲区的长度。
    - `flags`:通常设置为 0。
    - `src_addr`:用于存储源地址的 `struct sockaddr` 结构体指针(通常为 `struct sockaddr_in`)。
    - `addrlen`:一个指向 `socklen_t` 类型变量的指针,用于指定 `src_addr` 结构体的长度,并在函数返回时更新为实际接收到的源地址长度。
    - 返回值:成功时返回实际接收的字节数,失败时返回 -1,并设置 `errno` 变量。

多线程编程基础

线程概述

线程是进程中的一个执行单元,它共享进程的资源,如内存空间、文件描述符等。与进程相比,线程的创建和销毁开销较小,上下文切换速度更快,因此在需要并发执行多个任务的场景中,多线程编程是一种常用的技术。

线程的创建与终止

  1. 创建线程 在 POSIX 系统中,使用 pthread_create 函数来创建线程。其原型如下:
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
- **参数说明**
    - `thread`:指向 `pthread_t` 类型变量的指针,用于存储新创建线程的标识符。
    - `attr`:指向 `pthread_attr_t` 类型结构体的指针,用于设置线程的属性,如栈大小、调度策略等。如果设置为 `NULL`,则使用默认属性。
    - `start_routine`:指向线程函数的指针,新线程将从这个函数开始执行。
    - `arg`:传递给线程函数的参数。
- **返回值**

成功时返回 0,失败时返回一个非零错误码。 2. 终止线程 线程可以通过以下几种方式终止: - 线程函数返回:线程函数执行完毕并返回时,线程自动终止。 - 调用 pthread_exit 函数:线程可以调用 pthread_exit 函数来主动终止自己。其原型如下:

#include <pthread.h>
void pthread_exit(void *retval);

retval 参数用于传递线程的返回值,可以被其他线程通过 pthread_join 函数获取。 - 被其他线程取消:一个线程可以调用 pthread_cancel 函数来请求取消另一个线程。

线程同步

多线程编程中,由于多个线程可能同时访问共享资源,容易导致数据竞争和不一致的问题。因此,需要使用线程同步机制来确保线程安全。

  1. 互斥锁(Mutex)
    • 互斥锁是一种最基本的线程同步工具,它用于保护共享资源,确保在同一时间只有一个线程能够访问该资源。
    • 在 POSIX 系统中,使用 pthread_mutex_t 类型来表示互斥锁,通过 pthread_mutex_init 函数初始化互斥锁,pthread_mutex_lock 函数加锁,pthread_mutex_unlock 函数解锁,pthread_mutex_destroy 函数销毁互斥锁。
    • 示例代码:
#include <pthread.h>
#include <stdio.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int shared_variable = 0;

void *thread_function(void *arg) {
    pthread_mutex_lock(&mutex);
    shared_variable++;
    printf("Thread incremented shared_variable to %d\n", shared_variable);
    pthread_mutex_unlock(&mutex);
    return NULL;
}

int main() {
    pthread_t thread;
    pthread_create(&thread, NULL, thread_function, NULL);
    pthread_join(thread, NULL);
    pthread_mutex_destroy(&mutex);
    return 0;
}
  1. 条件变量(Condition Variable)
    • 条件变量用于线程间的同步,它允许线程在某个条件满足时被唤醒。
    • 在 POSIX 系统中,使用 pthread_cond_t 类型来表示条件变量,通过 pthread_cond_init 函数初始化条件变量,pthread_cond_wait 函数等待条件变量,pthread_cond_signal 函数唤醒一个等待在条件变量上的线程,pthread_cond_broadcast 函数唤醒所有等待在条件变量上的线程,pthread_cond_destroy 函数销毁条件变量。
    • 示例代码:
#include <pthread.h>
#include <stdio.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond_var = PTHREAD_COND_INITIALIZER;
int flag = 0;

void *waiting_thread(void *arg) {
    pthread_mutex_lock(&mutex);
    while (flag == 0) {
        pthread_cond_wait(&cond_var, &mutex);
    }
    printf("Waiting thread woke up. Flag is %d\n", flag);
    pthread_mutex_unlock(&mutex);
    return NULL;
}

void *signaling_thread(void *arg) {
    pthread_mutex_lock(&mutex);
    flag = 1;
    printf("Signaling thread set flag to %d\n", flag);
    pthread_cond_signal(&cond_var);
    pthread_mutex_unlock(&mutex);
    return NULL;
}

int main() {
    pthread_t waiting, signaling;
    pthread_create(&waiting, NULL, waiting_thread, NULL);
    pthread_create(&signaling, NULL, signaling_thread, NULL);
    pthread_join(waiting, NULL);
    pthread_join(signaling, NULL);
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond_var);
    return 0;
}

C 语言网络编程结合多线程实践

场景分析

在网络编程中,结合多线程可以实现更好的并发性能。例如,一个服务器需要同时处理多个客户端的连接请求,并且每个客户端的处理任务可能比较耗时。如果使用单线程,服务器只能依次处理每个客户端的请求,导致其他客户端需要等待较长时间。而使用多线程,服务器可以为每个客户端创建一个新线程来处理请求,从而提高服务器的并发处理能力。

示例代码:多线程 TCP 服务器

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

#define PORT 8080
#define MAX_CLIENTS 10

// 线程处理函数,用于处理单个客户端连接
void *handle_client(void *arg) {
    int client_socket = *((int *)arg);
    char buffer[1024] = {0};
    int valread = read(client_socket, buffer, 1024);
    if (valread < 0) {
        perror("Read failed");
        close(client_socket);
        pthread_exit(NULL);
    }
    printf("Received from client: %s\n", buffer);
    char response[] = "Message received successfully!";
    send(client_socket, response, strlen(response), 0);
    close(client_socket);
    pthread_exit(NULL);
}

int main(int argc, char const *argv[]) {
    int server_socket, client_socket;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);
    pthread_t threads[MAX_CLIENTS];

    // 创建套接字
    if ((server_socket = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 设置套接字选项,允许重用地址
    if (setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
        perror("Setsockopt failed");
        close(server_socket);
        exit(EXIT_FAILURE);
    }

    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    // 绑定套接字到指定地址和端口
    if (bind(server_socket, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("Bind failed");
        close(server_socket);
        exit(EXIT_FAILURE);
    }

    // 监听套接字
    if (listen(server_socket, MAX_CLIENTS) < 0) {
        perror("Listen failed");
        close(server_socket);
        exit(EXIT_FAILURE);
    }

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

    int client_count = 0;
    while (1) {
        // 接受客户端连接
        if ((client_socket = accept(server_socket, (struct sockaddr *)&address, (socklen_t *)&addrlen)) < 0) {
            perror("Accept failed");
            continue;
        }
        printf("New client connected: %d\n", client_socket);

        // 为每个客户端创建一个新线程来处理
        if (client_count < MAX_CLIENTS) {
            int *client_socket_ptr = (int *)malloc(sizeof(int));
            *client_socket_ptr = client_socket;
            if (pthread_create(&threads[client_count], NULL, handle_client, client_socket_ptr) != 0) {
                perror("Thread creation failed");
                free(client_socket_ptr);
                close(client_socket);
            } else {
                client_count++;
            }
        } else {
            char error_response[] = "Server is busy. Try again later.";
            send(client_socket, error_response, strlen(error_response), 0);
            close(client_socket);
        }
    }

    // 等待所有线程结束
    for (int i = 0; i < client_count; i++) {
        pthread_join(threads[i], NULL);
    }

    close(server_socket);
    return 0;
}

代码说明

  1. 套接字创建与绑定:首先创建一个 TCP 套接字,并绑定到指定的端口 PORT。设置套接字选项 SO_REUSEADDRSO_REUSEPORT 以允许重用地址。
  2. 监听与接受连接:服务器开始监听客户端连接,当有新的客户端连接时,使用 accept 函数接受连接,并为每个客户端创建一个新线程来处理。
  3. 线程处理函数handle_client 函数是线程的执行函数,它从客户端读取数据,打印接收到的消息,然后向客户端发送响应消息,最后关闭客户端套接字并退出线程。
  4. 多线程管理:使用一个数组 threads 来存储所有线程的标识符,通过 pthread_create 函数创建线程,并在最后使用 pthread_join 函数等待所有线程结束。

注意事项

  1. 资源管理:在多线程编程中,要注意共享资源的管理,避免数据竞争。例如,在上述代码中,如果有多个线程同时访问和修改某个共享变量,就需要使用互斥锁等同步机制来保护该变量。
  2. 错误处理:在网络编程和多线程编程中,都可能出现各种错误,如套接字创建失败、线程创建失败等。要做好全面的错误处理,确保程序的健壮性。
  3. 线程安全:对于一些库函数,如标准输入输出函数等,可能不是线程安全的。在多线程环境中使用这些函数时,需要特别小心,必要时使用同步机制来保证线程安全。

示例代码:多线程 UDP 服务器

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

#define PORT 8080
#define MAX_CLIENTS 10

// 线程处理函数,用于处理单个客户端的 UDP 数据
void *handle_udp_client(void *arg) {
    int server_socket = *((int *)arg);
    struct sockaddr_in client_address;
    socklen_t client_addrlen = sizeof(client_address);
    char buffer[1024] = {0};
    int n = recvfrom(server_socket, (char *)buffer, 1024, MSG_WAITALL, (struct sockaddr *)&client_address, &client_addrlen);
    buffer[n] = '\0';
    printf("Received from client: %s\n", buffer);
    char response[] = "Message received successfully!";
    sendto(server_socket, (const char *)response, strlen(response), MSG_CONFIRM, (const struct sockaddr *)&client_address, client_addrlen);
    pthread_exit(NULL);
}

int main(int argc, char const *argv[]) {
    int server_socket;
    struct sockaddr_in address;
    pthread_t threads[MAX_CLIENTS];

    // 创建 UDP 套接字
    if ((server_socket = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    // 绑定套接字到指定地址和端口
    if (bind(server_socket, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("Bind failed");
        close(server_socket);
        exit(EXIT_FAILURE);
    }

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

    int client_count = 0;
    while (1) {
        if (client_count < MAX_CLIENTS) {
            int *server_socket_ptr = (int *)malloc(sizeof(int));
            *server_socket_ptr = server_socket;
            if (pthread_create(&threads[client_count], NULL, handle_udp_client, server_socket_ptr) != 0) {
                perror("Thread creation failed");
                free(server_socket_ptr);
            } else {
                client_count++;
            }
        }
    }

    // 等待所有线程结束
    for (int i = 0; i < client_count; i++) {
        pthread_join(threads[i], NULL);
    }

    close(server_socket);
    return 0;
}

代码说明(UDP 服务器)

  1. 套接字创建与绑定:创建一个 UDP 套接字,并绑定到指定的端口 PORT
  2. 线程处理函数handle_udp_client 函数用于处理单个客户端的 UDP 数据。它从客户端接收数据,打印接收到的消息,然后向客户端发送响应消息。
  3. 多线程管理:与 TCP 服务器类似,为每个接收到的 UDP 数据创建一个新线程来处理,使用 pthread_create 函数创建线程,并使用 pthread_join 函数等待所有线程结束。

通过以上示例,我们展示了如何在 C 语言网络编程中结合多线程技术,提高程序的并发处理能力。在实际应用中,需要根据具体的需求和场景,合理地设计和实现多线程网络程序,以达到最佳的性能和稳定性。同时,要特别注意线程同步和资源管理等问题,确保程序的正确性和可靠性。