4. Socket 编程基础

Socket(套接字)是网络通信的端点,是应用层与传输层之间的抽象接口。在嵌入式设备与 Host 端通信中,Socket 是编写网络程序的核心工具。本章将系统讲解 Socket 的定义、类型(TCP/UDP)以及通信模型,为后续 IP 与 Port 配置及通信验证打下基础。

4.1 什么是 Socket

Socket 起源于 Unix 操作系统,遵循“一切皆文件”的设计哲学。应用程序通过 Socket 向网络发送数据或从网络接收数据,就像读写文件一样。Socket 本质上是一个编程接口(API),它封装了传输层(TCP/UDP)的细节,使开发者无需直接处理底层协议栈。

核心概念: Socket = IP 地址 + 端口号 + 协议类型。一个完整的 Socket 连接由五元组唯一标识:源 IP、源端口、目的 IP、目的端口、协议。
Socket 在通信协议栈中的位置 应用层(Application Layer)— HTTP、MQTT、CoAP、自定义协议 Socket API 抽象层 — 应用与传输层的桥梁 传输层(Transport Layer)— TCP / UDP 网络层(Network Layer)— IP(IPv4 / IPv6) 数据链路层 & 物理层 — 以太网、Wi-Fi、PPP

4.2 Socket 类型:TCP 与 UDP

根据传输层协议的不同,Socket 主要分为两种类型:流式 Socket(SOCK_STREAM)和数据报 Socket(SOCK_DGRAM),分别对应 TCP 和 UDP 协议。

对比维度 TCP Socket(SOCK_STREAM) UDP Socket(SOCK_DGRAM)
连接方式 面向连接,需先建立连接(三次握手) 无连接,直接发送数据
可靠性 可靠传输,保证数据顺序、不丢失、不重复 不可靠,可能丢包、乱序、重复
数据边界 无边界,字节流传输 有边界,每个数据报独立
传输效率 较低,有确认重传、流量控制等机制 较高,无额外开销
适用场景 文件传输、远程登录、Web 浏览、设备配置 实时音视频、DNS 查询、传感器广播
Socket 类型值 SOCK_STREAM SOCK_DGRAM
选型建议:在嵌入式设备与 Host 端通信中,若需要可靠配置和指令下发(如修改 IP、重启设备),优先选择 TCP;若传输传感器数据且允许少量丢包,可选择 UDP 以降低延迟。

4.3 Socket 通信模型

Socket 通信模型描述了客户端与服务器之间建立连接、交换数据、关闭连接的全过程。TCP 和 UDP 的通信模型有显著差异。

4.3.1 TCP Socket 通信模型

TCP 通信遵循“服务器-客户端”模式,服务器端先启动监听,等待客户端连接。核心流程如下:

  1. 服务器端:调用 socket() 创建 Socket → bind() 绑定 IP 和端口 → listen() 进入监听状态 → accept() 阻塞等待客户端连接。
  2. 客户端:调用 socket() 创建 Socket → connect() 向服务器发起连接请求(三次握手)。
  3. 数据传输:连接建立后,双方使用 send() / recv() 或 write() / read() 进行双向通信。
  4. 关闭连接:任一方调用 close() 或 shutdown() 释放资源。
TCP Socket 通信模型(三次握手与四次挥手) 服务器(Server) 客户端(Client) socket() socket() bind() listen() accept() connect() SYN SYN+ACK ACK 连接建立(三次握手完成) 数据传输:send() / recv() 关闭连接(四次挥手) FIN ACK FIN ACK

4.3.2 UDP Socket 通信模型

UDP 通信无需建立连接,服务器端只需绑定端口,客户端直接向服务器发送数据报。核心流程如下:

  1. 服务器端:socket() → bind() → recvfrom() 接收数据。
  2. 客户端:socket() → sendto() 向服务器发送数据。
  3. 无连接:服务器也可通过 sendto() 向客户端回复数据。
UDP Socket 通信模型(无连接) 服务器(Server) 客户端(Client) socket() socket() bind() recvfrom() sendto() 数据报(Data) 回复(可选)

4.4 关键 API 函数速查

函数 所属模型 功能描述 原型(简化)
socket() TCP / UDP 创建 Socket 描述符 int socket(int domain, int type, int protocol)
bind() TCP / UDP 绑定 IP 地址和端口号 int bind(int sockfd, struct sockaddr *addr, socklen_t addrlen)
listen() TCP 设置监听队列,等待客户端连接 int listen(int sockfd, int backlog)
accept() TCP 接受客户端连接请求,返回新 Socket int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
connect() TCP 客户端向服务器发起连接 int connect(int sockfd, struct sockaddr *addr, socklen_t addrlen)
send() TCP 发送数据(已连接 Socket) ssize_t send(int sockfd, void *buf, size_t len, int flags)
recv() TCP 接收数据(已连接 Socket) ssize_t recv(int sockfd, void *buf, size_t len, int flags)
sendto() UDP 发送数据报(需指定目标地址) ssize_t sendto(int sockfd, void *buf, size_t len, int flags, struct sockaddr *dest_addr, socklen_t addrlen)
recvfrom() UDP 接收数据报(获取发送方地址) ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen)
close() TCP / UDP 关闭 Socket 描述符 int close(int fd)

4.5 嵌入式设备中的 Socket 编程要点

常见陷阱:嵌入式设备资源受限,需注意以下事项:
  • 非阻塞模式:默认 Socket 为阻塞模式,在嵌入式实时系统中应使用 fcntl() 或 ioctl() 设置为非阻塞,避免线程挂死。
  • 缓冲区大小:TCP 发送/接收缓冲区默认通常为 8KB~64KB,UDP 数据报最大 65507 字节,需根据设备内存合理调整。
  • 字节序:网络字节序为大端(Big-Endian),嵌入式设备多为小端(Little-Endian),需使用 htons()、htonl()、ntohs()、ntohl() 进行转换。
  • 超时处理:使用 setsockopt() 设置 SO_RCVTIMEO 和 SO_SNDTIMEO,防止无限等待。

4.6 代码示例:TCP Socket 服务器与客户端(C 语言)

以下代码展示了在嵌入式 Linux 环境下,一个简单的 TCP 回显服务器(Echo Server)及其客户端。服务器将客户端发送的数据原样返回。

服务器端代码(tcp_server.c):

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

#define PORT 8888
#define BUFFER_SIZE 1024

int main() {
    int server_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_len = sizeof(client_addr);
    char buffer[BUFFER_SIZE] = {0};

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

    // 2. 绑定 IP 和端口
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;  // 监听所有网络接口
    server_addr.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 3. 监听
    if (listen(server_fd, 3) < 0) {
        perror("listen failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }
    printf("Server listening on port %d...\n", PORT);

    // 4. 接受客户端连接
    if ((client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len)) < 0) {
        perror("accept failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }
    printf("Client connected: %s:%d\n",
           inet_ntoa(client_addr.sin_addr),
           ntohs(client_addr.sin_port));

    // 5. 数据收发(回显)
    int bytes_read;
    while ((bytes_read = read(client_fd, buffer, BUFFER_SIZE)) > 0) {
        printf("Received: %s", buffer);
        write(client_fd, buffer, bytes_read);  // 原样返回
        memset(buffer, 0, BUFFER_SIZE);
    }

    // 6. 关闭连接
    close(client_fd);
    close(server_fd);
    return 0;
}

客户端代码(tcp_client.c):

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

#define SERVER_IP "127.0.0.1"
#define PORT 8888
#define BUFFER_SIZE 1024

int main() {
    int sock = 0;
    struct sockaddr_in server_addr;
    char buffer[BUFFER_SIZE] = {0};
    const char *message = "Hello from embedded device!\n";

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

    // 2. 设置服务器地址
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {
        perror("invalid address");
        close(sock);
        exit(EXIT_FAILURE);
    }

    // 3. 连接服务器
    if (connect(sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("connect failed");
        close(sock);
        exit(EXIT_FAILURE);
    }

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

    // 5. 接收回显
    int bytes_read = read(sock, buffer, BUFFER_SIZE);
    printf("Echo received: %s", buffer);

    // 6. 关闭 Socket
    close(sock);
    return 0;
}
编译与测试:在 Linux 或嵌入式 Linux 环境中,使用 gcc tcp_server.c -o server 和 gcc tcp_client.c -o client 编译。先运行 ./server,再运行 ./client,客户端将发送消息并收到服务器回显。

4.7 本章小结

  • Socket 是应用层与传输层之间的编程接口,封装了 TCP/UDP 的通信细节。
  • TCP Socket 面向连接、可靠、无边界,适用于设备配置、文件传输等场景。
  • UDP Socket 无连接、不可靠、有边界,适用于实时传感器数据广播。
  • 通信模型:TCP 需经历三次握手建立连接、数据传输、四次挥手关闭连接;UDP 直接发送数据报。
  • 嵌入式编程中需注意非阻塞模式、缓冲区大小、字节序、超时处理等关键点。