当前位置: 首页 > news >正文

Linux 入门十一:Linux 网络编程

一、概述

1. 网络编程基础

网络编程是通过网络应用编程接口(API)编写程序,实现不同主机上进程间的信息交互。它解决的核心问题是:如何让不同主机上的程序进行通信

2. 网络模型:从 OSI 到 TCP/IP

  • OSI 七层模型(理论模型):
    物理层(传输比特流)→ 数据链路层(组帧、差错控制)→ 网络层(路由选择,IP 协议)→ 传输层(端到端通信,TCP/UDP)→ 会话层(建立会话)→ 表示层(数据格式转换)→ 应用层(HTTP、FTP 等具体应用)。
    特点:层次清晰,适合理论分析,但实际开发中较少直接使用。

  • TCP/IP 四层模型(实用模型):
    网络接口层(对应 OSI 下两层,处理硬件通信)→ 网络层(IP 协议,寻址和路由)→ 传输层(TCP/UDP,端到端数据传输)→ 应用层(HTTP、FTP、SMTP 等,具体业务逻辑)。
    特点:简化层次,广泛应用于实际开发。

3. 常用网络协议速查表

协议名称英文全称核心功能典型场景
TCP传输控制协议面向连接、可靠传输网页浏览(HTTP)、文件传输(FTP)
UDP用户数据报协议无连接、不可靠传输视频直播、DNS 查询
IP网际协议网络层寻址与路由所有网络通信的基础
ICMP互联网控制消息协议网络状态检测(如 ping)故障排查(ping、traceroute)
FTP文件传输协议高效传输文件服务器文件共享
SMTP简单邮件传输协议发送电子邮件邮件服务器通信

二、网络通信三要素:IP、端口、套接字

1. IP 地址:主机的 “门牌号”

  • 定义:32 位(IPv4)或 128 位(IPv6)的二进制数,唯一标识网络中的主机。
    • IPv4 示例:192.168.1.1(点分十进制)
    • IPv6 示例:2001:0db8:85a3:0000:0000:8a2e:0370:7334(十六进制)
  • 查看本机 IP:终端输入 ifconfig(Linux)或 ipconfig(Windows)。
  • 特殊 IP
    • 127.0.0.1:本地回环地址,用于测试本机网络程序。
    • 0.0.0.0:监听所有可用网络接口。
    • 255.255.255.255:广播地址,向同一网络内所有主机发送数据。

2. 端口号:程序的 “房间号”

  • 定义:16 位无符号整数(0-65535),标识同一主机上的不同进程。
  • 分类
    • 保留端口(0-1023):系统专用(如 80 端口用于 HTTP,22 端口用于 SSH)。
    • 注册端口(1024-49151):分配给特定服务(如 3306 端口用于 MySQL)。
    • 动态端口(49152-65535):程序运行时动态申请,避免冲突。
  • 注意:编程时避免使用保留端口,可选择 1024 以上未被占用的端口(如 8888、3333)。

3. 套接字(Socket):通信的 “通道”

  • 定义:一种特殊的文件描述符,用于跨网络或本地进程通信。

  • 三要素:IP 地址 + 端口号 + 传输层协议(TCP/UDP)。

  • 类型

    1. 流式套接字(SOCK_STREAM):基于 TCP,可靠、面向连接(如打电话,需先接通)。
    2. 数据报套接字(SOCK_DGRAM):基于 UDP,无连接、不可靠(如发短信,无需确认对方是否在线)。
    3. 原始套接字(SOCK_RAW):直接访问底层协议(如 IP/ICMP),用于网络开发或抓包工具。
  • 地址结构体

    // IPv4 地址结构体(常用)
    struct sockaddr_in {sa_family_t sin_family;   // 地址族,固定为 AF_INET(IPv4)或 AF_INET6(IPv6)in_port_t sin_port;       // 端口号(网络字节序,需用 htons 转换)struct in_addr sin_addr;  // IP 地址(网络字节序,可用 inet_addr 转换字符串)
    };// 通用地址结构体(需强制转换使用)
    struct sockaddr {sa_family_t sa_family;   // 地址族char sa_data[14];        // 具体地址数据(不同协议族格式不同)
    };
    

三、TCP 编程:可靠的 “快递服务”

1. TCP 核心特点

  • 面向连接:通信前需先建立连接(三次握手),通信后释放连接(四次挥手)。
  • 可靠传输:通过确认机制、重传机制保证数据有序且无丢失。
  • 流式传输:数据像水流一样连续发送,无边界(需应用层自定义消息边界)。

2. TCP 服务器开发步骤(逐步解析)

步骤 1:创建套接字(socket)
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

  • 参数
    • domain:协议族,IPv4 用 AF_INET,IPv6 用 AF_INET6,本地通信用 AF_UNIX
    • type:套接字类型,TCP 用 SOCK_STREAM,UDP 用 SOCK_DGRAM
    • protocol:具体协议,通常为 0(自动选择对应类型的默认协议,如 TCP 选 IPPROTO_TCP)。
  • 返回值:成功返回套接字描述符(文件描述符),失败返回 -1。
  • 示例
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == -1) {perror("socket failed");exit(EXIT_FAILURE);
    }
    
步骤 2:绑定地址(bind)
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

  • 作用:将套接字与本地 IP 地址和端口号绑定,让客户端知道如何连接。
  • 参数
    • sockfd:步骤 1 创建的套接字描述符。
    • addr:地址结构体指针(需将 sockaddr_in 强制转换为 sockaddr)。
    • addrlen:地址结构体的长度(sizeof(struct sockaddr_in))。
  • 示例
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;           // IPv4 协议族
    server_addr.sin_port = htons(8888);         // 端口号(htons 转换为网络字节序)
    server_addr.sin_addr.s_addr = INADDR_ANY;   // 监听所有本地 IP(0.0.0.0)if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {perror("bind failed");exit(EXIT_FAILURE);
    }
    
  • 易错点:端口号必须用 htons 转换为网络字节序(大端模式),否则无法正确识别。
步骤 3:监听连接(listen)
#include <sys/socket.h>
int listen(int sockfd, int backlog);

  • 作用:将套接字设置为被动监听状态,等待客户端连接。
  • 参数
    • sockfd:已绑定的套接字描述符。
    • backlog:等待连接的最大队列长度(通常设为 5-10,视并发需求而定)。
  • 示例
    if (listen(server_fd, 5) == -1) {perror("listen failed");exit(EXIT_FAILURE);
    }
    
步骤 4:接受连接(accept)
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

  • 作用:阻塞等待客户端连接,成功后返回新的套接字描述符(用于与该客户端通信)。
  • 参数
    • sockfd:监听套接字描述符。
    • addr:用于存储客户端地址的结构体指针。
    • addrlen:客户端地址结构体的长度(传入前需初始化,传出时自动填充实际长度)。
  • 示例
    struct sockaddr_in client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len);
    if (client_fd == -1) {perror("accept failed");exit(EXIT_FAILURE);
    }
    printf("Client connected: IP %s, Port %d\n",inet_ntoa(client_addr.sin_addr),  // 将网络字节序 IP 转换为字符串ntohs(client_addr.sin_port));     // ntohs 将端口号转换为主机字节序
    
  • 关键点accept 返回的新套接字 client_fd 专门用于与当前客户端通信,原 server_fd 继续监听其他客户端。
步骤 5:数据交互(send/recv)
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

  • send 作用:向连接的套接字发送数据(TCP 保证可靠传输)。
    • flags 通常设为 0(默认模式)。
    • 返回值:成功发送的字节数,失败返回 -1。
  • recv 作用:从连接的套接字接收数据。
    • 返回值:成功接收的字节数,0 表示对方关闭连接,-1 表示失败。
  • 示例
    char buffer[1024] = "Hello, Client!";
    // 向客户端发送数据
    send(client_fd, buffer, strlen(buffer), 0);// 接收客户端数据
    memset(buffer, 0, sizeof(buffer));
    ssize_t recv_len = recv(client_fd, buffer, sizeof(buffer), 0);
    if (recv_len > 0) {printf("Received: %s\n", buffer);
    }
    
步骤 6:关闭连接(close)
#include <unistd.h>
int close(int fd);

  • 作用:释放套接字资源,触发 TCP 四次挥手释放连接。
  • 示例
    close(client_fd);  // 关闭与客户端的通信套接字
    close(server_fd);  // 关闭监听套接字
    

3. TCP 客户端开发步骤(简洁版)

  1. 创建套接字(同服务器):
    int client_fd = socket(AF_INET, SOCK_STREAM, 0);
    
  2. 连接服务器(connect):
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8888);
    server_addr.sin_addr.s_addr = inet_addr("192.168.1.100");  // 服务器 IPif (connect(client_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {perror("connect failed");exit(EXIT_FAILURE);
    }
    
  3. 数据交互(send/recv,同服务器)。
  4. 关闭连接(close)。

四、UDP 编程:轻量的 “明信片” 传输

1. UDP 核心特点

  • 无连接:无需提前建立连接,直接发送数据报(类似发短信,无需等待对方确认)。
  • 不可靠:不保证数据到达、不保证顺序、不处理重复。
  • 高效:省去连接开销,适合实时性要求高但允许少量丢包的场景(如视频通话、DNS)。

2. UDP 服务器开发步骤(对比 TCP)

步骤 1:创建套接字(socket)
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);  // 类型为 SOCK_DGRAM
步骤 2:绑定地址(bind,同 TCP)

需绑定端口号(可选绑定 IP,通常用 INADDR_ANY 监听所有接口)。

步骤 3:数据交互(sendto/recvfrom)
#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);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);

  • sendto 作用:向指定地址发送数据报(需包含目标 IP 和端口)。
  • recvfrom 作用:接收数据报,同时获取发送方的地址(用于回复)。
  • 示例
    struct sockaddr_in client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    char buffer[1024];// 接收客户端数据(含客户端地址)
    ssize_t recv_len = recvfrom(sockfd, buffer, sizeof(buffer), 0,(struct sockaddr *)&client_addr, &client_addr_len);
    if (recv_len > 0) {printf("Received from %s:%d: %s\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port), buffer);
    }// 回复客户端(用接收到的地址发送)
    sendto(sockfd, "Hello from server", 18, 0,(struct sockaddr *)&client_addr, client_addr_len);
    
步骤 4:关闭套接字(close)

同 TCP,直接关闭即可(无连接释放过程)。

3. UDP 客户端开发步骤

  1. 创建套接字(socket)。
  2. 可选绑定端口(若不绑定,系统自动分配临时端口)。
  3. 发送 / 接收数据(sendto/recvfrom,需指定服务器地址)。
  4. 关闭套接字(close)。

五、高级编程:处理多连接与性能优化

1. IO 多路复用:select 函数(解决单线程多连接阻塞问题)

核心作用

允许单线程同时监听多个套接字,当任意一个套接字就绪(可读 / 可写 / 异常)时,触发响应。
适用场景:客户端数量多但活动连接少的场景(如聊天服务器)。

函数原型
#include <sys/select.h>
int select(int maxfd, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,const struct timeval *timeout);

  • 参数
    • maxfd:监听的最大文件描述符 + 1(确保包含所有监听的 fd)。
    • readfds:可读事件集合(监听哪些 fd 有数据可读)。
    • writefds:可写事件集合(监听哪些 fd 可以写数据)。
    • exceptfds:异常事件集合(通常用不到,设为 NULL)。
    • timeout:超时时间(NULL 表示永久阻塞,{0, 0} 表示立即返回)。
  • 返回值:就绪的文件描述符数量,0 表示超时,-1 表示错误。
使用步骤(以 UDP 服务器监听为例)
  1. 初始化 fd_set
    fd_set read_fds;
    FD_ZERO(&read_fds);          // 清空集合
    FD_SET(sockfd, &read_fds);   // 将套接字添加到可读集合
    
  2. 计算 maxfd
    int maxfd = sockfd;  // 若有多个 fd,取最大值
    
  3. 调用 select
    struct timeval tv = {2, 0};  // 超时时间 2 秒
    int ready_fds = select(maxfd + 1, &read_fds, NULL, NULL, &tv);
    if (ready_fds == -1) {perror("select error");exit(EXIT_FAILURE);
    } else if (ready_fds == 0) {printf("Timeout, no data received\n");continue;
    }
    
  4. 检查就绪的 fd
    if (FD_ISSET(sockfd, &read_fds)) {// 处理数据接收recvfrom(sockfd, buffer, sizeof(buffer), 0, &client_addr, &client_addr_len);
    }
    

2. 非阻塞 IO:fcntl 函数(避免阻塞等待)

核心作用

将套接字设置为非阻塞模式,使 recv/accept 等操作立即返回,配合轮询或事件驱动处理多任务。

函数原型
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */);

  • 设置非阻塞模式
    int flags = fcntl(sockfd, F_GETFL);  // 获取当前文件状态标志
    fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);  // 添加非阻塞标志
    
  • 应用场景
    • 客户端同时发送和接收数据(如聊天程序边输入边接收消息)。
    • 服务器需要处理大量并发连接,避免单个连接阻塞整个程序。

六、实战:简单 TCP 服务器与客户端(完整代码)

1. TCP 服务器代码(server.c)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>#define PORT 8888
#define MAX_CLIENTS 5int main() {// 1. 创建监听套接字int server_fd = socket(AF_INET, SOCK_STREAM, 0);if (server_fd < 0) {perror("socket creation failed");exit(EXIT_FAILURE);}// 2. 绑定地址struct sockaddr_in server_addr;server_addr.sin_family = AF_INET;server_addr.sin_port = htons(PORT);server_addr.sin_addr.s_addr = INADDR_ANY;if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {perror("bind failed");exit(EXIT_FAILURE);}// 3. 开始监听if (listen(server_fd, MAX_CLIENTS) < 0) {perror("listen failed");exit(EXIT_FAILURE);}printf("Server listening on port %d...\n", PORT);while (1) {// 4. 接受客户端连接struct sockaddr_in client_addr;socklen_t client_addr_len = sizeof(client_addr);int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len);if (client_fd < 0) {perror("accept failed");continue;}// 5. 与客户端通信char buffer[1024] = {0};ssize_t recv_len = recv(client_fd, buffer, sizeof(buffer), 0);if (recv_len > 0) {printf("Received: %s\n", buffer);send(client_fd, "Message received", 16, 0);}// 6. 关闭客户端连接close(client_fd);}close(server_fd);return 0;
}

2. TCP 客户端代码(client.c)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>#define PORT 8888
#define SERVER_IP "127.0.0.1"int main() {// 1. 创建客户端套接字int client_fd = socket(AF_INET, SOCK_STREAM, 0);if (client_fd < 0) {perror("socket creation failed");exit(EXIT_FAILURE);}// 2. 连接服务器struct sockaddr_in server_addr;server_addr.sin_family = AF_INET;server_addr.sin_port = htons(PORT);if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {perror("invalid server IP address");exit(EXIT_FAILURE);}if (connect(client_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {perror("connection failed");exit(EXIT_FAILURE);}// 3. 发送数据给服务器char *message = "Hello, Server!";send(client_fd, message, strlen(message), 0);// 4. 接收服务器回复char buffer[1024] = {0};ssize_t recv_len = recv(client_fd, buffer, sizeof(buffer), 0);if (recv_len > 0) {printf("Received from server: %s\n", buffer);}// 5. 关闭连接close(client_fd);return 0;
}

3. 编译与运行

  1. 编译服务器
    gcc server.c -o server
    ./server  # 启动服务器
    
  2. 编译客户端
    gcc client.c -o client
    ./client  # 启动客户端(输出服务器回复)
    

七、常见易错点与最佳实践

1. 字节序转换:必须使用 htons/htonl/ntohs/ntohl

  • 原因:不同主机可能采用小端(x86)或大端(ARM)字节序,网络协议规定使用大端(网络字节序)。
  • 错误示例:直接赋值端口号 server_addr.sin_port = 8888;(未用 htons 转换,导致端口错误)。
  • 正确做法server_addr.sin_port = htons(8888);

2. 端口冲突:绑定前检查端口是否被占用

  • 检查命令netstat -tunlp | grep 端口号(查看端口占用情况)。
  • 解决方案:更换端口号,或确保上次运行的程序已正确关闭(避免 TIME_WAIT 状态残留)。

3. IP 地址转换:inet_addr 与 inet_pton 的区别

  • inet_addr:将点分十进制字符串转换为网络字节序(IPv4 专用,过时函数,建议用 inet_pton)。
  • inet_pton:支持 IPv4 和 IPv6,返回值更安全(成功返回 1,无效地址返回 0,错误返回 -1)。

4. 缓冲区溢出:固定缓冲区大小需谨慎

  • 风险:接收数据时未限制长度可能导致缓冲区溢出(如 recv(client_fd, buffer, sizeof(buffer), 0); 是安全的,而 recv(client_fd, buffer, 1024, 0); 若缓冲区不足 1024 字节则危险)。
  • 最佳实践:缓冲区大小固定为已知值,或使用动态内存分配(如 malloc)。

八、拓展学习:从入门到进阶

1. 必学工具

  • Wireshark:网络抓包工具,分析 TCP 三次握手、UDP 数据报格式。
  • netstat / ss:查看网络连接、端口状态(如 netstat -an 显示所有连接)。
  • telnet / nc:测试端口连通性(如 telnet 127.0.0.1 8888 检查服务器是否运行)。

2. 进阶知识点

  • HTTP 协议解析:基于 TCP 实现简单 Web 服务器(处理 GET/POST 请求)。
  • 多线程 / 多进程服务器:使用 pthread 或 fork 处理并发连接(解决 select 处理海量连接的性能瓶颈)。
  • IPv6 支持:修改地址结构体为 sockaddr_in6,协议族用 AF_INET6,实现跨 IPv4/IPv6 的兼容性。

3. 学习资源

  • 《UNIX 网络编程》:经典教材,深入理解套接字编程与协议细节。
  • Linux 官方文档man 2 socket 查看系统调用手册,man 7 ip 了解 IP 协议细节。

总结

Linux 网络编程是实现跨主机通信的核心技术,从基础的 TCP/UDP 套接字编程,到处理并发的 select/fcntl 高级技巧,需要逐步实践和调试。初学者应先掌握 TCP 服务器 / 客户端的基本流程,理解字节序、地址绑定等核心概念,再通过实战项目(如简易聊天室、文件传输工具)巩固知识。记住,网络编程的关键在于理解协议原理处理边界条件(如连接中断、数据丢失),多写代码、多抓包分析,才能真正掌握这门技术。

相关文章:

  • PyCharm 在 Linux 上的完整安装与使用指南
  • arxml文件中的schema是什么?有什么作用?
  • Kafka 在小流量和大流量场景下的顺序消费问题
  • typedef MVS_API CLISTDEF0IDX(ViewScore, IIndex) ViewScoreArr;
  • Vue3 源码解析(六):响应式原理与 reactive
  • DePIN驱动的分布式AI资源网络
  • Python 爬虫如何获取淘宝商品的 SKU 详细信息
  • 云服务器怎么选择防御最合适
  • 深度学习中的归一化技术:从原理到实战全解析
  • 使用 Logstash 迁移 MongoDB 数据到 Easysearch
  • C语言中联合体(Union)和结构体(Struct)的嵌套用法
  • Unity打开项目时目标平台被改变
  • 新能源汽车充电桩运营模式的发展与优化路径探析
  • 【Hive入门】Hive概述:大数据时代的数据仓库桥梁
  • 【KWDB 创作者计划】_本地化部署与使用KWDB 深度实践
  • 【TeamFlow】4.1 Git使用指南
  • spark—SQL3
  • 【网工第6版】第5章 网络互联②
  • 从0开始配置spark-local模式
  • FPGA 中 XSA、BIT 和 DCP 文件的区别
  • 哈萨克斯坦一名副市长遭枪击
  • 海口市美兰区委副书记、区长吴升娇去世,终年41岁
  • 习近平圆满结束对柬埔寨国事访问
  • 工作坊|早期左翼文学的多重张力与历史回响
  • 9部门发文促进家政服务消费扩容升级
  • 杨小伟被查,国家广播电视总局党组:坚决拥护党中央决定