网络编程

进程间通信-->进程的地址:ip+port

网络:实现资源共享信息交互

互联网:网络与网络连接

互联网 = 无数个局域网、计算机、路由器,互相连接在一起形成的 “全球大网”

  1. MAC 地址:在你自己的局域网里找设备

  2. IP 地址:在互联网上找到对方在哪

  3. 端口:找到对方电脑上的哪个程序

  4. TCP:在互联网上可靠地传数据

IP与MAC

  • IP地址----->标识唯一一台主机 ipv4,ipv6

    • 32位(ipv4),点分十进制 --->192.168.1.100

    • 系统配置的,随时可以改

    • 结构:网络号+主机号

    • 作用:在整个互联网 定位 设备

  • MAC物理地址

    • 48位二进制

    • 烧在网卡硬件里,终身不变

    • 作用:在同一个局域网内标识设备

  • 二者互为补充

    • 为什么设置IP地址 ?

      • IP 地址 = 网络号 + 主机号

      • 1. 缺一不可

        • 只靠 IP:找不到具体网卡

        • 只靠 MAC:无法跨网络通信

        2. 工作流程(ARP 协议)

        1. 主机知道目标 IP,想发数据

        2. 在局域网内发 ARP 广播:

          “谁的 IP 是 ×××?告诉我你的 MAC!”

        3. 目标设备回复自己的 MAC

        4. 最终数据包: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

  1. 应答确认(ACK):每发一段数据,对方必须回 ACK 确认收到。

  2. 超时重传:超过一定时间没收到 ACK,就认为丢包,重新发送。

  3. 去重:利用序列号,重复报文直接丢弃。

  4. 乱序重排:根据序列号排序,保证数据按发送顺序交给应用层。

  5. 流量控制:滑动窗口

  • 作用:防止发送方发太快,把接收方撑爆

  • 接收方在 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(文件描述符):

  1. sockfd监听套接字(只负责等客户端来连接,不收发数据)

  2. c连接套接字(专门和客户端通信,收数据、发数据)

🎯 两个 socket 到底有什么区别?(核心)

1. sockfd(监听 fd)

  • 服务器自己创建

  • bind 绑定的是自己的 IP + 端口

  • listen 让它开始监听

  • 只用来等待客户端连接

  • 不参与收发数据

  1. 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 多路复用

  • 单线程管理多连接,比多线程高效

致命缺点

  1. 硬限制 1024fd,无法扩容

  2. 每次调用用户内核拷贝 fd_set,开销大

  3. 内核线性遍历 O (n),fd 越多越慢

  4. 返回只给总数,必须遍历所有 fd 找就绪

  5. fd_set 会被修改,每次都要重置重建

select 两次全量拷贝:

  1. 调用 select 切入内核时:用户态 fd_set 全量拷到内核;

  2. 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;
}

工作原理(流程)

  1. 用户初始化 fd_set,把要监听的 socket/fd 加入集合

  2. 调用 select,用户态→内核态拷贝 fd_set

  3. 内核线性遍历所有 fd,检查就绪状态

  4. 标记就绪 fd,阻塞直到就绪 / 超时

  5. select 返回,用户遍历所有 fd,FD_ISSET 判断哪些就绪

  6. 处理就绪事件,重置 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 必须做的两件事

  1. 非阻塞 socket

  2. 循环读写,直到 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

Logo

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

更多推荐