MK
摩柯社区 - 一个极简的技术知识社区
AI 面试
Linux环境下网络编程指南
2021-07-096.1k 阅读

Linux 环境下网络编程指南

一、网络编程基础概念

1.1 网络通信模型

在 Linux 环境下进行网络编程,首先需要理解网络通信模型。最常用的是客户端 - 服务器(Client - Server)模型。在这个模型中,服务器程序监听特定的端口,等待客户端的连接请求。客户端发起连接请求,与服务器建立连接后,双方就可以进行数据传输。

例如,我们日常使用的浏览器就是客户端,而提供网页服务的服务器则运行着 Web 服务器程序。浏览器向服务器发送请求获取网页内容,服务器响应并返回网页数据。

1.2 网络协议

网络协议是网络编程的核心概念之一。在 Linux 网络编程中,主要涉及传输控制协议(TCP)和用户数据报协议(UDP)。

1.2.1 TCP 协议

TCP 是一种面向连接的、可靠的传输协议。它通过三次握手建立连接,在数据传输过程中,会对数据进行排序、确认和重传,以确保数据的完整性和准确性。例如,在文件传输、电子邮件等场景中,TCP 协议被广泛应用。

1.2.2 UDP 协议

UDP 是一种无连接的、不可靠的传输协议。它不保证数据的顺序和完整性,也不会进行重传。但是 UDP 的优点是传输速度快,开销小,适用于对实时性要求较高但对数据准确性要求相对较低的场景,如视频流、音频流的传输。

二、Linux 网络编程相关函数

2.1 socket 函数

socket 函数用于创建一个网络套接字,它是网络编程的基础。其函数原型如下:

#include <sys/socket.h>
int socket(int domain, int type, int protocol);
  • domain:指定协议族,常见的有 AF_INET(IPv4 协议)、AF_INET6(IPv6 协议)等。
  • type:指定套接字类型,如 SOCK_STREAM(TCP 套接字)、SOCK_DGRAM(UDP 套接字)。
  • protocol:通常设置为 0,让系统根据 domaintype 自动选择合适的协议。

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

int sockfd;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
    perror("socket creation failed");
    exit(EXIT_FAILURE);
}

2.2 bind 函数

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

#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd:由 socket 函数返回的套接字描述符。
  • addr:指向一个 struct sockaddr 类型的结构体,包含了要绑定的地址和端口信息。对于 IPv4,通常使用 struct sockaddr_in 结构体。
  • addrlenaddr 结构体的长度。

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

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

2.3 listen 函数

listen 函数用于将套接字设置为监听状态,准备接受客户端的连接请求。函数原型为:

#include <sys/socket.h>
int listen(int sockfd, int backlog);
  • sockfd:要设置为监听状态的套接字描述符。
  • backlog:指定等待连接队列的最大长度。

示例:

if (listen(sockfd, 5) < 0) {
    perror("listen failed");
    close(sockfd);
    exit(EXIT_FAILURE);
}

2.4 accept 函数

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

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

示例:

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

2.5 connect 函数

connect 函数用于客户端主动发起与服务器的连接。函数原型为:

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

示例:

struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);

if (connect(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
    perror("connect failed");
    close(sockfd);
    exit(EXIT_FAILURE);
}

2.6 send 和 recv 函数

2.6.1 send 函数

send 函数用于在已连接的套接字上发送数据。函数原型为:

#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
  • sockfd:已连接的套接字描述符。
  • buf:指向要发送数据的缓冲区。
  • len:要发送数据的长度。
  • flags:通常设置为 0。

示例:

char buffer[1024] = "Hello, Server!";
ssize_t n = send(connfd, buffer, strlen(buffer), 0);
if (n < 0) {
    perror("send failed");
    close(connfd);
    close(sockfd);
    exit(EXIT_FAILURE);
}

2.6.2 recv 函数

recv 函数用于在已连接的套接字上接收数据。函数原型为:

#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  • sockfd:已连接的套接字描述符。
  • buf:用于存储接收数据的缓冲区。
  • len:缓冲区的长度。
  • flags:通常设置为 0。

示例:

char buffer[1024];
ssize_t n = recv(connfd, buffer, sizeof(buffer), 0);
if (n < 0) {
    perror("recv failed");
    close(connfd);
    close(sockfd);
    exit(EXIT_FAILURE);
}
buffer[n] = '\0';
printf("Received: %s\n", buffer);

三、TCP 网络编程示例

3.1 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, connfd;
    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);
    // 接受客户端连接
    connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &len);
    if (connfd < 0) {
        perror("accept failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    char buffer[MAXLINE];
    ssize_t n = recv(connfd, buffer, MAXLINE, 0);
    if (n < 0) {
        perror("recv failed");
        close(connfd);
        close(sockfd);
        exit(EXIT_FAILURE);
    }
    buffer[n] = '\0';
    printf("Received: %s\n", buffer);

    char response[MAXLINE] = "Message received successfully!";
    n = send(connfd, response, strlen(response), 0);
    if (n < 0) {
        perror("send failed");
        close(connfd);
        close(sockfd);
        exit(EXIT_FAILURE);
    }

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

3.2 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 "127.0.0.1"

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);
    inet_pton(AF_INET, SERVER_IP, &servaddr.sin_addr);

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

    char message[MAXLINE] = "Hello, Server!";
    ssize_t n = send(sockfd, message, strlen(message), 0);
    if (n < 0) {
        perror("send failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    n = recv(sockfd, buffer, MAXLINE, 0);
    if (n < 0) {
        perror("recv failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }
    buffer[n] = '\0';
    printf("Received: %s\n", buffer);

    close(sockfd);
    return 0;
}

四、UDP 网络编程示例

4.1 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];
    struct sockaddr_in servaddr, cliaddr;

    // 创建 UDP 套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 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);
    ssize_t n = recvfrom(sockfd, (char *)buffer, MAXLINE, MSG_WAITALL, (struct sockaddr *)&cliaddr, &len);
    buffer[n] = '\0';
    printf("Received: %s\n", buffer);

    char response[MAXLINE] = "Message received successfully!";
    sendto(sockfd, (const char *)response, strlen(response), MSG_CONFIRM, (const struct sockaddr *)&cliaddr, len);

    close(sockfd);
    return 0;
}

4.2 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 "127.0.0.1"

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

    // 创建 UDP 套接字
    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);
    inet_pton(AF_INET, SERVER_IP, &servaddr.sin_addr);

    char message[MAXLINE] = "Hello, Server!";
    socklen_t len = sizeof(servaddr);
    ssize_t n = sendto(sockfd, (const char *)message, strlen(message), MSG_CONFIRM, (const struct sockaddr *)&servaddr, len);
    if (n < 0) {
        perror("sendto failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    n = recvfrom(sockfd, (char *)buffer, MAXLINE, MSG_WAITALL, (const struct sockaddr *)&servaddr, &len);
    buffer[n] = '\0';
    printf("Received: %s\n", buffer);

    close(sockfd);
    return 0;
}

五、高级网络编程技术

5.1 多路复用技术

在网络编程中,当需要同时处理多个客户端连接或者多个网络 I/O 操作时,使用多路复用技术可以提高程序的效率。Linux 提供了 selectpollepoll 等多路复用机制。

5.1.1 select 函数

select 函数通过监听多个文件描述符的状态变化来实现多路复用。函数原型为:

#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
  • nfds:需要检查的文件描述符的最大值加 1。
  • readfds:指向一个 fd_set 结构体,用于检查是否有数据可读。
  • writefds:指向一个 fd_set 结构体,用于检查是否可以进行写操作。
  • exceptfds:指向一个 fd_set 结构体,用于检查是否有异常发生。
  • timeout:设置等待的超时时间,如果为 NULL,则表示无限期等待。

示例:

fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(sockfd, &read_fds);
int activity = select(sockfd + 1, &read_fds, NULL, NULL, NULL);
if (activity < 0) {
    perror("select error");
} else if (activity > 0) {
    if (FD_ISSET(sockfd, &read_fds)) {
        // 处理客户端连接
    }
}

5.1.2 poll 函数

poll 函数与 select 类似,但在使用上有所不同。函数原型为:

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  • fds:指向一个 struct pollfd 结构体数组,每个结构体包含文件描述符、事件掩码和返回事件掩码。
  • nfds:数组中元素的个数。
  • timeout:等待的超时时间,单位为毫秒。

示例:

struct pollfd fds[1];
fds[0].fd = sockfd;
fds[0].events = POLLIN;
int ret = poll(fds, 1, -1);
if (ret < 0) {
    perror("poll error");
} else if (ret > 0) {
    if (fds[0].revents & POLLIN) {
        // 处理客户端连接
    }
}

5.1.3 epoll 函数

epoll 是 Linux 特有的多路复用机制,相比于 selectpoll,它在处理大量连接时具有更高的效率。epoll 有三个主要函数:epoll_createepoll_ctlepoll_wait

  • epoll_create 用于创建一个 epoll 实例,函数原型为:
#include <sys/epoll.h>
int epoll_create(int size);
  • epoll_ctl 用于控制 epoll 实例,添加、修改或删除文件描述符的事件监听。函数原型为:
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • epoll_wait 用于等待事件的发生。函数原型为:
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

示例:

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

struct epoll_event event;
event.data.fd = sockfd;
event.events = EPOLLIN;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event) == -1) {
    perror("epoll_ctl: listen_sock");
    close(sockfd);
    close(epfd);
    exit(EXIT_FAILURE);
}

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) {
        // 处理客户端连接
    }
}

5.2 线程与网络编程

在网络编程中,使用线程可以提高程序的并发处理能力。每个线程可以独立处理一个客户端连接,从而实现多个客户端的并发服务。

下面是一个简单的使用线程处理多个 TCP 客户端连接的示例:

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

#define PORT 8080
#define MAXLINE 1024

void *handle_client(void *arg) {
    int connfd = *((int *)arg);
    char buffer[MAXLINE];
    ssize_t n = recv(connfd, buffer, MAXLINE, 0);
    if (n < 0) {
        perror("recv failed");
    } else {
        buffer[n] = '\0';
        printf("Received: %s\n", buffer);

        char response[MAXLINE] = "Message received successfully!";
        n = send(connfd, response, strlen(response), 0);
        if (n < 0) {
            perror("send failed");
        }
    }
    close(connfd);
    pthread_exit(NULL);
}

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

    // 创建套接字
    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);
    while (1) {
        // 接受客户端连接
        connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &len);
        if (connfd < 0) {
            perror("accept failed");
            continue;
        }

        if (pthread_create(&tid, NULL, handle_client, (void *)&connfd) != 0) {
            perror("pthread_create");
            close(connfd);
        }
    }

    close(sockfd);
    return 0;
}

六、网络编程中的错误处理

在网络编程中,错误处理非常重要。常见的错误包括套接字创建失败、绑定失败、连接失败、读写错误等。

6.1 错误处理函数

在 Linux 中,perror 函数是一个常用的错误处理函数,它会根据 errno 变量的值输出相应的错误信息。例如:

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
    perror("socket creation failed");
    exit(EXIT_FAILURE);
}

6.2 常见错误及处理

  • 套接字创建失败:可能是因为系统资源不足或者协议族、套接字类型不支持。处理方法是检查 errno,根据错误类型进行相应处理,如提示用户或尝试重新创建。
  • 绑定失败:可能是端口已被占用或者地址无效。可以尝试更换端口,或者检查地址是否正确。
  • 连接失败:可能是服务器未启动、网络故障等原因。可以通过 ping 命令检查网络连接,或者确保服务器已正常运行。
  • 读写错误:可能是网络中断、缓冲区溢出等原因。处理方法包括检查网络连接、调整缓冲区大小等。

在实际编程中,要根据具体的应用场景和需求,合理地处理各种错误,以提高程序的稳定性和可靠性。

通过以上内容,相信你对 Linux 环境下的网络编程有了较为深入的了解。从基础概念到具体函数,再到高级技术和错误处理,这些知识将为你在 Linux 平台上开发高效、稳定的网络应用程序提供有力的支持。在实际开发中,可以根据具体需求进一步优化和扩展代码,以满足不同的业务场景。