1.1. 套接字类型

套接字主要分为以下几种类型:

①流套接字(SOCK_STREAM)

  • 提供可靠、有序的双向字节流通信
  • 使用传输控制协议(TCP),保证数据无差错、无重复发送,并按顺序接收。适用于对数据完整性要求高的场景(如文件传输)。

②数据报套接字(SOCK_DGRAM)

  • 提供一种无连接的服务
  • 使用用户数据报协议(UDP),不保证数据传输的可靠性,数据可能在传输过程中丢失或出现重复,且无法保证顺序接收。适用于实时性要求高、允许丢包的场景(如音视频流)。

③原始套接字(SOCK_RAW)

  • 允许对较低层次的协议直接访问,如IP、ICMP协议。
  • 常用于检验新的协议实现或访问现有服务中配置的新设备。
1.2. 通信模型
  • 客户端-服务器模型:服务器监听端口,客户端发起连接请求,建立通信通道。
  • 点对点模型:适用于UDP通信,双方直接发送数据报。

二、套接字的系统调用

在Linux中,套接字的使用涉及一系列系统调用,主要包括:

①socket()

  • 创建一个套接字,并返回一个套接字描述符。
  • 需要指定协议族(如AF_INET用于IPv4)、套接字类型(如SOCK_STREAM或SOCK_DGRAM)和协议(通常设为0,使用默认值)。

②bind()

  • 将一个地址(对于网络套接字,通常是IP地址和端口号的组合)赋给套接字。
  • 服务器在启动时会调用此函数绑定一个众所周知的地址,用于提供服务。

③listen()

  • 使服务器套接字进入被动监听状态,等待客户端的连接请求。
  • 需要指定一个队列长度,用于存放来自客户端的连接请求。

④accept()

  • 服务器调用此函数接受一个客户端的连接请求。
  • 返回一个新的套接字描述符,用于与客户端进行通信。原套接字描述符继续保留,用于处理其他客户端的连接请求。

⑤connect()

  • 客户端调用此函数与服务器建立连接。
  • 需要指定服务器的地址(IP地址和端口号)。

⑥send()/recv()(或write()/read()):用于在已建立的连接上发送和接收数据。

⑦close():关闭套接字连接,释放资源。

三、套接字通信流程

3.1. 服务器端

①创建套接字:使用socket()函数创建一个套接字,指定套接字类型和协议。

②绑定地址:使用bind()函数将套接字绑定到指定的 IP 地址和端口号,以便客户端能够连接到该服务器。

③监听连接:使用listen()函数开始监听指定端口,等待客户端的连接请求。

④接受连接:当有客户端连接请求时,使用accept()函数接受连接,并返回一个新的套接字描述符,用于与该客户端进行通信。

⑤数据传输:使用recv()send()函数在服务器端和客户端之间进行数据的接收和发送。

⑥关闭套接字:通信结束后,使用close()函数关闭套接字,释放资源。

3.2. 客户端

①创建套接字:与服务器端相同,使用socket()函数创建套接字。

②连接服务器:使用connect()函数连接到服务器指定的 IP 地址和端口号。

③数据传输:连接成功后,使用send()recv()函数与服务器进行数据交互。

④关闭套接字:通信结束后,关闭套接字。

3.3. 示例代码

示例1:本地UNIX域套接字(进程间通信)

服务器端代码:

代码语言:javascript

AI代码解释

#include <stdio.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>

#define SOCKET_PATH "/tmp/embedded_socket"

int main() {
    int server_fd, client_fd;
    struct sockaddr_un addr;
    char buffer[128];

    // 创建UNIX域套接字
    server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (server_fd == -1) {
        perror("socket");
        return 1;
    }

    // 绑定套接字文件
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path)-1);
    unlink(SOCKET_PATH); // 确保文件不存在

    if (bind(server_fd, (struct sockaddr*)&addr, sizeof(addr)) {
        perror("bind");
        close(server_fd);
        return 1;
    }

    // 监听并接受连接
    listen(server_fd, 5);
    printf("Server waiting for connection...\n");
    client_fd = accept(server_fd, NULL, NULL);
    if (client_fd == -1) {
        perror("accept");
        close(server_fd);
        return 1;
    }

    // 接收数据
    read(client_fd, buffer, sizeof(buffer));
    printf("Received: %s\n", buffer);

    // 清理资源
    close(client_fd);
    close(server_fd);
    unlink(SOCKET_PATH); // 删除套接字文件
    return 0;
}

客户端代码:

代码语言:javascript

AI代码解释

#include <stdio.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>

#define SOCKET_PATH "/tmp/embedded_socket"

int main() {
    int sock_fd;
    struct sockaddr_un addr;
    const char *msg = "Hello from client!";

    sock_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sock_fd == -1) {
        perror("socket");
        return 1;
    }

    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path)-1);

    if (connect(sock_fd, (struct sockaddr*)&addr, sizeof(addr)) {
        perror("connect");
        close(sock_fd);
        return 1;
    }

    write(sock_fd, msg, strlen(msg)+1);
    close(sock_fd);
    return 0;
}

示例2:TCP网络通信(跨设备)

服务器端(嵌入式设备)

代码语言:javascript

AI代码解释

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int server_fd, client_fd;
    struct sockaddr_in addr;
    socklen_t addr_len = sizeof(addr);
    char buffer[BUFFER_SIZE];

    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == -1) {
        perror("socket");
        return 1;
    }

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

    if (bind(server_fd, (struct sockaddr*)&addr, sizeof(addr)) {
        perror("bind");
        close(server_fd);
        return 1;
    }

    listen(server_fd, 5);
    printf("TCP Server listening on port %d...\n", PORT);

    client_fd = accept(server_fd, (struct sockaddr*)&addr, &addr_len);
    ssize_t bytes_read = read(client_fd, buffer, BUFFER_SIZE);
    printf("Received: %s\n", buffer);

    close(client_fd);
    close(server_fd);
    return 0;
}

四、套接字在嵌入式Linux应用开发中的应用

在嵌入式Linux应用开发中,套接字广泛应用于网络通信和本地进程间通信。以下是一些典型应用场景:

4.1. 客户端-服务器模型
  • 服务器创建一个套接字并绑定到一个众所周知的地址上,然后进入监听状态。
  • 客户端创建一个套接字,并尝试连接到服务器。
  • 连接建立后,双方可以通过send()/recv(或write()/read())函数进行数据传输。
4.2. 本地进程间通信
  • 套接字也可以用于同一台计算机上的进程间通信。
  • 本地套接字的名字是Linux文件系统中的文件名,通常放在/tmp或/usr/tmp目录中。
  • 使用方式与网络通信类似,但不需要指定IP地址和端口号。
4.3. 网络编程
  • 套接字是实现网络编程的基础。
  • 通过套接字,可以实现不同计算机之间的数据传输和通信。
  • 在嵌入式系统中,套接字常用于实现设备之间的远程控制和数据交换。
4.4. 适用场景对比

场景

推荐套接字类型

优点

本地进程间高速通信

UNIX域套接字(SOCK_STREAM)

无需网络协议栈,低延迟

跨设备可靠数据传输

TCP套接字

数据完整,自动重传

实时音视频流

UDP套接字

低延迟,容忍丢包

多客户端并发连接

TCP + epoll多路复用

高效管理大量连接

五、嵌入式开发中的注意事项

5.1. 资源优化
  • 减少内存占用:使用较小的接收缓冲区(通过setsockopt调整SO_RCVBUF)。
  • 轻量级协议:优先选择UDP(无连接开销)或UNIX域套接字(避免网络协议栈)。
5.2. 实时性与稳定性
  • 非阻塞模式:使用fcntl设置O_NONBLOCK标志,结合select/poll实现多路复用。
  • 超时机制:通过SO_RCVTIMEOSO_SNDTIMEO设置套接字超时,避免永久阻塞。
5.3. 错误处理
  • 检查所有系统调用返回值:尤其是acceptconnectread/write等可能阻塞的函数。
  • 处理信号中断:对EINTR错误进行重试(如accept被信号打断时)。
5.4. 安全性
  • 权限控制(UNIX域套接字):通过bind时设置文件权限(如chmod),限制非授权进程访问。
  • 网络隔离:嵌入式设备若暴露网络接口,需配置防火墙规则或禁用无用端口。

六、常见问题

6.1. 连接与通信问题

连接失败(connectaccept错误)

  • 常见原因
    • 地址未释放:服务器重启后未清理之前的UNIX域套接字文件(如/tmp/xxx.sock),导致bind失败。
    • 端口被占用:TCP端口已被其他进程占用(如未正确关闭之前的服务)。
    • 权限问题:UNIX域套接字文件权限不足,或网络端口需要root权限(如绑定端口号<1024)。
    • 防火墙限制:嵌入式设备防火墙阻止了端口通信(如iptables规则)。

Logo

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

更多推荐