网络编程概述
1.1 网络解决的问题
-
互联网的本质问题:实现设备与设备之间,在不同区域、不同网络环境下的数据交换操作。
-
设备类型:不限,包含手机、电脑、平板、智能穿戴设备等。
-
要求:设备必须支持数据传输的网络协议栈。
-
协议概念:
-
协议 = 数据传递的规则和格式标准
-
例如:
-
Socket 协议:提供进程间通信接口
-
IP 协议:提供主机寻址和路由功能
-
TCP/UDP:提供数据传输服务
-
链路层/硬件协议:保证物理传输
-
-
总结:网络就是通过标准化的协议,使不同设备、不同系统之间的数据能顺利交换。
1.2 网络起源
-
历史背景:
-
1957 年,苏联发射 Sputnik 卫星,引起美国对国防安全的关注。
-
1958 年 1 月 7 日,美国国会拨款成立 ARPA(高级研究计划署)。
-
-
计算机网络的设计原则:
-
不用于电话通信,而是用于数据传输
-
可靠地传输数据,即使部分节点损坏也能继续工作
-
能够连接不同类型的计算机(不同品牌、操作系统)
-
网络中每个节点同等重要(去中心化设计)
-
必须有冗余路由,保证在节点或链路故障时数据仍能到达
-
理解:这些原则奠定了现代互联网的分布式、冗余和可靠性的基础。
1.3 网络结构
-
网络分层:
-
终端设备:电脑、手机、传感器等
-
交换设备:路由器、交换机
-
通信链路:光纤、以太网、电缆、无线链路
-
-
分层逻辑:
-
每一层完成自己的任务,向上提供服务,向下使用服务
-
例如:应用层负责具体业务,传输层负责端到端通信,网络层负责路由,链路层负责物理传输
-
图示说明:
三层网络层级(可画图表示核心/汇聚/接入层)
网络组成图例(终端 ↔ 交换 ↔ 链路)
1.4 网络传递的主要协议(重点)
层级 | 功能说明 | 示例协议 |
---|---|---|
应用层 | 应用程序之间的数据通信 | FTP、HTTP、Telnet |
传输层 | 端到端进程数据传输,提供逻辑通信 | TCP、UDP |
网络层 | 数据包封装、寻址和路由,保证数据尽可能到达目的地 | IP、ICMP |
链路层 | 数据帧发送、接收和错误检测 | Ethernet、Wi-Fi |
七层模型
层级 | 功能说明 | 典型协议 / 技术 |
---|---|---|
应用层 | 提供用户应用服务,如文件传输、邮件、网页访问 | HTTP、FTP、SMTP、Telnet |
表示层 | 数据格式转换、加密解密、压缩 | JPEG、MPEG、SSL/TLS |
会话层 | 会话管理、建立/保持/关闭会话 | RPC、NetBIOS、PPTP |
传输层 | 端到端进程通信、可靠传输(TCP)或快速传输(UDP) | TCP、UDP |
网络层 | 路由选择、逻辑寻址 | IP、ICMP、ARP |
数据链路层 | 数据帧封装、差错检测、MAC 地址识别 | Ethernet、PPP、Wi-Fi |
物理层 | 信号传输、电压、电缆类型 | 光纤、同轴电缆、网线、无线信号 |
理解:
-
-
物理层:数据在媒介上真实传输
-
数据链路层:保证帧正确发送、接收
-
网络层:负责寻址、路由
-
传输层:提供进程间可靠/快速通信
-
会话层/表示层/应用层:管理会话、数据格式、提供具体应用服务
-
1.5 IP 地址
-
IPv4 地址:
-
32 位二进制
-
编程中存储方式:4 字节整数
-
常用表示:点分十进制
"192.168.16.125"
-
-
IP 地址作用:
-
唯一标识网络中的设备
-
传输数据时必须提供目标 IP
-
-
存储示例(编程中):
二进制 | 对应十进制 |
---|---|
1100 0000 | 192 |
1010 1000 | 168 |
0001 0000 | 16 |
0111 1101 | 125 |
1.6 端口号
-
概念:
-
每台主机的每个进程都有唯一端口号
-
数据到达主机后,根据端口号交给对应进程
-
-
范围:0 ~ 65535
-
建议使用 1000 以上端口
-
避开常用端口:22、80、3306、8080
-
-
总结:数据传输必须同时提供目标 IP + 端口号
1.7 补充说明
-
MAC 地址:网卡物理地址,无法修改,可用于过滤白名单/黑名单
-
子网掩码 NetMask:判断两个 IP 是否在同一网段
-
回环地址/本地地址:
-
IPv4:127.0.0.1
-
IPv6:::1
-
网络字节序 / 大端字节序
2.1 字节序概念
-
字节序(Endianness):多字节数据在内存中存储顺序的约定
-
常见类型:
-
小端序(Little Endian)
-
数据低位存放在低地址,高位存放在高地址
-
CPU 类型:x86 系列常用小端序
-
内存示例(32-bit 整数 0x12345678):
地址↑ : 低 → 高 内容 : 78 56 34 12
-
-
大端序(Big Endian)
-
数据高位存放在低地址,低位存放在高地址
-
网络传输标准采用大端序
-
内存示例(32-bit 整数 0x12345678):
地址↑ : 低 → 高 内容 : 12 34 56 78
-
-
总结:不同 CPU 架构可能使用不同字节序,而网络协议要求统一的大端序,保证多设备间通信一致。
2.2 网络传输与本地存储
-
问题:
-
本地存储可能是小端
-
网络协议要求大端
-
如果不做转换,多字节数据(IP、端口、整数)在不同主机间可能解析错误
-
-
解决方案:
-
提供 字节序转换函数,在本地和网络间转换数据
-
函数分为两类:
-
处理 4 字节数据(32-bit 整数)
-
本地 → 网络:
htonl
-
网络 → 本地:
ntohl
-
-
处理 2 字节数据(16-bit 整数)
-
本地 → 网络:
htons
-
网络 → 本地:
ntohs
-
-
-
2.3 头文件
#include <arpa/inet.h>
-
提供字节序转换函数:
-
htonl(uint32_t hostlong)
:本地 32-bit → 网络大端 -
htons(uint16_t hostshort)
:本地 16-bit → 网络大端 -
ntohl(uint32_t netlong)
:网络 32-bit → 本地 -
ntohs(uint16_t netshort)
:网络 16-bit → 本地
-
2.4 函数详细说明
函数 | 功能说明 | 参数说明 | 返回值 |
---|---|---|---|
uint32_t htonl(uint32_t hostlong) | 将本地 32-bit 无符号整数转为网络大端字节序 | hostlong :本地整数 | 转换为大端字节序整数 |
uint16_t htons(uint16_t hostshort) | 将本地 16-bit 无符号整数转为网络大端字节序 | hostshort :本地短整数 | 转换为大端字节序短整数 |
uint32_t ntohl(uint32_t netlong) | 将网络大端 32-bit 数据转为本地字节序 | netlong :网络数据 | 本地字节序整数 |
uint16_t ntohs(uint16_t netshort) | 将网络大端 16-bit 数据转为本地字节序 | netshort :网络数据 | 本地字节序短整数 |
2.5 应用场景
-
IP 地址转换:
-
IPv4 地址存储为 4 字节整数,需要在本地和网络间转换
-
-
端口号转换:
-
16-bit 端口号发送前必须转为网络字节序
-
-
跨平台通信:
-
保证不同 CPU 架构(小端/大端)设备间数据一致
-
2.6 代码案例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <arpa/inet.h>
#define IP "192.168.16.96"
#define IP_B 11000000101010000001000001100000
#define IP_D 3232239712
#define PORT 8889
int main(int argc, char const *argv[])
{
uint32_t net = htonl((uint32_t)IP_D);
uint16_t netp = htons((uint16_t)PORT);
uint32_t IpD = ntohl(net);
uint16_t port = ntohs(netp);
printf("%d %d\n",net,netp);
printf("%u %d\n",IpD,port);
return 0;
}
你可以自己填入 IP、端口和打印代码进行测试。
IP 地址转换操作函数
3.1 IP 地址存储与表示
-
IPv4 地址:
-
4 字节无符号整数(编程中使用,网络传输)
-
点分十进制字符串(常见表示方式,便于阅读)
192.168.16.125
-
-
网络传输要求:
-
数据必须以 4 字节整数大端字节序传输
-
-
本地表示要求:
-
为了可读性和操作方便,常使用字符串表示
-
总结:网络传输用整数(大端),本地显示用字符串(点分十进制)
3.2 字符串 IP → 网络 IP(本地 → 网络)
函数:inet_pton
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
参数 | 说明 |
---|---|
int af | 协议族,IPv4 使用 AF_INET ,IPv6 使用 AF_INET6 |
const char *src | 点分十进制字符串 IPv4 或 IPv6 地址 |
void *dst | 存储网络字节序 IP 地址的大端字节序变量地址 |
-
返回值:
-
1
:转换成功 -
0
:字符串不合法 -
-1
:协议族非法,设置errno
-
应用场景:UDP/TCP 发送端和接收端设置目标 IP 时,必须将字符串 IP 转为网络字节序整数。
3.3 网络 IP → 字符串 IP(网络 → 本地)
函数:inet_ntop
#include <arpa/inet.h>
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
参数 | 说明 |
---|---|
int af | 协议族,IPv4 使用 AF_INET ,IPv6 使用 AF_INET6 |
const void *src | 网络字节序大端 IP 地址 |
char *dst | 存储转换后的字符串缓冲区 |
socklen_t size | 缓冲区字节长度,一般 IPv4 用 16,IPv6 用 46 |
-
返回值:
-
成功:返回
dst
地址 -
失败:返回
NULL
,同时设置errno
-
应用场景:UDP/TCP 接收端打印发送端 IP 时,将网络大端 IP 转为可读字符串。
3.4 使用流程总结
-
发送端:
-
IP 地址:字符串 → 网络字节序整数
-
端口号:主机字节序 → 网络字节序(htons)
-
-
接收端:
-
网络字节序 IP → 字符串显示(inet_ntop)
-
网络字节序端口 → 本地主机字节序(ntohs)
-
3.5 代码案例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#define IP "192.168.16.96"
#define IP_B 11000000101010000001000001100000
#define IP_D 3232239712
#define PORT 8889
int main()
{
uint32_t net_ip;
char ip_buf[16];
if (inet_pton(AF_INET, IP, &net_ip) != 1) {
perror("inet_pton failed");
return 1;
}
if (inet_ntop(AF_INET, &net_ip, ip_buf, sizeof(ip_buf)) == NULL) {
perror("inet_ntop failed");
return 1;
}
printf("Original IP: %s\n", IP);
printf("After conversion: %s\n", ip_buf);
return 0;
}
UDP 网络传输
4.1 UDP 特性
-
无连接:不需要建立连接就能发送数据
-
不可靠:不保证数据到达顺序,也不确认接收
-
数据包传输:以独立数据包(Datagram)方式传输
-
速度快:因无连接、无需握手确认,传输效率高
-
发送端/接收端概念:
-
没有严格客户端和服务器
-
只有发送端和接收端
-
总结:UDP 更适合实时性要求高、允许少量丢包的场景,如视频流、语音通信、传感器数据。
4.2 UDP 通信流程
流程概览
-
创建套接字
-
使用
socket
申请 UDP 套接字
-
-
发送端操作
-
准备发送数据
-
准备目标接收端 IP 与端口(字符串 IP → 网络字节序)
-
使用
sendto
发送数据包
-
-
接收端操作
-
准备接收 IP 和端口绑定结构体
-
使用
bind
绑定本机端口(本机 IP + 端口) -
调用
recvfrom
接收数据,并获取发送端信息
-
-
关闭资源
-
使用
close
关闭套接字
-
4.3 UDP 核心 API
1️⃣ socket
- 套接字创建
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
参数 | 说明 |
---|---|
domain | 协议族,IPv4:AF_INET |
type | 套接字类型:SOCK_DGRAM (UDP) |
protocol | IP 协议:IPPROTO_IP |
-
返回值:
-
≥0:套接字文件描述符
-
-1:创建失败,设置
errno
-
2️⃣ bind
- 绑定本地端口
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数 | 说明 |
---|---|
sockfd | socket 文件描述符 |
addr | struct sockaddr 指针,包含 IP 协议、网络字节序 IP、端口 |
addrlen | 结构体大小 |
-
返回值:
-
0:成功
-
-1:失败,设置
errno
-
注意:接收端必须绑定本机 IP + 端口,发送端可不绑定(系统自动分配端口)。
3️⃣ sendto
- 发送 UDP 数据
#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 | UDP 套接字 |
buf | 发送缓冲区 |
len | 发送字节数 |
flags | 标志位,一般 0 |
dest_addr | 目标接收端 IP + 端口结构体 |
addrlen | 结构体字节数 |
-
返回值:
-
≥0:发送成功,返回发送字节数
-
-1:失败,设置
errno
-
4️⃣ recvfrom
- 接收 UDP 数据
#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 | UDP 套接字 |
buf | 接收缓冲区 |
len | 缓冲区大小 |
flags | 标志位,一般 0 |
src_addr | 可选,用于存储发送端 IP + 端口信息 |
addrlen | 指向 socklen_t ,存储结构体大小 |
-
返回值:
-
0:接收有效字节数
-
0:EOF 或数据结束
-
-1:接收失败
-
注意:
recvfrom
可以不传发送端信息(src_addr=NULL
)UDP 无连接,所以每个数据包独立
4.4 UDP 发送端 / 接收端流程总结
步骤 | 发送端 | 接收端 |
---|---|---|
1 | 创建套接字 | 创建套接字 |
2 | 准备目标 IP + 端口 | 准备本地 IP + 端口结构体 |
3 | 数据打包 | 绑定端口(bind) |
4 | sendto 发送 | recvfrom 接收数据 |
5 | 可选处理发送反馈 | 可选处理接收端信息 |
6 | 关闭套接字 | 关闭套接字 |
核心思想:
UDP 不建立连接
数据包独立传输
发送端主动指定目标,接收端绑定端口监听
4.5 代码案例
1️⃣发送
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#define DEST_IP_ADDR "192.168.16.96"
#define DEFT_PORT (8999)
int main(int argc, char const *argv[])
{
// 1. 创建 UDP 套接字
int sockFd = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
if (sockFd < 0)
{
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 2. 准备发送数据
char *str = "hello UDP";
if (str == NULL)
{
fprintf(stderr, "Invalid data to send\n");
close(sockFd);
exit(EXIT_FAILURE);
}
// 3. 准备目标地址结构体
struct sockaddr_in destAddr;
memset(&destAddr, 0, sizeof(destAddr));
destAddr.sin_family = AF_INET;
destAddr.sin_port = htons(DEFT_PORT);
if (inet_pton(AF_INET, DEST_IP_ADDR, &(destAddr.sin_addr.s_addr)) != 1)
{
fprintf(stderr, "Invalid IP address: %s\n", DEST_IP_ADDR);
close(sockFd);
exit(EXIT_FAILURE);
}
// 4. 发送数据
ssize_t sentBytes = sendto(sockFd, str, strlen(str), 0,
(const struct sockaddr *)&destAddr,
(socklen_t)sizeof(destAddr));
if (sentBytes < 0)
{
perror("sendto failed");
close(sockFd);
exit(EXIT_FAILURE);
}
else
{
printf("Sent %zd bytes to %s:%d\n", sentBytes, DEST_IP_ADDR, DEFT_PORT);
}
// 5. 关闭套接字
if (close(sockFd) < 0)
{
perror("close socket failed");
exit(EXIT_FAILURE);
}
return 0;
}
2️⃣接收
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#define HOST_IP_ADDR "192.168.16.96"
#define HOST_PORT (8999)
#define BUFFER_SIZE (512)
int main(int argc, char const *argv[])
{
// 1. 创建 UDP 套接字
int sockFd = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
if (sockFd < 0)
{
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 2. 准备本机接收端地址
struct sockaddr_in hostAddr;
memset(&hostAddr, 0, sizeof(hostAddr));
hostAddr.sin_family = AF_INET;
hostAddr.sin_port = htons(HOST_PORT);
if (inet_pton(AF_INET, HOST_IP_ADDR, &hostAddr.sin_addr.s_addr) != 1)
{
fprintf(stderr, "Invalid host IP address: %s\n", HOST_IP_ADDR);
close(sockFd);
exit(EXIT_FAILURE);
}
socklen_t addrLen = sizeof(hostAddr);
// 3. 绑定端口
if (bind(sockFd, (const struct sockaddr *)&hostAddr, addrLen) < 0)
{
perror("bind failed");
close(sockFd);
exit(EXIT_FAILURE);
}
// 4. 接收数据
char buff[BUFFER_SIZE] = {0};
struct sockaddr_in srcAddr;
memset(&srcAddr, 0, sizeof(srcAddr));
socklen_t srcLen = sizeof(srcAddr);
ssize_t recvBytes = recvfrom(sockFd, buff, BUFFER_SIZE, 0,
(struct sockaddr *)&srcAddr, &srcLen);
if (recvBytes < 0)
{
perror("recvfrom failed");
close(sockFd);
exit(EXIT_FAILURE);
}
// 5. 打印接收信息
char src_ip_addr[16] = {0};
if (inet_ntop(AF_INET, &(srcAddr.sin_addr.s_addr), src_ip_addr, sizeof(src_ip_addr)) == NULL)
{
perror("inet_ntop failed");
close(sockFd);
exit(EXIT_FAILURE);
}
printf("Data from %s:%u, Data: %s\n",
src_ip_addr,
ntohs(srcAddr.sin_port),
buff);
fflush(stdout);
// 6. 关闭套接字
if (close(sockFd) < 0)
{
perror("close socket failed");
exit(EXIT_FAILURE);
}
return 0;
}