课程摘要

本课程是一份面向嵌入式 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 服务器流程:
  1. socket():创建套接字(买个手机)
  2. bind():绑定 IP 和端口(给手机办个号)
  3. listen():进入监听状态(手机开机等着别人打)
  4. accept():接受连接,返回新的套接字(有人打电话了,接听)
  5. read/write:收发数据(通话)
  6. close():关闭套接字(挂电话)
TCP 客户端流程:
  1. socket():创建套接字(买个手机)
  2. 设置服务器地址(拨号号码)
  3. connect():连接服务器(拨电话)
  4. read/write:收发数据(通话)
  5. 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;
}
现在来讲讲代码的实现:

运行方法:

  1. 先编译服务器:gcc server.c -o server,然后运行 ./server
  2. 再开一个终端编译客户端: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 服务器流程:
  1. socket():创建套接字
  2. bind():绑定 IP 和端口
  3. recvfrom():接收数据(能拿到对方的地址)
  4. sendto():发送数据(要指定对方地址)
  5. close():关闭
UDP 客户端流程:
  1. socket():创建套接字
  2. sendto():直接发数据给服务器
  3. recvfrom():收回复
  4. 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):

  1. 服务器监听 8888 端口
  2. 客户端连接后,发送一行字符串
  3. 服务器收到后,把收到的字符串原封不动发回去
  4. 客户端收到回显后打印出来
  5. 可以用 read 循环读取,支持多次收发

提示:可以用 fgets 从标准输入读用户输入的字符串,然后发给服务器,再收回复。


学习产出:

  • 技术笔记 1 篇
  • TCP 客户端 + 服务器 各 1 份
  • UDP 客户端 + 服务器 各 1 份
  • TCP 回显服务器综合练习 1 份

有疑问或是建议的同学们可以评论或私信我,我看到会及时解答或改正的。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐