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

Linux C语言网络编程基础

2022-06-106.9k 阅读

网络编程基础概念

网络通信基础

在深入探讨 Linux C 语言网络编程之前,我们需要先了解一些网络通信的基本概念。网络通信是指不同设备之间通过网络进行数据交换的过程。这涉及到多个层面的知识,从底层的物理连接到高层的应用协议。

在网络通信中,IP 地址是设备在网络中的标识。IPv4 地址由 32 位二进制数组成,通常以点分十进制的形式表示,例如 192.168.1.1。而 IPv6 则使用 128 位二进制数,以冒号十六进制表示,如 2001:0db8:85a3:0000:0000:8a2e:0370:7334。

端口号则用于区分同一设备上的不同应用程序。它是一个 16 位的无符号整数,范围从 0 到 65535。其中,0 到 1023 为知名端口,预留给特定的服务,如 HTTP 的 80 端口,FTP 的 21 端口等。

网络协议分层

网络协议采用分层结构,常见的有 TCP/IP 协议族。它大致分为四层:网络接口层、网络层、传输层和应用层。

  1. 网络接口层:负责处理物理网络连接,包括硬件驱动程序和链路层协议,如以太网协议。它将数据帧发送到物理网络,并从网络接收数据帧。
  2. 网络层:主要协议是 IP 协议,负责将数据包从源地址传输到目的地址。它处理路由选择和拥塞控制等功能。
  3. 传输层:有两个重要协议,TCP(传输控制协议)和 UDP(用户数据报协议)。TCP 提供可靠的面向连接的通信,通过三次握手建立连接,保证数据的有序传输和完整性。UDP 则是无连接的,不保证数据的可靠传输,但具有较低的开销,适用于对实时性要求高但对数据准确性要求相对较低的应用,如视频流和音频流。
  4. 应用层:包含各种应用协议,如 HTTP、FTP、SMTP 等,直接与用户应用程序交互。

Linux 网络编程相关函数

socket 函数

在 Linux 网络编程中,socket 函数是创建套接字的关键。套接字是网络通信的端点,它可以看作是应用程序与网络之间的接口。

#include <sys/socket.h>
int socket(int domain, int type, int protocol);
  • domain:指定协议族,常见的有 AF_INET(IPv4 协议)、AF_INET6(IPv6 协议)、AF_UNIX(用于本地进程间通信)等。
  • type:指定套接字类型,如 SOCK_STREAM(面向连接的流套接字,通常用于 TCP)、SOCK_DGRAM(无连接的数据报套接字,用于 UDP)。
  • protocol:通常设置为 0,由系统根据 domaintype 选择合适的协议。

例如,创建一个 IPv4 的 TCP 套接字:

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

bind 函数

bind 函数用于将套接字绑定到一个特定的地址和端口。

#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd:通过 socket 函数创建的套接字描述符。
  • addr:指向要绑定的地址结构的指针,对于 IPv4 是 struct sockaddr_in,对于 IPv6 是 struct sockaddr_in6
  • addrlen:地址结构的长度。

以下是一个绑定 IPv4 地址和端口的示例:

struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8080);
servaddr.sin_addr.s_addr = INADDR_ANY;

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

这里 htons 函数将主机字节序转换为网络字节序,INADDR_ANY 表示绑定到所有可用的网络接口。

listen 函数

在服务器端,使用 listen 函数将套接字设置为监听状态,准备接受客户端的连接请求。

#include <sys/socket.h>
int listen(int sockfd, int backlog);
  • sockfd:要监听的套接字描述符。
  • backlog:指定在未接受的连接请求队列中允许的最大连接数。

示例如下:

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

这里设置最大连接数为 5。

accept 函数

accept 函数用于接受客户端的连接请求,返回一个新的套接字描述符,用于与客户端进行通信。

#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • sockfd:处于监听状态的套接字描述符。
  • addr:用于存储客户端地址的结构指针。
  • addrlen:指向 addr 结构长度的指针。

示例代码:

struct sockaddr_in cliaddr;
socklen_t clilen = sizeof(cliaddr);
int connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &clilen);
if (connfd == -1) {
    perror("accept failed");
    close(sockfd);
    return -1;
}

connect 函数

在客户端,使用 connect 函数连接到服务器。

#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd:客户端套接字描述符。
  • addr:指向服务器地址结构的指针。
  • addrlen:服务器地址结构的长度。

示例:

struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8080);
inet_pton(AF_INET, "192.168.1.100", &servaddr.sin_addr);

if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
    perror("connect failed");
    close(sockfd);
    return -1;
}

这里 inet_pton 函数将点分十进制的 IP 地址转换为网络字节序的二进制格式。

TCP 网络编程示例

简单的 TCP 服务器

下面是一个简单的 TCP 服务器示例,它监听指定端口,接受客户端连接,并向客户端发送一条消息。

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

#define PORT 8080
#define MAXLINE 1024

int main() {
    int sockfd;
    char buffer[MAXLINE];
    char *hello = "Hello from server";
    struct sockaddr_in servaddr, cliaddr;

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

    memset(&servaddr, 0, sizeof(servaddr));
    memset(&cliaddr, 0, sizeof(cliaddr));

    // 填充服务器地址结构
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = INADDR_ANY;
    servaddr.sin_port = htons(PORT);

    // 绑定套接字到地址
    if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("bind failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // 设置为监听模式
    if (listen(sockfd, 5) < 0) {
        perror("listen failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    socklen_t len = sizeof(cliaddr);
    int connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &len);
    if (connfd < 0) {
        perror("accept failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // 向客户端发送消息
    send(connfd, hello, strlen(hello), 0);
    printf("Hello message sent\n");

    // 关闭连接
    close(connfd);
    close(sockfd);
    return 0;
}

简单的 TCP 客户端

对应的 TCP 客户端示例,它连接到服务器并接收服务器发送的消息。

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

#define PORT 8080
#define MAXLINE 1024
#define SERVER_IP "192.168.1.100"

int main() {
    int sockfd;
    char buffer[MAXLINE];
    struct sockaddr_in servaddr;

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

    memset(&servaddr, 0, sizeof(servaddr));

    // 填充服务器地址结构
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(PORT);
    servaddr.sin_addr.s_addr = inet_addr(SERVER_IP);

    // 连接到服务器
    if (connect(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("connect failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // 接收服务器消息
    int n = recv(sockfd, (char *)buffer, MAXLINE, 0);
    buffer[n] = '\0';
    printf("Message from server: %s\n", buffer);

    // 关闭套接字
    close(sockfd);
    return 0;
}

UDP 网络编程示例

简单的 UDP 服务器

UDP 服务器示例,它接收客户端发送的数据并回显。

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

#define PORT 8080
#define MAXLINE 1024

int main() {
    int sockfd;
    char buffer[MAXLINE];
    char *hello = "Hello from server";
    struct sockaddr_in servaddr, cliaddr;

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

    memset(&servaddr, 0, sizeof(servaddr));
    memset(&cliaddr, 0, sizeof(cliaddr));

    // 填充服务器地址结构
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = INADDR_ANY;
    servaddr.sin_port = htons(PORT);

    // 绑定套接字到地址
    if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("bind failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    socklen_t len = sizeof(cliaddr);
    int n = recvfrom(sockfd, (char *)buffer, MAXLINE, MSG_WAITALL, (struct sockaddr *)&cliaddr, &len);
    buffer[n] = '\0';
    printf("Message from client: %s\n", buffer);

    // 回显消息给客户端
    sendto(sockfd, (const char *)hello, strlen(hello), MSG_CONFIRM, (const struct sockaddr *)&cliaddr, len);
    printf("Hello message sent\n");

    // 关闭套接字
    close(sockfd);
    return 0;
}

简单的 UDP 客户端

UDP 客户端示例,它向服务器发送数据并接收服务器的回显。

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

#define PORT 8080
#define MAXLINE 1024
#define SERVER_IP "192.168.1.100"

int main() {
    int sockfd;
    char buffer[MAXLINE];
    char *hello = "Hello from client";
    struct sockaddr_in servaddr;

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

    memset(&servaddr, 0, sizeof(servaddr));

    // 填充服务器地址结构
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(PORT);
    servaddr.sin_addr.s_addr = inet_addr(SERVER_IP);

    // 向服务器发送消息
    sendto(sockfd, (const char *)hello, strlen(hello), MSG_CONFIRM, (const struct sockaddr *)&servaddr, sizeof(servaddr));
    printf("Hello message sent\n");

    // 接收服务器回显
    socklen_t len = sizeof(servaddr);
    int n = recvfrom(sockfd, (char *)buffer, MAXLINE, MSG_WAITALL, (const struct sockaddr *)&servaddr, &len);
    buffer[n] = '\0';
    printf("Message from server: %s\n", buffer);

    // 关闭套接字
    close(sockfd);
    return 0;
}

网络编程中的错误处理

在网络编程中,错误处理至关重要。常见的错误包括套接字创建失败、绑定失败、连接失败等。通过 perror 函数可以打印系统错误信息,帮助我们定位问题。

例如,在 socket 函数调用失败时:

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

perror 函数会根据 errno 的值打印相应的错误信息,如 No such file or directory 等。

另外,在处理网络通信时,还需要考虑网络超时等情况。可以通过设置套接字选项来实现超时机制。例如,对于 TCP 套接字,可以使用 setsockopt 函数设置 SO_RCVTIMEOSO_SNDTIMEO 选项来设置接收和发送超时时间。

struct timeval timeout;
timeout.tv_sec = 5; // 5 秒超时
timeout.tv_usec = 0;

if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (const char *)&timeout, sizeof(timeout)) < 0) {
    perror("setsockopt failed");
    close(sockfd);
    return -1;
}

高级网络编程主题

多路复用 I/O

在网络编程中,有时需要同时处理多个套接字的 I/O 操作,例如同时监听多个客户端连接或处理多个网络请求。多路复用 I/O 技术可以解决这个问题。常见的多路复用 I/O 函数有 selectpollepoll

  1. select 函数
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

select 函数允许程序监视一组文件描述符(包括套接字描述符),等待其中一个或多个描述符变为可读、可写或出现异常。nfds 是要监视的文件描述符集合中最大的文件描述符加 1。readfdswritefdsexceptfds 分别是要监视的可读、可写和异常的文件描述符集合。timeout 用于设置等待的超时时间。

示例代码:

fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(sockfd, &read_fds);

struct timeval timeout;
timeout.tv_sec = 10;
timeout.tv_usec = 0;

int activity = select(sockfd + 1, &read_fds, NULL, NULL, &timeout);
if (activity < 0) {
    perror("select error");
} else if (activity > 0) {
    if (FD_ISSET(sockfd, &read_fds)) {
        // 有数据可读,进行处理
    }
}
  1. poll 函数
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

poll 函数与 select 类似,但它使用一个 struct pollfd 数组来表示要监视的文件描述符及其事件。nfds 是数组中元素的个数,timeout 是等待的超时时间(单位为毫秒)。

struct pollfd fds[1];
fds[0].fd = sockfd;
fds[0].events = POLLIN;

int ret = poll(fds, 1, 1000);
if (ret < 0) {
    perror("poll error");
} else if (ret > 0) {
    if (fds[0].revents & POLLIN) {
        // 有数据可读,进行处理
    }
}
  1. epoll 函数 epoll 是 Linux 特有的多路复用 I/O 接口,在处理大量并发连接时具有更高的效率。它通过一个 epoll 实例来管理要监视的文件描述符。
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

epoll_create 创建一个 epoll 实例,size 参数在 Linux 2.6.8 以后被忽略。epoll_ctl 用于控制 epoll 实例,添加、修改或删除要监视的文件描述符及其事件。epoll_wait 等待 epoll 实例中注册的文件描述符上有事件发生。

示例代码:

int epfd = epoll_create1(0);
if (epfd == -1) {
    perror("epoll_create1");
    return -1;
}

struct epoll_event event;
event.data.fd = sockfd;
event.events = EPOLLIN;

if (epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event) == -1) {
    perror("epoll_ctl");
    close(epfd);
    return -1;
}

struct epoll_event events[10];
int nfds = epoll_wait(epfd, events, 10, -1);
for (int i = 0; i < nfds; ++i) {
    if (events[i].data.fd == sockfd) {
        // 有数据可读,进行处理
    }
}

线程与网络编程

在网络编程中,使用线程可以提高程序的并发处理能力。例如,一个服务器可以为每个客户端连接创建一个独立的线程来处理数据,从而实现同时服务多个客户端。

下面是一个简单的示例,展示如何在网络编程中使用线程。假设我们有一个 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 MAXLINE 1024

void *handle_client(void *arg) {
    int connfd = *((int *)arg);
    char buffer[MAXLINE];
    int n = recv(connfd, buffer, MAXLINE, 0);
    buffer[n] = '\0';
    printf("Message from client: %s\n", buffer);

    char *response = "Message received";
    send(connfd, response, strlen(response), 0);

    close(connfd);
    pthread_exit(NULL);
}

int main() {
    int sockfd;
    struct sockaddr_in servaddr, cliaddr;

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

    memset(&servaddr, 0, sizeof(servaddr));
    memset(&cliaddr, 0, sizeof(cliaddr));

    // 填充服务器地址结构
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = INADDR_ANY;
    servaddr.sin_port = htons(PORT);

    // 绑定套接字到地址
    if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("bind failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // 设置为监听模式
    if (listen(sockfd, 5) < 0) {
        perror("listen failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    while (1) {
        socklen_t len = sizeof(cliaddr);
        int connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &len);
        if (connfd < 0) {
            perror("accept failed");
            continue;
        }

        pthread_t tid;
        pthread_create(&tid, NULL, handle_client, &connfd);
        pthread_detach(tid);
    }

    close(sockfd);
    return 0;
}

在这个示例中,每当有新的客户端连接时,服务器创建一个新线程来处理该客户端的通信。handle_client 函数是线程的执行函数,负责接收客户端消息并发送响应。

然而,在使用线程进行网络编程时,需要注意线程安全问题。例如,共享资源(如全局变量)的访问需要进行同步,以避免数据竞争和不一致。可以使用互斥锁(pthread_mutex_t)来实现同步。

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void *handle_client(void *arg) {
    int connfd = *((int *)arg);
    pthread_mutex_lock(&mutex);
    // 访问共享资源的代码
    pthread_mutex_unlock(&mutex);

    char buffer[MAXLINE];
    int n = recv(connfd, buffer, MAXLINE, 0);
    buffer[n] = '\0';
    printf("Message from client: %s\n", buffer);

    char *response = "Message received";
    send(connfd, response, strlen(response), 0);

    close(connfd);
    pthread_exit(NULL);
}

网络安全与加密

在网络编程中,数据的安全性至关重要。特别是在传输敏感信息时,需要对数据进行加密。常见的加密算法有对称加密(如 AES)和非对称加密(如 RSA)。

  1. 对称加密 - AES 示例 AES(高级加密标准)是一种广泛使用的对称加密算法。在 Linux 下,可以使用 OpenSSL 库来实现 AES 加密。

首先,确保安装了 OpenSSL 库。然后编写如下代码示例:

#include <stdio.h>
#include <string.h>
#include <openssl/aes.h>

#define AES_KEY_LENGTH 16
#define PLAINTEXT_LENGTH 16

int main() {
    unsigned char key[AES_KEY_LENGTH] = "0123456789abcdef";
    unsigned char iv[AES_BLOCK_SIZE] = "1234567890abcdef";
    unsigned char plaintext[PLAINTEXT_LENGTH] = "Hello, World!";
    unsigned char ciphertext[PLAINTEXT_LENGTH];
    unsigned char decryptedtext[PLAINTEXT_LENGTH];

    AES_KEY aes_key;
    AES_set_encrypt_key(key, AES_KEY_LENGTH * 8, &aes_key);
    AES_cbc_encrypt(plaintext, ciphertext, PLAINTEXT_LENGTH, &aes_key, iv, AES_ENCRYPT);

    AES_set_decrypt_key(key, AES_KEY_LENGTH * 8, &aes_key);
    AES_cbc_encrypt(ciphertext, decryptedtext, PLAINTEXT_LENGTH, &aes_key, iv, AES_DECRYPT);

    printf("Plaintext: %s\n", plaintext);
    printf("Ciphertext: ");
    for (int i = 0; i < PLAINTEXT_LENGTH; ++i) {
        printf("%02x ", ciphertext[i]);
    }
    printf("\nDecryptedtext: %s\n", decryptedtext);

    return 0;
}

在这个示例中,我们使用 AES - 128(密钥长度为 16 字节)的 CBC(Cipher Block Chaining)模式进行加密和解密。AES_set_encrypt_keyAES_set_decrypt_key 分别用于设置加密和解密密钥,AES_cbc_encrypt 进行加密和解密操作。

  1. 非对称加密 - RSA 示例 RSA 是非对称加密算法,使用一对密钥(公钥和私钥)。同样使用 OpenSSL 库来实现。
#include <stdio.h>
#include <string.h>
#include <openssl/rsa.h>
#include <openssl/pem.h>
#include <openssl/err.h>

#define KEY_LENGTH 2048
#define PLAINTEXT_LENGTH 200

void handleErrors() {
    ERR_print_errors_fp(stderr);
    abort();
}

int main() {
    RSA *rsa_private = NULL, *rsa_public = NULL;
    FILE *fp_private, *fp_public;
    unsigned char plaintext[PLAINTEXT_LENGTH] = "Hello, RSA!";
    unsigned char encrypted[KEY_LENGTH / 8];
    unsigned char decrypted[PLAINTEXT_LENGTH];

    // 读取私钥文件
    fp_private = fopen("private_key.pem", "rb");
    if (!fp_private) {
        perror("Error opening private key file");
        return -1;
    }
    rsa_private = PEM_read_RSAPrivateKey(fp_private, NULL, NULL, NULL);
    fclose(fp_private);
    if (!rsa_private) {
        handleErrors();
    }

    // 读取公钥文件
    fp_public = fopen("public_key.pem", "rb");
    if (!fp_public) {
        perror("Error opening public key file");
        return -1;
    }
    rsa_public = PEM_read_RSA_PUBKEY(fp_public, NULL, NULL, NULL);
    fclose(fp_public);
    if (!rsa_public) {
        handleErrors();
    }

    // 加密
    int encrypted_len = RSA_public_encrypt(strlen((char *)plaintext), plaintext, encrypted, rsa_public, RSA_PKCS1_PADDING);
    if (encrypted_len == -1) {
        handleErrors();
    }

    // 解密
    int decrypted_len = RSA_private_decrypt(encrypted_len, encrypted, decrypted, rsa_private, RSA_PKCS1_PADDING);
    if (decrypted_len == -1) {
        handleErrors();
    }
    decrypted[decrypted_len] = '\0';

    printf("Plaintext: %s\n", plaintext);
    printf("Decryptedtext: %s\n", decrypted);

    RSA_free(rsa_private);
    RSA_free(rsa_public);
    return 0;
}

在这个示例中,我们从文件中读取私钥和公钥,使用公钥进行加密,私钥进行解密。RSA_public_encryptRSA_private_decrypt 分别是加密和解密函数,RSA_PKCS1_PADDING 是填充模式。

在实际网络编程中,加密通常与网络通信结合使用。例如,在 TCP 或 UDP 通信中,在发送数据前对数据进行加密,接收数据后进行解密,以确保数据在传输过程中的安全性。

通过以上内容,我们全面介绍了 Linux C 语言网络编程的基础知识、相关函数、TCP 和 UDP 编程示例、错误处理以及一些高级主题,希望能帮助读者深入理解和掌握 Linux C 语言网络编程技术。