计算机网络与网络编程

目录

知识点

网络协议模型

网络字节序 (很绕的概念)

什么是TCP

交换机与路由器

TCP三次握手

TCP四次挥手

TCP与UDP区别 概念 适用范围

TCP可靠性保证

TCP三次握手时产生的队列

SYN 攻击

发生RST情况

HTTP协议(超文本传输协议)

------------------------------------------------------------------------

网络编程模块

知识点

TCP编程模型

TCP三次握手

TCP四次挥手

套接字地址结构

进程与内核传递地址结构

socket()函数

connect()函数

bind()函数

listen()函数

Accept()函数

UDP编程模型

recvfrom()

sendto()

------------------------------------------------------------------------

多进程并发服务器

知识点

accept后的处理

一个服务器对多个客户端

I/O复用—select

I/O符用—epoll

模型小结

I/O小结

2020/12/28 小结

------------------------------------------------------------------------

附录

客户端编程模型

服务端编程模型

改—服务端编程模型

SELECT-服务端模型

EPOLL-服务端模型


知识点

网络协议模型

  • 数据链路层:

    • ARP(address resolve protocol)

      • (1)主机向自己所在的网络发送一个ARP请求,请求包含目标机器的网络地址,网络上所有其他机器都将收到这个请求,但是只有目标机器(符合网络地址)才会作出 ARP应答,应答内容包含目标机器的MAC地址。

      • (2)维护一个ARP高速缓存,包含 【IP -> MAC】的映射。

        • arp -a  # 查看arp缓存内容
          sudo arp -d 192.168.1.109  # 删除某个IP的mac地址
          sudo arp -s 192.168.1.109 08:00:27:53:10:67  # 创建某个IP的mac地址
          ​
          # 测试arp请求,须删除arp对应缓存
          # laptop 执行
          tcpdump -i eth0 -ent '(dst 192.168.1.109 and src 192.168.1.108) or (dst 192.168.1.108 and src 192.168.1.109)'  
          telnet 192.168.1.109 echo
      • (3) 

         

 

    • 封装成帧(18字节)后在物理层上传输:

      •  

  • 应用层:

    • DNS(Domain name system)

    • (1)包信息 

      •  

    • (2)DNS服务器的IP地址:

      • cat /etc/resolv.conf  #可能包含首选DNS服务器和备选DNS服务器
    • (3)访问DNS服务器的客户端程序host:

      • host -t A www.baidu.com  # -t A 表示使用A类型,通过机器的域名获得IP地址 
    • (4)使用tcpdump抓包:

      • sudo tcpdump -i eth0 -nt -s 500 port domain
        host -t A www.baidu.com
  • 网络层协议:

    • 无状态服务:1、无须保持通信状态而分配内核资源。2、传输数据无须携带状态信息。

    • 无连接服务:1、不长久地维持对方的信息。 2、每次发送需要明确IP地址。

    • IPV4包

      •  

      •  

      •  

      •  

    • IP数据包分片实验

      • sudo tcpdump -ntv icmp
        ping www.baidu.com -s 1473
    • ICMP (Internet control message protocol)

      •  

  • 传输层:

    • TCP

      •  

      • Linux超时重传(时间间隔 1s - 2s - 4s - 8s - 16s - 32s)

      •  

      • 访问不存在端口:返回RST

网络字节序 (很绕的概念)

  • 一种数据存储格式,与之对应的还有本机字节序,按序列内部组织方式,分成:

    • 大端序列:高位数据存储在低位内存中

    • 小端序列:反过来

      • 数据:int : 0x12345678 : 4 bytes

      • 内存增长方向:低 -|字节|-|字节|-|字节|-|字节|-> 高

      • 大端: 低 - 12 - 34 - 56 - 78 -> 高 (符合右手书写习惯)

      • 小端: 低 - 78 - 56 - 34 - 12 -> 高

    其中,网络字节序列属于大端序列,本机则两种都有可能。

  • // Linux 采用以下4种函数完成主机字节序与网络字节序之间的转换
    #include <netinet/in.h>
    unsigned long int htol(unsigned long int hostlong);   //  IP
    unsigned short int htos(unsigned long short hostshort);  // Port
    unsigned long int ntohl(unsigned long int netlong); 
    unsigned short int ntohs(unsigned long short netshort); 
  • // 查看主机序列 与 网络序列
    #include <arpa/inet.h>
    #include <stdio.h>
    int main() 
    {
        int a = 0x12345678;
        char *p = (char *)(&a);
    ​
        printf("主机字节序:%0x    %0x   %0x   %0x\n",  p[0], p[1], p[2], p[3]);
    ​
        int b = htonl(a);  //将主机字节序转化成了网络字节序
    ​
        p = (char *)(&b);
    ​
        printf("网络字节序:%0x    %0x   %0x   %0x\n",  p[0], p[1], p[2], p[3]);
        return 0;
    }
    // 主机字节序:78    56   34   12
    // 网络字节序:12    34   56   78
  • #include <iostream>
    using namespace std;
    bool TestEndin()
    {
        union // 不加类型名称,可以直接使用变量名调用
        {
            int numbers = 0x10000000;
            char pointer;
        };
        // 假定写入的数据是从高位数据开始写入
        if (pointer == 1) // 若从低位开始存放数据,则说明是大端模式
            return true;
        return false; // 否则,从高位开始存放数据,说明是小端模式
    }
    int main()
    {
        if (TestEndin())
            cout << "BigEndin" <<endl;
        else
            cout << "SmallEndin" <<endl;
    }

什么是TCP

  • TCP是传输控制层协议,这是一种面向连接的,提供可靠传输的协议。

    • 面向连接:三次握手 --》 传输数据 --》 四次挥手

      • 什么是面向连接?经过TCP三次握手之后,双方建立的资源就是所谓的链接,它不是一条真实的链路。

    • 可靠传输:

      • 确认机制

      • 超时重传

      • 流量控制

      • 拥塞控制

交换机与路由器

  • 交换机:主要管理MAC地址。

  • 路由器:管理IP地址。

  • 家里路由器:交换机(四个接口) + 路由器

TCP三次握手

  • 目的:只建立一次链接。

  • TCP机制:

    • (1)不带数据的ACK报文不会重传,(且不消耗序列号)。

    • (2)双方序列号都确定后,才进行链接传输。

  • 握手过程:

    • client --- | SYN = 1,seq = x | --->>> server

    • client <<<--- | ACK = 1,SYN = 1,seq = y,ack = x + 1| --- server

    • client --- | ACK = 1,seq = x + 1,ack = y + 1| --->>> server

  • 为什么三次:

    • 正常讲应该是:4次,分别为: SYN -> ACK -> SYN -> ACK。但是,为了调高效率,将发送的ACK报文,添加上了SYN数据,合并这两次的传输,因为变成了3次,更多参考TCP机制。

TCP四次挥手

  • 挥手过程:

    • client --- | FIN = 1,seq = u | --->>> server

    • client <<<--- | ACK = 1,seq = v,ack = u + 1 | --- server

      • server进入close-wait状态,并可能仍要向client发送数据

    • client <<<--- | FIN = 1,seq = w | --- server

    • client --- | ACK = 1,seq = u + 1,ack = w + 1 | --->>> server

      • clinet进入time-wait等待2MSL(Maximum Segment Lifetime)后,释放TCP连接

  • 细节:

    • client等待2MSL(2MSL:2个最大报文段的生存时间)

      • (1)确保C端确认报文到达S端,若S端收到,会重新发送FIN报文。

      • (2)防止已经失效的请求连接报文出现在本连接中,2MSL足够让所有C端产生的报文在网络中消失。

TCP与UDP区别 概念 适用范围

  •  TCP(20bytes)UDP(8bytes)
    面向连接是,需要建立连接后才能传输数据
    可靠交付是,保证可靠交付——确认、超时重传、流量控制、拥塞控制否,只检验和丢失报文等处理
    工作效率低,控制多、网络开销大、系统开销大高,传输控制简单、系统开销小
    实时性
    安全性机制多,容易被利用而被攻击机制少,相对安全
    适用场景要求传输质量,对实时性要求不高相反
    示例HTTP,HTTPS,FTP等传输文件的协议以及POP,SMTP等邮件传输协议比如视频传输、实时通信等
    传输单位数据流模式数据报模式

TCP可靠性保证

  • (1)序列号、确认机制、超时重传。

    • 双方确认序列号后,才能进行信息传输,并根据序列号判断报文是否是需要的。

    • 通过确认机制,来确定哪些报文到达,哪些需要重传。

  • (2)checksum校验和。

    • 三部分进行校验和:TCP伪首部、TCP首部、TCP数据。

  • (3)流量控制。

    • 流量窗口,指无需等待确认信号的情况下,发送端还能发送的最大数据量。

    • 该机制设置了大量缓冲区。

  • (4)拥塞控制。

    • 慢开始:从1开始,以2的倍数增加窗口值。

    • 拥塞避免:拥塞窗口到达阈值后,每次增加1个窗口值;发生超时重传时,阈值为当前窗口一半,回到慢开始。

    • 快重传:收到三个连续的重复确认,立即重传当前编号的下一个报文段,而不等超时重传。

    • 快恢复:收到三个连续的重复确认,将阈值与窗口值设置为当前阈值的一半,然后执行拥塞避免。

      • 这里每次,指的是收到一个确认报文,即为1次。

TCP三次握手时产生的队列

  • (1)半连接队列:SYN队列。

    • 服务端接受客户端的SYN请求后,内核会将该连接存储到半连接队列中

    • 同时,服务器会为该连接分配TCB(传输控制块),至少要280字节,甚至1300字节。

  • (2)全连接队列:ACCEPT队列。

    • 服务端接受客户端的ACK后,将连接从SYN队列中移除,然后将创建一个新的完全连接,并将其添加到accept队列,等待进程调用accept函数将连接取出

  •  

  • (3)实战部分:

SYN 攻击

  • (1)属于DOS攻击的一种,DOS攻击(Denial of Service,即拒接服务),另外,DDOS (Distribute)分布式攻击,指多台机器对服务器发起DOS攻击。

  • (2)而SYN攻击,指的是向服务器的TCP端口发送很多SYN请求,但是却不完成3次握手过程

    • 不完成握手的方法可以是:使用假的IP地址;

  • (3)攻击的是服务器的SYN队列。

  • (4)解决方式:

    • 4-1、延迟分配TCB。等待正常连接建立后,再分配TCB。

    • 4-2、SYN Cache技术,HASH表保存半连接信息,直到收到正确的回应ACK报文再分配TCB。

    • 4-3、SYN Cookie技术:

      • 考虑了报文中的一些固定信息,加上一些服务端的固定信息,计算出一个序列号,然后发送给客户端。

      • 这时并不保存和分配任何信息。

      • 接受最后一次ACK报文时,通过相同方式的计算,查看得到的结果是否时客户端发送SYN报文中的序列号。

      • 确认则通过匹配,否则拒绝匹配。

    • 设置以下参数

      • net.ipv4.tcp_syncookies = 1

      • net.ipv4.tcp_max_syn_backlog = 8192

      • net.ipv4.tcp_synack_retries = 2

发生RST情况

  • (1)客户端 established 而 服务端 closed(断电重启)。

    • 客户端发送任何信息给服务端,服务器都发回RST。

    • 若客户端接受的到RST,则立即释放该TCP的端口号和内存资源,并使用当前窗口号,重新建立TCP连接。

HTTP协议(超文本传输协议)


  • (1)超文本传输协议,从服务器传输超文本到本地浏览器的协议,它基于TCP/IP,属于应用层的协议。

    • (1-1)HTTP协议工作在 客户端 - 服务器模型上,浏览器作为HTTP客户端,向服务端发送HTTP请求。

  • (2)简单快速

    • 请求服务时,只需要传输 请求方法 和 路径,由于HTTP协议简单,使得HTTP服务器的程序规模小,通讯快速。

    • 请求方法:GET、POST、HEAD

  • (3)灵活:允许传输任意类型的数据对象。

  • (4)无连接:含义,每次连接只处理一个请求,并在接受客户端确认后,断开连接。

  • (5)无状态(cookie解决该问题):对事务处理没有记忆,若后续要处理前面的信息,则必须重传,导致传输数据量大,但应答速度块。


  • (1)HTTP代理服务:

    • (1-1)正向代理:客户端,设置并发HTTP请求到正向代理服务器,由正向代理服务器返回请求的目标资源。(透明代理为正向代理的一种特殊情况)

    • (1-2)反向代理:服务端,存在反向代理服务器,接受HTTP请求,根据请求类型,将请求转发到内部服务器中,获取目标资源给客户端。

    • (1-3)开源软件:squid(均支持)、varnish(反向代理)

  • (2)传输过程:

    • IP层的源IP与目的IP一般是持久不变的,而帧头部的源MAC与目的MAC是一直变化的。

  • (3)请求方法:

    # HTTP请求
    Get http://www.baidu.com/index.html HTTP/1.0
    User-Agent: Wget/1.12 (linux-gnu)
    Host: www.baidu.com  # 头部必须信息
    Connection: close
    # 空行
    # 消息体
    • GET :申请获取资源、不对服务器造成任何影响

    • HEAD :类似GET,仅要求服务器头部信息,不需要任何实际内容

    • POST :向服务器提交数据,影响服务器(创建新资源或者更新原有资源)

    • PUT :上传资源

    • DELETE : 删除资源

    • TRACE :要求服务器返回原始的HTTP请求内容,可以查看中间服务器对HTTP请求的影响。

    • OPTIONS :查看服务器对某个特定URL都支持哪些请求方法,设置URL为*,则获得服务器所有请求方法。

    • CONNECT :用于某些代理服务器,它们能把请求的连接转化为一个安全隧道。

    • PATCH :对资源做部分修改。

    • 注:加粗为安全方法,不对服务器产生影响。

  • (4)Connecton:

    • (值为close)-短链接:旧版HTTP,1个TCP连接只能服务1个HTTP,随后WEB服务端主动关闭TCP。

    • (keep-alive)-长连接:1个TCP可以处理多个HTTP,节约TCP建立时间,加快传输效率。

  • (5)状态码 与 状态信息:

    # HTTP应答
    HTTP/1.0 200 ok
    Server: BWS/1.0  # 服务器名称
    Content-Length: 8024
    Content-Type: text/html;charset = gbk
    Set-Cookie: ...
    Via: 1.0 localhost (squid/3.0 STABLE18)  # 所经过的所有代理服务器
    # 空行
    # 请求文档内容
    • 1xx信息:100 Continue : 服务器收到请求行和头部信息,告诉客户端继续发送数据部分信息

    • 2xx成功:200 OK : 请求成功

    • 3xx重定向:

      • 301 Moved Permanently : 资源被转移了,请求将被重定向

      • 302 Found : 通知资源在其他地方可以找到,但必须以GET获取

      • 304 Not Modified : 申请的资源没有更新,和之前获得的相同

      • 307 Temporary Redirect : 类似302,但可以使用原始的请求方法

    • 4xx客户端错误:

      • 400 Bad Request : 通常客户请求错误

      • 401 Unauthorized : 请求需要认证信息

      • 403 Forbidden : 没有权限访问资源

      • 404 Not Found : 资源没找到

      • 407 Proxy Authentication Required : 客户端需要代理服务器的认证

    • 5xx服务端错误:

      • 500 Internet Server Error : 通常服务器错误

      • 503 Service Unavailable : 暂时无法访问服务器

  • (6)Cookie

    • 目的:保持HTTP连接状态

      • HTTP应答:set-Cookie - 用以标识每个客户端

      • HTTP请求:每个请求需要附带Cookie信息

     

------------------------------------------------------------------------

 

网络编程模块

知识点

TCP编程模型

TCP三次握手

  • (1)由内核完成,客户端仅仅是调用了connect函数;握手过程在connect函数执行过程中完成,而服务端则是在调用accept函数执行过程中完成。

TCP四次挥手

  • (1)双端分别调用close(fd)后,执行挥手,既可以是服务端先调用close(),也可以是客户端执行close()。

  • (2)客户端调用close()关闭后,服务端调用的read()随即返回0,不再阻塞。

套接字地址结构

  • (1)通用地址结构:

    • struct sockaddr
      {
          sa_family_t sa_family;
          char sa_data[14];
      }
  • (2)IPv4地址结构:

    • struct in_addr
      {
          in_addr_t s_addr;
      }
      struct sockaddr_in
      {
          sa_family_t sin_family;
          in_port_t sin_port;
          struct in_addr sin_addr;
          char sin_zero[0];
      }
  • (3)IPv6地址结构

    • struct in6_addr
      {
          in_addr_t s6_addr[16];
      }
      struct sockaddr_in6
      {
          sa_family_t sin_family;
          in_port_t sin_port;
          struct in6_addr sin6_addr;
          uint32_t sin6_flowinfo;
          uint32_t sin6_scope_id;
      }
  • (4)地址转换函数

    • inet_addr(); inet_aton(); inet_ntoa(); inet_pton(); inet_ntop();

进程与内核传递地址结构

  • (1)由于数据经过层层封装后,最后通过网卡发送出去,而网卡等硬件设备的控制权在内核手上,因此必定会存在进程数据结构传递到内核的阶段。同样,接收数据后,也由内核先解包后传递到进程空间。

  • (2)进程 --> 内核:bind、connect、sendto

  • (3)内核 --> 进程:accept、recvfrom、getsockname、getpeername

socket()函数

  • // 1、通用socket地址
    #include <bits/socket.h>
    struct sockaddr  //  表示sock地址的结构体
    {
        sa_family_t sa_family;  //  地址族类型变量 对应  协议族类型
        char sa_data[14];  //  存放socket的地址值
    }
    /*
        常见协议族:
        | 协议族 | 地址族 | 描述 |
        | PF_UNIX | AF_UNIX | UNIX本地域协议族 |
        | PF_INET | AF_INET | TCP/IPv4 协议族 |
        | PF_INET6| AF_INET6| TCP/IPv6 协议族 |
    */
    // 2、通用socket地址2
    略
    // 3、专用socket地址
    // 3-1、UNIX本地域协议族
    #include <sys/un.h>
    struct sockaddr_un
    {
        sa_family_t sin_family;  //  地址族:AF_UNIX
        char sun_path[108];      //  文件地址名
    };
    // 3-2、TCP/IP
    struct sockaddr_in
    {
        sa_family_t sin_family;  // 地址族:AF_INET
        u_int16_t sin_port;      // 端口号,要使用网络字节序
        struct in_addr sin_addr; // IPv4地址结构体
    };
    struct in_addr 
    {
        u_int32_t s_addr;        // IPv4地址
    };
  • // IP地址转换函数
    #include <arpa/inet.h>
    in_addr_t inet_addr(const char *strptr);  // 字符串转网络字节序整数
    int inet_aton(const char *cp, struct in_addr* inp);  // 同上,但错误存储在inp指向的地址
    char * inet_ntoa(struct in_addr in); // 网络字节序整数转换成字符串 --> 此代码不可重入
    // 还有
    int inet_pton(int af, const char* src, void* dst);
    const char* inet_ntop(int af, const char* src, char * dst, socklen_t cnt);
  •  

  • int socket(int domain, int type, int protocol);
    int domain : AF_INET IPv4协议 | AF_INET6 IPv6协议 | AF_LOCAL/AF_UNIX 本地套接字 | AF_PACKET
    int type : SOCK_STREAM 字节流 | SOCK_DGRAM 数据报 | SOCK_RAW 原始套接字
    int protocol : --

connect()函数

  • (1)没有与之相对应的端口发生连接,hardware error,客户端收到RST,返回ECONNREFUSED。

  • (2)发送请求时IP不可达(no route to host),协议ICMP,software error,通常是发送arp请求没有响应。

bind()函数

  • int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    // 发送服务端地址给内核
        // IP地址可以使用通配地址,端口号0表示由内核分配临时端口
        // INADDR_ANY,表示由内核选择IP,可以选择主机任意IP
        // 分配的临时端口,使用getsockname来返回临时端口
    // 通常错误:Address already in use
        // 使用套接字选项来设置:setsockopt | SO_REUSEADDR

listen()函数

  • int listen(int sockfd, int backlog);
    // backlog 的理解
    // 内核会维护两个队列:
    // 1. 半连接队列:客户端发送了SYN,被服务端接受后,就会进入半连接队列。
    // 2. 全连接队列:三次握手完成后,半连接队列的结点将进入全完成队列。

Accept()函数

  • // 从Listen监听队列中获取一个连接,而不关心其状态。
  •  

UDP编程模型

  • (1)客户端:socket() --> sendto() --> recvfrom --> close()

  • (2)服务端:socket() --> bind() --> recvfrom() --> sendto() -->close()

  • (3)服务端通过recvfrom()获知客户端的地址,然后sendto()进行消息交互。

  • (4)客户端首先发送sendto()给服务端,然后通过recvfrom()获取消息。

recvfrom()

  • ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags
                    struct sockaddr *src_addr, socklen_t *addrlen);
    // 从内核中获取 buf数据、源地址和长度

sendto()

  • ssize_t sendto(int sockfd, void *buf, size_t len, int flags
                   struct sockaddr *dest_addr, socklen_t addrlen);
    // 向内核传输 buf数据、目标地址和长度

------------------------------------------------------------------------

多进程并发服务器

知识点

accept后的处理

  • (1)多进程:在接收通信套接字后,使用fork()创建子进程。

  • (2)子进程必须先关闭 监听套接字,因为子进程复制时,套接字引用计数+1,这么做是为了避免套接字无法关闭。

  • (3)父进程也需要关闭 通信套接字

  • (4)创建listen的过程,将其封装,如改—服务端编程模型

  • (5)另外,需要注意子进程先结束时,要避免其成为僵尸进程

一个服务器对多个客户端

  • (1)服务端可以使用多进程的模式,对客户端请求做出响应。

  • (2)进行多次交互时,记得清空buf,以免发生意想不到的错误。

I/O复用—select

  • (1)修改了服务端模型,使用I/O符用来代替多进程模型。SELECT-服务端模型

  • (2)很多细节:

    • (2-1)FD_ZERO(&global_rdfs); // 全局文件描述符置0;重置

    • (2-2)FD_SET(listen_fd, &global_rdfs); // 设定想要关注的文件描述符

    • (2-3)select(max_fd + 1, &current_rdfs, NULL, NULL, NULL) // 注意第一个参数 ==》 最大文件描述符 + 1

    • (2-4)for (int i = 0; i <= max_fd; ++i) // 循环查看是哪个描述符,产生了新的内容,注意大于等于

    • (2-5) if ( FD_ISSET(i, &current_rdfs) ) // 查看是否是第 i 个描述符产生了新内容

  • (3)select 最大描述符数量 --> 1024(2048,看系统),太大容易给客户端造成延时的现象。

    • 用户数量 ≈ 1000

  • (4)缓冲区的数据没有读完,select仍然会有提示有数据产生。

  • (5)poll 与select 的区别在于,poll使用链表来保存fd,可以存的fd更多。

I/O符用—epoll

  • (1)EPOLL-服务端模型,~容错处理没有完善。

  • (2)可以使用的量

    •  

  • (3)epoll编程模型框架

    • // 1. 创建 epoll,MAX_EVENTS 指定 epoll 容纳的文件描述符数量
      epoll_fd = epoll_create(MAX_EVENTS);    
      // 2. 设置 监听描述符为非阻塞
      fcntl(listen_fd, F_SETFL, O_NONBLOCK);
      // 3. 添加 监听描述符 为epoll 关注对象
      epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &tmp_ev);
      // 4. 循环中,获取有相应的文件描述符
      while (1)
      {
          fd_nums = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); 
          // events 事件列表,用于存储触发事件的相关信息
          ...
      }
      // 5. 循环遍历所有文件描述符
      for (int i = 0; i < fd_nums; ++i)
      {
      // 6.获取对应文件描述符信息,并进行操作。
          if (events[i].data.fd == listen_fd)
          {
              // 有客户端连接,添加到epoll中
              tmp_ev.data.fd = sock_fd;  tmp_ev.events = EPOLLIN | EPOLLET; // ET边缘触发
              epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock_fd, &tmp_ev);
          }
          else
          {
              // 传递fd,随后处理数据
              process_data(events[i].data.fd);
              // 若要关闭连接
              // 删除对文件描述符的关注
              epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, &tmp_ev); 
              close(events[i].data.fd);
          }
      }
  • (4)LT(Level Triggered) 与 ET(Edge Triggered)

    • LT :默认模式,select, poll 也采用这种模式,当缓冲还有数据没有读完时,内核会继续通知服务器。

    • ET :高速模式,缓冲有数据时,内核只通知服务器1次,不管缓冲数据是否被读完。

模型小结

  • (1)PPC(process per connection) | TPC (thread per connection)

  • (2)select | poll

  • (3)epoll

I/O小结

  • bio, 阻塞io

  • 多线程,切换开销大

  • nio,非阻塞方式询问io

  • 多路复用,select/poll,循环访问fd,且有限个数,但可以修改,ulimited。

  • epoll,网卡进入信息后,结合事件+中断,DMA等技术,不需要循环遍历,直接返回有响应的fd。

2020/12/28 小结

  • (1)select

    • 1-1、FD上限1024。

    • 1-2、重复初始化:每次调用select(),都需要将fd集合从用户态拷贝到内核态。

    • 1-3、内核遍历集合:逐个排查FD,查看时候有消息产生。

    • 1-4、外面也需要循环遍历:查看是哪个FD产生了消息。

  • (2)poll

    • 2-1、取消FD上限为1024。

  • (3)epoll (仅支持linux)

    • 3-1、取消FD上限为1024

    • 3-2、使用epoll_ctl()注册FD,用户态到内核态只需要1次拷贝。

    • 3-3、epoll_wait只关心“就绪”的文件描述符,而不需要遍历所有FD。

      • 通过设定FD的回调函数,将就绪的FD加入到就绪队列中。

  • (4)select、poll和epoll 小结

    • 4-1、并非选择epoll最好,因为回调函数也需要消耗。

    • 4-2、若FD较少或者(FD很多且活跃),三种都可以用。

    • 4-3、出现较多空连接或者死链接,可以使用epoll。

  • (5)信号驱动I/O

    • 设定信号处理函数,接受成功信号后,通过信号处理函数来完成后续的操作。(工程少用)

      • 不是异步,因为接受数据(即从内核空间到用户空间这段,需要阻塞)

  • (6)异步I/O (linux下,支持两种异步I/O)

    • 6-1、用户态实现的aio_read、aio_write等。

      • 实现原理是,创建一个新的线程用于阻塞等待接受数据。

    • 6-2、内核实现异步I/O接口。

      •  

------------------------------------------------------------------------

附录

客户端编程模型

#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
​
const int MAX_BUFFER_SIZE = 1024;
​
// 处理错误函数
void handdle_error (char * msg)
{
    perror(msg);
    abort();
}
​
int main(int argc, char * argv[])
{
    struct sockaddr_in server_addr;
​
    // 创建套接字文件描述符
    int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (socket_fd < 0) 
        handdle_error(const_cast<char*>("socket failed"));
​
    // 设定服务端IP及端口号
    bzero(&server_addr, sizeof(server_addr)); // memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;  
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    server_addr.sin_port = htons(15522);
​
    // 连接服务端
    if (connect(socket_fd, (struct sockaddr *) &server_addr, sizeof(server_addr)) < 0) // sizeof(server_addr) 的长度是分了区别使用的协议
        handdle_error(const_cast<char*>("connect failed"));
​
    // 读取套接字文件描述符
    char buffer[MAX_BUFFER_SIZE];
    bzero(buffer, sizeof(buffer));    // 若不将buffer置0,输出将出现乱码
    int bytes = read(socket_fd, buffer, MAX_BUFFER_SIZE); // 表示最大读取 MAX_BUFFER_SIZE个字节
    if (bytes < 0) 
        handdle_error(const_cast<char*>("read failed"));
    if (0 == bytes)
        handdle_error(const_cast<char*>("read 0 bytes"));
​
    // 客户端本地显示信息
    printf("Receive bytes: %d\n", bytes);
    printf("Time : %s\n", buffer);
​
    // 关闭套接字文件描述符
    close(socket_fd);
}
​

服务端编程模型

#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <ctime>
​
const int MAX_BUFFER_SIZE = 1024;
const int MAX_LISTEN_QUE = 5;
​
// 错误处理函数
void handdle_error (char * msg)
{
    perror(msg);
    abort();
}
​
int main (int argc, char * argv[])
{
    struct sockaddr_in server_addr, client_addr;
​
    // 监听套接字
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd < 0) 
        handdle_error(const_cast<char*>("Listen_fd failed"));
​
    // 设置端口地址可重用
    int opt;
    if ((setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) < 0) 
        handdle_error(const_cast<char*>("reset socket reuse falied"));
​
    // 设定本机地址
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 若有多个IP地址,均用 
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(15522);
​
    // 绑定当前主机的套接字地址
    if (bind(listen_fd, (struct sockaddr *) &server_addr, sizeof(struct sockaddr)) < 0)
        handdle_error(const_cast<char*>("Bind server failed"));
​
    // 监听
    listen(listen_fd, MAX_LISTEN_QUE);  // 创建监听队列,并赋值长度
    
    char buffer[MAX_BUFFER_SIZE];
    time_t real_time;
    while (1) 
    {
        socklen_t len;
        // 接受端口输入
        int socket_fd = accept(listen_fd, (struct sockaddr*) &client_addr, &len); // 获取客户端信息及结构体长度
        if (socket_fd < 0) 
            handdle_error(const_cast<char*>("accept socket_fd failed"));
​
        // 写入信息
        real_time = time(nullptr);
        snprintf(buffer, sizeof(buffer), "%s", ctime(&real_time));   // 把时间字符串写入缓冲中,然后写入套接字文件描述符
        write(socket_fd, buffer, strlen(buffer)); // 控制写入字符个数
        close(socket_fd);      // 必须关闭,不然占用文件描述符个数
    }
    close(listen_fd);
}

改—服务端编程模型

#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <ctime>
​
const int MAX_QUEUE = 5;
const int MAX_BUFER_SIZE = 100;
​
// 错误处理函数
void handdle_error (char * msg)
{
    perror(msg);
    abort();
}
​
// 建立监听,获取监听文件描述符
int listen_and_get_sockfd()
{
    int listen_fd, opt = 1;
    sockaddr_in server_addr;
​
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(15520);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
​
    if ( (listen_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
        handdle_error(const_cast<char*>("create listen socket error"));
​
    if ( setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) 
        handdle_error(const_cast<char*>("setsockopt error"));
  
    if ( bind(listen_fd, (struct sockaddr *) &server_addr, sizeof(struct sockaddr)) < 0)
        handdle_error(const_cast<char*>("bind error"));
​
    listen(listen_fd, MAX_QUEUE);
​
    return listen_fd;
}
​
int main(int argc, char * argv[])
{
    int listen_fd, sock_fd;
    socklen_t len;
    sockaddr_in client_addr;
    char buf[MAX_BUFER_SIZE];
    time_t real_time;
​
    bzero(buf, sizeof(buf));
​
    listen_fd = listen_and_get_sockfd();
​
    while (1) 
    {
        if ( (sock_fd = accept(listen_fd, (sockaddr *)&client_addr, &len)) < 0) 
            handdle_error( const_cast<char*>("create socket error") );
        
        real_time = time(nullptr);
        snprintf(buf, sizeof(buf), "%s", ctime(&real_time));
        write(sock_fd, buf, strlen(buf));
        close(sock_fd);
    }
​
    close(listen_fd);
}

SELECT-服务端模型

#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <ctime>
​
const short PORT = 15521;
const int MAX_LISTEN_QUEUE = 5;
const int MAX_BUFFER_SIZE = 1024;
​
void handdle_error(char * msg)
{
    perror(msg);
    abort();
}
​
int listen_and_get_sockfd()
{
    int listen_fd, opt = 1;
    sockaddr_in server_addr;
​
    socklen_t len = sizeof(sockaddr_in);
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
​
    if ( ( listen_fd = socket(AF_INET, SOCK_STREAM, 0) ) < 0 )
    {
        handdle_error(const_cast<char*> ("create listen_fd falied"));
    }
​
    if ( setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) 
    {
        handdle_error(const_cast<char*>("setsockopt error"));
    }
   
    if ( bind(listen_fd, (sockaddr*) &server_addr, len) < 0 ) 
    {
        handdle_error(const_cast<char*> ("bind failed"));
    }
​
    if ( listen(listen_fd, MAX_LISTEN_QUEUE) < 0)
    {
        handdle_error(const_cast<char*> ("listen failed"));
    }
​
    return listen_fd;
}
​
int main()
{
    int sock_fd, max_fd = 0;
    sockaddr_in client;
    socklen_t len = sizeof(sockaddr_in);
    char buf[MAX_BUFFER_SIZE];
    fd_set global_rdfs, current_rdfs;
​
    bzero(&client, sizeof(client));
​
    int listen_fd = listen_and_get_sockfd();
​
    FD_ZERO(&global_rdfs);   // 全局文件描述符置0;重置
    FD_SET(listen_fd, &global_rdfs);    //  设定想要关注的文件描述符
    max_fd = max_fd > listen_fd? max_fd : listen_fd;  //  记录最大的文件描述符的数量
​
    while (1)
    {
        current_rdfs = global_rdfs;  // 重置
        if( select(max_fd + 1, &current_rdfs, NULL, NULL, NULL) < 0)   
        {
            // 注意第一个参数 ==》 最大文件描述符 + 1 ,易错点
            // 阻塞监听max_fd个文件描述符,与recv阻塞监听的效果类似,不同在于这里监听的对象不止一个
            // 后面三个NULL,对应  写文件描述符集,异常文件描述符集,阻塞
            handdle_error(const_cast<char*> ("select failed"));
        }
​
        for (int i = 0; i <= max_fd; ++i)   // 循环查看是哪个描述符,产生了新的内容
        {
            if ( FD_ISSET(i, &current_rdfs) ) // 查看是否是第 i 个描述符产生了新内容
            {
                if (i == listen_fd) // 如果是描述符是 监听描述符
                {
                    if( (sock_fd = accept(listen_fd, (sockaddr*) &client, &len)) < 0)
                    {
                        handdle_error(const_cast<char*> ("accpet failed"));
                    }
                    FD_CLR(i, &current_rdfs);   // 清除这部分产生的内容信息,防止下次信号。
                    max_fd = max_fd > sock_fd? max_fd : sock_fd;   // 查看文件描述符是否增加
                    FD_SET(sock_fd, &global_rdfs);  //  加入到关注文件描述列表
​
                    printf("client in : %d ", sock_fd);
                }
                else  // 其它文件描述符
                {
                    // printf("read socket : %s", i )
​
                    bzero(buf, sizeof(buf));
​
                    int bytes = recv(i, buf, MAX_BUFFER_SIZE, 0);
​
                    if (bytes < 0) 
                    {
                        handdle_error(const_cast<char*> ("recv failed"));
                    }
​
                    if (bytes == 0) // 接受FIN信号
                    {
                        FD_CLR(i, &global_rdfs);   // 取消关注该文件描述符号
                        close(i);
                        continue;
                    }
​
                    printf("buf : %s\n", buf);
                    send(i, buf, strlen(buf), 0);
                }
            }
        }
    }
}
​
/*
    BUG 日志: 
    1. 轮询fd的时候,由于 < max_fd,导致一直没有结果出现,加上<=后,正常工作。
*/

EPOLL-服务端模型

#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <ctime>
#include <sys/epoll.h>
#include <fcntl.h>
​
const short PORT = 15521;
const int MAX_LISTEN_QUEUE = 5;
const int MAX_BUFFER_SIZE = 1024;
const int MAX_EVENTS = 100;
​
const int ERR = -1;
const int EXIT = -2;
const int OK = 0;
​
void handdle_error(char * msg)
{
    perror(msg);
    abort();
}
​
int listen_and_get_sockfd()
{
    int listen_fd, opt = 1;
    sockaddr_in server_addr;
​
    socklen_t len = sizeof(sockaddr_in);
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
​
    if ( ( listen_fd = socket(AF_INET, SOCK_STREAM, 0) ) < 0 )
    {
        handdle_error(const_cast<char*> ("create listen_fd falied"));
    }
​
    if ( setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) 
    {
        handdle_error(const_cast<char*>("setsockopt error"));
    }
   
    if ( bind(listen_fd, (sockaddr*) &server_addr, len) < 0 ) 
    {
        handdle_error(const_cast<char*> ("bind failed"));
    }
​
    if ( listen(listen_fd, MAX_LISTEN_QUEUE) < 0)
    {
        handdle_error(const_cast<char*> ("listen failed"));
    }
​
    return listen_fd;
}
​
int process_data(int fd)
{
    char buf[MAX_BUFFER_SIZE];
    int bytes;
​
    bytes = recv(fd, buf, MAX_BUFFER_SIZE, 0);
​
    if (bytes < 0)
    {   
        perror("recv error");
        return ERR;
    }
​
    if (bytes == 0)
    {
        return EXIT;
    }   
​
    printf("buf : %s", buf);
    send(fd, buf, bytes, 0);
    return OK;
}
​
int main()
{
​
    int sock_fd, fds, tmp_return_value;
    sockaddr_in client;
    socklen_t len = sizeof(sockaddr_in);
​
    struct epoll_event tmp_ev, events[MAX_EVENTS];    // 创建时间的临时对象
​
    bzero(&client, sizeof(client));
​
    int epoll_fd = epoll_create(MAX_EVENTS);    // 创建 epoll,指定epoll容纳的文件描述符数量
    if (epoll_fd < 0) 
    {
        handdle_error(const_cast<char*>("create epoll error"));
    }
​
    int listen_fd = listen_and_get_sockfd();
​
    fcntl(listen_fd, F_SETFL, O_NONBLOCK);  // 设置监听文件描述为 非阻塞模式
​
    tmp_ev.data.fd = listen_fd;  //  设置文件描述符
    tmp_ev.events = EPOLLIN;  //  设置事件的类型 EPOLLIN 表示有数据进来,可以读取
​
    tmp_return_value = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &tmp_ev);  // 将文件描述符及其对应的事件,添加到epoll中,予以关注
    if (tmp_return_value < 0)
    {
        handdle_error(const_cast<char*>("control epoll error"));
    }
​
    while (1)
    {
        // 检测超时  time_out 
        fds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);  // 返回有相应的文件描述符
        // 表示事件,-1为永远等待,
        if (fds < 0) 
        {
            handdle_error(const_cast<char*> ("epoll wait error"));
        }
​
        for (int i = 0; i < fds; ++i)
        {
            if (events[i].data.fd == listen_fd)
            {
                sock_fd = accept(listen_fd, (sockaddr*) &client, &len);
                if (sock_fd < 0) 
                {
                    handdle_error(const_cast<char*> ("accept error"));
                }
                tmp_ev.data.fd = sock_fd;
                tmp_ev.events = EPOLLIN | EPOLLET;  // Edge trigger 边缘触发模式
                epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock_fd, &tmp_ev);   // 添加到关注列表中
            }
            else 
            {
                tmp_return_value = process_data(events[i].data.fd);  // 处理数据
                if (tmp_return_value == EXIT)
                {
                    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, &tmp_ev);  // 删除对文件描述符的关注
                    close(events[i].data.fd);   // 关闭文件描述符
                }
            }
        }
    }
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值