Linux-网络编程实战
嵌入式Linux网络编程的核心在于理解Socket抽象和TCP/UDP协议的特性。从简单的回声服务器入手,逐步掌握多路复用、非阻塞I/O等高级技术,是构建稳定、高效嵌入式网络应用的必经之路。交叉编译工具链的配置。目标板的网络配置(IP、路由、防火墙)。使用更上层的库(如libeventasio)来简化开发。进行充分的压力测试与异常测试,模拟网络断连、数据包乱序等场景。希望本文能为你打开嵌入式Lin
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网络编程的大门。实践出真知,赶紧在开发板上运行这些示例代码吧!
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)