Linux 应用层开发入门(二十五)| 网络编程

前言

网络编程是 Linux 应用层开发的核心技能之一,无论是服务端程序、客户端工具,还是物联网、音视频、分布式系统等场景,都离不开网络通信。本章作为网络编程的入门开篇,将聚焦通信标识、通信角色、传输协议三大基础核心,为后续编写TCP/UDP代码打下扎实理论基础。


1 网络编程核心基础概念

1.1 IP 和端口:网络通信的「地址标识」

所有网络数据传输,都离不开三个核心要素:源、目的、长度。其中,如何精准定位数据的发送方(源)和接收方(目的),是网络通信的首要问题。

在网络传输中,我们通过IP+端口的组合,唯一标识一个网络通信端点:

  • IP地址 :定位网络中的主机(电脑、服务器、嵌入式设备等),相当于设备的「家庭住址」,解决「数据发给哪台设备」的问题;
  • 端口号 :定位主机上的具体应用进程,相当于设备上的「房间号」,解决「数据发给设备上的哪个程序」的问题。

二者缺一不可:仅有IP无法区分同一设备上的多个网络程序,仅有端口无法定位目标设备。IP:端口的组合,是网络通信中源和目的的标准表示方式。

1.2 网络传输的两大核心角色

网络通信是双向交互的过程,一般抽象为两个标准角色:服务端(Server)客户端(Client),这是所有网络程序的基础模型。

以日常访问网站为例:

  • 网站后台程序是Server:被动等待,持续监听网络请求,不会主动发起通信;
  • 浏览器是Client:主动发起连接请求,向服务端索要数据,接收服务端响应。

两大角色的核心区别:

  • 服务端(Server):启动后持续监听指定IP和端口,等待客户端连接,被动响应请求;
  • 客户端(Client):主动发起连接请求,主动发送数据,等待服务端处理后返回结果。

++Linux网络编程的核心,就是分别实现服务端和客户端的通信逻辑++ 。

1.3 两种核心传输方式:TCP与UDP

网络协议分为5层模型,对于Linux应用层开发者而言,无需关注底层网络层、链路层、物理层 ,我们的应用程序运行在应用层 ,编写网络程序时,直接使用传输层 提供的两种协议:TCPUDP

  • 应用层 :为用户进程提供服务(HTTP、FTP、DNS 等协议),我们的业务代码运行于此;
  • 传输层:实现主机进程间的通信,提供 TCP/UDP 两种传输服务,是网络编程的核心依赖;
  • 网络层:负责数据包在主机间的路由转发;
  • 链路层:配合路由器完成数据报的传输;
  • 物理层:以比特流形式传输原始数据。
1.3.1 TCP与UDP的区别

①TCP(传输控制协议)

  • 核心特点:面向连接、可靠交付
  • 传输单位:报文段;
  • 核心能力:✅建立专属连接(三次握手),通信结束后断开连接(四次挥手);✅保证数据不丢失、不重复、按顺序到达;✅自带流量控制、拥塞控制,匹配发送方与接收方的传输速率;✅自动将大报文拆分、重组,适配网络传输。

②UDP(用户数据包协议)

  • 核心特点:无连接、尽最大努力交付
  • 传输单位:用户数据报;
  • 核心能力:❌不建立连接,直接发送数据;❌不保证可靠交付,可能丢包、乱序;❌无流量控制、无拥塞控制。
1.3.2 为什么需要UDP?

很多初学者会疑惑:TCP能保证可靠传输,为什么还要用UDP?答案是:**场景决定选择,**UDP拥有TCP无法替代的优势:

  1. 传输延迟极低:UDP收到应用数据后立即发送,无重传等待,实时性极强;
  2. 无连接开销:无需三次握手建立连接,没有连接延迟;
  3. 资源占用更低:无连接状态管理,单服务端可支持海量客户端;
  4. 首部开销小:数据传输的额外消耗更低。
1.3.3 典型应用场景
  • UDP:视频通话、直播、在线游戏、DNS查询(容忍少量丢包,追求低延迟);
  • TCP:文件传输、网页访问、邮件发送(不容忍数据丢失,追求可靠性)。
1.3.4 TCP与UDP通信模式对比
  • TCP通信 :面向连接的流模式,类似打电话,先拨号建立连接,通话过程中数据连续传输,挂断后结束通信;
  • UDP通信 :无连接的数据包模式,类似寄快递,无需提前建立连接,直接打包数据发送,不保证对方一定收到。

2 网络编程核心函数

2.1 socket()函数

在Linux 网络编程中,socket()函数是创建套接字的核心入口,用于初始化一个网络通信端点,成功后返回套接字文件描述符,是TCP/UDP通信的第一步。

cs 复制代码
#include <sys/types.h>
#include <sys/socket.h>

int socket(int domain, int type, int protocol);

/*
创建一个用于网络通信的套接字(socket),
返回对应的文件描述符,
后续的绑定、监听、连接、数据收发等操作都基于该描述符展开。
*/

参数说明:

[1] domain:协议族/地址族

指定套接字使用的通信协议簇,决定了网络通信的范围和地址格式,常用取值:

  • AF_UNIX/AF_LOCAL:本地进程间通信协议族,仅适用于同一台Unix/Linux主机内的进程通信,无法跨网络。
  • AF_INETIPv4网络协议族,适用于Internet网络通信,支持远程主机间的数据传输(最常用)。
  • AF_INET6:IPv6网络协议族,用于IPv6网络通信。

补充:PF_xxxAF_xxx等价,Linux中可通用,推荐使用AF_xxx

[2] type:套接字类型

指定套接字的数据传输方式,对应TCP/UDP等通信协议,常用取值:

  • SOCK_STREAM流式套接字,基于TCP协议,特点:✅面向连接、可靠传输✅数据按顺序到达、无丢失无重复✅双向字节流传输
  • SOCK_DGRAM数据报套接字,基于 UDP 协议,特点:❌无连接、不可靠传输❌数据可能丢失、乱序✅传输速度快、开销小
  • SOCK_RAW:原始套接字,用于底层协议开发(如 ping、抓包)。

[3] protocol:协议编号

指定具体的传输协议,通常填0即可

  • domaintype确定后,系统会自动匹配默认协议(如AF_INET+SOCK_STREAM默认对应 TCP)。
  • 仅在同一类型套接字支持多种协议时,需要指定具体编号(极少使用)。

返回值

  • 成功 :返回一个非负整数,即套接字文件描述符(后续操作的唯一标识)。
  • 失败 :返回-1,通过errno变量可获取具体错误原因。

2.2 bind()函数

cs 复制代码
#include <sys/socket.h>

int bind(int sockfd, struct sockaddr *my_addr, int addrlen);

/*
将本机IP地址 + 端口号绑定到创建好的套接字上。
*/

参数说明:

[1] sockfd:socket () 函数返回的套接字文件描述符

[2] my_addr:指向本机地址结构体的指针

[3] addrlen:地址结构体的长度(sizeof)

重点:地址结构体

通用结构体(实际使用时强制转换)

cs 复制代码
struct sockaddr{
    unsigned short sa_family;  // 协议族
    char sa_data[14];          // 地址数据
};

实际使用:IPv4 专用结构体(sockaddr_in)

cs 复制代码
struct sockaddr_in{
    unsigned short sin_family;   // 协议族,Internet填 AF_INET
    unsigned short sin_port;     // 绑定的端口号
    struct in_addr sin_addr;     // IP地址
    unsigned char sin_zero[8];   // 填充,保持长度一致
};
  • sin_addr.s_addr = INADDR_ANY:绑定本机所有网卡IP
  • sin_port:需要绑定的监听端口

返回值

  • 成功:返回0
  • 失败:返回**-1**,通过errno获取错误信息

2.3 listen()函数

cs 复制代码
#include <sys/socket.h>

int listen(int sockfd, int backlog);

/*
将套接字设为监听模式,等待客户端连接,仅TCP服务端使用。
*/

参数说明:

[1] sockfd :经过bind()绑定后的套接字文件描述符

[2] backlog:等待连接队列的最大长度(客户端连接排队上限)

返回值

  • 成功:返回0
  • 失败:返回**-1**,通过errno获取错误信息

2.4 accept()函数

cs 复制代码
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, int *addrlen);

/*
TCP服务端接受客户端连接,建立通信链路(阻塞等待直到有客户端连接)。
*/

参数说明:

[1] sockfd :经过listen()监听后的套接字文件描述符

[2] addr :输出参数,用于存储客户端的IP和端口

[3] addrlen:输出参数,客户端地址结构体长度

返回值

  • 成功:返回新的套接字描述符(专门用于和该客户端通信)
  • 失败:返回-1

2.5 connect()函数

cs 复制代码
#include <sys/socket.h>

int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);

/*
客户端专用函数,用于主动与服务端建立 TCP 连接。
*/

参数说明:

[1] sockfd:socket ()创建的套接字文件描述符

[2] serv_addr:服务端的IP地址 + 端口号结构体

[3] addrlen:地址结构体长度(sizeof)

返回值

  • 成功:返回0
  • 失败:返回**-1**

2.6 send()函数

cs 复制代码
#include <sys/socket.h>

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

/*
用于TCP连接中,向对端发送数据。
*/

参数说明:

[1] sockfd:已连接的套接字文件描述符

[2] buf:发送数据的缓冲区

[3] len:要发送的数据长度(字节)

[4] flags :标志位,一般填0

返回值

  • 成功:返回实际发送的字节数
  • 失败:返回-1

2.7 recv()函数

cs 复制代码
#include <sys/socket.h>

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

/*
TCP连接中,接收对方发送的数据(阻塞等待直到收到数据)。
*/

参数说明:

[1] sockfd:已连接的套接字文件描述符

[2] buf:接收数据的缓冲区

[3] len:缓冲区最大长度(字节)

[4] flags :标志位,一般填0

返回值

  • 成功:返回实际接收到的字节数
  • 连接关闭:返回0
  • 失败:返回-1

2.8 recvfrom()函数

cs 复制代码
#include <sys/socket.h>

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, 
                 struct sockaddr *src_addr, socklen_t *addrlen);

/*
UDP专用,接收数据,并获取发送端的IP和端口。
*/

参数说明:

[1] sockfd:套接字文件描述符

[2] buf:接收数据的缓冲区

[3] len:缓冲区最大长度

[4] flags :标志位,一般填0

[5] src_addr :输出参数,存储发送端IP+端口

[6] addrlen:地址结构体长度

返回值

  • 成功:返回实际接收的字节数
  • 失败:返回-1

2.9 sendto()函数

cs 复制代码
#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);

/*
UDP专用,向指定目标地址发送数据(无连接传输)。
*/

参数说明:

[1] sockfd:套接字文件描述符

[2] buf:接收数据的缓冲区

[3] len:缓冲区最大长度

[4] flags :标志位,一般填0

[5] dest_addr:目标主机的IP+端口结构体

[6] addrlen:地址结构体长度

返回值

  • 成功:返回实际发送的字节数
  • 失败:返回-1

3 TCP编程

3.1 完整代码

3.1.1 TCP 服务端(多进程并发)
cs 复制代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <stdlib.h>

#define SERVER_PORT 8888
#define BACKLOG 10

int main()
{
    int server_fd;                  // 监听 socket
    int client_fd;                  // 连接 socket
    struct sockaddr_in server_addr; // 服务器地址
    struct sockaddr_in client_addr; // 客户端地址
    socklen_t addr_len;
    char buf[1000];
    int ret;

    // 1. 忽略子进程退出信号,避免僵尸进程
    signal(SIGCHLD, SIG_IGN);

    // 2. 创建 socket
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == -1)
    {
        perror("socket");
        return -1;
    }

    // 3. 配置服务器地址
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    server_addr.sin_addr.s_addr = INADDR_ANY;
    memset(server_addr.sin_zero, 0, 8);

    // 4. 绑定端口
    ret = bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    if (ret == -1)
    {
        perror("bind");
        close(server_fd);
        return -1;
    }

    // 5. 开始监听
    listen(server_fd, BACKLOG);
    printf("TCP Server 已启动,端口:%d\n", SERVER_PORT);

    while (1)
    {
        // 6. 等待客户端连接
        addr_len = sizeof(client_addr);
        client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &addr_len);
        if (client_fd == -1)
        {
            perror("accept");
            continue;
        }

        printf("客户端连接:%s\n", inet_ntoa(client_addr.sin_addr));

        // 7. 创建子进程处理客户端
        if (fork() == 0)
        {
            // 子进程:关闭监听socket
            close(server_fd);

            while (1)
            {
                // 接收数据
                ret = recv(client_fd, buf, sizeof(buf) - 1, 0);
                if (ret <= 0)
                {
                    printf("客户端断开连接\n");
                    break;
                }

                buf[ret] = '\0';
                printf("收到:%s", buf);
            }

            close(client_fd);
            exit(0);
        }

        // 父进程:关闭连接socket,继续监听
        close(client_fd);
    }

    close(server_fd);
    return 0;
}
3.1.2 TCP 客户端
cs 复制代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define SERVER_PORT 8888

int main(int argc, char *argv[])
{
    int client_fd;
    struct sockaddr_in server_addr;
    char buf[1000];
    int ret;

    // 检查参数
    if (argc != 2)
    {
        printf("用法:%s <服务器IP>\n", argv[0]);
        return -1;
    }

    // 1. 创建客户端 socket
    client_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (client_fd == -1)
    {
        perror("socket");
        return -1;
    }

    // 2. 配置服务器地址
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    inet_aton(argv[1], &server_addr.sin_addr);

    // 3. 连接服务器
    ret = connect(client_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    if (ret == -1)
    {
        perror("connect");
        close(client_fd);
        return -1;
    }

    printf("连接服务器成功!\n");

    // 4. 循环发送数据
    while (fgets(buf, sizeof(buf), stdin) != NULL)
    {
        send(client_fd, buf, strlen(buf), 0);
    }

    close(client_fd);
    return 0;
}

3.2 TCP 编程核心流程

服务端流程

复制代码
socket() → bind() → listen() → accept() → recv()/send() → close()

客户端流程

复制代码
socket() → connect() → send()/recv() → close()

3.3 函数逐行分析

  1. socket()创建一个套接字(网络通信的文件描述符)
cs 复制代码
socket(AF_INET, SOCK_STREAM, 0);
  • AF_INET:IPv4 协议
  • SOCK_STREAM:TCP 协议(面向连接、可靠)
  • 返回值:成功返回文件描述符,失败返回 -1
  1. bind()把 socket 绑定到 IP + 端口
cs 复制代码
bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
  • 服务端必须绑定,否则客户端不知道连哪里
  • INADDR_ANY:监听本机所有网卡
  1. listen()让 socket 变成被动监听模式,等待客户端连接
cs 复制代码
listen(server_fd, BACKLOG);
  • BACKLOG:等待连接队列的长度
  1. accept()阻塞等待客户端连接 ,成功后返回一个新的socket
cs 复制代码
client_fd = accept(server_fd, ...);
  • server_fd:监听 socket
  • client_fd专门和这个客户端通信的socket(关键)
  1. connect():客户端主动连接服务器
cs 复制代码
connect(client_fd, &server_addr, ...);
  1. send() / recv()发送 / 接收数据
cs 复制代码
recv(client_fd, buf, size, 0);
send(client_fd, buf, size, 0);
  1. fork()创建子进程,实现多客户端并发连接
  • 父进程:继续监听新客户端
  • 子进程:专门和当前客户端通信

3.4 核心总结

  1. TCP 是面向连接的,必须先建立连接才能通信
  2. 服务端必须:socket → bind → listen → accept
  3. 客户端必须:socket → connect
  4. accept会返回新的socket用于通信
  5. 多进程可以让服务端同时处理多个客户端
  6. 端口号需要用htons()转成网络字节序

4 UDP编程

4.1 完整代码

4.1.1 UDP服务端
cs 复制代码
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>

#define SERVER_PORT 8888  // 端口号

int main(void)
{
    int server_fd;                // UDP 套接字
    ssize_t recv_len;             // 接收数据长度
    unsigned char recv_buf[1024]; // 接收缓冲区
    socklen_t addr_len;           // 地址长度(必须用这个类型)

    struct sockaddr_in server_addr; // 服务器地址结构体
    struct sockaddr_in client_addr; // 客户端地址结构体

    // 1. 创建 UDP 套接字
    server_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (server_fd == -1)
    {
        perror("socket failed");
        return -1;
    }

    // 2. 配置服务器地址
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    server_addr.sin_addr.s_addr = INADDR_ANY; // 监听本机所有网卡
    memset(server_addr.sin_zero, 0, sizeof(server_addr.sin_zero));

    // 3. 绑定 IP 和端口
    if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1)
    {
        perror("bind failed");
        close(server_fd);
        return -1;
    }

    printf("UDP Server Running on Port %d...\n", SERVER_PORT);

    // 4. 循环接收客户端数据
    while (1)
    {
        addr_len = sizeof(client_addr);

        // 接收 UDP 数据(自动获取客户端地址)
        recv_len = recvfrom(server_fd, recv_buf, sizeof(recv_buf) - 1, 0,
                            (struct sockaddr *)&client_addr, &addr_len);

        if (recv_len > 0)
        {
            recv_buf[recv_len] = '\0'; // 添加字符串结束符
            printf("[%s:%d] 发送消息: %s",
                   inet_ntoa(client_addr.sin_addr),
                   ntohs(client_addr.sin_port),
                   recv_buf);
        }
    }

    // 关闭套接字
    close(server_fd);
    return 0;
}
4.1.2 UDP 客户端
cs 复制代码
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>

#define SERVER_PORT 8888

int main(int argc, char **argv)
{
    int client_fd;
    ssize_t send_len;
    char send_buf[1024];
    socklen_t addr_len;
    struct sockaddr_in server_addr;

    // 检查参数:需要输入服务器 IP
    if (argc != 2)
    {
        printf("用法: %s <服务器IP>\n", argv[0]);
        printf("示例: %s 127.0.0.1\n", argv[0]);
        return -1;
    }

    // 1. 创建 UDP 套接字
    client_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (client_fd == -1)
    {
        perror("socket failed");
        return -1;
    }

    // 2. 配置服务器地址
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    inet_aton(argv[1], &server_addr.sin_addr); // 转换服务器 IP
    memset(server_addr.sin_zero, 0, 8);

    printf("UDP 客户端已启动,输入消息发送(Ctrl+C 退出)\n");

    // 3. 循环读取输入并发送
    while (fgets(send_buf, sizeof(send_buf), stdin) != NULL)
    {
        addr_len = sizeof(server_addr);

        // 发送 UDP 数据(必须指定服务器地址)
        send_len = sendto(client_fd, send_buf, strlen(send_buf), 0,
                          (struct sockaddr *)&server_addr, addr_len);

        if (send_len == -1)
        {
            perror("sendto failed");
            break;
        }
    }

    // 关闭套接字
    close(client_fd);
    return 0;
}

4.2 UDP 编程核心流程

服务端流程

复制代码
socket() → bind() → recvfrom() → 处理数据 → close()

客户端流程

复制代码
socket() → sendto() → close()

4.3 函数逐行分析

  1. socket(AF_INET, SOCK_DGRAM, 0)创建一个 UDP 网络套接字
  • AF_INET:使用 IPv4 协议
  • SOCK_DGRAMUDP 数据报类型
  • 返回值:成功返回文件描述符,失败返回 -1
  1. bind()将套接字与 IP + 端口 绑定
  • 只有服务端需要绑定,客户端不需要
  1. recvfrom():UDP 专用接收函数
  • 会自动获取发送方的 IP 和端口
  • 是阻塞函数,没有数据时会一直等待
  1. sendto()UDP 专用发送函数
  • 必须指定目标 IP + 端口
  • 不需要建立连接,直接发送

4.4 UDP 特点

  1. 无连接不需要 connect、listen、accept,发数据直接指定目标地址。
  2. 不可靠不保证数据一定到达,不保证有序。
  3. 数据报模式数据分开发送、分开接收。
  4. 速度快没有连接建立与断开,开销小。

4.5 UDP 与 TCP 核心区别

特性 TCP UDP
连接 面向连接 无连接
可靠性 可靠传输 不可靠
速度 较慢 极快
适用场景 文件、网页、指令传输 视频、直播、游戏、DNS
核心函数 connect / send / recv sendto / recvfrom
服务端 需要 listen + accept 不需要

4.6 核心总结

  1. UDP 是无连接、不可靠、高速的传输协议
  2. 服务端:socket → bind → recvfrom
  3. 客户端:socket → sendto
  4. 不需要连接、不需要多进程、结构简单
相关推荐
shughui1 小时前
2026最新JDK版本选择及下载安装详细图文教程【windows、mac附安装包】
java·linux·开发语言·windows·jdk·mac
忡黑梨1 小时前
eNSP_DHCP配置
c语言·网络·c++·python·算法·网络安全·智能路由器
D4c-lovetrain1 小时前
Linux个人心得28(k8s实战)
linux·运维·kubernetes
淼淼爱喝水1 小时前
openEuler 环境下 Ansible Playbook 实战:批量创建用户并修改 Shell 属性
linux·运维·服务器·openeuler·playbook
莎士比亚的文学花园2 小时前
Linux驱动开发(2)——驱动编程
linux·运维·驱动开发
YaBingSec2 小时前
玄机网络安全靶场:Jackson-databind 反序列化漏洞(CVE-2017-7525)
linux·网络·笔记·安全·web安全
计算机安禾2 小时前
【Linux从入门到精通】第30篇:综合案例:编写一个Linux系统体检脚本
linux·运维·服务器
TechWayfarer2 小时前
网络安全溯源实战:78.1%网络攻击来自境外,如何精准定位攻击源
网络·安全·web安全
海的预约2 小时前
Bootloader应用分析
linux·运维·服务器