网络编程:简单的网络抓包、网络协议(TCP、UDP、HTTP)、io复用(select、poll、epoll)详解
网络编程
进程间通信-->进程的地址:ip+port
网络:实现资源共享、信息交互
互联网:网络与网络连接
互联网 = 无数个局域网、计算机、路由器,互相连接在一起形成的 “全球大网”
-
MAC 地址:在你自己的局域网里找设备
-
IP 地址:在互联网上找到对方在哪
-
端口:找到对方电脑上的哪个程序
-
TCP:在互联网上可靠地传数据
IP与MAC
-
IP地址----->标识唯一一台主机 ipv4,ipv6
-
32位(ipv4),点分十进制 --->192.168.1.100
-
系统配置的,随时可以改
-
结构:网络号+主机号
-
作用:在整个互联网 定位 设备
-
-
MAC物理地址:
-
48位二进制
-
烧在网卡硬件里,终身不变
-
作用:在同一个局域网内标识设备
-
-
二者互为补充
-
为什么设置IP地址 ?
-
IP 地址 = 网络号 + 主机号
-
1. 缺一不可
-
只靠 IP:找不到具体网卡
-
只靠 MAC:无法跨网络通信
2. 工作流程(ARP 协议)
-
主机知道目标 IP,想发数据
-
在局域网内发 ARP 广播:
“谁的 IP 是 ×××?告诉我你的 MAC!”
-
目标设备回复自己的 MAC
-
最终数据包:IP 头 + MAC 头 + 数据
3. 一句话概括配合逻辑
IP 指路跨网段,MAC 投递到网卡。
-
-
对比项 MAC 地址 IP 地址 全称 物理地址 / 硬件地址 网络协议地址 层级 数据链路层(二层) 网络层(三层) 是否固定 出厂固定,终身不变 可动态修改 作用范围 同一局域网内 整个互联网 标识内容 标识 “哪块网卡” 标识 “在哪个网络的哪台主机” 能否跨网 不能 能 -
网络协议:tcp,udp,http
端口号:
端口 = 电脑里给各个程序分配的 “门牌号”
IP 找到这台电脑,端口找到具体哪个程序
tcpdump----->网络抓包
tcpdump [选项] [过滤表达式]
-
选项:控制抓包行为、输出格式、保存格式
-
过滤表达式:精准筛选数据包
核心选项
-
-i <interface>指定网卡(any--->所有网卡)
-
-n:不解析主机名(显示IP)
-
-nn:不解析主机名+端口名(显示数字)
-
-c 10 抓10个包就自动停止
-
-s 0 抓完整包(默认只抓96字节)
-
-q 精简输出(quiet)
-
-v,-vv,-vvv:输出详细度递增(TTL、校验)
-
-w <file.pcap>:将原始数据写入文件
-
-r <file.pcap>:读取本地pcap文件
-
-A:以ASCII显示数据包内容(HTTP)
-
-X:以Hex+ASCII显示(深度分析)
-
-e :显示数据链路层头部(HAC地址)
过滤表达式
1. 按地址过滤
-
host 192.168.1.100:抓取与该 IP 相关的所有包 -
src host 192.168.1.100:仅源 IP -
dst host 192.168.1.100:仅目的 IP -
net 192.168.1.0/24:网段过滤
2. 按端口过滤
-
port 80:80 端口(HTTP) -
src port 53:源端口 53(DNS 响应) -
dst port 22:目的端口 22(SSH) -
portrange 1-1024:端口范围
3. 按协议过滤
-
tcp/udp/icmp/arp/ip6
4. 逻辑运算符
-
and/or/not(或&&/||/!) -
括号
()分组(Shell 中需加引号或转义)
| 部分 | 含义 |
|---|---|
tcpdump |
抓包工具 |
-i ens33 |
指定在 ens33 网卡上抓包(虚拟机常见有线网卡名) |
-n |
不解析主机名,直接显示 IP,提升抓包速度 |
-t |
不显示时间戳,精简输出 |
(src 192.168.1.124 and dst 192.168.1.186) |
过滤「源 IP 为 192.168.1.124,目的 IP 为 192.168.1.186」的包 |
or |
逻辑或,匹配两种方向的包 |
(src 192.168.1.186 and dst 192.168.1.124) |
过滤「源 IP 为 192.168.1.186,目的 IP 为 192.168.1.124」的包 |
TCP
面向连接的 可靠的 流式服务
三次握手 建立连接
四次挥手 断开连接
可靠性:应答确认超时重传机制
乱序重排、去重,滑动窗口

Win: ipconfig Linux: ifconfig --->查看 / 配置网卡、IP 地址、子网掩码、MAC 地址 netstat -natp
3+4

-
应答确认(ACK):每发一段数据,对方必须回 ACK 确认收到。
-
超时重传:超过一定时间没收到 ACK,就认为丢包,重新发送。
-
去重:利用序列号,重复报文直接丢弃。
-
乱序重排:根据序列号排序,保证数据按发送顺序交给应用层。
-
流量控制:滑动窗口
-
作用:防止发送方发太快,把接收方撑爆
-
接收方在 ACK 中告诉对方自己窗口大小
-
发送方根据窗口大小控制发送速率
三次握手

四次挥手


tcp连接状态
TIME_WAIT
状态是:主动断开连接的一端收到对端的FIN报文段并且将ACK报文 段发出后的一种状态。
意义:
1) 保证迟来的报文段能被识别并丢弃。
2) 保证可靠的终止TCP连接。保证对端能收到最后的一个ACK,如果ACK丢失, 在TIME_WAIT状态本端还可以接受到对端重传的FIN报文段并重新发送ACK。 所以TIME_WAIT 的存在时间为2MSL。
TIME_WAIT 和CLOSE_WAIT有什么区别?
-
CLOSE_WAIT 是被动关闭的一端在接收到对端关闭请求(FIN报文段)并且将ACK 发送出去后所处的状态,这种状态表示:收到了对端关闭的请求,但是本端还没有完成 工作,还未关闭。
-
TIME_WAIT 状态是主动关闭的一端在本端已经关闭的前期下,收到对端的关闭请 求(FIN报文段)并且将ACK发送出去后所处的状态,这种状态表示:双方都已经完成 工作,只是为了确保迟来的数据报能被是被识别并丢弃,可靠的终止TCP连接。
tcpdump----->网络抓包
握手时机======>connect()
三次挥手?------->可以 第二次close()发FIN 可以和之前的ACK 一起发送
tcp服务器



多线程
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<pthread.h>
void* fun(void* arg)
{
int c=(int)(long)arg;
while(1)
{
char buff[128]={0};
int n=recv(c,buff,127,0);//read
if(n<=0)
{
break;
}
printf("recv(%d):%s\n",c,buff);
send(c,"OK",2,0);
}
close(c);
printf("client close(%d)\n",c);
return NULL;
}
int main()
{
// 1. 创建【监听套接字】 sockfd
int sockfd=socket(AF_INET,SOCK_STREAM,0);//创建套接字
if(sockfd==-1)
{
exit(1);
}
// 2. 配置【自己服务器的 IP + 端口】
struct sockaddr_in saddr,caddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family=AF_INET;
saddr.sin_port=htons(6000);//转网络字节序列(大端) 端口
saddr.sin_addr.s_addr=inet_addr("127.0.0.1");//服务器ip
//Linux:ifconfig
// 3. bind:绑定到【自己的IP+端口】
int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
if(res==-1)
{
printf("bin err\n");
exit(1);
}
// 4. 开始监听(等别人来连)
listen(sockfd,5);//监听队列
while(1)
{
int len=sizeof(caddr);
//c连接套接字(专门和客户端通信,收发数据)
// 5. 阻塞等待客户端连接
// 连接成功 → 返回【新的通信套接字 c】
int c=accept(sockfd,(struct sockaddr*)&caddr,&len);
if(c<0)
{
continue;
}
printf("accept c=%d\n ip=%s port=%d\n",c,inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port));
//多线程
pthread_t id;
pthread_create(&id,NULL,fun,(void*)(long)c);
}
}
多进程
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
int main()
{
// 1. 创建【监听套接字】 sockfd
int sockfd=socket(AF_INET,SOCK_STREAM,0);//创建套接字
if(sockfd==-1)
{
exit(1);
}
// 2. 配置【自己服务器的 IP + 端口】
struct sockaddr_in saddr,caddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family=AF_INET;
saddr.sin_port=htons(6000);//转网络字节序列(大端) 端口
saddr.sin_addr.s_addr=inet_addr("127.0.0.1");//服务器ip
//Linux:ifconfig
// 3. bind:绑定到【自己的IP+端口】
int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
if(res==-1)
{
printf("bin err\n");
exit(1);
}
// 4. 开始监听(等别人来连)
res = listen(sockfd,5);//创建监听队列
if( res == -1 )
{
exit(1);
}
while(1)
{
int len=sizeof(caddr);
//c连接套接字(专门和客户端通信,收发数据)
// 5. 阻塞等待客户端连接
// 连接成功 → 返回【新的通信套接字 c】
int c=accept(sockfd,(struct sockaddr*)&caddr,&len);
if(c<0)
{
continue;
}
printf("accept c=%d\n ip=%s port=%d\n",c,inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port));
//多进程
pid_t pid = fork();
if(pid==0)
{
close(sockfd);// 子进程不需要监听套接字
while(1)
{
char buff[128]={0};
int n=recv(c,buff,127,0);//注意 数据量太大的情况
if(n<=0)
{
break;
}
printf("buff=%s\n",buff);
send(c,"ok",2,0);
}
close(c);
printf("client close\n");
exit(0);
}
else if(pid > 0) // 父进程
{
close(c); // 父进程不需要通信套接字
}
}
}
收发不对应,发送缓冲区和接受缓冲区 粘包
服务器有两个 socket(文件描述符):
-
sockfd:监听套接字(只负责等客户端来连接,不收发数据)
-
c:连接套接字(专门和客户端通信,收数据、发数据)
🎯 两个 socket 到底有什么区别?(核心)
1. sockfd(监听 fd)
-
是服务器自己创建的
-
bind 绑定的是自己的 IP + 端口
-
listen 让它开始监听
-
只用来等待客户端连接
-
不参与收发数据
-
c(连接 fd)
-
accept () 才新生成的
-
一个客户端对应一个 c
-
专门用来和客户端通信
-
recv /send 都用它
-
通信完 close (c)
tcp客户端
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
int main()
{
// 1. 创建客户端自己的套接字
int sockfd=socket(AF_INET,SOCK_STREAM,0);
if(sockfd==-1)
{
exit(1);
}
// 2. 填服务器的地址
struct sockaddr_in saddr;//服务器连接地址,你将要连接的服务器地址
memset(&saddr,0,sizeof(saddr));
saddr.sin_family=AF_INET;
saddr.sin_port=htons(6000);
saddr.sin_addr.s_addr=inet_addr("127.0.0.1");
// 3. 用自己的 sockfd 去连接服务器
int res=connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
if(res==-1)
{
printf("onnect err\n");
exit(1);
}
while(1)
{
char buff[128]={0};
printf("input:\n");
fgets(buff,128,stdin);
if(strncmp(buff,"end",3)==0)
{
break;
}
// 4. 收发数据都是用自己的 sockfd
send(sockfd,buff,strlen(buff)-1,0);//write
memset(buff,0,128);
recv(sockfd,buff,127,0);//接收 服务器返回的数据
printf("buff=%s\n",buff);
}
close(sockfd);
exit(0);
}
sockfd 属于客户端
它代表客户端的一个发送 / 接收通道
connect 之后,这个通道连接到了服务器
但它不是服务器的东西,依然是客户端自己的
| 对比项 | 客户端 socket | 服务器监听 socket | 服务器连接 socket |
|---|---|---|---|
| 谁创建 | 客户端 | 服务器 | 服务器 accept 生成 |
| 要不要 bind | 不需要(系统自动分配) | 必须 bind | 不需要 |
| 主要调用函数 | connect、send、recv | listen、accept | recv、send |
| 作用 | 主动连接 + 收发数据 | 等待连接 | 真正通信 |
| 数量 | 一个客户端只有一个 | 服务器只有一个 | 来几个客户端就有几个 |
UDP协议
-
无连接、不可靠的数据报服务
-
必须保证 收端 一次性收完数据,否则会丢包
不同协议之间------->可以在同一个端口运行ser
netstat -natp | grep 600
Ser
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define PORT 8888
#define BUF_SIZE 1024
int main() {
int sockfd;
char buf[BUF_SIZE];
struct sockaddr_in serv_addr, cli_addr;
socklen_t cli_len = sizeof(cli_addr);
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(PORT);
bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
printf("UDP Server running on port %d...\n", PORT);
while (1) {
int n = recvfrom(sockfd, buf, BUF_SIZE, 0,
(struct sockaddr*)&cli_addr, &cli_len);
if (n <= 0) continue;
buf[n] = '\0';
// 判断客户端退出消息
if (strcmp(buf, "[EXIT]") == 0) {
printf("Client disconnected\n");
continue;
}
printf("Recv: %s\n", buf);
// 回显
//sendto(sockfd, buf, n, 0,(struct sockaddr*)&cli_addr, cli_len);
sendto(sockfd,"ok",2,0,(struct sockaddr*)&caddr,sizeof(caddr));
}
close(sockfd);
return 0;
}
Cli
用信号捕获
关键:捕获 Ctrl+C / 正常退出,自动发送 [EXIT] 给服务器
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8888
#define BUF_SIZE 1024
int sockfd;
struct sockaddr_in serv_addr;
// 信号处理:Ctrl+C 时发送退出消息
void handle_exit(int sig) {
const char *exit_msg = "[EXIT]";
sendto(sockfd, exit_msg, strlen(exit_msg), 0,
(struct sockaddr*)&serv_addr, sizeof(serv_addr));
printf("\nClient exit\n");
close(sockfd);
exit(0);
}
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <server_ip>\n", argv[0]);
return 1;
}
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
inet_pton(AF_INET, argv[1], &serv_addr.sin_addr);
// 注册 Ctrl+C 信号
signal(SIGINT, handle_exit);
char buf[BUF_SIZE]={0};
while (1) {
printf("> ");
if (!fgets(buf, BUF_SIZE, stdin)) break;
if (strcmp(buf, "quit") == 0) {
handle_exit(0); // 主动退出
}
sendto(sockfd, buf, strlen(buf), 0,
(struct sockaddr*)&serv_addr, sizeof(serv_addr));
memset(buf, 0, BUF_SIZE);
int n = recvfrom(sockfd, buf, BUF_SIZE, 0, NULL, NULL);
if (n > 0) {
printf("Server echo: %s\n", buf);
}
}
handle_exit(0);
return 0;
}
HTTP
http 应用层协议:80 Linux:root权限
http协议 在传输层使用tcp协议
请求方法:GET、POST
# 1. 请求行(第一行,固定格式)
POST /api/login HTTP/1.1
# 2. 请求头(多行键值对)
Host: www.xxx.com
User-Agent: Mozilla/5.0...
Content-Type: application/json
Content-Length: 45
# 3. 空行(必须!分隔请求头和请求体,只有\r\n)
# 4. 请求体(可选,POST/PUT才有)
{"username":"admin","pwd":"123456"}
| 对比项 | GET 请求 | POST 请求 |
|---|---|---|
| 请求体 | 无 | 有 |
| 参数位置 | URL 拼接 (?id=1) | 请求体中 |
| 长度限制 | URL 长度限制 | 理论无限 |
| 缓存 | 可缓存 | 默认不缓存 |
| 安全性 | 明文暴露,不适合密码 | 参数隐藏在请求体 |
状态码

http1.0和2.0区别/差异
| 特性 | HTTP/1.0 (1996) | HTTP/2 (2015) |
|---|---|---|
| 协议格式 | 纯文本(人类可读) | 二进制分帧(机器高效解析) |
| 连接模型 | 短连接:1 请求 1 连接,请求完即关闭 | 单长连接 + 多路复用:一个 TCP 并发多请求 |
| 并发能力 | 无;必须串行,或开多个 TCP(浏览器限 6 个) | 同连接任意并发流,帧交错传输 |
| 队头阻塞 | 严重:一个请求慢,整条连接阻塞 | 应用层消除:流独立、不互相阻塞 |
| 头部 | 纯文本、全量重复发送、无压缩 | HPACK 压缩(静态表 + 动态表 + Huffman) |
| 服务器推送 | ❌ 不支持 | ✅ Server Push:主动推 CSS/JS 等 |
| 请求优先级 | ❌ 无 | ✅ 可设置权重与依赖,关键资源优先 |
| 性能 | 延迟高、连接开销大、带宽浪费 | 低延迟、高吞吐、连接数极少 |
http和https区别
| 项目 | HTTP | HTTPS |
|---|---|---|
| 安全性 | 明文传输,极易被窃听、篡改 | 加密传输,安全、可信 |
| 端口 | 80 | 443 |
| 加密方式 | 无 | TLS/SSL 非对称 + 对称加密 |
| 证书 | 不需要 | 需要 CA 证书(自签名也可用) |
| 性能 | 更快(少一层加密) | 稍慢(加解密开销) |
| 浏览器标识 | 不安全,显示 “不安全” | 安全,显示小锁 |
wget
wet=web get------>Linux下 命令行下载工具
短连接、长连接对比
| 对比 | 短连接 | 长连接 (Keep-Alive) |
|---|---|---|
| 一次连接处理请求 | 1 次请求 | 多次请求 |
| 断开时机 | 响应结束立刻断开 | 空闲超时才断开 |
| TCP 握手挥手 | 频繁,开销大 | 很少,效率高 |
| 默认版本 | HTTP/1.0 | HTTP/1.1 |
| 请求头 | Connection: close | Connection: Keep-Alive |
http服务器实现
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#define PATH "/home/stu/mycode/day22"
int socket_init();
char* get_filename(char buff[])
{
if( buff == NULL)
{
return NULL;
}
char* s = strtok(buff," ");
if( s == NULL )
{
return NULL;
}
s = strtok(NULL," ");
return s;
}
int open_file(char* filename, int * fsize )
{
char path[256] = {PATH};
if( strcmp(filename,"/") == 0 )
{
strcat(path,"/index.html");// "/"
}
else
{
strcat(path,filename);// "/a.jpg"
}
int fd = open(path,O_RDONLY);
if( fd == -1)
{
return -1;
}
*fsize = lseek(fd,0,SEEK_END);//末尾
lseek(fd,0,SEEK_SET);//起始位置
return fd;
}
int main()
{
int sockfd = socket_init();
if( sockfd == -1)
{
exit(1);
}
while( 1 )
{
int c = accept(sockfd,NULL,NULL);
if( c < 0 )
{
continue;
}
char buff[1024]= {0};
int n = recv(c,buff,1023,0);
if( n <= 0 )
{
close(c);
continue;
}
char* filename = get_filename(buff);
if( filename == NULL)
{
send(c,"404",3,0);
close(c);
continue;
}
int filesize = 0;
int fd = open_file(filename,&filesize);
if( fd == -1 )
{
send(c,"404",3,0);
close(c);
continue;
}
char head[256] = {"HTTP/1.1 200 OK\r\n"};
strcat(head,"Server: myhttp\r\n");
sprintf(head+strlen(head),"Content-Length: %d\r\n",filesize);
strcat(head,"\r\n");
send(c,head,strlen(head),0);//发送http应答报头
//发送 数据
char data[1024] = {0};
int num = 0;
while( (num = read(fd,data,1024)) > 0 )
{
send(c,data,num,0);
}
close(fd);
close(c);
}
}
int socket_init()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if( sockfd == -1)
{
return -1;
}
struct sockaddr_in saddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(80);//http
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
if( res == -1)
{
printf("bind err\n");
return -1;
}
res = listen(sockfd,5);
if( res == -1)
{
return -1;
}
return sockfd;
}
io复用
-
同时监听 多个文件描述符
-
select() poll() epoll()------>Linux特有
select()
优点
-
跨平台(Linux/Windows 都有)
-
简单入门 IO 多路复用
-
单线程管理多连接,比多线程高效
致命缺点
-
硬限制 1024fd,无法扩容
-
每次调用用户内核拷贝 fd_set,开销大
-
内核线性遍历 O (n),fd 越多越慢
-
返回只给总数,必须遍历所有 fd 找就绪
-
fd_set 会被修改,每次都要重置重建
select 两次全量拷贝:
-
调用 select 切入内核时:用户态 fd_set 全量拷到内核;
-
select 返回用户态前:内核态修改后的 fd_set 全量拷回用户。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/select.h>
#include <sys/time.h>
#define STDIN 0
int main()
{
int fd=STDIN;//标准输入,键盘
fd_set fdset;//收集描述符的集合
while(1)
{
FD_ZERO(&fdset);//清空fdset,每个位置0
FD_SET(fd,&fdset);//将键盘对应的描述符 添加到fdset
struct timeval tv={5,0};//超时时间5s
int n=select(fd+1,&fdset,NULL,NULL,&tv);//阻塞
if(n==-1)
{
printf("select err\n");
}
else if(n==0)
{
printf("time out\n");
}
else
{
if(FD_ISSET(fd,&fdset))//测试是否有读事件
{
char buff[128]={0};
read(fd,buff,127);//处理读事件
printf("read:%s\n",buff);
}
}
}
return 0;
}
工作原理(流程)
-
用户初始化 fd_set,把要监听的 socket/fd 加入集合
-
调用 select,用户态→内核态拷贝 fd_set
-
内核线性遍历所有 fd,检查就绪状态
-
标记就绪 fd,阻塞直到就绪 / 超时
-
select 返回,用户遍历所有 fd,FD_ISSET 判断哪些就绪
-
处理就绪事件,重置 fd_set 重新监听
文件描述符=操作系统给【打开的文件/设备】分配一个整数编号
tcp_ser
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <sys/time.h>
#define MAXFD 10
int socket_init();
void fds_init(int fds[])
{
for(int i=0;i<MAXFD;i++)
{
fds[i]=-1;
}
}
void fds_add(int fd,int fds[])
{
for(int i=0;i<MAXFD;i++)
{
if(fds[i]==-1)
{
fds[i]=fd;
break;//只添加 一次
}
}
}
int main()
{
int sockfd=socket_init();//创建监听套接字
if(sockfd==-1)
{
exit(1);
}
int fds[MAXFD];//收集 文件描述符
fds_init(fds);//置空,-1
fds_add(sockfd,fds);//添加 监听套接字的描述符
fd_set fdset;
while(1)
{
int maxfd=-1;
FD_ZERO(&fdset);
for(int i=0;i<MAXFD;i++)
{
if(fds[i]==-1)
{
continue;
}
if(fds[i]>maxfd)
{
maxfd=fds[i];
}
FD_SET(fds[i],&fdset);
}
struct timeval tv={5,0};//最多阻塞5s
int n=select(maxfd+1,&fdset,NULL,NULL,&tv);//选择就绪 文件描述符
if(n==-1)
{
printf("select err!!!\n");
}
else if(n==0)
{
printf("time out!!!\n");
}
else
{
for(int i=0;i<MAXFD;i++)
{
if(fds[i]==-1)
{
continue;
}
if(FD_ISSET(fds[i],&fdset))//判断读事件是否就绪
{
if(fds[i]==sockfd)//监听套接字
{
int c=accept(sockfd,NULL,NULL);
if(c>=0)
{
printf("accept c=%d\n",c);
fds_add(c,fds);
}
}
else//连接套接字
{
char buff[128]={0};
int num=recv(fds[i],buff,127,0);
if(num<=0)
{
close(fds[i]);
fds_del(fds[i],fds);
printf("client close\n");
}
else
{
printf("rev:%s\n",buff);
send(fds[i],"OK",2,0);
}
}
}
}
}
}
}
int socket_init()
{
int sockfd=socket(AF_INET,SOCK_STREAM,0);
if(sockfd==-1)
{
return -1;
}
struct sockaddr_in saddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family=AF_INET;
saddr.sin_port=htons(6000);
saddr.sin_addr.s_addr=inet_addr("127.0.0.1");
int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
if(res==-1)
{
printf("bind err!!!\n");
return -1;
}
res=listen(sockfd,5);
if(res==-1)
{
printf("listen err!!!\n");
return -1;
}
return sockfd;
}
poll

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>
#define MAXFD 10
int socket_init();
void fds_init(struct pollfd fds[])
{
for(int i = 0; i < MAXFD; i++)
{
fds[i].fd = -1;
fds[i].events = 0;//关心事件
fds[i].revents = 0;//实际发生事件
}
}
void fds_add(int fd, struct pollfd fds[])
{
for(int i = 0; i < MAXFD; i++)
{
if( fds[i].fd == -1 )
{
fds[i].fd = fd;
fds[i].events = POLLIN;//读事件
fds[i].revents = 0;
break;
}
}
}
void fds_del(int fd, struct pollfd fds[])
{
for(int i = 0; i < MAXFD; i++)
{
if( fds[i].fd == fd )
{
fds[i].fd = -1;
fds[i].events = 0;
fds[i].revents = 0;
break;
}
}
}
void accept_client(int sockfd, struct pollfd fds[])
{
int c = accept(sockfd,NULL,NULL);
if( c < 0 )
{
return;
}
printf("accept c=%d\n",c);
fds_add(c,fds);
}
void recv_data(int c, struct pollfd fds[])
{
char buff[128] = {0};
int n = recv(c,buff,127,0);
if( n <= 0 )
{
close(c);
fds_del(c,fds);
return;
}
printf("recv:%s\n",buff);
send(c,"ok",2,0);
}
int main()
{
int sockfd = socket_init();
if( sockfd == -1)
{
exit(1);
}
//poll 结构体
struct pollfd fds[MAXFD];
fds_init(fds);
fds_add(sockfd,fds);//目前唯一的描述法sockfd,添加到fds数组
while( 1 )
{
int n = poll(fds,MAXFD,5000);//可能阻塞 -1永久阻塞直至事情发生 5s
if( n == -1)
{
printf("poll err\n");
}
else if ( n == 0 )
{
printf("time out\n");
}
else
{
for(int i = 0; i < MAXFD; i++ )
{
if( fds[i].fd == -1 )
{
continue;
}
if( fds[i].revents & POLLIN )//读事件
{
//sockfd accept, c recv
if( fds[i].fd == sockfd)
{
accept_client(sockfd,fds);
}
else
{
recv_data(fds[i].fd,fds);
}
}
}
}
}
}
int socket_init()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if( sockfd == -1 )
{
return -1;
}
struct sockaddr_in saddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
if( res == -1)
{
printf("bind err\n");
return -1;
}
res = listen(sockfd,5);
if(res == -1 )
{
return -1;
}
return sockfd;
}
epoll

LT
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#define MAXFD 10
int socket_init();
//内核事件表
void epoll_add(int epfd, int fd)
{
struct epoll_event ev;
ev.data.fd = fd;//ptr
ev.events = EPOLLIN;//读
if( epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev) == -1)
{
printf("epoll ctl add err\n");
}
}
void epoll_del(int epfd, int fd)
{
if( epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL) == -1)
{
printf("epoll ctl del err\n");
}
}
void accept_client(int epfd, int sockfd)
{
int c = accept(sockfd,NULL,NULL);
if( c < 0 )
{
return;
}
printf("accept c=%d\n",c);
epoll_add(epfd,c);//将新的连接套接子添加到内核事件表
}
void recv_data(int epfd, int c)
{
char buff[128] = {0};
int n = recv(c,buff,1,0);//recv() ==0 说明对方关闭了连接
if( n <= 0 )
{
epoll_del(epfd,c);
close(c);
printf("client close\n");
return;
}
printf("recv:%s\n",buff);
send(c,"ok",2,0);
}
//epoll tcp 服务器
int main()
{
int sockfd = socket_init();
if( sockfd == -1)
{
exit(1);
}
int epfd = epoll_create(MAXFD);//创建内核事件表, 红黑树,就绪队列
if( epfd == -1)
{
exit(1);
}
epoll_add(epfd,sockfd);//将监听套接字sockfd,添加到内核事件表
struct epoll_event evs[MAXFD];//存放就绪描述
//内核把 内核态就绪队列 里的就绪事件,批量拷贝 到用户态提前定义好的数组
while( 1 )
{
//将 epfd内核事件表(内核态)中的 就绪描述符----->evs(用户态)
int n = epoll_wait(epfd,evs,MAXFD,5000);//可能阻塞
if( n < 0 )
{
printf("epoll err\n");
}
else if( n == 0 )
{
printf("time out\n");
}
else
{
for(int i = 0; i < n; i++)
{
int fd = evs[i].data.fd;
if( evs[i].events & EPOLLIN )
{
if( fd == sockfd)
{
accept_client(epfd,fd);
}
else
{
recv_data(epfd,fd);
}
}
}
}
}
}
int socket_init()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if( sockfd == -1 )
{
return -1;
}
struct sockaddr_in saddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
if( res == -1)
{
printf("bind err\n");
return -1;
}
res = listen(sockfd,5);
if(res == -1 )
{
return -1;
}
return sockfd;
}
ET
#include<errho.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
#define MAXFD 10
int socket_init();
void setnonblock(int fd)//设置非阻塞
{
int oldfl = fcntl(fd,F_GETFL);
int newfl = oldfl | O_NONBLOCK;
if( fcntl(fd,F_SETFL,newfl) == -1)
{
printf("set noblock err\n");
}
}
void epoll_add(int epfd, int fd)
{
struct epoll_event ev;
ev.data.fd = fd;//ptr
ev.events = EPOLLIN |EPOLLET;//读
setnonblock(fd);//设置非阻塞
if( epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev) == -1)
{
printf("epoll ctl add err\n");
}
}
void epoll_del(int epfd, int fd)
{
if( epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL) == -1)
{
printf("epoll ctl del err\n");
}
}
void accept_client(int epfd, int sockfd)
{
while(1)
{
int c = accept(sockfd,NULL,NULL);
if(c<0)
{
if(errno==EAGAIN||errno==EWOULDBLOCK)
{
break;
}
perror("accept err");
break;
}
printf("accept c=%d\n",c);
epoll_add(epfd,c);//将新的连接套接子添加到内核事件表
}
}
void recv_data(int epfd, int c)
{
while( 1 )
{
char buff[128] = {0};
int num = recv(c,buff,1,0);
if( num == -1)
{
if( errno == EAGAIN ||errno == EWOULDBLOCK )//没数据了
{
send(c,"ok",2,0);
}
else//出错误
{
printf("recv err\n");
}
break;
}
else if ( num == 0 )
{
epoll_del(epfd,c);
close(c);
printf("close\n");
break;
}
else
{
printf("recv:%s\n",buff);
}
}
/*
char buff[128] = {0};
int n = recv(c,buff,1,0);//recv() ==0 说明对方关闭了连接
if( n <= 0 )
{
epoll_del(epfd,c);
close(c);
printf("client close\n");
return;
}
printf("recv:%s\n",buff);
send(c,"ok",2,0);
*/
}
//epoll tcp 服务器
int main()
{
int sockfd = socket_init();
if( sockfd == -1)
{
exit(1);
}
int epfd = epoll_create(MAXFD);//创建内核事件表, 红黑树,就绪队列
if( epfd == -1)
{
exit(1);
}
epoll_add(epfd,sockfd);//将监听套接子sockfd,添加到内核事件表
struct epoll_event evs[MAXFD];//存放就绪描述
while( 1 )
{
int n = epoll_wait(epfd,evs,MAXFD,5000);//可能阻塞
if( n < 0 )
{
printf("epoll err\n");
}
else if( n == 0 )
{
printf("time out\n");
}
else
{
for(int i = 0; i < n; i++)
{
int fd = evs[i].data.fd;
if( evs[i].events & EPOLLIN )
{
if( fd == sockfd)
{
accept_client(epfd,fd);
}
else
{
recv_data(epfd,fd);
}
}
}
}
}
}
int socket_init()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if( sockfd == -1 )
{
return -1;
}
struct sockaddr_in saddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
if( res == -1)
{
printf("bind err\n");
return -1;
}
res = listen(sockfd,5);
if(res == -1 )
{
return -1;
}
return sockfd;
}
LT/ET
LT:水平触发 —— 只要缓冲区有数据,就一直通知(温柔)
ET:边缘触发 —— 只有状态变化时,才通知一次(高冷)
工作原理:
1. LT(默认)
-
只要fd 缓冲区有数据 / 可写
-
epoll_wait 就一直返回
-
你不用一次读完,读一点也行,下次还会通知
2. ET(需要设置)
-
只有状态改变时才通知(空 → 有数据 / 不可写 → 可写)
-
只通知一次
-
你必须一次性把数据读完 / 写满,否则就再也收不到通知了
LT 模式代码
char buf[1024]; int n = recv(fd, buf, 1024, 0); // 没读完?没关系,下次 epoll_wait 还会告诉你
ET 模式代码
// 必须循环读!直到返回 EAGAIN
while (true) {
int n = recv(fd, buf, 1024, 0);
if (n <= 0) break;
}
ET 必须做的两件事
-
非阻塞 socket
-
循环读写,直到 EAGAIN(EWOULDBLOCK)
面试必问 3 个问题
1. ET 为什么比 LT 快?
ET 只通知一次,减少了 epoll_wait 返回次数,减少 内核态<------>用户态 切换。
2. ET 为什么必须用非阻塞?
因为你要循环读,如果是阻塞,最后一次读会卡住程序。
3. ET 模式下如果不一次性读完会怎样?
数据永远留在缓冲区,再也不会通知,相当于丢数据!
poll和epoll的区别
| 对比维度 | poll | epoll |
|---|---|---|
| 时间复杂度 | O (n)(遍历全部 fd) | O (1)(仅就绪 fd) |
| 数据拷贝 | 每次调用全量拷贝 | 注册时拷一次,等待无拷贝 |
| 最大 fd 数 | 无硬限,但性能随 n 线性下降 | 无硬限,百万级稳定 |
| 触发模式 | 仅 LT | LT + ET(边缘触发) |
| 移植性 | 跨平台(POSIX) | Linux 专属 |
| 适用场景 | 千级以下连接 | 万级~百万级高并发 |
| 维度 | select | poll | epoll |
|---|---|---|---|
| 最大连接数 | 受限于 1024(FD_SETSIZE) | 无上限 | 无上限 |
| 监听方式 | 遍历全部 fd | 遍历全部 fd | 事件回调 + 就绪链表 |
| 时间复杂度 | O(n) | O(n) | O (就绪数) |
| 用户态→内核态拷贝 | 每次全量拷贝 | 按需拷贝 | 仅注册时拷贝一次 |
| 内核存储 | 无存储,每次重新传入 | 无存储,每次重新传入 | 红黑树持久存储 |
| 返回结果 | 全部返回,需重新遍历 | 全部返回,需重新遍历 | 只返就绪 fd |
| 触发模式 | 仅水平触发 LT | 仅水平触发 LT | LT + 边缘触发 ET |
| 跨平台 | 全平台支持 | 全平台支持 | Linux 专属 |
| 性能 | 差(千级连接就卡) | 差(连接越多越慢) | 强(百万级稳定) |
| 使用复杂度 | 低 | 低 | 中 |

io复用----->单个线程同时监听多个文件描述符/socket,
select:fd数组+fd set
poll:fd结构体
epoll_create内核事件表
ulimit
-n 10240
| 选项 | 含义 | 单位 | 默认 | 典型场景 |
|---|---|---|---|---|
-n |
最大打开文件描述符(nofile) | 个数 | 1024 | Nginx/MySQL 高并发 |
-u |
用户最大进程数(nproc) | 个数 | 1024 | 防 fork 炸弹、容器 |
-s |
线程栈大小 | KB | 8192 | 递归程序、多线程 |
-c |
core 转储文件大小 | 块 | 0(关闭) | 程序崩溃调试 |
-v |
进程虚拟内存 | KB | unlimited | Java / 大内存应用 |
-t |
最大 CPU 时间 | 秒 | unlimited | 限制批处理任务 |
一个进程 文件描述最多 1024 -3=1021
perror---->打印错误信息
cli------>connect 失败 临时端口耗尽
扩大临时端口
-
在root中配置
sysctl -w net.ipv4.ip_local_port_range="1024 65535"
sysctl -p (不重启生效)
1024--->65535-1024---->
sysctl -w:整机一次
ulimit -n:每个终端一次
epoll_test

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐







所有评论(0)