1. 服务器模型
在网络程序中,通常都是一个服务器处理多个客户端的连接请求。为了高效处理多个客户端的并发请求,服务器程序采用了不同的处理模型。
1.1 循环服务器模型
循环服务器模型是最简单的服务器模型,它采用顺序处理的方式,同一时刻只能响应一个客户端的请求。当有多个客户端连接时,必须等待当前客户端处理完毕后才能处理下一个客户端。
工作原理:
- 服务器按顺序接受客户端连接
- 处理完一个客户端的全部请求后,再处理下一个
- 适用于请求处理时间短、客户端数量少的场景
伪代码如下:
cpp
// 创建套接字
socket();
// 绑定地址
bind();
// 监听连接
listen();
while(1) {
// 接受客户端连接
accept();
while(1) {
// 处理客户端请求
process(); // 处理
}
// 关闭连接
close();
}
优缺点分析:
- **优点:**实现简单,资源消耗少
- **缺点:**无法并发处理多个客户端,客户端需要排队等待
1.2 并发服务器模型
并发服务器模型能够同时响应多个客户端的请求,大大提高了服务器的处理能力。常用的并发模型包括多进程模型、多线程模型和I/O多路复用模型。
1.2.1 多进程模型
多进程模型为每个客户端连接创建一个独立的子进程来处理请求。父进程负责接受连接,子进程负责处理具体的客户端通信。
实现原理:
- 主进程监听端口,接受客户端连接
- 每接受一个连接,就fork()一个子进程
- 子进程独立处理该客户端的全部请求
- 子进程结束后由系统回收资源
伪代码如下:
cpp
// 创建套接字
socket();
// 绑定地址
bind();
// 监听连接
listen();
while(1) {
// 接受客户端连接
accept();
if(fork() == 0) { // 子进程
while(1) {
// 处理和客户端的通信
process();
}
// 关闭连接
close();
// 退出子进程
exit();
} else { // 父进程
// 继续监听其他连接
}
}
多进程特点总结:
- **进程复制:**fork()出来的子进程几乎拷贝了父进程的全部内容,包括代码、数据、系统数据段中的PC值、栈中的数据、父进程中打开的文件等。但它们的PID(进程ID)和PPID(父进程ID)是不同的。
- **代码执行:**虽然子进程拷贝了父进程的代码,但是fork()之前的代码不会执行,只执行fork()之后的代码。
- **地址空间:**父子进程有独立的地址空间,互不影响。在各自进程中改变全局变量、静态变量,都不会影响对方。
- **文件描述符:**fork()之前打开的文件,fork()之后拿到的是同一个文件描述符,操作的是同一个文件指针。
优缺点:
- **优点:**实现相对简单,进程间隔离性好
- **缺点:**系统开销较大(进程创建、销毁、上下文切换成本高)
1.2.2 多线程模型(重点)
多线程模型为每个客户端连接创建一个独立的线程来处理请求。相比多进程模型,线程共享进程的资源,创建和切换开销更小,是目前使用最广泛的并发模型之一。
实现原理:
- 主线程监听端口,接受客户端连接
- 每接受一个连接,就创建一个新线程
- 线程共享进程的地址空间和资源
- 需要处理线程同步问题
基本框架伪代码:
cpp
// 创建套接字
socket();
// 绑定地址
bind();
// 监听连接
listen();
while(1) {
// 接受客户端连接
accept();
// 创建线程处理连接
pthread_create();
}
完整示例代码:pthread_ser.c
cpp
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
// 线程处理函数
void *mythread(void *arg)
{
int acceptfd = *(int *)arg;
while (1) {
// 接收客户端数据
char buf[128] = "";
int ret;
ret = recv(acceptfd, buf, 128, 0);
if (ret < 0) {
perror("recv err");
return NULL;
} else if (ret == 0) { // 客户端退出
printf("client exit!\n");
break;
}
printf("recv: %s\n", buf);
}
// 关闭连接
close(acceptfd);
pthread_exit(NULL);
return NULL;
}
int main(int argc, char const *argv[])
{
// 1. 创建流式套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket err");
return -1;
}
printf("sockfd:%d\n", sockfd);
// 2. 填充结构体,指定服务器自己的网络信息
struct sockaddr_in saddr, caddr;
saddr.sin_family = AF_INET;
// saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
saddr.sin_addr.s_addr = INADDR_ANY;
saddr.sin_port = htons(atoi(argv[1]));
// 3. 绑定信息
if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0) {
perror("bind err");
return -1;
}
printf("bind ok\n");
// 4. 监听
if (listen(sockfd, 5) < 0) {
perror("listen err");
return -1;
}
printf("listen ok\n");
// 5. 等待连接
int len = sizeof(caddr);
while (1) {
int acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);
if (acceptfd < 0) {
perror("accpet err");
return -1;
}
printf("accept ok\n");
// 打印来自客户端的IP地址和端口号
printf("client ip:%s port:%d\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
// 创建线程处理客户端连接
pthread_t tid;
if (pthread_create(&tid, NULL, mythread, &acceptfd) != 0) {
perror("create thread err");
return -1;
}
pthread_detach(tid); // 让线程结束时自动回收其资源
}
close(sockfd);
return 0;
}
多线程模型特点:
- **资源占用少:**线程共享进程资源,创建和切换开销小
- **通信方便:**线程间共享内存,通信简单高效
- **需要同步:**需要处理线程同步问题,避免竞态条件
- **稳定性:**一个线程崩溃可能影响整个进程
1.2.3 select实现并发服务器
select模型使用I/O多路复用技术,通过一个进程同时监控多个文件描述符(包括socket),当某个描述符就绪时进行处理。这种方式可以避免创建大量进程或线程,适合连接数较多但活跃连接较少的场景。
**核心思想:**每多一个客户端就多一路I/O,放到文件描述符的表中统一管理。
完整示例代码:
cpp
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/select.h>
int main(int argc, char const argv[])
{
/ 引入TCP流程 */
// 1. 创建流式套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket err");
return -1;
}
printf("sockfd:%d\n", sockfd);
// 2. 填充结构体,指定服务器自己的网络信息
struct sockaddr_in saddr, caddr;
saddr.sin_family = AF_INET;
// saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
saddr.sin_addr.s_addr = INADDR_ANY;
saddr.sin_port = htons(atoi(argv[1]));
// 3. 绑定信息
if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0) {
perror("bind err");
return -1;
}
printf("bind ok\n");
// 4. 监听
if (listen(sockfd, 5) < 0) {
perror("listen err");
return -1;
}
printf("listen ok\n");
/* 引入select实现一个服务器可以有多个客户端链接从而实现并发服务器模型 */
int maxfd = sockfd; // 最大文件描述符
char buf[128] = "";
int acceptfd;
int len = sizeof(caddr);
// (1) 创建文件描述符集合
fd_set readfds, tempfds;
// (2) 清空集合
FD_ZERO(&readfds);
// (3) 添加关心的文件描述符
FD_SET(0, &readfds); // 键盘(标准输入)
FD_SET(sockfd, &readfds); // 用于连接的套接字描述符
while (1) {
// 每次把原集合拷贝给临时集合
tempfds = readfds;
// (4) select监听描述符是否产生了事件
if (select(maxfd + 1, &tempfds, NULL, NULL, NULL) < 0) {
perror("select err");
return -1;
}
// (5) 判断哪个描述符发生了事件
if (FD_ISSET(0, &tempfds)) { // 键盘事件
scanf("%s", buf);
printf("keyboard:%s\n", buf);
}
if (FD_ISSET(sockfd, &tempfds)) { // 新的客户端连接
acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);
if (acceptfd < 0) {
perror("accpet err");
return -1;
}
printf("accept ok\n");
// 打印来自客户端的IP地址和端口号
printf("client ip:%s port:%d\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
// 把产生的用于通信的套接字描述符也添加到集合中
FD_SET(acceptfd, &readfds); // 添加的是原集合
// 判断最大描述符是否产生变化
if (maxfd < acceptfd)
maxfd = acceptfd;
}
// 检查所有客户端连接是否有数据到达
for (int i = sockfd + 1; i <= maxfd; i++) {
if (FD_ISSET(i, &tempfds)) {
int n = recv(i, buf, 128, 0);
if (n < 0) {
perror("recv err");
return -1;
} else if (n == 0) { // 客户端退出
printf("client %d exit!\n", i);
close(i);
FD_CLR(i, &readfds); // 在原集合中清除
// 更新最大文件描述符
while (!FD_ISSET(maxfd, &readfds))
maxfd--;
} else {
printf("client %d:%s\n", i, buf);
}
}
}
}
close(sockfd);
return 0;
}
select模型特点:
- **单进程处理多连接:**一个进程可以同时监控多个连接
- **效率高:**避免了进程/线程创建和切换的开销
- **连接数限制:**受限于FD_SETSIZE(通常1024)
- **效率问题:**每次调用select都需要遍历所有描述符
2. 网络调试工具
网络调试是网络编程和系统维护中的重要环节,掌握常用的网络调试工具能够快速定位和解决网络问题。
2.1 ping命令
ping是网络连通性检测最常用的命令,它通过发送ICMP回显请求包来测试网络连接。
主要作用:
- **检测网络连通情况:**测试本地主机与目标主机之间的网络是否通畅
- **域名解析:**根据域名得到服务器的IP地址
- **路由追踪:**根据ping返回的TTL(Time To Live)值来判断数据包经过的路由器数量
常用参数:
ping -c 4 www.example.com:发送4个数据包后停止ping -i 2 www.example.com:设置发送间隔为2秒ping -s 1000 www.example.com:设置数据包大小为1000字节
2.2 netstat命令
netstat(Network Statistics)用于显示网络连接、路由表、接口统计等网络相关信息。
**主要作用:**测试网络状态,查看网络连接和端口监听情况
常用命令:
netstat -a:查看所有网络连接状态(包括监听和已建立的连接)netstat -at:查看所有TCP网络状态netstat -au:查看所有UDP网络状态netstat -l:仅显示监听状态的套接字netstat -p:显示进程ID和程序名称netstat -n:以数字形式显示地址和端口号
实用组合:
netstat -tlnp:查看所有TCP监听端口及对应的进程netstat -an | grep ESTABLISHED:查看所有已建立的连接
3. 网络协议头分析
理解网络协议头的结构和数据封装过程是网络编程和协议分析的基础。
3.1 数据的封装与传递过程
数据在网络中传输时,需要经过多层协议的封装和解封装过程。这个过程遵循OSI七层模型或TCP/IP四层模型,每一层都会为数据添加自己的头部信息,形成特定的数据单元。
3.1.1 数据封装的基本流程
数据封装过程:
- **应用层:**用户数据(如HTTP请求、FTP文件等)
- **传输层:**添加TCP或UDP头部,形成数据段(Segment)或数据报(Datagram)
- **网络层:**添加IP头部,形成数据包(Packet)
- **数据链路层:**添加以太网头部和尾部,形成数据帧(Frame)
- **物理层:**将数据帧转换为比特流在物理介质上传输
**数据解封装过程:**接收端按照相反的顺序逐层剥离头部,最终还原出原始数据。
3.1.2 以太网帧格式
以太网帧是数据链路层的基本传输单元,其标准格式如下:
text
+----------------+----------------+----------------+----------------+----------------+----------------+
| 目的MAC地址 | 源MAC地址 | 类型/长度 | 数据 | 填充 | 帧校验序列 |
| (6字节) | (6字节) | (2字节) | (46-1500字节) | (可选) | (4字节) |
+----------------+----------------+----------------+----------------+----------------+----------------+
各字段说明:
- **目的MAC地址:**接收方的物理地址
- **源MAC地址:**发送方的物理地址
- **类型/长度:**标识上层协议类型(如0x0800表示IPv4,0x0806表示ARP)或数据长度
- **数据:**承载的上层协议数据(IP数据包)
- **填充:**当数据部分小于46字节时,填充到最小长度
- **帧校验序列:**CRC校验码,用于检测传输错误
3.1.3 IP数据报格式
IP数据报是网络层的基本传输单元,IPv4数据报格式如下:
text
+---------------+---------------+---------------+---------------+---------------+---------------+---------------+---------------+
| 版本(4位) | 首部长度(4位) | 服务类型(8位) | 总长度(16位) | 标识(16位) | 标志(3位) | 片偏移(13位) |
+---------------+---------------+---------------+---------------+---------------+---------------+---------------+---------------+
| 生存时间(8位) | 协议(8位) | 首部校验和(16位) | 源IP地址(32位) |
+---------------+---------------+---------------+---------------+---------------+---------------+---------------+---------------+
| 目的IP地址(32位) |
+---------------+---------------+---------------+---------------+---------------+---------------+---------------+---------------+
| 选项(可选) |
+---------------+---------------+---------------+---------------+---------------+---------------+---------------+---------------+
| 数据 |
+---------------+---------------+---------------+---------------+---------------+---------------+---------------+---------------+
关键字段说明:
- **版本:**IP协议版本,IPv4为4
- **首部长度:**IP头部的长度,以4字节为单位
- **总长度:**整个IP数据报的长度,包括头部和数据
- **生存时间:**数据报在网络中可经过的最大路由器跳数
- **协议:**标识上层协议(如6表示TCP,17表示UDP)
- **源/目的IP地址:**发送方和接收方的IP地址
3.1.4 TCP报文段格式
TCP是面向连接的可靠传输协议,其报文段格式如下:
text
+---------------+---------------+---------------+---------------+---------------+---------------+---------------+---------------+
| 源端口(16位) | 目的端口(16位) | 序列号(32位) | 确认号(32位) | 数据偏移(4位) | 保留(6位) | 控制位(6位) | 窗口大小(16位)|
+---------------+---------------+---------------+---------------+---------------+---------------+---------------+---------------+
| 校验和(16位) | 紧急指针(16位) | 选项(可选) | 填充 | 数据 | | | |
+---------------+---------------+---------------+---------------+---------------+---------------+---------------+---------------+
控制位说明:
- **URG:**紧急指针有效
- **ACK:**确认号有效
- **PSH:**接收方应尽快将数据交付应用层
- **RST:**重置连接
- **SYN:**同步序列号,用于建立连接
- **FIN:**发送方数据发送完毕,用于关闭连接
3.1.5 UDP数据报格式
UDP是无连接的不可靠传输协议,其数据报格式简单:
text
+---------------+---------------+---------------+---------------+---------------+---------------+---------------+---------------+
| 源端口(16位) | 目的端口(16位) | 长度(16位) | 校验和(16位) | 数据 | | | |
+---------------+---------------+---------------+---------------+---------------+---------------+---------------+---------------+
字段说明:
- **源端口/目的端口:**发送方和接收方的端口号
- **长度:**UDP数据报的总长度(包括头部和数据)
- **校验和:**可选字段,用于错误检测
3.1.6 实际传输示例
以一个HTTP GET请求为例,数据封装过程如下:
text
应用层: GET /index.html HTTP/1.1\r\nHost: www.example.com\r\n\r\n
传输层: [TCP头部] + GET /index.html HTTP/1.1\r\nHost: www.example.com\r\n\r\n
网络层: [IP头部] + [TCP头部] + GET /index.html HTTP/1.1\r\nHost: www.example.com\r\n\r\n
数据链路层:[以太网头部] + [IP头部] + [TCP头部] + GET /index.html HTTP/1.1\r\nHost: www.example.com\r\n\r\n + [以太网尾部]
物理层: 0101010101010101010101010101010101010101010101010101010101010101...
理解数据的封装与传递过程对于网络编程、协议分析和网络故障排查都至关重要。通过分析各层协议头部,可以深入了解数据在网络中的传输机制。