1、网络发展历史和分层
1.1 Internet 的历史
起源:
1957 年:苏联发射第一颗人造卫星 "Sputnik"。
1958 年:美国总统艾森豪威尔成立 DARPA(国防部高级研究计划署)。
1968 年:DARPA 提出 "资源共享计算机网络"(ARPAnet),成为 Internet 的雏形。
TCP/IP 协议的诞生:
1973 年:Robert Kahn 和 Vinton Cerf 开发了新的互联协议。
1974 年:TCP 协议首次发布,但存在数据包丢失时无法有效纠正的问题。
1974 年后:TCP 协议拆分为 TCP 和 IP 两个独立协议。
1983 年:ARPAnet 停止使用 NCP,全面采用 TCP/IP 协议。
1.2 网络的体系结构
分层设计:将网络功能划分为不同模块,每层实现特定功能,对外部透明。
两种体系结构:
OSI 模型:七层模型(物理层、数据链路层、网络层、传输层、会话层、表示层、应用层)。
TCP/IP 模型:四层模型(网络接口层、网络层、传输层、应用层)。
1.3 TCP/IP 协议通信模型
数据封装与传递过程:
应用层数据 → 传输层(TCP/UDP)→ 网络层(IP)→ 数据链路层(以太网帧)→ 物理层。
数据包结构:
包含以太网头部、IP 头部、TCP/UDP 头部、应用层头部和用户数据。
2、TCP 和 UDP 协议特点
2.1 TCP 协议特点
面向连接:提供高可靠性通信(无丢失、无失序、无重复)。
适用场景:
传输大量数据或对质量要求较高的场景。
即时通讯软件的用户登录管理。
2.2 UDP 协议特点
无连接:高效传输,但不可靠。
适用场景:
小数据量传输(如 DNS 查询)。
广播/组播通信。
实时数据传输(如流媒体、VoIP)。
3、网络编程预备知识
3.1 Socket 简介
定义:Socket 是一种编程接口,用于网络通信。
类型:
流式套接字(SOCK_STREAM):面向连接,可靠。
数据报套接字(SOCK_DGRAM):无连接,不可靠。
原始套接字(SOCK_RAW):直接访问底层协议。
3.2 IP 地址
IPv4:32 位,点分十进制表示(如 192.168.1.1)。
IPv6:128 位。
子网掩码:用于区分网络部分和主机部分。
3.3 端口号
范围:
众所周知端口:1~1023。
注册端口:1024~49150。
动态端口:49151~65535。
TCP 和 UDP 端口独立。
3.4 字节序
主机字节序(HBO):因 CPU 架构不同而不同(如 Intel 为小端序,ARM 为大端序)。
网络字节序(NBO):统一为大端序。
转换函数:
htonl()
、htons()
:主机字节序转网络字节序。ntohl()
、ntohs()
:网络字节序转主机字节序。
3.5 IP 地址转换
inet_aton()
:字符串转 32 位网络字节序。inet_ntoa()
:32 位网络字节序转点分十进制字符串。
4、TCP/IP 网络编程
4.1 网络编程 API
4.1.1 socket()
函数
作用:创建一个套接字。
原型:
int socket(int domain, int type, int protocol);
参数:
domain
:协议族(如AF_INET
表示 IPv4,AF_INET6
表示 IPv6)。type
:套接字类型(如SOCK_STREAM
表示 TCP,SOCK_DGRAM
表示 UDP)。protocol
:协议类型(通常为 0,表示默认协议)。
返回值:成功返回套接字描述符,失败返回 -1。
示例:
int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd == -1) { perror("创建套接字失败"); return -1; } printf("创建套接字成功\n");
4.1.2 bind()
函数
作用:将套接字绑定到指定的地址和端口。
原型:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
sockfd
:套接字描述符。addr
:指向sockaddr
结构的指针,包含地址信息。addrlen
:addr
的长度。
返回值:成功返回 0,失败返回 -1。
示例:
struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(8888); server_addr.sin_addr.s_addr = INADDR_ANY; if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) { perror("绑定失败"); close(sockfd); return -1; } printf("绑定成功\n");
4.1.3 listen()
函数
作用:将套接字设置为监听状态。
原型:
int listen(int sockfd, int backlog);
参数:
sockfd
:套接字描述符。backlog
:最大连接请求数。
返回值:成功返回 0,失败返回 -1。
示例:
if (listen(sockfd, 5) == -1) { perror("监听失败"); close(sockfd); return -1; } printf("开始监听\n");
4.1.4 accept()
函数
作用:接受客户端连接请求。
原型:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
sockfd
:监听套接字描述符。addr
:指向sockaddr
结构的指针,用于存储客户端地址。addrlen
:addr
的长度。
返回值:成功返回新套接字描述符,失败返回 -1。
示例:
struct sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr); int clientfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_len); if (clientfd == -1) { perror("接受连接失败"); close(sockfd); return -1; } printf("客户端连接成功\n");
4.1.5 connect()
函数
作用:主动连接服务器。
原型:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
sockfd
:套接字描述符。addr
:指向sockaddr
结构的指针,包含服务器地址信息。addrlen
:addr
的长度。
返回值:成功返回 0,失败返回 -1。
示例:
struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(8888); server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) { perror("连接失败"); close(sockfd); return -1; } printf("连接服务器成功\n");
4.1.6 send()
和 recv()
函数
send()
作用:发送数据。send()
原型:ssize_t send(int sockfd, const void *buf, size_t len, int flags);
recv()
作用:接收数据。recv()
原型:ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数:
sockfd
:套接字描述符。buf
:数据缓冲区。len
:缓冲区长度。flags
:控制标志。
返回值:成功返回发送/接收的字节数,失败返回 -1。
示例:
char buffer[1024]; memset(buffer, 0, sizeof(buffer)); strcpy(buffer, "你好,服务器"); if (send(sockfd, buffer, strlen(buffer), 0) == -1) { perror("发送失败"); close(sockfd); return -1; } printf("发送数据成功\n"); int bytes_read = recv(sockfd, buffer, sizeof(buffer), 0); if (bytes_read <= 0) { perror("接收失败"); close(sockfd); return -1; } printf("收到服务器回复:%s\n", buffer);
4.1.7 close()
和 shutdown()
函数
close()
作用:关闭套接字。close()
原型:int close(int sockfd);
shutdown()
作用:关闭套接字的读或写操作。shutdown()
原型:int shutdown(int sockfd, int how);
参数:
how
:SHUT_RD
表示关闭读,SHUT_WR
表示关闭写,SHUT_RDWR
表示关闭读和写。
返回值:成功返回 0,失败返回 -1。
示例:
close(sockfd); printf("关闭套接字成功\n");
5、TCP 编程模型
5.1 循环服务器
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
int main() {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("创建套接字失败");
return -1;
}
printf("创建套接字成功\n");
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8888);
server_addr.sin_addr.s_addr = INADDR_ANY;
if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("绑定失败");
close(sockfd);
return -1;
}
printf("绑定成功\n");
if (listen(sockfd, 5) == -1) {
perror("监听失败");
close(sockfd);
return -1;
}
printf("开始监听\n");
while (1) {
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int clientfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_len);
if (clientfd == -1) {
perror("接受连接失败");
close(sockfd);
return -1;
}
printf("客户端连接成功\n");
char buffer[1024];
while (1) {
int bytes_read = recv(clientfd, buffer, sizeof(buffer), 0);
if (bytes_read <= 0) {
if (bytes_read == 0) {
printf("客户端断开连接\n");
} else {
perror("接收失败");
}
break;
}
buffer[bytes_read] = '\0';
printf("收到客户端消息:%s\n", buffer);
send(clientfd, buffer, bytes_read, 0);
}
close(clientfd);
printf("关闭客户端连接\n");
}
close(sockfd);
return 0;
}
5.2 多进程/多线程并发服务器
多进程示例:
#include <stdio.h> #include <sys/socket.h> #include <netinet/in.h> #include <string.h> #include <unistd.h> #include <stdlib.h> void handle_client(int clientfd) { char buffer[1024]; while (1) { int bytes_read = recv(clientfd, buffer, sizeof(buffer), 0); if (bytes_read <= 0) { if (bytes_read == 0) { printf("客户端断开连接\n"); } else { perror("接收失败"); } break; } buffer[bytes_read] = '\0'; printf("收到客户端消息:%s\n", buffer); send(clientfd, buffer, bytes_read, 0); } close(clientfd); printf("关闭客户端连接\n"); exit(0); } int main() { int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd == -1) { perror("创建套接字失败"); return -1; } printf("创建套接字成功\n"); struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(8888); server_addr.sin_addr.s_addr = INADDR_ANY; if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) { perror("绑定失败"); close(sockfd); return -1; } printf("绑定成功\n"); if (listen(sockfd, 5) == -1) { perror("监听失败"); close(sockfd); return -1; } printf("开始监听\n"); while (1) { struct sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr); int clientfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_len); if (clientfd == -1) { perror("接受连接失败"); close(sockfd); return -1; } printf("客户端连接成功\n"); if (fork() == 0) { close(sockfd); handle_client(clientfd); } else { close(clientfd); } } close(sockfd); return 0; }
6、UDP 编程模型
6.1 循环服务器
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
int main() {
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd == -1) {
perror("创建套接字失败");
return -1;
}
printf("创建套接字成功\n");
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8888);
server_addr.sin_addr.s_addr = INADDR_ANY;
if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("绑定失败");
close(sockfd);
return -1;
}
printf("绑定成功\n");
char buffer[1024];
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
while (1) {
int bytes_read = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&client_addr, &client_len);
if (bytes_read <= 0) {
perror("接收失败");
continue;
}
buffer[bytes_read] = '\0';
printf("收到客户端消息:%s\n", buffer);
sendto(sockfd, buffer, bytes_read, 0, (struct sockaddr *)&client_addr, client_len);
}
close(sockfd);
return 0;
}
7、网络调试和协议分析
7.1 工具
Wireshark:图形化网络抓包工具,用于分析网络封包。
tcpdump:命令行网络抓包工具。
8、I/O 模型和多路复用模型
8.1 I/O 模型
阻塞 I/O:
套接字默认为阻塞模式。
读写操作(如
read
、write
、accept
、connect
)会阻塞进程,直到操作完成。
非阻塞 I/O:
套接字设置为非阻塞模式后,I/O 操作无法立即完成时会立即返回错误。
需要轮询(polling)检查 I/O 是否就绪,浪费 CPU 资源。
I/O 多路复用:
同时监控多个文件描述符的 I/O 状态。
使用
select()
或poll()
函数实现。
信号驱动 I/O:
异步通信模型,通过信号通知 I/O 事件。
8.2 阻塞 I/O 的问题
读操作:如果套接字接收缓冲区没有数据,
read
会阻塞,直到数据到达。写操作:如果发送缓冲区空间不足,
write
会阻塞,直到缓冲区有足够空间。UDP 协议中不存在发送缓冲区满的情况,写操作不会阻塞。
8.3 非阻塞模式的实现
使用
fcntl()
或ioctl()
函数将套接字设置为非阻塞模式。示例代码:
int flag = fcntl(sockfd, F_GETFL, 0); flag |= O_NONBLOCK; fcntl(sockfd, F_SETFL, flag);
8.4 I/O 多路复用
select()
函数:监控多个文件描述符的读、写和异常事件。
参数:
maxfd
:监控的文件描述符中最大的值加 1。read_fds
、write_fds
、except_fds
:文件描述符集合。timeout
:超时时间。
poll()
函数:类似
select()
,但更节省空间,适合监控大量文件描述符。
9、网络分析测试工具
9.1 常用工具
Wireshark:图形化网络抓包工具,用于分析网络封包。
tcpdump:命令行网络抓包工具。
telnet:测试 TCP 服务器端。
netstat:显示网络连接、路由表等信息。
lsof:列出当前系统打开的文件和网络连接。
sniffer:网络协议分析工具。
9.2 网络封包格式
以太网头:包含源和目的 MAC 地址。
IP 头:包含源和目的 IP 地址、协议类型等。
TCP/UDP 头:包含源和目的端口号、序列号等。
应用层数据:用户数据。
10、TCP 握手过程
10.1 三次握手
客户端发送 SYN 包到服务器。
服务器回复 SYN-ACK 包。
客户端发送 ACK 包,建立连接。
10.2 四次挥手
客户端发送 FIN 包,请求关闭连接。
服务器回复 ACK 包,确认收到 FIN。
服务器发送 FIN 包,请求关闭连接。
客户端回复 ACK 包,确认收到 FIN。
11、网络信息检索和设置
11.1 网络信息检索函数
gethostname()
:获取主机名。getpeername()
:获取远程协议地址。getsockname()
:获取本地套接口地址。gethostbyname()
:根据主机名获取主机信息。getservbyname()
:根据服务名获取服务信息。
11.2 网络属性设置
使用
setsockopt()
和getsockopt()
设置和获取套接字选项。常见选项:
SO_REUSEADDR
:允许重用本地地址和端口。SO_KEEPALIVE
:保持连接。SO_RCVTIMEO
和SO_SNDTIMEO
:设置接收和发送超时。
11.3 网络超时检测
方法一:设置 socket 属性
SO_RCVTIMEO
。方法二:使用
select()
检测 socket 是否就绪。方法三:设置定时器捕获
SIGALRM
信号。
12、思考与应用
12.1 如何动态检查网络连接状态?
应用层:使用心跳检测机制。
内核层:启用周期性检查定时器(如 2.6 内核)。
硬件层:通过网卡硬件或 GPIO 检测网线插拔。
13、广播和组播
13.1 广播
定义:将数据包发送给局域网中的所有主机。
实现方式:使用 UDP 协议套接字。
广播地址:
以 192.168.1.0 网段为例,广播地址为 192.168.1.255。
255.255.255.255 代表所有网段的广播地址。
发送广播:
创建 UDP 套接字。
使用
setsockopt()
设置套接字为广播模式。指定广播地址和端口。
使用
sendto()
发送数据包。
接收广播:
创建 UDP 套接字。
绑定本机 IP 地址和端口。
使用
recvfrom()
接收数据。
13.2 组播
定义:将数据包发送给特定多播组中的主机。
优势:避免广播风暴,减少网络负载。
组播地址范围:224.0.0.1 – 239.255.255.255。
发送组播:
创建 UDP 套接字。
指定组播地址和端口。
使用
sendto()
发送数据包。
接收组播:
创建 UDP 套接字。
加入多播组(使用
setsockopt()
)。绑定本机 IP 地址和端口。
使用
recvfrom()
接收数据。
加入多播组示例:
struct ip_mreq mreq; bzero(&mreq, sizeof(mreq)); mreq.imr_multiaddr.s_addr = inet_addr("235.10.10.3"); mreq.imr_interface.s_addr = htonl(INADDR_ANY); setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));
14、UNIX 域套接字
14.1 定义
用于本地进程间通信的套接字。
14.2 类型
流式套接字:提供可靠、有序的通信。
数据报套接字:提供无连接、不可靠的通信。
14.3 地址结构
struct sockaddr_un
:struct sockaddr_un { sa_family_t sun_family; char sun_path[108]; // 套接字文件的路径 };
填充地址结构示例:
struct sockaddr_un myaddr; bzero(&myaddr, sizeof(myaddr)); myaddr.sun_family = AF_UNIX; strcpy(myaddr.sun_path, "/tmp/mysocket");
14.4 流式套接字使用示例
服务器端:
int sockfd = socket(AF_UNIX, SOCK_STREAM, 0); struct sockaddr_un server_addr; bzero(&server_addr, sizeof(server_addr)); server_addr.sun_family = AF_UNIX; strcpy(server_addr.sun_path, "/tmp/mysocket"); if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) { perror("绑定失败"); close(sockfd); return -1; } printf("绑定成功\n"); if (listen(sockfd, 5) == -1) { perror("监听失败"); close(sockfd); return -1; } printf("开始监听\n"); struct sockaddr_un client_addr; socklen_t client_len = sizeof(client_addr); int clientfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_len); if (clientfd == -1) { perror("接受连接失败"); close(sockfd); return -1; } printf("客户端连接成功\n"); char buffer[1024]; while (1) { int bytes_read = recv(clientfd, buffer, sizeof(buffer), 0); if (bytes_read <= 0) { if (bytes_read == 0) { printf("客户端断开连接\n"); } else { perror("接收失败"); } break; } buffer[bytes_read] = '\0'; printf("收到客户端消息:%s\n", buffer); send(clientfd, buffer, bytes_read, 0); } close(clientfd); close(sockfd); unlink("/tmp/mysocket"); printf("关闭连接并删除套接字文件\n");
客户端:
int sockfd = socket(AF_UNIX, SOCK_STREAM, 0); struct sockaddr_un server_addr; bzero(&server_addr, sizeof(server_addr)); server_addr.sun_family = AF_UNIX; strcpy(server_addr.sun_path, "/tmp/mysocket"); if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) { perror("连接失败"); close(sockfd); return -1; } printf("连接服务器成功\n"); char buffer[1024]; while (1) { printf("请输入消息:"); fgets(buffer, sizeof(buffer), stdin); int len = strlen(buffer); if (buffer[len - 1] == '\n') { buffer[len - 1] = '\0'; } if (send(sockfd, buffer, strlen(buffer), 0) == -1) { perror("发送失败"); close(sockfd); return -1; } printf("发送消息:%s\n", buffer); int bytes_read = recv(sockfd, buffer, sizeof(buffer), 0); if (bytes_read <= 0) { if (bytes_read == 0) { printf("服务器断开连接\n"); } else { perror("接收失败"); } break; } buffer[bytes_read] = '\0'; printf("收到服务器回复:%s\n", buffer); } close(sockfd); printf("关闭连接\n");
14.5 数据报套接字使用示例
服务器端:
int sockfd = socket(AF_UNIX, SOCK_DGRAM, 0); struct sockaddr_un server_addr; bzero(&server_addr, sizeof(server_addr)); server_addr.sun_family = AF_UNIX; strcpy(server_addr.sun_path, "/tmp/mysocket"); if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) { perror("绑定失败"); close(sockfd); return -1; } printf("绑定成功\n"); char buffer[1024]; struct sockaddr_un client_addr; socklen_t client_len = sizeof(client_addr); while (1) { int bytes_read = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&client_addr, &client_len); if (bytes_read <= 0) { perror("接收失败"); continue; } buffer[bytes_read] = '\0'; printf("收到客户端消息:%s\n", buffer); sendto(sockfd, buffer, bytes_read, 0, (struct sockaddr *)&client_addr, client_len); } close(sockfd); unlink("/tmp/mysocket"); printf("关闭连接并删除套接字文件\n");
客户端:
int sockfd = socket(AF_UNIX, SOCK_DGRAM, 0); struct sockaddr_un server_addr; bzero(&server_addr, sizeof(server_addr)); server_addr.sun_family = AF_UNIX; strcpy(server_addr.sun_path, "/tmp/mysocket"); struct sockaddr_un client_addr; bzero(&client_addr, sizeof(client_addr)); client_addr.sun_family = AF_UNIX; strcpy(client_addr.sun_path, "/tmp/myclientsocket"); if (bind(sockfd, (struct sockaddr *)&client_addr, sizeof(client_addr)) == -1) { perror("绑定失败"); close(sockfd); return -1; } printf("绑定成功\n"); if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) { perror("连接失败"); close(sockfd); return -1; } printf("连接服务器成功\n"); char buffer[1024]; while (1) { printf("请输入消息:"); fgets(buffer, sizeof(buffer), stdin); int len = strlen(buffer); if (buffer[len - 1] == '\n') { buffer[len - 1] = '\0'; } if (sendto(sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) { perror("发送失败"); close(sockfd); return -1; } printf("发送消息:%s\n", buffer); struct sockaddr_un from_addr; socklen_t from_len = sizeof(from_addr); int bytes_read = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&from_addr, &from_len); if (bytes_read <= 0) { perror("接收失败"); continue; } buffer[bytes_read] = '\0'; printf("收到服务器回复:%s\n", buffer); } close(sockfd); unlink("/tmp/myclientsocket"); printf("关闭连接并删除套接字文件\n");
15、网络总结
15.1 网络编程的关键点
Socket 编程:掌握不同类型的套接字及其使用方法。
I/O 模型:理解阻塞、非阻塞、多路复用等模型。
网络工具:熟练使用 Wireshark、tcpdump 等工具。
网络协议:熟悉 TCP/IP 协议族及其应用。
15.2 应用场景
单播:点对点通信。
广播:局域网内所有主机通信。
组播:特定多播组内的主机通信。
UNIX 域套接字:本地进程间通信。
【至此Linux C系列文章完毕,会不断的优化完整。文章仅用于分享学习以及自己复习使用,若有什么问题,麻烦指出,谢谢】