嵌入式 Linux 快速入门(五)
课程摘要
本课程是一份面向嵌入式 Linux 初学者的实战导向速成指南。上一篇我们学习了多线程编程,这一篇我们进入网络编程的世界。课程摒弃枯燥的理论,以动手实操为核心,带你掌握 Socket 编程基础、TCP 和 UDP 的客户端服务器实现,理解网络通信的底层原理,为后续物联网、嵌入式网络项目开发打下扎实基础。
开始前的准备:跟上一篇一样,VMware + Ubuntu 虚拟机、VSCode + Remote-SSH 插件环境准备好就行。可以开两个终端分别跑服务器和客户端,直接开干!
学习目标:
Socket 网络编程:
理解什么是 Socket 以及 TCP/UDP 的区别,学会 TCP 客户端服务器编程、UDP 客户端服务器编程,掌握字节序转换和 IP 地址转换函数
学习内容:
1. Socket 介绍:
我们先理解三个问题:
什么是 Socket?
为什么需要 Socket?
TCP 和 UDP 有什么区别?
前面我们学的进程间通信(管道、共享内存、消息队列等)都是在同一台机器上的进程之间通信。那如果两台不同的电脑上的进程要通信呢?这就需要 Socket(套接字) 了。
Socket 是网络通信的编程接口,你可以把它理解成“网络上的文件描述符”——就像操作文件一样,用 read/write 读写数据,只不过数据是通过网络发出去的。
Socket 通信的核心是客户端-服务器模型:
- 服务器:监听某个端口,等着客户端来连接
- 客户端:主动发起向服务器的 IP 和端口的连接
- 连接建立之后,两边就可以互相发数据了
几个重要概念:
| 概念 | 说明 |
|---|---|
| IP 地址 | 网络上一台主机的唯一标识,比如 192.168.1.100 |
| 端口号 | 16 位数字(0~65535),标识主机上的一个应用程序 |
| 套接字 | IP + 端口号,唯一标识一条网络连接的一端 |
| 服务器 | 提供服务的一方,被动等待连接 |
| 客户端 | 请求服务的一方,主动发起连接 |
🔔 类比一下:IP 地址就像小区地址,端口号就像门牌号。你要找一个人,得先找到哪个小区(IP),再找到哪一户(端口)。
TCP 和 UDP 的区别:
| 对比项 | TCP | UDP |
|---|---|---|
| 连接方式 | 面向连接,三次握手建立连接 | 无连接,直接发数据 |
| 可靠性 | 可靠,有确认重传,保证数据完整有序 | 不可靠,不保证不丢、不保证顺序 |
| 传输效率 | 相对慢,有额外开销 | 快,开销小 |
| 应用场景 | 网页、邮件、文件传输(要求可靠) | 视频、语音、游戏(要求实时) |
简单说:
- TCP 像打电话:先接通(建立连接),然后说话(传数据),确保对方能听到(可靠传输)
- UDP 像寄信:写好地址直接发(无连接),对方收不收得到你不知道(不可靠),但快
2. TCP 编程
TCP 是面向连接的可靠传输协议,也是最常用的。我们分别来看看服务器和客户端的流程。
TCP 服务器流程:
- socket():创建套接字(买个手机)
- bind():绑定 IP 和端口(给手机办个号)
- listen():进入监听状态(手机开机等着别人打)
- accept():接受连接,返回新的套接字(有人打电话了,接听)
- read/write:收发数据(通话)
- close():关闭套接字(挂电话)
TCP 客户端流程:
- socket():创建套接字(买个手机)
- 设置服务器地址(拨号号码)
- connect():连接服务器(拨电话)
- read/write:收发数据(通话)
- close():关闭套接字(挂电话)
socket 函数:
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
参数说明
| 参数 | 数据类型 | 详细说明 | 使用规则不补充 |
|---|---|---|---|
| domain | int | 协议族 | AF_INET:IPv4 AF_INET6:IPv6 |
| type | int | 套接字类型 | SOCK_STREAM:TCP 流套接字 SOCK_DGRAM:UDP 数据报套接字 |
| protocol | int | 具体协议 | 一般填 0,让系统自动选 |
| 返回值 | 含义 |
|---|---|
| ≥ 0 | 成功,返回套接字文件描述符 |
| -1 | 失败,设置 errno |
示例:
// 创建 TCP 套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 创建 UDP 套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
sockaddr_in 地址结构体:
设置 IP 和端口用这个结构体:
#include <netinet/in.h>
struct sockaddr_in {
sa_family_t sin_family; // 协议族,AF_INET
in_port_t sin_port; // 端口号(网络字节序)
struct in_addr sin_addr; // IP 地址
};
struct in_addr {
uint32_t s_addr; // IP 地址(网络字节序)
};
这里有两个重要的转换函数:
htons(主机字节序转网络字节序):
#include <arpa/inet.h>
uint16_t htons(uint16_t hostshort);
- 作用:把 16 位的端口号从主机字节序转成网络字节序
- h = host(主机),to = 转,n = network(网络),s = short(16位)
- 为什么要转?因为不同 CPU 的字节序可能不一样(大端/小端),网络上统一用大端序
inet_pton(IP 字符串转二进制):
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
参数说明
| 参数 | 数据类型 | 详细说明 | 使用规则不补充 |
|---|---|---|---|
| af | int | 地址族 | AF_INET 或 AF_INET6 |
| src | const char * | IP 地址字符串 | 比如 “192.168.1.100” |
| dst | void * | 目标缓冲区 | 存转换后的二进制地址 |
| 返回值 | 含义 |
|---|---|
| 1 | 成功 |
| 0 | 无效的 IP 地址字符串 |
| -1 | 失败,设置 errno |
还有一个常用的 inet_aton 函数,功能类似:
int inet_aton(const char *cp, struct in_addr *inp);
bind 函数(服务器用):
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明
| 参数 | 数据类型 | 详细说明 | 使用规则不补充 |
|---|---|---|---|
| sockfd | int | 套接字文件描述符 | socket() 返回的 |
| addr | const struct sockaddr * | 要绑定的地址 | 一般传 sockaddr_in 的指针,要强转 |
| addrlen | socklen_t | 地址结构体的长度 | sizeof(struct sockaddr_in) |
| 返回值 | 含义 |
|---|---|
| 0 | 成功 |
| -1 | 失败,设置 errno |
🔔 服务器一般用
INADDR_ANY表示监听所有网卡的 IP,这样不管客户端连哪个 IP 都能连上。
listen 函数(服务器用):
#include <sys/socket.h>
int listen(int sockfd, int backlog);
参数说明
| 参数 | 数据类型 | 详细说明 | 使用规则不补充 |
|---|---|---|---|
| sockfd | int | 套接字文件描述符 | |
| backlog | int | 连接队列的大小 | 等待 accept 的连接队列最大长度 |
| 返回值 | 含义 |
|---|---|
| 0 | 成功 |
| -1 | 失败,设置 errno |
accept 函数(服务器用):
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数说明
| 参数 | 数据类型 | 详细说明 | 使用规则不补充 |
|---|---|---|---|
| sockfd | int | 监听套接字 | 一直在等连接的那个 socket |
| addr | struct sockaddr * | 传出参数,存客户端地址 | 不需要可以传 NULL |
| addrlen | socklen_t * | 传入传出参数 | 传入 addr 的大小,传出实际大小 |
| 返回值 | 含义 |
|---|---|
| ≥ 0 | 成功,返回新的套接字描述符(专门和这个客户端通信) |
| -1 | 失败,设置 errno |
🔔 注意:accept 会返回一个新的 socket,原来的监听 socket 继续等着下一个连接。新的 socket 用来和这个客户端收发数据。
connect 函数(客户端用):
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明
| 参数 | 数据类型 | 详细说明 | 使用规则不补充 |
|---|---|---|---|
| sockfd | int | 套接字文件描述符 | |
| addr | const struct sockaddr * | 服务器的地址 | 要连哪个服务器就填哪个的 IP 和端口 |
| addrlen | socklen_t | 地址长度 |
| 返回值 | 含义 |
|---|---|
| 0 | 成功,连接建立 |
| -1 | 失败,设置 errno |
TCP 服务器代码示例:
/*
头文件说明:
<stdio.h> printf、perror
<string.h> strlen、memset
<unistd.h> read、write、close
<arpa/inet.h> htons、inet_pton 等网络函数
<stdlib.h> exit
*/
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <stdlib.h>
#define PORT 8080 // 监听端口
#define BUFFER_SIZE 1024 // 缓冲区大小
int main()
{
int server_fd, new_socket;
struct sockaddr_in address;
char buffer[BUFFER_SIZE] = {0};
const char *message = "Hello from server";
/*
第一步:创建套接字
AF_INET:IPv4
SOCK_STREAM:TCP
0:默认协议
*/
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0)
{
perror("socket 创建失败");
exit(EXIT_FAILURE);
}
/*
第二步:设置地址并绑定
*/
address.sin_family = AF_INET; // IPv4
address.sin_addr.s_addr = INADDR_ANY; // 监听所有网卡
address.sin_port = htons(PORT); // 端口,转网络字节序
// bind 绑定
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0)
{
perror("bind 失败");
close(server_fd);
exit(EXIT_FAILURE);
}
/*
第三步:监听
backlog = 3,连接队列最多 3 个
*/
if (listen(server_fd, 3) < 0)
{
perror("listen 失败");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("服务器:正在监听端口 %d...\n", PORT);
/*
第四步:接受连接
accept 会阻塞,直到有客户端连上来
返回一个新的 socket,专门和这个客户端通信
*/
if ((new_socket = accept(server_fd, NULL, NULL)) < 0)
{
perror("accept 失败");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("服务器:客户端连接成功!\n");
/*
第五步:收发数据
先读客户端发来的消息
*/
read(new_socket, buffer, BUFFER_SIZE);
printf("服务器收到:%s\n", buffer);
// 再给客户端回消息
write(new_socket, message, strlen(message));
printf("服务器发送:%s\n", message);
/*
第六步:关闭
先关新连接的 socket
再关监听 socket
*/
close(new_socket);
close(server_fd);
return 0;
}
TCP 客户端代码示例:
#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 **argv)
{
int sockfd;
struct sockaddr_in serv_addr;
char buffer[BUFFER_SIZE] = {0};
const char *message = "Hello from client";
// 简单的参数检查
if (argc != 2)
{
printf("用法:%s <服务器IP>\n", argv[0]);
exit(EXIT_FAILURE);
}
/* 第一步:创建套接字 */
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
perror("socket 创建失败");
exit(EXIT_FAILURE);
}
/* 第二步:设置服务器地址 */
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
/*
把 IP 字符串转成二进制
用 inet_aton 函数
*/
int ret = inet_aton(argv[1], &serv_addr.sin_addr);
if (ret == 0)
{
printf("IP 地址无效\n");
close(sockfd);
exit(EXIT_FAILURE);
}
/* 第三步:连接服务器 */
if (connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
{
perror("connect 失败");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("客户端:连接服务器成功!\n");
/* 第四步:收发数据 */
// 先发消息
write(sockfd, message, strlen(message));
printf("客户端发送:%s\n", message);
// 再收回复
read(sockfd, buffer, BUFFER_SIZE);
printf("客户端收到:%s\n", buffer);
/* 第五步:关闭 */
close(sockfd);
return 0;
}
现在来讲讲代码的实现:
运行方法:
- 先编译服务器:
gcc server.c -o server,然后运行./server - 再开一个终端编译客户端:
gcc client.c -o client,然后运行./client 127.0.0.1
🔔 127.0.0.1 是本机回环地址,客户端和服务器在同一台机器上就用这个。
服务器和客户端的 sockaddr_in 有什么区别?
-
服务器的 sockaddr_in:存自己的地址
- 服务器要告诉内核:“我在本机所有网卡的 8080 端口上监听,任何客户端连这个地址都能找到我”
- 用
INADDR_ANY表示监听所有网卡
-
客户端的 sockaddr_in:存服务器的地址
- 客户端要告诉内核:“我要连接到 192.168.1.100 这个 IP 的 8080 端口”
- 客户端自己的 IP 和端口系统会自动分配,不用管
3. UDP 编程
UDP 是无连接的,不用建立连接,直接发数据就行,比 TCP 简单。
UDP 服务器流程:
- socket():创建套接字
- bind():绑定 IP 和端口
- recvfrom():接收数据(能拿到对方的地址)
- sendto():发送数据(要指定对方地址)
- close():关闭
UDP 客户端流程:
- socket():创建套接字
- sendto():直接发数据给服务器
- recvfrom():收回复
- close():关闭
🔔 UDP 没有 connect、listen、accept,因为不需要建立连接,直接发包就行。
recvfrom 函数:
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
参数说明
| 参数 | 数据类型 | 详细说明 | 使用规则不补充 |
|---|---|---|---|
| sockfd | int | 套接字 | |
| buf | void * | 接收缓冲区 | 存收到的数据 |
| len | size_t | 缓冲区大小 | |
| flags | int | 控制标志 | 一般填 0(阻塞) |
| src_addr | struct sockaddr * | 传出参数,存发送方地址 | 不需要可以传 NULL |
| addrlen | socklen_t * | 地址长度 | 传入传出参数 |
| 返回值 | 含义 |
|---|---|
| ≥ 0 | 成功,返回实际收到的字节数 |
| -1 | 失败,设置 errno |
sendto 函数:
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
参数说明
| 参数 | 数据类型 | 详细说明 | 使用规则不补充 |
|---|---|---|---|
| sockfd | int | 套接字 | |
| buf | const void * | 要发送的数据 | |
| len | size_t | 数据长度 | |
| flags | int | 控制标志 | 一般填 0 |
| dest_addr | const struct sockaddr * | 目标地址 | 发给谁就填谁的 IP 和端口 |
| addrlen | socklen_t | 地址长度 |
| 返回值 | 含义 |
|---|---|
| ≥ 0 | 成功,返回实际发送的字节数 |
| -1 | 失败,设置 errno |
UDP 服务器代码示例:
/*
头文件说明:
<stdio.h> printf、perror
<stdlib.h> exit
<string.h> strlen、memset
<unistd.h> close
<arpa/inet.h> htons、inet_ntoa 等
*/
#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;
struct sockaddr_in address;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {0};
/* 第一步:创建 UDP 套接字 */
if ((server_fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{
perror("socket 创建失败");
exit(EXIT_FAILURE);
}
/* 第二步:绑定地址和端口 */
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0)
{
perror("bind 失败");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("UDP 服务器:正在监听端口 %d...\n", PORT);
/*
第三步:接收数据
recvfrom 会阻塞,直到收到数据
同时能拿到客户端的地址,存在 address 里
*/
int n = recvfrom(server_fd, buffer, BUFFER_SIZE, 0,
(struct sockaddr *)&address, (socklen_t *)&addrlen);
printf("UDP 服务器收到:%s\n", buffer);
printf("客户端地址:%s:%d\n",
inet_ntoa(address.sin_addr), ntohs(address.sin_port));
/*
第四步:回复数据
用 sendto 发回给客户端,地址就是刚才 recvfrom 拿到的
*/
const char *message = "Hello from UDP server";
sendto(server_fd, message, strlen(message), 0,
(struct sockaddr *)&address, addrlen);
printf("UDP 服务器发送:%s\n", message);
/* 第五步:关闭 */
close(server_fd);
return 0;
}
UDP 客户端代码示例:
#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 sockfd;
struct sockaddr_in serv_addr;
char buffer[BUFFER_SIZE] = {0};
/* 第一步:创建 UDP 套接字 */
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{
perror("socket 创建失败");
exit(EXIT_FAILURE);
}
/* 第二步:设置服务器地址 */
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
// IP 地址转换
if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0)
{
perror("地址转换失败");
close(sockfd);
exit(EXIT_FAILURE);
}
/*
第三步:直接发送数据(不用 connect!)
UDP 是无连接的,直接 sendto 就行
*/
const char *message = "Hello from UDP client";
sendto(sockfd, message, strlen(message), 0,
(struct sockaddr *)&serv_addr, sizeof(serv_addr));
printf("UDP 客户端发送:%s\n", message);
/*
第四步:接收回复
不需要对方地址的话,后两个参数可以传 NULL
*/
int n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0, NULL, NULL);
printf("UDP 客户端收到:%s\n", buffer);
/* 第五步:关闭 */
close(sockfd);
return 0;
}
总结:
其实现在实际开发中,直接用 Socket API 写应用的情况不多了,网络上有很多成熟的库可以用:
- 做 HTTP 用 curl 库
- 做 MQTT 用 Paho 库
- 做 WebSocket 也有现成的库
这些库封装得很好,不用我们自己处理底层的细节,企业开发一般都是用这些成熟的库,避免重复造轮子。
但为什么还要学底层 Socket 呢?因为这些库的底层都是基于 Socket 封装的,懂了底层原理,以后遇到 HTTP、MQTT、WebSocket 这些协议的时候,理解起来就轻松多了,出了问题也知道大概是哪一层的事。
以上就是 Linux Socket 网络编程的基础入门了,觉得有帮助的同学们可以点赞收藏支持一下小弟!!!
学完了可以自己尝试动手完成一个小练习,步骤如下:
写一个简单的 TCP 回显服务器(echo server):
- 服务器监听 8888 端口
- 客户端连接后,发送一行字符串
- 服务器收到后,把收到的字符串原封不动发回去
- 客户端收到回显后打印出来
- 可以用
read循环读取,支持多次收发
提示:可以用 fgets 从标准输入读用户输入的字符串,然后发给服务器,再收回复。
学习产出:
- 技术笔记 1 篇
- TCP 客户端 + 服务器 各 1 份
- UDP 客户端 + 服务器 各 1 份
- TCP 回显服务器综合练习 1 份
有疑问或是建议的同学们可以评论或私信我,我看到会及时解答或改正的。
更多推荐




所有评论(0)