嵌入式Linux C应用编程——socket网络编程(二、socket应用)
Socket 网络编程基础(二)—— Socket 应用
一、Socket 简介
Socket(套接字) 是 Linux 下的一种进程间通信机制(socket IPC),可实现不同主机或同一主机上不同应用程序之间的通信。
- 采用 客户端-服务器(Client-Server) 模式,多个客户端可同时连接服务器进行数据交互。
- Socket 的本质:是应用层与 TCP/IP 协议通信的中间软件抽象层,属于门面模式——将复杂的 TCP/IP 协议隐藏在简单接口后面,开发者无需深入理解 TCP/UDP 细节即可编程。
- BSD Socket 标准:跨平台性强,可在 Linux、LwIP、Windows、RT-Thread 等多平台移植。
二、核心 API 详解
1. 头文件
#include <sys/types.h>
#include <sys/socket.h>
2. socket() —— 创建套接字
int socket(int domain, int type, int protocol);
功能:类似于 open(),创建一个网络通信端点,返回 socket 描述符(本质是文件描述符)。调用成功后通过该描述符进行后续读写操作;不再使用时调用 close() 释放。
参数:
-
domain(协议族):指定通信域,选择协议族。AF_INET— IPv4 Internet 协议(最常用)AF_INET6— IPv6 Internet 协议AF_UNIX / AF_LOCAL— 本地通信AF_PACKET— 底层包接口
-
type(套接字类型):指定套接字的通信语义。SOCK_STREAM— TCP 协议:提供有序、可靠、双向、基于连接的字节流,能保证数据正确传送到对方,可支持带外数据传输SOCK_DGRAM— UDP 协议:固定长度的、无连接的、不可靠的报文传递SOCK_RAW— 原始套接字:允许应用程序访问网络层的原始数据包SOCK_SEQPACKET— 固定长度的、有序的、可靠的、面向连接的报文传递
-
protocol:通常设为0,表示为给定的通信域和套接字类型选择默认协议。AF_INET+SOCK_STREAM→ 默认 TCPAF_INET+SOCK_DGRAM→ 默认 UDP
返回值:成功返回 socket 描述符(非负整数);失败返回 -1 并设置 errno。
示例:
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (0 > sockfd) {
perror("socket error");
exit(-1);
}
close(sockfd);
3. bind() —— 绑定地址
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:将一个 IP 地址和端口号与一个套接字进行绑定(关联)。服务器必须调用,以便客户端知道服务器的地址(固定的 IP + 端口);客户端通常不需要显式调用,由内核在 connect() 时自动分配一个临时端口。
参数:
-
sockfd:socket()返回的套接字描述符。 -
addr:指向struct sockaddr类型变量的指针,包含待绑定的 IP 地址和端口号。实际编程中通常使用struct sockaddr_in(与struct sockaddr空间兼容,对用户更友好):struct sockaddr_in { sa_family_t sin_family; // 协议族,如 AF_INET in_port_t sin_port; // 端口号(网络字节序) struct in_addr sin_addr; // IP 地址(网络字节序) unsigned char sin_zero[8]; // 保留,填充为 0 };sin_family— 必须与socket()的domain一致,通常为AF_INETsin_port— 端口号,需用htons()转换为主机字节序到网络字节序sin_addr.s_addr— IP 地址,常用htonl(INADDR_ANY)(绑定任意本地 IP)
-
addrlen:addr所指向的结构体字节大小,通常为sizeof(struct sockaddr_in)。
返回值:成功返回 0;失败返回 -1 并设置 errno。
htons()/htonl()是字节序转换宏,将主机字节序转换为网络字节序,需包含<netinet/in.h>。INADDR_ANY表示绑定所有本地网卡(值为0.0.0.0)。
什么是主机字节序和网络字节序?
主机字节序取决于 CPU 架构:
- 小端(Little-Endian):低地址存低位字节。x86 架构(如 PC)使用小端。例如
0x1234在内存中为[0x34, 0x12]- 大端(Big-Endian):低地址存高位字节。ARM 架构可配置,但 Linux 下通常也设为小端
网络字节序:TCP/IP 协议规定,所有多字节整数在网络上传输时必须使用大端,以确保不同架构的机器能正确解析对方的数据。
端口号(16 位)和 IP 地址(32 位)都是多字节整数,所以在填充
sin_port和sin_addr.s_addr时必须用htons()/htonl()将主机字节序转为网络字节序。对应的反向转换是ntohs()/ntohl()(网络序 → 主机序),用于解析收到的地址数据。
为什么用
INADDR_ANY(0.0.0.0)而不是服务器真实 IP?
INADDR_ANY表示绑定所有本地网络接口。服务器可能有多个网卡(如 eth0:192.168.1.100、wlan0:10.0.0.1):
htonl(INADDR_ANY): 监听所有网卡上的连接请求,无论客户端连哪个 IP 都能收到inet_addr("192.168.1.100"): 只监听该特定网卡,其他网卡上的连接会被忽略在嵌入式开发中,开发板的 IP 常由 DHCP 动态分配,用
INADDR_ANY更灵活,无论分配到什么 IP 都不会影响服务器接收连接。
为什么客户端不需要 bind?
客户端其实也需要地址,否则服务器收到请求后不知道该把响应数据发往哪里。区别在于:
- 服务器的地址(IP + 端口)必须是固定、众所周知的,这样客户端才能找到它,所以需要显式
bind()- 客户端的地址无需提前固定,内核会在调用
connect()时自动从本地选择一个空闲的临时端口(通常 > 32768)分配给客户端套接字,同时客户端的 IP 地址由网络接口配置决定,所以不需要显式bind()服务器在
accept()返回时,可以通过传出参数addr获取到客户端的自动分配地址,从而知道数据该发回给谁。
示例:
struct sockaddr_in socket_addr;
memset(&socket_addr, 0x0, sizeof(socket_addr));
socket_addr.sin_family = AF_INET;
socket_addr.sin_addr.s_addr = htonl(INADDR_ANY);
socket_addr.sin_port = htons(5555);
bind(sockfd, (struct sockaddr *)&socket_addr, sizeof(socket_addr));
4. listen() —— 监听连接
int listen(int sockfd, int backlog);
功能:仅服务器使用,使服务器进程进入监听状态,等待客户端的连接请求。通常在 bind() 之后、accept() 之前调用。无法在一个已连接的套接字上执行 listen()。
参数:
sockfd:socket()返回的套接字描述符。backlog:等待连接队列能够达到的最大值。当服务器进程正处理客户端的连接请求时,若有其他客户端同时请求连接,内核将这些请求放入队列中,按先来后到顺序处理。backlog告诉内核使用该值作为队列上限。队列满时,新客户端会收到连接失败错误。
返回值:成功返回 0;失败返回 -1 并设置 errno。
5. accept() —— 接受连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能:仅服务器使用,获取客户端的连接请求并建立连接。accept() 返回一个新的套接字,该套接字与调用 connect() 的客户端建立了连接,服务器后续通过这个新套接字与该客户端进行数据交互。
socket()返回的是服务器监听套接字(一直监听),而accept()返回的是代表与客户端之间连接的新套接字。
参数:
sockfd:socket()返回的服务器套接字描述符。addr:传出参数,指向struct sockaddr的指针,用于返回已连接客户端的 IP 地址和端口号等信息。若不关心可设为NULL。addrlen:传入/传出参数,应初始化为addr所指向对象的字节长度,函数返回时会更新为实际长度。若addr为NULL则此参数也应置NULL。
返回值:成功返回新的套接字描述符(非负整数);失败返回 -1 并设置 errno。
服务器标准流程:
socket() → bind() → listen() → accept() → read/recv、write/send → close()
6. connect() —— 发起连接
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:客户端使用,将套接字 sockfd 与远程服务器进行连接。
- 对于 TCP 连接:调用该函数将触发三次握手过程,最终建立一个 TCP 连接。
- 对于 UDP 协议:调用该函数只是在
sockfd中记录服务器的 IP 地址与端口号,不发送任何数据。
参数:
sockfd:socket()返回的客户端套接字描述符。addr:指向struct sockaddr的指针,指定待连接的服务器的 IP 地址和端口号。addrlen:addr所指向的结构体字节大小。
返回值:成功返回 0;失败返回 -1 并设置 errno。
7. 数据收发函数
read() —— 从套接字读取数据
ssize_t read(int fd, void *buf, size_t count);
功能:从一个文件描述符中读取指定字节大小的数据并放入到指定缓冲区。套接字描述符也是文件描述符,所以可直接使用 read() 读取网络数据。
参数:
fd:套接字描述符。buf:指向数据接收缓冲区的指针。count:期望读取的字节数。
返回值:成功返回实际读取到的字节数(可能小于 count);已到达末尾返回 0;出错返回 -1 并设置 errno。
recv() —— 从套接字接收数据(带标志)
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
功能:与 read() 类似,从套接字描述符中读取网络数据,但可通过 flags 参数指定标志来控制接收行为。
参数:
sockfd:套接字描述符。buf:指向数据接收缓冲区的指针。len:读取数据的字节大小。flags:控制接收方式的标志,通常设为0。可选标志:MSG_DONTWAIT— 启动非阻塞操作MSG_OOB— 如果协议支持,获取带外数据MSG_PEEK— 窥探数据但不取走(再次调用仍可读到相同数据)MSG_WAITALL— 等待所有数据就绪才返回(仅SOCK_STREAM)MSG_TRUNC— 即使数据包被截断,也返回数据包的长度MSG_ERRQUEUE— 接收错误信息作为辅助数据
返回值:成功返回实际读取到的字节数;对方已关闭返回 0;出错返回 -1 并设置 errno。
write() —— 向套接字发送数据
ssize_t write(int fd, const void *buf, size_t count);
功能:向套接字描述符中写入数据(发送网络数据)。
参数:
fd:套接字描述符。buf:指向待发送数据的缓冲区。count:写入的字节数。
返回值:成功返回写入的字节数;失败返回 -1 并设置 errno。
send() —— 向套接字发送数据(带标志)
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
功能:与 write() 类似,向套接字描述符发送数据,但可通过 flags 参数指定标志来控制发送行为。
send()成功返回仅表示数据已无错误地发送到网络驱动程序上,并不代表连接的另一端已接收数据。
参数:
sockfd:套接字描述符。buf:指向待发送数据的缓冲区。len:发送数据的字节大小。flags:控制发送方式的标志,通常设为0。可选标志:MSG_DONTWAIT— 允许非阻塞操作MSG_NOSIGNAL— 在写无连接的套接字时不产生SIGPIPE信号MSG_OOB— 如果协议支持,发送带外数据MSG_CONFIRM— 提供链路层反馈以保持地址映射有效MSG_DONTROUTE— 勿将数据包路由出本地网络MSG_MORE— 延迟发送数据包,允许写更多数据
返回值:成功返回实际发送的字节数;失败返回 -1 并设置 errno。
8. close() —— 关闭套接字
int close(int fd);
功能:当不再需要套接字描述符时,调用 close() 关闭套接字,释放相应资源。
参数:
fd:要关闭的套接字描述符(可以是socket()返回的监听套接字,也可以是accept()返回的连接套接字)。
返回值:成功返回 0;失败返回 -1 并设置 errno。
三、IP 地址格式转换
传统函数(已废弃,仅旧代码中可见)
| 函数 | 功能 |
|---|---|
inet_aton() |
点分十进制字符串 → 二进制 |
inet_addr() |
点分十进制字符串 → 二进制 |
inet_ntoa() |
二进制 → 点分十进制字符串 |
推荐函数(支持 IPv4 和 IPv6)
inet_pton() —— 点分十进制字符串 → 二进制地址
int inet_pton(int af, const char *src, void *dst);
功能:将点分十进制表示的字符串形式的 IP 地址转换为二进制的 IPv4 或 IPv6 地址。
参数:
af:指定地址族。AF_INET— 转换 IPv4 地址,dst应为struct in_addr *AF_INET6— 转换 IPv6 地址,dst应为struct in6_addr *
src:指向点分十进制字符串(如"192.168.1.222")。dst:指向存放转换后二进制地址的结构体对象。
返回值:成功返回 1;src 是无效地址返回 0;af 无效返回 -1 并设置 errno 为 EAFNOSUPPORT。
示例:
struct in_addr addr;
inet_pton(AF_INET, "192.168.1.222", &addr);
printf("ip addr: 0x%x\n", addr.s_addr);
// 输出: 0xde01a8c0
inet_ntop() —— 二进制地址 → 点分十进制字符串
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
功能:执行与 inet_pton() 相反的操作,将二进制 IP 地址转换为点分十进制形式的字符串。
参数:
af:指定地址族(AF_INET或AF_INET6)。src:指向二进制地址结构体(struct in_addr *或struct in6_addr *)。dst:指向存放转换后点分十进制字符串的缓冲区。size:指定dst缓冲区的大小。
返回值:成功返回 dst 指针;size 太小返回 NULL 并设置 errno 为 ENOSPC。
示例:
struct in_addr addr;
char buf[20] = {0};
addr.s_addr = 0xde01a8c0;
inet_ntop(AF_INET, &addr, buf, sizeof(buf));
printf("ip addr: %s\n", buf);
// 输出: 192.168.1.222
in_addr这个结构体实际上只有一个参数,所以&addr实际上就是addr.s_addr。
四、编程实战 —— TCP 服务器与客户端
服务器程序流程
① socket() — 创建 TCP 套接字
② bind() — 绑定端口和 IP 地址
③ listen() — 进入监听状态
④ accept() — 阻塞等待并接受客户端连接
⑤ 通信循环 — recv() 接收数据 / send() 发送数据
⑥ close() — 关闭套接字
服务器示例(端口 8888):
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define SERVER_PORT 8888
int main(void)
{
struct sockaddr_in server_addr = {0};
struct sockaddr_in client_addr = {0};
char ip_str[20] = {0};
int sockfd, connfd;
int addrlen = sizeof(client_addr);
char recvbuf[512];
int ret;
/* ① 打开套接字 */
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (0 > sockfd) {
perror("socket error");
exit(EXIT_FAILURE);
}
/* ② 绑定地址与端口 */
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(SERVER_PORT);
ret = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (0 > ret) {
perror("bind error");
close(sockfd);
exit(EXIT_FAILURE);
}
/* ③ 进入监听状态 */
ret = listen(sockfd, 50);
if (0 > ret) {
perror("listen error");
close(sockfd);
exit(EXIT_FAILURE);
}
/* ④ 阻塞等待客户端连接 */
connfd = accept(sockfd, (struct sockaddr *)&client_addr, &addrlen);
if (0 > connfd) {
perror("accept error");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("有客户端接入...\n");
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip_str, sizeof(ip_str));
printf("客户端 IP: %s\n", ip_str);
printf("客户端端口: %d\n", client_addr.sin_port);
/* ⑤ 通信循环 */
for ( ; ; ) {
memset(recvbuf, 0x0, sizeof(recvbuf));
ret = recv(connfd, recvbuf, sizeof(recvbuf), 0);
if (0 >= ret) {
perror("recv error");
close(connfd);
break;
}
printf("from client: %s\n", recvbuf);
/* 客户端发送 "exit" 时,双方约定断开连接 */
if (0 == strncmp("exit", recvbuf, 4)) {
printf("server exit...\n");
close(connfd); // 关闭与客户端的连接
break; // 退出通信循环,服务结束
}
}
/* ⑥ 关闭套接字 */
close(sockfd);
exit(EXIT_SUCCESS);
}
端口选择:端口不能与其它服务冲突,不常用的端口号通常大于 5000。
客户端程序流程
① socket() — 创建 TCP 套接字
② connect() — 连接远端服务器
③ 交互循环 — 用户输入 → send() 发送数据
④ close() — 关闭套接字
客户端示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define SERVER_PORT 8888
#define SERVER_IP "192.168.1.150" // 根据实际服务器 IP 修改
int main(void)
{
struct sockaddr_in server_addr = {0};
char buf[512];
int sockfd;
int ret;
/* ① 打开套接字 */
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (0 > sockfd) {
perror("socket error");
exit(EXIT_FAILURE);
}
/* ② 连接服务器 */
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);
ret = connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (0 > ret) {
perror("connect error");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("服务器连接成功...\n\n");
/* ③ 交互循环:用户输入并发送 */
for ( ; ; ) {
memset(buf, 0x0, sizeof(buf));
printf("Please enter a string: ");
fgets(buf, sizeof(buf), stdin);
ret = send(sockfd, buf, strlen(buf), 0);
if (0 > ret) {
perror("send error");
break;
}
/* 用户输入 "exit" 时,退出循环,关闭连接 */
if (0 == strncmp(buf, "exit", 4))
break;
}
/* ④ 关闭套接字 */
close(sockfd);
exit(EXIT_SUCCESS);
}
编译与测试
# 编译服务器(ARM 交叉编译,用于开发板)
arm-linux-gnueabihf-gcc -o server socket_server.c
# 编译客户端(PC 端 Ubuntu)
gcc -o client socket_client.c
测试步骤:
- 将
server拷贝到开发板/home/root/目录 - 开发板运行
./server(进入监听状态) - Ubuntu 运行
./client(连接服务器) - 客户端输入字符串,服务器端打印接收到的内容
- 客户端输入
exit,双方断开连接


值得注意的是,开发板的IP,也就是服务器的地址是动态分配的,有可能会发生改变,此时客户端的代码中的IP就需要修改后重新编译运行才能连接上。
否则可能出现问题:


浙公网安备 33010602011771号