1. 网络基础与Socket API

1.1 TCP/IP协议栈简述

嵌入式Linux网络编程建立在标准的TCP/IP协议栈之上。这是一个分层模型,开发者通常只需关注传输层(TCP/UDP)和应用层

  • TCP (传输控制协议):面向连接、可靠、基于字节流的传输。适用于要求数据完整性的场景,如文件传输、远程登录(SSH)。
  • UDP (用户数据报协议):无连接、不可靠、基于数据报的传输。适用于实时性要求高、可容忍少量丢失的场景,如音视频流、DNS查询。

1.2 Socket(套接字)概念

Socket是网络通信的端点,是操作系统提供给应用程序的一组编程接口(API)。在Linux中,一切皆文件,Socket也被视为一种特殊的文件描述符(File Descriptor)。

一个Socket由协议类型本地IP地址与端口远程IP地址与端口唯一确定。

1.3 核心Socket API函数

以下是最基础的几个Socket API函数,它们是网络编程的基石。

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

// 1. 创建Socket
int socket(int domain, int type, int protocol);
// domain: 地址族,如 AF_INET (IPv4), AF_INET6 (IPv6)
// type: 套接字类型,如 SOCK_STREAM (TCP), SOCK_DGRAM (UDP)
// protocol: 通常为0,由前两个参数自动选择
// 返回值: 成功返回文件描述符,失败返回-1

// 2. 绑定地址 (服务器端)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 将Socket与一个本地IP地址和端口号绑定

// 3. 监听连接 (TCP服务器端)
int listen(int sockfd, int backlog);
// backlog: 等待连接队列的最大长度

// 4. 接受连接 (TCP服务器端)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 从监听队列中取出一个已建立的连接,返回一个新的Socket文件描述符用于通信

// 5. 发起连接 (TCP客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

// 6. 发送数据
ssize_t send(int sockfd, const void *buf, size_t len, int flags); // TCP
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen); // UDP

// 7. 接收数据
ssize_t recv(int sockfd, void *buf, size_t len, int flags); // TCP
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen); // UDP

// 8. 关闭Socket
int close(int fd);

2. TCP编程实战:回声服务器与客户端

让我们通过一个经典的“回声服务器/客户端”例子来理解TCP通信流程。

2.1 TCP服务器端代码

// tcp_echo_server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);
    char buffer[BUFFER_SIZE] = {0};

    // 1. 创建Socket
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 2. 设置Socket选项(允许地址重用)
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("setsockopt");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY; // 绑定到本机所有IP
    address.sin_port = htons(PORT);       // 端口号,htons转换字节序

    // 3. 绑定地址
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 4. 开始监听
    if (listen(server_fd, 3) < 0) {
        perror("listen");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

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

    while (1) {
        // 5. 接受客户端连接
        if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
            perror("accept");
            continue;
        }

        printf("Client connected: %s:%d\n", inet_ntoa(address.sin_addr), ntohs(address.sin_port));

        // 6. 循环读取并回显数据
        int valread;
        while ((valread = recv(new_socket, buffer, BUFFER_SIZE, 0)) > 0) {
            printf("Received: %s\n", buffer);
            send(new_socket, buffer, valread, 0); // 原样发回
            memset(buffer, 0, BUFFER_SIZE);
        }

        if (valread == 0) {
            printf("Client disconnected.\n");
        } else if (valread < 0) {
            perror("recv");
        }

        // 7. 关闭客户端Socket
        close(new_socket);
    }

    // 服务器通常不会执行到这里
    close(server_fd);
    return 0;
}

2.2 TCP客户端代码

// tcp_echo_client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main(int argc, char const *argv[]) {
    int sock = 0;
    struct sockaddr_in serv_addr;
    char buffer[BUFFER_SIZE] = {0};
    char *message = "Hello from embedded client!";

    if (argc != 2) {
        printf("Usage: %s <server_ip>\n", argv[0]);
        return -1;
    }

    // 1. 创建Socket
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        printf("\n Socket creation error \n");
        return -1;
    }

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);

    // 2. 将IP地址从字符串转换为二进制形式
    if (inet_pton(AF_INET, argv[1], &serv_addr.sin_addr) <= 0) {
        printf("\nInvalid address/ Address not supported \n");
        close(sock);
        return -1;
    }

    // 3. 连接服务器
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        printf("\nConnection Failed \n");
        close(sock);
        return -1;
    }

    printf("Connected to server.\n");

    // 4. 发送数据
    send(sock, message, strlen(message), 0);
    printf("Message sent: %s\n", message);

    // 5. 接收回显数据
    int valread = recv(sock, buffer, BUFFER_SIZE, 0);
    printf("Echo from server: %s\n", buffer);

    // 6. 关闭Socket
    close(sock);
    return 0;
}

2.3 编译与运行

在嵌入式Linux开发板或交叉编译环境中编译:

# 交叉编译示例 (使用arm-linux-gnueabihf工具链)
arm-linux-gnueabihf-gcc tcp_echo_server.c -o tcp_echo_server
arm-linux-gnueabihf-gcc tcp_echo_client.c -o tcp_echo_client

# 在开发板上运行
# 终端1 (服务器):
./tcp_echo_server

# 终端2 (客户端,假设服务器IP为192.168.1.100):
./tcp_echo_client 192.168.1.100

3. UDP编程实战:简单数据报收发

UDP编程更为简单,无需建立连接。

3.1 UDP服务器端

// udp_server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 8888
#define BUFFER_SIZE 1024

int main() {
    int sockfd;
    struct sockaddr_in serv_addr, cli_addr;
    socklen_t cli_len = sizeof(cli_addr);
    char buffer[BUFFER_SIZE];

    // 1. 创建UDP Socket
    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    serv_addr.sin_port = htons(PORT);

    // 2. 绑定地址
    if (bind(sockfd, (const struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("bind failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

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

    while (1) {
        // 3. 接收数据 (recvfrom会获取发送者地址)
        int n = recvfrom(sockfd, (char *)buffer, BUFFER_SIZE, MSG_WAITALL,
                         (struct sockaddr *)&cli_addr, &cli_len);
        buffer[n] = '\0';
        printf("Received from %s:%d - %s\n",
               inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port), buffer);

        // 4. 发送回复 (sendto指定目标地址)
        sendto(sockfd, (const char *)buffer, strlen(buffer), MSG_CONFIRM,
               (const struct sockaddr *)&cli_addr, cli_len);
        printf("Echo sent.\n");
    }

    close(sockfd);
    return 0;
}

3.2 UDP客户端

// udp_client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 8888
#define BUFFER_SIZE 1024

int main(int argc, char const *argv[]) {
    int sockfd;
    struct sockaddr_in serv_addr;
    char buffer[BUFFER_SIZE];
    char *hello = "Hello UDP Server";

    if (argc != 2) {
        printf("Usage: %s <server_ip>\n", argv[0]);
        return -1;
    }

    // 1. 创建UDP Socket
    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("socket creation failed");
        return -1;
    }

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);

    // 2. 转换服务器IP地址
    if (inet_pton(AF_INET, argv[1], &serv_addr.sin_addr) <= 0) {
        printf("\nInvalid address/ Address not supported \n");
        close(sockfd);
        return -1;
    }

    socklen_t len = sizeof(serv_addr);

    // 3. 发送数据
    sendto(sockfd, (const char *)hello, strlen(hello), MSG_CONFIRM,
           (const struct sockaddr *)&serv_addr, len);
    printf("Hello message sent.\n");

    // 4. 接收回复
    int n = recvfrom(sockfd, (char *)buffer, BUFFER_SIZE, MSG_WAITALL,
                     (struct sockaddr *)&serv_addr, &len);
    buffer[n] = '\0';
    printf("Server reply: %s\n", buffer);

    close(sockfd);
    return 0;
}

4. 高级话题与嵌入式优化

4.1 I/O多路复用 (select/poll/epoll)

在嵌入式系统中,一个进程可能需要同时处理多个网络连接。使用fork或多线程会带来较大开销。I/O多路复用技术允许单个进程监视多个文件描述符(Socket),是构建高性能网络服务器的关键。

  • select: 最古老、可移植性最好,但效率较低,有文件描述符数量限制(通常1024)。
  • poll: 解决了select的文件描述符数量限制,但效率依然不高。
  • epoll (Linux特有): 效率最高,是嵌入式Linux高性能网络编程的首选。
// epoll 使用简例
int epoll_fd = epoll_create1(0);
struct epoll_event event, events[MAX_EVENTS];

event.events = EPOLLIN; // 监听可读事件
event.data.fd = server_socket_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_socket_fd, &event);

while (1) {
    int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
    for (int i = 0; i < nfds; i++) {
        if (events[i].data.fd == server_socket_fd) {
            // 接受新连接
        } else {
            // 处理客户端数据
        }
    }
}

4.2 非阻塞I/O与超时设置

嵌入式设备网络环境可能不稳定,设置超时和非阻塞模式至关重要。

// 设置Socket为非阻塞
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

// 设置接收超时 (5秒)
struct timeval tv;
tv.tv_sec = 5;
tv.tv_usec = 0;
setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv));

4.3 资源管理与错误处理

嵌入式系统资源有限,必须谨慎管理:

  • 及时关闭Socket:使用close()释放文件描述符。
  • 检查返回值:所有Socket API调用都应检查返回值。
  • 处理信号中断:系统调用可能被信号中断(如EINTR),需要重试。
  • 使用getaddrinfo:替代过时的gethostbyname,支持IPv4/IPv6。

5. 总结

嵌入式Linux网络编程的核心在于理解Socket抽象和TCP/UDP协议的特性。从简单的回声服务器入手,逐步掌握多路复用、非阻塞I/O等高级技术,是构建稳定、高效嵌入式网络应用的必经之路。

在实际项目中,还需考虑:

  • 交叉编译工具链的配置。
  • 目标板的网络配置(IP、路由、防火墙)。
  • 使用更上层的库(如libevent, asio)来简化开发。
  • 进行充分的压力测试与异常测试,模拟网络断连、数据包乱序等场景。

希望本文能为你打开嵌入式Linux网络编程的大门。实践出真知,赶紧在开发板上运行这些示例代码吧!

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐