C/C++ Linux网络编程

第十一章 网络基础

1. 网卡、MAC地址、IP地址

网卡:网络适配器,用于收发数据

1)MAC地址(6个字节48位)

网卡物理地址,是唯一的、不变的,标识网卡的id,使用ifconfig命令后如下图,一般用来找到主机

C/C++ Linux网络编程_第1张图片

2)IP地址

IP地址是标识主机的id,是虚拟的,会改变。ipv4有32位(局域网),ipv6有128位(公网)。

一个IP将其分为子网id(网段)和主机id,需要和子网掩码一起来看,被子网掩码中连续的1覆盖的是子网id,被连续的0覆盖的是主机id

  • 10.1.1.2和255.255.255.0

C/C++ Linux网络编程_第2张图片

3)lo本地回环

ping命令用于测试两台主机的连通性

Linux设置IP:ifconfig [网卡名称] [IP地址] netmask [子网掩码]

C/C++ Linux网络编程_第3张图片

功能:测试本机网络配置,能ping通127.0.0.1说明本机网卡和IP协议安装无误

注意:127.0.0.1~127.255.255.254中的任何地址都将回环到本地主机中

4)IP地址分类

A类:默认8bit子网ID,第一位为0(广域网)

B类:默认16bit子网ID,前两位为10(城域网)

C类:默认24bit子网ID,前三位为110(局域网)

D类:前四位为1110,多播地址

E类:前五位为11110,保留位今后使用(A、B、C类最常用)

5)其他

桥接模式:直接连接物理网络,会给虚拟机分配IP

NAT模式:共享主机的IP,使用虚拟网络,只与主机通信

2. 端口

标识某应用程序(进程)的缓冲区,在主机间通信时会使用,每次启动进程时端口号是不会变的,一个进程可能有多个端口

port:两个字节0~65535(0~1023 知名端口,减少使用)

FTP:21,HTTP:80等

3. OSI七层模型(Open System Internet)、TCP/IP四层模型

C/C++ Linux网络编程_第4张图片

1)“物数网传会表应”

物理层:双绞线(网线)接口类型,光纤的传输速率等

数据链路层:mac负责收发数据

网络层:ip给两台主机提供连接和路径选择

传输层:port区分数据递送到哪一个应用程序(进程)

会话层:建立连接

表示层:解码

应用层:应用程序获取数据

2)在实际开发中,使用TCP/IP四层模型

网络接口层:也叫链路层,对应“物数”

网络层:对应“网”

传输层:对应“传”

应用层:对应“会表应”

C/C++ Linux网络编程_第5张图片

4. 协议(了解)

规定数据传输的方式和格式

应用层:FTP文件传输协议、HTTP超文本传输协议、NFS网络文件系统

传输层:TCP传输控制协议(丢包重传)、UDP用户数据报协议(高效但不重传)

网络层:IP因特网互联协议、ICMP因特网控制报文协议(ping)、IGMP因特网组管理协议

链路层:ARP地址解析协议(找mac地址)、RARP反向地址解析协议(通过mac找ip)

对应的一些单词:File Transfer Protocol、Hyper Text Transfer Protocol、Network File System;Transmission Control Protocol、User Datagram Protocol;Internet Protocol、Internet Control Message Protocol、Internet Group Management Protocol;Address Resolution Protocol、Reverse Address Resolution Protocol

1)UDP报头(8字节)

C/C++ Linux网络编程_第6张图片

2)TCP报头(20字节)

C/C++ Linux网络编程_第7张图片

SYN:同步序列号标志位,tcp三次握手中,第一次会将SYN=1,ACK=0,此时表示这是一个连接请求报文段,对方会将SYN=1,ACK=1,表示同意连接,连接完成之后将SYN=0。

FIN:在tcp四次挥手时第一次将FIN=1,表示此报文段的发送方数据已经发送完毕,这是一个释放链接的标志。

ACK:当ACK=1时,我们的确认序列号ack才有效,当ACK=0时,确认序号ack无效,TCP规定:所有建立连接的ACK必须全部置为1。

序号(seq):占 32位4 个字节,序号范围[0,2^32-1],序号增加到 2^32-1 后,下个序号又回到 0。TCP 是面向字节流的,通过 TCP 传送的字节流中的每个字节都按顺序编号,而报头中的序号字段值则指的是本报文段数据的第一个字节的序号。例如:我们的seq = 201,携带的数据有100,那么最后一个字节的序号就为300,那么下一个报文段就应该从301开始。

确认号(ack):占 32位4 个字节,期望收到对方下个报文段的第一个数据字节的序号。当标志位ACK值为1时,才能产生有效的确认号ack。并且:ack=seq+1。

RST:当RST=1时,表明TCP连接出现严重错误,此时必须释放连接,之后重新连接,又叫重置位。

URG:紧急指针标志位,当URG=1时,表明紧急指针字段有效。它告诉系统中有紧急数据,应当尽快传送,这时不会按照原来的排队序列来传送。而会将紧急数据插入到本报文段数据的最前面。

PSH:推送操作,提示接收端应用程序立即从TCP缓冲区把数据读走。

3)IP报头(20字节)

C/C++ Linux网络编程_第8张图片

TTL:数据生存时间,一般是64或128,经过一个路由器就会减1,防止网络阻塞

C/C++ Linux网络编程_第9张图片

C/C++ Linux网络编程_第10张图片

4)MAC头部(链路层)(14个字节)

C/C++ Linux网络编程_第11张图片

5. ARP通信

ARP地址解析协议:通过IP找MACA

C/C++ Linux网络编程_第12张图片

ARP请求包:

C/C++ Linux网络编程_第13张图片

C/C++ Linux网络编程_第14张图片

4c359956b2cdb50254b305e7c010d4da.png

6. 网络设计模式

1)B/S模式

性能较低,服务器做计算,客户端安全,开发周期短

2)C/S模式

性能较好,本地做计算,但客户端容易篡改数据,开发周期长

第十二章 Socket和TCP

1. 进程间通信的回顾

无名管道、有名管道、mmap映射、文件、信号、消息队列、进程间共享内存,这些只能用于本机的进程间通信。

而socket解决的是不同主机进程间通信问题。

2. socket套接字

socket总是成对出现(pair),是一个伪文件

C/C++ Linux网络编程_第15张图片

3. 字节序

1)大小端

小端:低位存低地址,高位存高地址

大端:低位存高地址,高位存低地址

C/C++ Linux网络编程_第16张图片

(图中的L、H是地址的高低)

C/C++ Linux网络编程_第17张图片

单字节的数据不用转换

4. 字节序转换函数

C/C++ Linux网络编程_第18张图片

C/C++ Linux网络编程_第19张图片

转换的时候注意长度,比如IPv4是4字节,端口号是2字节

C/C++ Linux网络编程_第20张图片

5. 点分十进制串转换(IP地址的转换)

09683bb4a91193fb3f2986b7e051f770.png

支持IPv4和IPv6

1)inet_pton()函数

将点分十进制串 转成32位网络大端的数据

参数:

        af:AF_INET IPv4

        AF_INET6 IPv6

        src:点分十进制串的首地址

        dst:32位网络数据的地址

返回值:1成功

2)inet_ntop()函数

将32位网络大端数据转成十进制串

参数:

        af:AF_INET

        src:32位大端网络数据的地址

        dst:存储点分十进制串的地址

        size:字符串大小(INET_ADDSRTLEN宏的值16用于IPv4)

返回值:存储点分十进制串的首地址

C/C++ Linux网络编程_第21张图片

6. IPv4结构体、通用套接字结构体

协议、端口、IP(使用man 7 ip查看)

C/C++ Linux网络编程_第22张图片

        sin_family:协议 AF_INET

        sin_port:端口

        sin_addr:IP地址

在使用send函数时,由于IPv4和IPv6结构体不同,需要使用到通用套接字结构体

C/C++ Linux网络编程_第23张图片

C/C++ Linux网络编程_第24张图片

7. TCP通信

Transmission Control Protocol传输控制协议

特点:出错重传,每次发送数据对方都会回AKC(确认包),可靠

C/C++ Linux网络编程_第25张图片

1)创建套接字API(使用man socket 2查看)

C/C++ Linux网络编程_第26张图片

功能:创建套接字

参数:

        domain:协议,AF_INET

        type:套接字通信类型(原始套接字通信,UDP,TCP),SOCK_STREAM流式套接字(TCP),SOCK_DGRAM(UDP),SOCK_RAW(原始,要组深层的包)

        protocol:0

返回值:成功返回文件描述符,失败返回-1

2)连接服务器(使用man connect 2查看)

C/C++ Linux网络编程_第27张图片

功能:连接服务器

参数:

        sockfd:套接字文件描述符

        addr:通用套接字结构体(IPv4和IPv6需要类型转换)

        addrlen:套接字结构体长度

返回值:0成功,-1失败

C/C++ Linux网络编程_第28张图片

8. TCP服务器通信

1)服务器和客户端通信流程

C/C++ Linux网络编程_第29张图片

  1. 创建套接字
  2. 给套接字绑定,bind的内容有IP地址、端口号(固定的)
  3. 监听,①将套接字由主动变被动(变为监听套接字);②创建连接队列(三次握手后放到已完成连接队列)

C/C++ Linux网络编程_第30张图片

  1. 提取连接Accept(),从已完成连接队列提取连接得到一个新的已连接套接字,后面用此套接字和客户端通信
  2. 读写
  3. 关闭

2)bind()函数(使用man bind 2查看)

C/C++ Linux网络编程_第31张图片

参数:

        sockfd:套接字文件描述符

        addr:通用套接字结构体地址(注意转型)

        addrlen:套接字大小

返回值:成功返回0,失败返回-1

3)listen()函数

C/C++ Linux网络编程_第32张图片

参数:

        sockfd:套接字文件描述符

        backlog:Max(已完成连接队列容量+未完成连接队列容量),此处可填128

4)accept()函数

C/C++ Linux网络编程_第33张图片

功能:从已完成连接队列提取新的连接

参数:

        socket:套接字

        address:获取客户端的IP信息和端口信息,通用套接字结构体

        address_len:客户端的结构体的大小,可以声明一个socklen_t类型变量来容纳

返回值:成功返回新的已连接套接字的文件描述符

C/C++ Linux网络编程_第34张图片

9. 粘包及其处理方法

服务器在客户端读取前,发送了两次数据到客户端的缓冲区,导致客户端无法区分。

解决方案:

  1. 报头+数据:报头可以用四个字节长度:例如:0010(字节)字符串转成整型长度,四个字节长度+数据部分。(内容也有细则,例如每一部分有多长,代表什么数据)
  2. 添加结尾标记。(不建议用,效率低)
  3. 发送定长数据:数据包定长。(数据量不一样)

10. 包裹函数

详见下面一篇文章

socket编程中函数封装的思想,异常处理和wrapSocket.c-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/m0_75034791/article/details/135266762?spm=1001.2014.3001.5501

11. TCP通信时序(三次握手、四次挥手)

1)三次握手

简单说,三次握手就是建立连接的过程,而四次挥手就是断开连接的过程。

先回看下TCP协议报头:

C/C++ Linux网络编程_第35张图片

SYN:请求报文段

ACK:确认报文段

序列号:图中seq

确认序列号(图中ack)的含义:1. 确认收到对方的报文;2. 期望下一次对方的序列号为我的确认序列号

确认序列号 = 对方发过来的序列号 + 标志位长度SYN(1) +数据长度

问题1:如果客户端发送SYN后,客户端不反回ACK?

未完成连接队列会持续被占用,如果占满,那么服务器就再收不到请求了(这也是早期SYN攻击的原理)。

C/C++ Linux网络编程_第36张图片

问题2:为什么不是两次握手?

因为SYN为1的报文段不能携带数据,所以在前两次握手后,目前服务器只知道客户端可以发送、自己可以收发,而不知道客户端是否能正常接收,所以需要第三次握手。

详细而言:

C/C++ Linux网络编程_第37张图片

初始时:客户端处于Closed状态,服务器处于Listen状态;

第一次握手:客户端发送SYN报文给服务器,初始序列号为x(seq=x), 此时客户端进入SYN_SENT状态;客户端可以知道自己发送正常,服务器可以知道自己接收正常,客户端发送正常。

第二次握手:服务器通过自己的SYN报文给与客户端确认和响应,服务器进入SYN_RECV状态;客户端可以知道服务器收发正常,自己收发正常;服务器知道自己收发正常,但不知道客户端接收正常,因此需要第三次握手。服务器发送报文的四个参数具体含义如下:

SYN=1,表示为连接请求报文,也不能携带数据;

seq=y,服务端的序列号为y;

ACK=1,表示确认客户端序列号有效,此时确认号(ack)有值;

ack=seq+1:ack的值为客户端传来的序列号(seq)加1,即ack=x+1;

第三次握手:客户端收到服务器的SYN+ACK的包,此时客户端处于ESTABLISHED(已确认)状态,表示客户端和服务器都表示同意连接,因此客户端发送一个ACK报文,ack仍为序列号+1,即y+1,初始序列号为x,因此客户端发送的第二次报文,序列号要+1,即x+1;这时服务器可以确认客户端收发正常;第三次握手可以携带数据。

1e80e2b919c7646dc97e2660607eb9f2.png

2)四次挥手

C/C++ Linux网络编程_第38张图片

与三次握手不同的是,断开连接的报文由客户端或服务器都是可能的。(上图以客户端请求断开为例)

close()关闭的是应用层发讯,而其他层还可以收发。

问题1:客户端在发送ACK后等待2MSL才会关闭所有层?

主动请求断开的一方会等待2MSL。

在第三次挥手之后,服务器收到客户端ACK才会关闭,而此时客户端会等待两倍的最大报文生存时长(2*MSL),以等待服务器是否会发送FIN报文(再次发FIN一般是没收到客户端ACK)。

问题2:为什么挥手比握手多一次?

握手时没有数据传输,但挥手时有数据传输,所以SYN和ACK可以一起发送,而FIN和ACK不能。

问题3:为什么三次挥手不行?

因为在服务器接收到FIN后,会等到服务器所有报文发完了才会发送FIN,所以会先发一个ACK,让客户端知道服务区接受了它的FIN报文,等其他报文发完,服务器才会发送FIN给客户端(第四次挥手)。

3)半连接队列、全连接队列

完成第一次和第二次握手之后,将socket放到半连接队列;

完成三次握手之后,socket会从半连接队列移动到全连接队列;在调用accept()之后,会创建新的socket来通信。

4)半双工、全双工

全双工:客户端在给服务器发送消息的同时,服务器也可以给客户端发送消息。

半双工:可以互相发,但不能同时发。

12. 滑动窗口

滑动窗口(Sliding window)是一种流量控制技术。即接收方会告诉发送方在某一时刻能送多少包(win窗口尺寸),当滑动窗口为0时,发送方一般不能再发送数据。

C/C++ Linux网络编程_第39张图片

注意:通信双方都有发送缓冲区和接收缓冲区

13. 多进程实现并发服务器

父进程fork之后,只保留父进程的监听套接字,只保留每个子进程中的已连接套接字。回收子进程资源的时候将涉及到SIGCHLD。

C/C++ Linux网络编程_第40张图片

#include 
#include 
#include 
#include "wrapSocket.h"
#include 
#include 

void free_process(int sig)
{
    pid_t pid;
    while(1)
    {
        waitpid(-1, NULL, WNOHANG);
        if(pid <= 0)//没有等待的子进程或者没有进程退出(没有要回收的)
        {
            break;
        }
        else
        {
            printf("child pid = %d\n",pid);
        }
    }
}

int main ()
{
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set,SIGCHLD);
    sigprocmask(SIG_BLOCK,&set,NULL);
    //创建套接字,绑定
    int lfd = tcp4bind(8080,NULL);
    //监听
    Listen(lfd,128);
    //提取
    //回射
    struct sockaddr_in cliaddr;
    socklen_t len = sizeof(cliaddr);
    while(1)
    {   
        char ip[16]="";
        //提取连接
        int cfd = Accept(lfd,(struct sockaddr *)&cliaddr,&len);
        printf("new client ip=%s port=%d\n",
        inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,ip,16),
        ntohs(cliaddr.sin_port));
        //创建子进程
        pid_t pid;
        pid = fork();
        if(pid < 0)
        {
            perror("");
            return 0;
        }
        else if (pid == 0)//子进程
        {
            //关闭lfd
            close(lfd);
            while (1)
            {
                char buf[1024] = "";
                int n = read(cfd,buf,sizeof(buf));
                if(n < 0)
                {
                    perror("");
                    close(cfd);
                    exit(0);
                }
                else if (n == 0)//对方关闭
                {
                    printf("client close\n");
                    close(cfd);
                    exit(0);
                }
                else
                {
                    printf("%s\n",buf);
                    write(cfd,buf,n);
                }
            }
        }
        else//父进程
        {
            close(cfd);
            //回收子进程资源,注册SIGCHLD信号的回调
            struct sigaction act;
            act.sa_flags = 0;
            act.sa_handler = free_process;
            sigemptyset(&act.sa_mask);
            sigaction(SIGCHLD, &act, NULL);
            sigprocmask(SIG_UNBLOCK,&set,NULL);
        }    
    }
    //关闭
    return 0;
}

14. 多线程实现并发服务器

#include 
#include 
#include "wrapSocket.h"

typedef struct client_info
{
    int cfd;
    struct sockaddr_in client_addr;
}CINFO;

void* client_func(void *arg);

int main(int argc, char *argv[])
{
    if(argc < 2)
    {
        printf("argc < 2???\n ./a.out 8080 \n");
        return 0;
    }
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
    short port = atoi(argv[1]);
    int lfd = tcp4bind(port,NULL);//创建套接字、绑定
    Listen(lfd,128);
    struct sockaddr_in cliaddr;
    socklen_t len = sizeof(cliaddr);
    CINFO *info;
    while(1)
    {
        int cfd = Accept(lfd,(struct sockaddr *)&cliaddr,&len);
        pthread_t tid;
        info = malloc(sizeof(CINFO));
        info->cfd = cfd;
        info->client_addr = cliaddr;
        pthread_create(&tid,&attr,client_func,info);
    }
    return 0;
}

void* client_func(void *arg)
{
    CINFO *info = (CINFO *)arg;
    char ip[16] = "";
    printf("new client ip=%s port=%d\n",
            inet_ntop(AF_INET,&(info->client_addr.sin_addr.s_addr),ip,16),
            ntohs(info->client_addr.sin_port));
    while(1)
    {
        char buf[1024] = "";
        int count = 0;
        count = read(info->cfd,buf,sizeof(buf));
        if(count < 0)
        {
            perror("");
            break;
        }
        else if (count == 0)
        {
            printf("client close\n");
            break;
        }
        else
        {
            printf("%s\n",buf);
            write(info->cfd,buf,count);
        }
    }
    close(info->cfd);
    free(info);
}

在主线程提取已连接套接字cfd时,要注意用于存放client信息的结构体存放的位置,因为每个子线程创建需要时间,而他们的栈区又是不共享的,为了不让主线程在子线程创建前就拷贝其中的信息(可能会覆盖原来的),所以要把这个结构体存放在堆区,并且记得最后手动释放。

第十三章 TCP状态转移和IO多路复用

1. TCP状态转换图、netstat命令

C/C++ Linux网络编程_第41张图片

netstat命令的使用

netstat -a		#列出所有端口
netstat -at		#列出所有的TCP端口
netstat -au		#列出所有的UDP端口

netstat -l		#只显示监听端口
netstat -s		#显示所有端口的统计信息

netstat -p		#显示PID和进程名称
netstat -n		#直接使用IP地址,而不通过域名服务器

2. 半关闭、2MSL

1)半关闭

在FIN_WAIT2时,主动方应用层写端关闭。

在请求断开连接时,系统会进行相关的处理,但是如果要自己实现,则需要调用函数:

C/C++ Linux网络编程_第42张图片

2)2MSL

2MSL(Maximum Segment Lifetime)TIME_WAIT状态。通常为几分钟

让4次握手的关闭流程更加可靠,在主动方发送FIN报文并收到被动方ACK报文和FIN报文并发送ACK后(即第四次握手后),会进入TIME_WAIT状态,等待2MSL。此举是为了确保被动方收到了最后一次ACK报文(没收到的话被动方会重复发送FIN报文)。保持2MSL是确保最后一次的ACK能最大可能被接收到

3. 心跳包

场景:如果对方异常断开,本机检测不到,一直等待,浪费资源

需要设置TCP的保持连接,作用就是每隔一段时间发送探测分节,如果连续发送多个探测分节对方还未回,就将此连接断开。

心跳包:最小粒度(携带数据要少)

乒乓包:携带比较多数据的心跳包

1)设置套接字选项函数setsockopt()

功能:设置与某个套接字关联的选项

参数:

        sock:将要被设置或获取选项的套接字

        level:选项所在的协议层,SOL_SOCKET(通用套接字选项)等

        optname:需要访问的选项名

C/C++ Linux网络编程_第43张图片

        optval:对于getsockopt(),指向返回选项值的缓冲。对于setsockopt(),指向包含新选项值得缓冲。

        optlen:对于get方法,作为入口参数时,选项值的最大长度。作为出口参数时,选项值得实际长度。

返回值:

2)设置心跳包(有更高需求一般需要自己编写)

SO_KEEPALIVE保持连接检测对方主机是否崩溃,避免阻塞。设置该选项后,如果2个小时内在此套接字接口的任一方向都没有数据交换,TCP就自动给对方发一个保持存活探测分节(keepalive probe)。正常:ACK响应;对方已崩溃且已重新启动:RST响应

4. 端口复用

端口重新启用,也是调用setsockopt()函数(1启用,0禁用)。

在上方写的wrapSocket.c中的tcp4bind()函数中也封装了这两行。

C/C++ Linux网络编程_第44张图片

5. 多路IO转接技术(高并发服务器)

1)一些实现高并发的方法

①阻塞等待:

比如一个进程服务一个客户端,客户端不发数据的时候就阻塞等待,很浪费资源。

C/C++ Linux网络编程_第45张图片

②非阻塞忙轮询:

消耗CPU,一个进程负责所有,包括监听和收发和多个客户端之间的数据。

③多路IO转接(多路IO复用):
通过内核,poll、epoll、select三种多路IO转接技术,监听文件描述符中的读写缓冲区属性变化。

(epoll linux,windows select跨平台,poll较少用)

6. selectAPI

1)存放在PCB块的文件描述符表

C/C++ Linux网络编程_第46张图片

2)select函数

C/C++ Linux网络编程_第47张图片

功能:监听多个文件描述符的属性变化(读、写、异常)

参数:

        nfds:最大文件描述符+1

        readfds:需要监听的读的文件描述符集合

        writefds:需要监听的写的文件描述符的集合 NULL

        exceptfds:需要监听的异常的文件描述符的集合 NULL

        timeout:多长时间监听一次,固定的时间,限时等待,NULL永久监听

C/C++ Linux网络编程_第48张图片

返回值:返回变化的文件描述符的个数

注意:变化的文件描述符会存在监听的集合中,为变化的文件描述符会被删除。

7. select的代码实现、优缺点

1)代码实现

#include 
#include 
#include 
#include 
#include "wrapSocket.h"
#include 

#define PORT 8080

int main(int argc, char *argv[])
{
    //创建套接字,绑定
    int lfd = tcp4bind(PORT,NULL);
    //监听
    Listen(lfd,128);
    int maxfd = lfd;//最大文件描述符
    fd_set oldset, rset;
    FD_ZERO(&oldset);
    FD_ZERO(&rset);
    //将lfd添加到oldset集合中
    FD_SET(lfd,&oldset);
    while(1)
    {
        rset = oldset;//将oldset赋值给需要监听的rset
        int n = select(maxfd+1,&rset,NULL,NULL,NULL);
        if(n < 0)
        {
            perror("");
            break;
        }
        else if (n == 0)
        {
            continue;//没有变化,返回重新监听
        }
        else//监听到了文件描述符的变化
        {
            //lfd变化:有新连接
            if(FD_ISSET(lfd,&rset))
            {
                struct sockaddr_in cliaddr;
                socklen_t len = sizeof(cliaddr);
                char ip[16] = "";
                //提取新的连接
                int cfd = Accept(lfd,(struct sockaddr *)&cliaddr,&len);
                printf("new client ip=%s port=%d\n",
                    inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,ip,16),
                    ntohs(cliaddr.sin_port));
                //将cfd添加至oldset集合中,以用来下次监听
                FD_SET(cfd,&oldset);
                //更新maxfd
                if(cfd > maxfd)
                    maxfd = cfd;
                //如果只有lfd变化,则continue
                if(--n == 0)
                    continue;
            }
            //cfd变化:有数据收发(遍历lfd之后的文件描述符是否在rset中)
            for (int i = lfd+1; i <= maxfd; i++)
            {
                if(FD_ISSET(i,&rset))
                {   
                    char buf[1500] = "";
                    int ret = Read(i,buf,sizeof(buf));
                    if(ret < 0)//出错,将cfd关闭,从oldset中删除cfd
                    {
                        perror("");
                        Close(i);
                        FD_CLR(i,&oldset);
                    }
                    else if(ret == 0)
                    {
                        printf("client close\n");
                        Close(i);
                        FD_CLR(i,&oldset);
                    }
                    else
                    {
                        printf("%s\n",buf);
                        Write(i,buf,ret);
                    }
                }
            }
        }
    } 
    return 0;
}

2)优缺点、可能的问题

优点:跨平台

缺点:

①文件描述符1024的限制(FD_SETSIZE限制)

②只返回变化的文件描述符的个数,具体是哪个需要遍历

③每次都需要将需要监听的文件描述符,从应用层拷贝到内核

④大量并发,少量活跃问题(问题2)

问题1:假设现在4-1023个文件描述符需要监听,但是5-1000这些文件描述符关闭了?--①要监听的添加进自定义数组,②使用dup2()重定向文件描述符

问题2:假设现在4-1023个文件描述符需要监听,但是只有5,1002发来消息?--无解

8. poll

优点:①相对于select没有最大文件描述符的限制②请求和返回分离

缺点:①每次都需要将需要监听的文件描述符拷贝到内核,②每次都需要将数组中的元素遍历一边才知道哪一个变化了,③大量并发,少量活跃会有效率低的问题(如同select)

功能:监听多个文件描述符的属性变化

参数:

        fds:监听的数组的首地址

        nfds:数组中有效元素的最大下标+1

        timout:超时时间,-1永久监听

数组元素:

C/C++ Linux网络编程_第49张图片

9. epollAPI

1)epoll工作流程:

  1. 创建一棵红黑树(在应用层不需要关注)
  2. 将需要监听的文件描述符上树
  3. 监听

特点:①没有文件描述符1024的限制;②以后每次监听不需要将此需要监听的文件描述符拷贝到内核;③返回的是已经变化的文件描述符,不需要遍历

2)epollAPI

i)创建红黑树

参数:

        size:监听的文件描述符上限,在2.6版本后写1即可

返回值:树的句柄(类似于文件描述符)

ii)上树、下树、修改节点

参数:

        epfd:树的句柄

        op:ADD上树,DEL下树,MOD修改

C/C++ Linux网络编程_第50张图片

               fd:上树、下树、修改节点的文件描述符

        event:上树的节点

C/C++ Linux网络编程_第51张图片

例子:

C/C++ Linux网络编程_第52张图片

iii)监听epoll_wait()

功能:监听树上文件描述符的变化

参数:

        epfd:树的句柄

        events:接收变化的节点的首地址(一般用结构体数组接回来)

        maxevents:数组元素的个数

        timeout:超时时间,-1永久监听

10. epoll的水平触发和边缘触发

水平触发LT:只要读缓冲区有数据就会触发epoll_wait

边缘触发ET:数据来一次,epoll_wait只触发一次

2)监听写缓冲区

水平触发:只要可以写,就会触发

边缘触发:数据从有到无,就会触发

3)添加边缘触发

epoll_wait是一个系统调用,要尽可能减少使用(使用边缘触发)。通过修改上树前创建的结构体,加上“ | EPOLLET”。

struct epoll_event ev,evs[1024];
ev.data.fd = cfd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);

C/C++ Linux网络编程_第53张图片

可是当入输入的内容超出每次读取的长度,会堆积在读缓冲区,则需要循环读取。而在循环读取到没有内容可读时,read()又会被阻塞,此时就无法返回去监听其他套接字。为了避免上述现象,又需要设置已连接套接字的属性,如下:

//设置cfd为非阻塞
int flag = fcntl(cfd, F_GETFL);//获取cfd的标签位
flags |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flags);

最后,上一部分的代码就成了这样,这种“边缘触发 + 非阻塞”也叫做“高速模式”:

#include 
#include 
#include 
#include "wrapSocket.h"

int main(int argc, char *argv[])
{
    // 创建套接字
    int lfd = tcp4bind(8080, NULL);
    // 监听
    Listen(lfd, 128);
    // 创建树
    int epfd = epoll_create(1);
    // 将lfd上树
    struct epoll_event ev, evs[1024];
    ev.data.fd = lfd;
    ev.events = EPOLLIN;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
    // while监听
    while (1)
    {
        int nready = epoll_wait(epfd, evs, 1024, -1);
        if (nready < 0)
        {
            perror("");
            break;
        }
        else if (nready == 0)
        {
            continue;
        }
        else // 有文件描述符变化
        {
            for (int i = 0; i < nready; i++)
            {
                // 判断lfd变化,且是读事件变化
                if (evs[i].data.fd == lfd && evs[i].events & EPOLLIN)
                {
                    struct sockaddr_in cliaddr;
                    char ip[16] = "";
                    socklen_t len = sizeof(cliaddr);
                    int cfd = Accept(lfd, (struct sockaddr *)&cliaddr, &len);
                    //设置已连接套接字非阻塞
                    int flags = fcntl(cfd, F_GETFL);
                    flags |= O_NONBLOCK;
                    fcntl(cfd, F_SETFL, flags);

                    printf("new client ip=%s port=%d\n",
                           inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip, 16),
                           ntohs(cliaddr.sin_port));
                    // 将cfd上树
                    ev.data.fd = cfd;
                    ev.events = EPOLLIN | EPOLLET;
                    epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
                }
                else if (evs[i].events & EPOLLIN) // cfd变化,且是读事件变化
                {
                    while (1)
                    {
                        char buf[1024] = "";
                        //如果读一个缓冲区,缓冲区没有数据,就阻塞等待,
                        //如果是非阻塞,返回值为-1,并设置errno为EAGAIN
                        int n = read(evs[i].data.fd, buf, sizeof(buf));
                        if (n < 0) // 出错,节点下树
                        {
                            if(errno == EAGAIN )//缓冲区读干净了,跳出循环,继续监听
                            {
                                break;
                            }
                            //普通错误
                            perror("");
                            epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, &evs[i]);
                            break;
                        }
                        else if (n == 0) // 客户端关闭
                        {
                            printf("client close\n");
                            close(evs[i].data.fd);
                            epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, &evs[i]);
                            break;
                        }
                        else
                        {
                            //printf("%s\n", buf);
                            write(STDOUT_FILENO, buf, 1024);
                            write(evs[i].data.fd, buf, n);
                        }
                    }
                }
            }
        }
    }
    return 0;
}

第十四章 反应堆模型和线程池模型、UDP通信、本地套接字

1. epoll反应堆

把文件描述符、事件、回调函数用结构体封装。

下文将会提到libevent库,就是对epoll reactor的实现。

2. 线程池的概念

事先创建几个线程,一个任务队列。线程池中的线程不停地取任务队列中的任务;有任务来就往队列中添加(生产者和消费者模型),省去了创建线程和销毁线程的时间和资源。

C/C++ Linux网络编程_第54张图片

3. UDP通信

1)UDP和TCP

TCP:丢包重传 面向连接(电话模型)

UDP:丢包不重传(邮件模型)

tcp通信流程:

服务器:创建流式套接字 绑定 监听 提取 读写 关闭

客户端:创建流式套接字 连接 读写 关闭

收发数据: 

i)read recv 

        flags==MSG_PEEK读数据不会删除缓冲区的数据,通常填0即可。

ii)write send

        flags==1紧急数据,通常填0即可。

udp通信流程:

服务器:创建报式套接字 绑定 读写 关闭

客户端:创建报式套接字 读写 关闭

发数据:

        dest_addr:目的地的地址信息

        addrlen:结构体大小

收数据:

        src_addr:对方的地址信息

        addrlen:结构体大小的地址

2)UDP通信服务器、客户端代码实现

因为不需要建立连接,而是像发送信件一样通讯,UDP中不存在客户端与服务器一说,而我们一般把先发送信息的称为客户端。

#include 
#include 
#include 
#include 
#include 

int main(int argc, char *argv[])
{
    //创建套接字
    int fd = socket(AF_INET, SOCK_DGRAM, 0);
    //绑定
    struct sockaddr_in myaddr;
    myaddr.sin_family = AF_INET;
    myaddr.sin_port = htons(8080);
    myaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    int ret = bind(fd, (struct sockaddr *)&myaddr, sizeof(myaddr));
    if(ret < 0)
    {
        perror("");
        return 0;
    }
    //读写
    char buf[1500] = "";
    struct sockaddr_in cliaddr;
    socklen_t len = sizeof(cliaddr);
    while (1)
    {
        int n = recvfrom(fd, buf, sizeof(buf), 0, (struct sockaddr *)&cliaddr, &len);
        memset(buf, 0, sizeof(buf));
        if(n < 0)
        {
            perror("");
            break;
        }
        else
        {
            printf("%s\n",buf);
            sendto(fd, buf, n, 0, (struct sockaddr *)&cliaddr, sizeof(cliaddr));
        }
    }
    //关闭
    close(fd);
    return 0;
}

4. 本地套接字

1)Unix domain socket

本地套接字通信,全双工,套接字用一个文件来标识,文件在绑定前不能创建。

创建本地套接字用于tcp通信:

int socket(int domain, int type, int protocol);

参数:

        domain:AF_UNIX(本地套接字)

        type:SOCK_STREAM(流式套接字)

        protocol:0

返回值:文件描述符

绑定:

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数:

        sockfd:本地套接字

        addr:本地套接字结构体地址

C/C++ Linux网络编程_第55张图片

        addrlen:sockaddr_un大小

监听

提取:

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

读写

关闭

2)代码实现:本地套接字单任务tcp服务器

#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, char *argv[])
{
    unlink("sock.s");//删除可能已经创建的sock.s文件
    //创建unix流式套接字
    int lfd = socket(AF_UNIX, SOCK_STREAM, 0);
    //绑定
    struct sockaddr_un myaddr;
    myaddr.sun_family = AF_UNIX;
    strcpy(myaddr.sun_path, "sock.s");//绑定前,文件不能存在
    //sizeof(myaddr)也可以是int len = offsetof(struct sockaddr_un, sun_path) + strlen(myaddr.sun_path);
    bind(lfd, (struct sockaddr *)&myaddr, sizeof(myaddr));
    //监听
    listen(lfd, 128);
    //提取
    struct sockaddr_un cliaddr;
    socklen_t len = sizeof(cliaddr);
    int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
    
    printf("new client file = %s\n", cliaddr.sun_path);
    //读写
    char buf[1500] = "";
    while(1)
    {
        int n = recv(cfd, buf, sizeof(buf), 0);
        if(n <= 0)
        {
            printf("err or client close\n");
            break;
        }
        else
        {
            printf("%s\n", buf);
            send(cfd, buf, n, 0);
        }
    }
    //关闭
    close(cfd);
    close(lfd);
    return 0;
}

在另一个终端使用"nc -U sock.s"来测试连接。

需要注意的点:

客户端可以隐式绑定,但是服务器不可以;绑定指定文件时,这个文件必须不存在(在代码中使用unlink删除)。

3)代码实现:本地套接字客户端

#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, char *argv[])
{
    unlink("sock.c");
    //创建unix流式套接字
    int cfd = socket(AF_UNIX, SOCK_STREAM, 0);
    //如果不绑定,会隐式绑定
    struct sockaddr_un myaddr;
    myaddr.sun_family = AF_UNIX;
    strcpy(myaddr.sun_path, "sock.c");
    if(bind(cfd, (struct sockaddr *)&myaddr, sizeof(myaddr)) < 0)
    {
        perror("");
        return 0;
    }
    //连接
    struct sockaddr_un seraddr;
    seraddr.sun_family = AF_UNIX;
    strcpy(seraddr.sun_path, "sock.s");
    connect(cfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
    //读写
    while(1)
    {
        char buf[1500] = "";
        int n = read(STDIN_FILENO, buf, sizeof(buf));
        send(cfd, buf, n, 0);
        memset(buf, 0, sizeof(buf));
        n = recv(cfd, buf, sizeof(buf), 0);
        if(n <= 0)
        {
            printf("err or server close\n");
            break;
        }
        else
        {
            printf("%s\n", buf);
        }
    }
    //关闭
    close(cfd);
    return 0;
}

第十五章 libevent

1. libevent安装

在Ubuntu 18.04下安装libevent以及一些问题解决-CSDN博客文章浏览阅读92次。error while loading shared libraries: libevent-2.1.so.7: cannot open shared object filehttps://blog.csdn.net/m0_75034791/article/details/135411165

2. libevent事件触发流程

在使用libevent的函数之前,需要先申请一个event_base结构,相当于盖房子时的地基,在event_base基础上有一个事件集合,可以检测哪个事件是激活的(就绪)。

1)创建、释放根节点

//创建event_base根节点
struct event_base *event_base_new(void);
//释放根节点
void event_base_free(struct event_base *);
//如果fork出子进程,想在子进程继续使用event_base,
//那么子进程需要对event_base重新初始化(较少用)
int event_reinit(struct event_base *base);

2)循环监听

//效果如同while(1){epoll_wait};
int event_base_dispatch(stuct event_base *base);

3)退出循环监听

C/C++ Linux网络编程_第56张图片

两个接口一个是等待固定时间退出,一个是立即退出。

4)事件触发流程

C/C++ Linux网络编程_第57张图片

C/C++ Linux网络编程_第58张图片

3. libeventAPI

1)初始化上树节点

参数:

        base:event_base根节点

        fd:上树的文件描述符

        events:监听的事件

C/C++ Linux网络编程_第59张图片

        cb:回调函数

        arg:回调函数的参数

返回值:初始化好的节点的地址

2)节点上树

参数:

        ev:上树的节点的地址

        timeout:NULL永久监听

3)下树

参数:

        ev:下树节点的地址

4)释放节点

4. 使用libevent编写tcp服务器流程

创建套接字——绑定——监听——创建event_base根节点——初始化上树节点lfd——上树——循环监听——收尾

#include 
#include 
#include "wrapSocket.h"

void cfdcb(int cfd, short event, void *arg)
{
    char buf[1500] = "";
    int n = Read(cfd, buf, sizeof(buf));
    if(n <= 0)
    {
        perror("err or close\n");
        //下树
    }
    else
    {
        printf("%s\n", buf);
        Write(cfd, buf, sizeof(buf));
    }
}

void lfdcb(int lfd, short event, void *arg)
{   
    struct event_base *base = (struct event_base *)arg;
    //提取新的cfd
    int cfd = Accept(lfd, NULL, NULL);
    //cfd上树
    struct event *ev = event_new(base, cfd, EV_READ | EV_PERSIST, cfdcb, NULL);
    event_add(ev, NULL);
}

int main(int argc, char *argv[])
{
    //创建套接字
    //绑定
    int lfd = tcp4bind(8080, NULL);
    //监听
    Listen(lfd, 128);
    //创建event_base根节点
    struct event_base *base = event_base_new();
    //初始化lfd上树节点
    struct event *ev = event_new(base, lfd, EV_READ | EV_PERSIST, lfdcb, NULL);
    //上树
    event_add(ev, NULL);
    //循环监听
    event_base_dispatch(base);//阻塞
    //收尾
    close(lfd);
    event_base_free(base);
    return 0;
}

5. bufferevent事件介绍

普通的event事件:文件描述符 事件(底层缓冲区的读或写)触发 回调

高级的event事件:bufferevent事件

核心:一个文件描述符、两个缓冲区、三个回调

C/C++ Linux网络编程_第60张图片

6. bufferevent监听流程

C/C++ Linux网络编程_第61张图片

7. buffereventAPI

1)创建新的节点

参数:

base:event_base根结点

fd:要初始化上树的文件描述符

options:

返回值:新建节点的地址

2)设置节点的回调

参数:

bufev:新建的节点的地址

readcb:读回调

writecd:写回调

eventcb:异常回调

cbarg:传给回调函数的参数

C/C++ Linux网络编程_第62张图片

3)设置事件使能

4)发送数据

5)接收数据

6)创建套接字、连接服务器

填-1是因为还没有文件描述符

8. 链接监听器(创建、绑定、监听、提取)

功能:创建套接字、绑定、监听、提取

参数:

base:event_base根节点

cd:提取套接字(cfd)后调用的回调

ptr:传给回调函数的参数

flags:

C/C++ Linux网络编程_第63张图片

backlog:监听队列的长度,填-1自动填充

sa:绑定的地址信息

socklen:sa的大小

返回值:链接监听器的地址

C/C++ Linux网络编程_第64张图片

9. 对sample中hello-world.c的阅读

/*
  This example program provides a trivial server program that listens for TCP
  connections on port 9995.  When they arrive, it writes a short message to
  each client connection, and closes each connection once it is flushed.

  Where possible, it exits cleanly in response to a SIGINT (ctrl-c).
*/


#include 
#include 
#include 
#include 
#ifndef _WIN32
#include 
# ifdef _XOPEN_SOURCE_EXTENDED
#  include 
# endif
#include 
#endif

#include 
#include 
#include 
#include 
#include 

static const char MESSAGE[] = "Hello, World!\n";

static const int PORT = 9995;

static void listener_cb(struct evconnlistener *, evutil_socket_t,
    struct sockaddr *, int socklen, void *);
static void conn_writecb(struct bufferevent *, void *);
static void conn_eventcb(struct bufferevent *, short, void *);
static void signal_cb(evutil_socket_t, short, void *);

int
main(int argc, char **argv)
{
    struct event_base *base;
    struct evconnlistener *listener;
    struct event *signal_event;

    struct sockaddr_in sin = {0};
#ifdef _WIN32
    WSADATA wsa_data;
    WSAStartup(0x0201, &wsa_data);
#endif

    //创建event_base根节点
    base = event_base_new();
    if (!base) {
        fprintf(stderr, "Could not initialize libevent!\n");
        return 1;
    }

    //创建绑定监听提取套接字(链接监听器)
    sin.sin_family = AF_INET;
    sin.sin_port = htons(PORT);
    listener = evconnlistener_new_bind(base, listener_cb, (void *)base,
        LEV_OPT_REUSEABLE|LEV_OPT_CLOSE_ON_FREE, -1,
        (struct sockaddr*)&sin,
        sizeof(sin));
    if (!listener) {
        fprintf(stderr, "Could not create a listener!\n");
        return 1;
    }
    
    //创建信号触发的节点(Ctrl+c信号)
    signal_event = evsignal_new(base, SIGINT, signal_cb, (void *)base);
    //将信号节点上树
    if (!signal_event || event_add(signal_event, NULL)<0) {
        fprintf(stderr, "Could not create/add a signal event!\n");
        return 1;
    }

    event_base_dispatch(base);//循环监听

    //释放
    evconnlistener_free(listener);
    event_free(signal_event);
    event_base_free(base);

    printf("done\n");
    return 0;
}

//回调函数
static void
listener_cb(struct evconnlistener *listener, evutil_socket_t fd,
    struct sockaddr *sa, int socklen, void *user_data)
{
    struct event_base *base = user_data;
    struct bufferevent *bev;

    //将fd上树
    //新建一个bufferevent节点
    bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);
    if (!bev) {
        fprintf(stderr, "Error constructing bufferevent!");
        event_base_loopbreak(base);
        return;
    }
    //设置回调
    bufferevent_setcb(bev, NULL, conn_writecb, conn_eventcb, NULL);
    bufferevent_enable(bev, EV_WRITE);//设置写事件使能
    bufferevent_disable(bev, EV_READ);//设置读事件非使能

    bufferevent_write(bev, MESSAGE, strlen(MESSAGE));//给cfd(bev)发送消息"hello world"
}

static void
conn_writecb(struct bufferevent *bev, void *user_data)
{
    struct evbuffer *output = bufferevent_get_output(bev);//获取缓冲区类型
    if (evbuffer_get_length(output) == 0) {//判断缓冲区是否有数据
        printf("flushed answer\n");
        bufferevent_free(bev);//释放节点,自动关闭
    }
}

static void
conn_eventcb(struct bufferevent *bev, short events, void *user_data)
{
    if (events & BEV_EVENT_EOF) {
        printf("Connection closed.\n");
    } else if (events & BEV_EVENT_ERROR) {
        printf("Got an error on the connection: %s\n",
            strerror(errno));/*XXX win32*/
    }
    /* None of the other events can happen here, since we haven't enabled
     * timeouts */
    bufferevent_free(bev);
}

static void
signal_cb(evutil_socket_t sig, short events, void *user_data)
{
    struct event_base *base = user_data;
    struct timeval delay = { 2, 0 };//两秒后

    printf("Caught an interrupt signal; exiting cleanly in two seconds.\n");

    event_base_loopexit(base, &delay);//退出循环监听
}

 

你可能感兴趣的:(Linux,C/C++,网络,ubuntu,tcp/ip,c++,c语言)