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、目的端口、协议。
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 通信遵循“服务器-客户端”模式,服务器端先启动监听,等待客户端连接。核心流程如下:
- 服务器端:调用
socket()创建 Socket →bind()绑定 IP 和端口 →listen()进入监听状态 →accept()阻塞等待客户端连接。 - 客户端:调用
socket()创建 Socket →connect()向服务器发起连接请求(三次握手)。 - 数据传输:连接建立后,双方使用
send()/recv()或write()/read()进行双向通信。 - 关闭连接:任一方调用
close()或shutdown()释放资源。
4.3.2 UDP Socket 通信模型
UDP 通信无需建立连接,服务器端只需绑定端口,客户端直接向服务器发送数据报。核心流程如下:
- 服务器端:
socket()→bind()→recvfrom()接收数据。 - 客户端:
socket()→sendto()向服务器发送数据。 - 无连接:服务器也可通过
sendto()向客户端回复数据。
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 直接发送数据报。
- 嵌入式编程中需注意非阻塞模式、缓冲区大小、字节序、超时处理等关键点。