Linux下C/C++开发之网络编程

  • Post author:
  • Post category:linux


文章目录



网络结构模式

  • C/S结构
  • B/S 结构(Browser/Server,浏览器/服务器模式)

    协议一般是固定的:

    http/https



MAC地址、IP地址、端口


  1. MAC地址(OSI模型的第二层 数据链路层)
  • 每一个网卡都有一个被称为 MAC 地址的独一无二的

    48 位

    串行号。
  • 网卡的

    主要功能

    :1.数据的封装与解封装、2.链路管理、3.数据编码与译码。
  • MAC 地址的长度为 48 位(6个字节),通常表示为 12 个 16 进制数,如:

    00-16-EA-AE-3C-40

    就是一个MAC 地址,其中前 3 个字节,16 进制数 00-16-EA 代表网络硬件制造商的编号,它由

    IEEE(电气与电子工程师协会)分配,而后 3 个字节,16进制数 AE-3C-40 代表该制造商所制造的某个网络产品(如网卡)的系列号。

  1. IP(OSI :第三层网络层)

    • IP 地址是一个 32 位的二进制数,通常被分割为

      4 个“ 8 位二进制数”

      (也就是 4 个字节)。IP 地址

      通常用“点分十进制”表示成(a.b.c.d)的形式,其中,a,b,c,d都是 0~255 之间的十进制整数。

      例:点分十进IP地址(100.4.5.6),实际上是 32 位二进制数

      (01100100.00000100.00000101.00000110)。
  • ABCD特殊 类网络介绍

    A类网路:第一个字节是网络地址,其最高位必须为‘0’

    B类网络:第一、二个字节是网络地址,其从‘10’开始

    C类网络:第一、二、三个字节是网络地址,其从’110‘开始

    D类网络:D 类 IP 地址在历史上被叫做多播地址(multicast address),即组播地址。在以太网中,多播地址命名了一组应该在这个网络中应用接收到一个分组的站点。多播地址的最高位必须是 “1110”,范围从224.0.0.0 – 239.255.255.255。

    特殊网络:每一个字节都为 0 的地址( “0.0.0.0” )对应于当前主机;

    IP 地址中的每一个字节都为 1 的 IP 地址( “255.255.255.255” )是当前子网的广播地址;

    IP 地址中凡是以 “11110” 开头的 E 类 IP 地址都保留用于将来和实验使用。

    IP地址中不能以十进制 “127” 作为开头,该类地址中数字 127.0.0.1 到 127.255.255.255 用于回路测

    试,如:127.0.0.1可以代表本机IP地址。

  • 网络数和单个网段数表格


    单个网段最大主机数减去2

    是因为有 一个网关地址和一个广播地址
    在这里插入图片描述


  • 子网掩码subnet mask

    又叫网络掩码、地址掩码、子网络遮罩,它是一种用来指明一个 IP 地

    址的哪些位

    标识

    的是主机所在的子网,以及哪些位标识的是主机的位掩码。不能单独存在。


  1. 端口 (port)


    虚拟端口:指计算机内部或交换机路由器内的端口,不可见,是特指TCP/IP协议中的端口。

    物理端口:也称为接口,是可见端口,计算机背板的 RJ45 网口等
  • 端口类型

    • 周知端口(Well Known Ports)

      范围从 0 到 1023,它们紧密绑定于一些特定的服务。例如

      80 端口分配给 WWW 服务

      ,21 端口分配给 FTP 服务,23 端口分配给Telnet服务等等。
    • 注册端口(Registered Ports)
    • 动态端口 / 私有端口(Dynamic Ports / Private Ports)



网络七层模型

在这里插入图片描述



TCP/IP 四层模型

在这里插入图片描述



协议

在这里插入图片描述



封装与分用(传输用到的)

在这里插入图片描述

  • 网络通信过程

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

  • ARP请求例子

    简述:整个过程就是当前A主机向整个局域网的主机发送ARP请求,只有ARP请求所包含的目标IP地址与B主机IP地址相同,B主机返回一个ARP应答。

    在这里插入图片描述



Socket通信



概述

  1. Socket可以抽象成网络中不同的主机之间双向通信的端点。
  2. socket 是由 IP 地址和端口结合的,提供向应用层进程传送数据包的机制。
  3. 在 Linux 环境下,用于表示进程间网络通信的特殊文件类型。本质为内核借助缓冲区形成的伪文件。
  4. 与管道的区别在于管道是负责主机内部的进程间通信,而socket是负责不同主机的进程间通信。



字节序






:在传输数据时都是转成大端字节序,若接收数据的主机是小端字节序则会通过API转换为小端字节序。

在这里插入图片描述

在这里插入图片描述



字节序转换函数

  • 主机字节序转网络字节序:

    htons、htonl

    函数(host to net 无符号short/int)
  • 网络字节序转主机字节序:

    ntohs、ntohl

    (net to host 无符号short/int)

    端口一般用 htons 、ntohs
#include <arpa/inet.h> 
// 转换端口 
uint16_t htons(uint16_t hostshort); 
// 主机字节序 - 网络字节序 
uint16_t ntohs(uint16_t netshort); 
// 主机字节序 - 网络字节序 

// 转IP
uint32_t htonl(uint32_t hostlong); 
// 主机字节序 - 网络字节序 
uint32_t ntohl(uint32_t netlong); 
// 主机字节序 - 网络字节序



Socket地址

  • 概念:socket地址其实是一个结构体,封装端口号和IP等信息。后面的socket相关的api中需要使用到这个 socket地址。
  • Linux 定义的 socket 地址结构体兼容PF_UNIX、PF_INET、PF_INET6

    协议族
#include <bits/socket.h> 
struct sockaddr_storage { 
	sa_family_t sa_family; 
	unsigned long int __ss_align; 
	char __ss_padding[ 128 - sizeof(__ss_align) ]; 
};

typedef unsigned short int sa_family_t;



专有的Socket地址

很多网络编程函数诞生早于

IPv4 协议

,那时候都使用的是 struct sockaddr 结构体,

为了向前兼容

,现在sockaddr 退化成了(void *)的作用,传递一个地址给函数,至于这个函数是 sockaddr_in 还是

sockaddr_in6,由地址族确定,然后函数内部再

强制类型转化

为所需的地址类型。

在这里插入图片描述

  • UNIX 本地域协议族使用专用的 socket 地址结构体
  • TCP/IP 协议族有

    sockaddr_in



    sockaddr_in6

    两个专用的 socket 地址结构体。

所有专用 socket 地址(以及 sockaddr_storage)类型的变量在实际使用时都需要转化为通用 socket 地址类型 sockaddr(强制转化即可),因为所有 socket 编程接口使用的地址参数类型都是 sockaddr。



IP地址转换(字符串ip-整数 ,主机、网络字节序的转换)


  1. 点分十进制字符串

    表示 IPv4 地址,以及用

    十六进制字符串

    表示 IPv6 地址
  2. 编程中我们需要先把它们转化为整数(二进制数)方能使用,而记录日志时则相反。
  3. 3 个函数可用于用

    点分十进制字符串表示的 IPv4或者6 地址

    和用

    网络字节序整数表示的 IPv4或者6 地址

    之间的转换
#include <arpa/inet.h> 
p:点分十进制的IP字符串,n:表示network,网络字节序的整数
 int inet_pton(int af, const char *src, void *dst); 
  • af:地址族: AF_INET AF_INET6
  • src:需要转换的点分十进制的IP字符串
  • dst:转换后的结果保存在这个里面

  • 返回值

    为0 表示成功!
#include <arpa/inet.h> 
将网络字节序的整数,转换成点分十进制的IP地址字符串 
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); 

  • af:地址族: AF_INET AF_INET6
  • src: 要转换的ip的整数的地址
  • dst: 转换成IP地址字符串保存的地方
  • size:第三个参数的大小(数组的大小)

  • 返回值

    :返回转换后的数据的地址(字符串),和 dst 是一样的



TCP通信流程



TCP和UDP对比

在这里插入图片描述



通信流程

在这里插入图片描述



  • 服务器端 (被动接受连接的角色)

  1. 创建一个

    用于监听的套接字


    监听:监听有客户端的连接 – 套接字:这个套接字其实就是一个文件描述符

  2. 将这个监听文件描述符和本地的IP和端口绑定

    (IP和端口就是服务器的地址信息)

    客户端连接服务器的时候使用的就是这个IP和端口

  3. 设置监听

    ,监听的fd开始工作

  4. 阻塞等待

    ,当有客户端发起连接,解除阻塞,接受客户端的连接,会得到一个和客户端通信的套接字 (fd)

  5. 通信


    接收数据

    发送数据

  6. 通信结束

    ,断开连接


  • 客户端

  1. 创建一个用于

    通信的套接字

    (fd)

  2. 连接服务器

    ,需要指定连接的服务器的 IP 和 端口

  3. 连接成功了

    ,客户端可以直接和服务器通信

    接收数据

    发送数据

  4. 通信结束

    ,断开连接



socket(套接字)函数

在这里插入图片描述



TCP三次握手、四次挥手



三次握手

TCP 提供了一种可靠、面向连接、字节流、传输层的服务,采用三次握手建立一个连接。采用 四次挥手

来关闭一个连接。

在这里插入图片描述


  • 第一次握手


    1.客户端将SYN标志位置为1

    2. 客户端发送生成的32位随机序号seq =J,后面可以加数据信息。

  • 第二次握手


    1.服务端收到客户端请求,ACK置为1

    2. 服务端会回发一个确认序号 ack = 客户端序号+数据长度 + SYN/FIN(按一个字节算)

    3. 服务端向客户端发起连接请求 ,SYN = 1

    4. 服务器会生成一个随机序列号发送给客户端 seq = K

  • 第三次握手


    1.客户端应答服务器请求 ACK = 1

    2. 客户端向服务器发送 ack = 服务端的序号 + 数据长度 + SYN/FIN(按一个字节算)



四次挥手

  • 四次挥手发生在断开连接的时候,在程序中当调用了close()会使用TCP协议进行四次挥手。
  • 客户端和服务器端都可以主动发起断开连接,谁先调用close()谁就是发起。
  • 因为在TCP连接的时候,采用三次握手建立的的连接是双向的,在断开的时候需要双向断开。

在这里插入图片描述



TCP滑动窗口


  • 滑动窗口

    是 TCP 中实现诸如

    ACK 确认、流量控制、拥塞控制的承载结构


    在这里插入图片描述
  • 图解

    在这里插入图片描述



多进程实现并发服务器

  • 服务端案例代码

    1. 父进程创建socket并绑定和设置监听
    2. 父进程不断循环等待连接accept
    3. 定义struct sigaction act,注册信号捕捉SIGHLD(子进程退出会产生的信号)
    4. 在父进程中创建子进程,由子进程进行TCP通信
    5. 关闭cfd并回收子进程
    6. 关闭lfd
    • 注意

    1. write(cfd,revbuf,strlen(revbuf)+1);

      //加1是为了把 字符串终止符发送过去 以免产生bug(该bug跟字符串长短相关)
    2. 信号捕捉流程(子进程回收)复习
//多进程服务端TCP通信
#include<stdio.h>
//字节序
#include<arpa/inet.h>
//socket通信
#include <sys/types.h>         
#include <sys/socket.h>
//exit
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
//信号捕捉,子进程回收
#include<errno.h>
#include <signal.h>
#include <sys/wait.h>

void recyleChild(int arg) {
     while(1) {
        int ret = waitpid(-1, NULL, WNOHANG);
        if(ret == -1) {
            // 所有的子进程都回收了
            break;
        }else if(ret == 0) {
            // 还有子进程活着
            break;
        } else if(ret > 0){
            // 被回收了
            printf("子进程 %d 被回收了\n", ret);
        }
    }
}
int main() {
    //定义act相关参数
    struct sigaction act;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    act.sa_handler = recyleChild;
    //注册信号捕捉
    sigaction(SIGCHLD,&act,NULL);

    //创建socket
    int lfd = socket(AF_INET,SOCK_STREAM,0);
    if(lfd == -1) {
        perror("socket");
        exit(-1);
    }
    //绑定本机ip地址和端口
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);
    saddr.sin_addr.s_addr = INADDR_ANY;
    int ret = bind(lfd,(struct sockaddr*)&saddr,sizeof(saddr));
    if(ret == -1) {
        perror("bind");
        exit(-1);
    }
    //监听连接
    ret = listen(lfd,8);
        if(ret == -1) {
            perror("listen");
            exit(-1);
        }
    //循环接收客户端连接
    while(1) {
        
        struct sockaddr_in caddr;
        int len = sizeof(caddr);
        int cfd = accept(lfd,(struct sockaddr*)&caddr,&len);
        if(cfd == -1) {
            if(errno == EINTR) continue;
            perror("accept");
            exit(-1);
        }
        //创建子进程,输出客户端信息并进行通信
        pid_t spid = fork();
        if(spid == 0) {
            //子进程
            //输出客户端ip 和端口号
            char cip[16];
            inet_ntop(AF_INET,&caddr.sin_addr.s_addr,cip,strlen(cip));
            unsigned short cport = ntohs(caddr.sin_port);
            printf("Client ip is %s and port is %d\n",cip,cport);
            //创建接收缓冲区
            char revbuf[1024];
            while(1) {
                //接收客户端信息
                int rlen = read(cfd,revbuf,sizeof(revbuf));
                 if(rlen == -1) {
                    perror("read");
                    exit(-1);
                } else if(rlen > 0) {
                    printf("Sever have recieved :%s\n",revbuf);
                } else if(rlen == 0) {
                    printf("client have closed..\n");
                    break;
                }
                sleep(1);
                //发送信息给客户端
                
                write(cfd,revbuf,strlen(revbuf)+1);//加1是为了把 字符串终止符发送过去 以免产生bug(该bug跟字符串长短相关)
            }
            //关闭客户端文件描述符
            close(cfd);
            //退出当前子进程
            exit(0);
        }
    }
    
    //关闭监听描述符
    close(lfd);
    
    return 0;
}
  • 客户端案例代码

    1. 创建socket并绑定,连接服务端
    2. 通信
    3. 关闭fd
    • 注意


      write(cfd,revbuf,strlen(revbuf)+1);

      //加1是为了把 字符串终止符发送过去 以免产生bug(该bug跟字符串长短相关)
//TCP通信的客户端(无多进程)
#include<stdio.h>
#include<arpa/inet.h>
#include <sys/types.h>         
#include <sys/socket.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>



int main() {
   

    //1创建socket
    int cfd = socket(AF_INET,SOCK_STREAM,0);
    if(cfd == -1) {
        perror("socket");
        exit(-1);
    }
    //2与服务端连接
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);
    inet_pton(AF_INET,"172.26.4.132",&saddr.sin_addr.s_addr);
    int ret = connect(cfd,(struct sockaddr *)&saddr,sizeof(saddr));
    if(ret == -1) {
        perror("connect");
        exit(-1);
    }
    //3通信
    char revbuf[1024];
    int i = 0;
    while(1) {
        //发送信息给服务端
        sprintf(revbuf,"hello ! I am Client:%d\n",i++);
        //sprintf(revbuf, "data : %d\n", i++);
        write(cfd,revbuf,strlen(revbuf)+1);//加1是为了把 字符串终止符发送过去 以免产生bug(该bug跟字符串长短相关)
        //i++;
        //接收服务端信息
        
        int len = read(cfd,revbuf,sizeof(revbuf));
        if(len == -1) {
            perror("read");
            exit(-1);
        } else if(len > 0) {
            printf("Client have recieved :%s\n",revbuf);
        } else if(len == 0) {
            printf("Sever have closed..");
            break;
        }
    }
   
    
    //4关闭
    close(cfd);

    return 0;
}



多线程实现并发服务器

  • 服务端案例代码(客户端案例代码同上此处‘’略‘’)
  • 注意

    1.创建sockInfo 结构体的原因、如何初始化,如何获取参数(

    指针

    )和分配结构体空闲元素(

    for循环检测-1,倒一等待1s

    )。

    2. 线程创建的流程及void * (working)(void*)

    3. 回收线程运用 pthread_detach(非阻塞)而不运用 pthread_join(阻塞)。
//多线程服务端TCP通信
#include<stdio.h>
//字节序
#include<arpa/inet.h>
//socket通信
#include <sys/types.h>         
#include <sys/socket.h>
//exit
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<pthread.h>


//创建结构体的原因:  线程处理需要获取多个参数,那么pthread_creat 的第四个参数可以作为传入,
//                 但只能传入一个,所以只需传入一个结构体指针即可获得三个参数。
struct sockInfo {
    int fd; // 通信的文件描述符
    struct sockaddr_in addr;
    pthread_t tid;  // 线程号
};

struct sockInfo sockinfos[128];

//——————————————————————创建线程后执行的区域——————————————————————————————//
void * working(void* arg) {
    // 子线程和客户端通信   cfd 客户端的信息 线程号
    struct sockInfo * pinfo = (struct sockInfo *)arg;
    //输出客户端ip 和端口号
    char cip[16];
    inet_ntop(AF_INET,&pinfo->addr.sin_addr.s_addr,cip,strlen(cip));
    unsigned short cport = ntohs(pinfo->addr.sin_port);
    printf("Client ip is %s and port is %d\n",cip,cport);
    //创建接收缓冲区
    char revbuf[1024];
    while(1) {
        //接收客户端信息
        int rlen = read(pinfo->fd,revbuf,sizeof(revbuf));
            if(rlen == -1) {
            perror("read");
            exit(-1);
        } else if(rlen > 0) {
            printf("Sever have recieved :%s\n",revbuf);
        } else if(rlen == 0) {
            printf("client have closed..\n");
            break;
        }
        sleep(1);
        //发送信息给客户端
        
        write(pinfo->fd,revbuf,strlen(revbuf)+1);//加1是为了把 字符串终止符发送过去 以免产生bug(该bug跟字符串长短相关)
    }
    //关闭客户端文件描述符
    close(pinfo->fd);
    return NULL;
}
//——————————————————————创建线程后执行的区域——————————————————————————————//

int main() {
     //创建socket
    int lfd = socket(AF_INET,SOCK_STREAM,0);
    if(lfd == -1) {
        perror("socket");
        exit(-1);
    }
    //绑定本机ip地址和端口
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);
    saddr.sin_addr.s_addr = INADDR_ANY;
    int ret = bind(lfd,(struct sockaddr*)&saddr,sizeof(saddr));
    if(ret == -1) {
        perror("bind");
        exit(-1);
    }
    //监听连接
    ret = listen(lfd,8);
        if(ret == -1) {
            perror("listen");
            exit(-1);
        }
    //初始化
    int max = sizeof(sockinfos)/sizeof(sockinfos[0]);
    for(int i = 0; i < max; i++) {
        bzero(&sockinfos[i], sizeof(sockinfos[i]));//让多个字节为零
        sockinfos[i].fd = -1;
        sockinfos[i].tid = -1;
    }
    //循环接收客户端连接
    while(1) {
        //子线程
        struct sockaddr_in caddr;
        int len = sizeof(caddr);
        int cfd = accept(lfd,(struct sockaddr*)&caddr,&len);
        if(cfd == -1) {
            if(errno == EINTR) continue;
            perror("accept");
            exit(-1);
        }
        struct sockInfo * pinfo;
        //查找空闲的子进程
        for(int i = 0;i<max;i++) {
            if(sockinfos[i].fd == -1) {
                pinfo = &sockinfos[i];
                break;
            }
            if(i == max - 1) {//当没有空闲的 让客户端等待一秒。
                sleep(1);
                i--;
            }
        }
        pinfo->fd = cfd;
        memcpy(&pinfo->addr, &caddr, len);//注意这里拷贝结构体的方式!
        
        //创建子线程
        pthread_create(&pinfo->tid,NULL, working, pinfo);

        //子线程自动回收,不用父进程回收
           //不能用另一个pthread_join的原因是该函数阻塞
        pthread_detach(pinfo->tid);
    }
    //关闭lfd (cfd在线程中已经关闭)
    close(lfd);
    return 0;
}



TCP状态转换

在这里插入图片描述

在这里插入图片描述


绿线:服务端

红线:客户端


黑线:产生错误会发生的状态转换

  • 2MSL(Maximum Segment Lifetime)

    1. 主动断开连接的一方, 最后进入一个

      TIME_WAIT

      状态, 这个状态会持续:

      2msl
    2. msl: 官方建议: 2分钟, 实际是30s

    3. 等待2msl的原因


      在这里插入图片描述



端口复用


端口复用最常用的用途

:

  1. 防止服务器重启时之前绑定的端口还未释放
  2. 程序突然退出而系统没有释放端口



setsockopt 设置端口复用(还可以设置其他功能)

#include <sys/types.h> 
#include <sys/socket.h> 
// 设置套接字的属性(不仅仅能设置端口复用) 
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

参数:

  • sockfd : 要操作的文件描述符
  • level :选项所在的协议层(level指定控制套接字的层次,可以取三种值:1)

    SOL_SOCKET

    :通用套接字选项.2)

    IPPROTO_IP

    :IP选项.3)

    IPPROTO_TCP

    :TCP选项. )

    • SOL_SOCKET (“端口复用”这一功能所在的协议层)
  • optname : 选项的名称

    • SO_REUSEADDR
    • SO_REUSEPORT
  • optval : 端口复用的值(整形)

    • 1 : 可以复用 –
    • 0 : 不可以复用
  • optlen : optval参数的大小



设置端口复用的位置

  • 应在bind()之前就设置
//形式参数省略
setsockopt(); 
bind();



IO多路复用(select、poll、epoll)


  • 概念

    :I/O 多路复用使得程序能同时监听多个文件描述符,能够提高程序的性能,Linux 下实现 I/O 多路复用的

    系统调用主要有

    select、poll 和 epoll。



select



概念

在这里插入图片描述



select函数详解

在这里插入图片描述



操作fd_set函数

fd_set 是一个文件描述符表 ,有1024位 。前0 1 2三位默认占用。

在这里插入图片描述



select 服务端应用案例

重点看:IO多路复用部分

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>

int main() {

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);
    //----------------------------------IO多路复用------------------------------//
    // 创建一个fd_set的集合,存放的是需要检测的文件描述符
    fd_set rdset, tmp;//tmp 的目的是防止本地的fd_set 文件被内核改变                /
    FD_ZERO(&rdset);
    FD_SET(lfd, &rdset);
    int maxfd = lfd;

    while(1) {

        tmp = rdset;

        // 调用select系统函数,让内核帮检测哪些文件描述符有数据
        int ret = select(maxfd + 1, &tmp, NULL, NULL, NULL);
        if(ret == -1) {
            perror("select");
            exit(-1);
        } else if(ret == 0) {
            continue;
        } else if(ret > 0) {
            // 说明检测到了有文件描述符的对应的缓冲区的数据发生了改变
            //判断fd对应的标志位是0还是1, 返回值 : fd对应的标志位的值,0,返回0, 1,返回1
            if(FD_ISSET(lfd, &tmp)) {
                // 表示有新的客户端连接进来了
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

                // 将新的文件描述符加入到集合中
                FD_SET(cfd, &rdset);

                // 更新最大的文件描述符
                maxfd = maxfd > cfd ? maxfd : cfd;
            }

            //遍历内核发回来的 fd_set  接收有数据的文件描述符
            for(int i = lfd + 1; i <= maxfd; i++) {//注意等于号!!!
                //判断fd对应的标志位是0还是1, 返回值 : fd对应的标志位的值,0,返回0, 1,返回1
                if(FD_ISSET(i, &tmp)) {
                    // 说明这个文件描述符对应的客户端发来了数据
                    char buf[1024] = {0};
                    int len = read(i, buf, sizeof(buf));
                    if(len == -1) {
                        perror("read");
                        exit(-1);
                    } else if(len == 0) {
                        printf("client closed...\n");
                        close(i);
                        FD_CLR(i, &rdset);
                    } else if(len > 0) {
                        printf("read buf = %s\n", buf);
                        write(i, buf, strlen(buf) + 1);
                    }
                }
            }
        }
    }
 //----------------------------------IO多路复用------------------------------//
    close(lfd);
    return 0;
}



poll



poll函数详解

在这里插入图片描述



poll函数操作案例

  • 注意:

  1. if(fds[i].revents & POLLIN) {


    //注意是用”&”,而不用”==”,因为 revents 可能 有 “|” 进行拼接多个事件,双等号不能判断。
  2. 关闭客户端文件描述符后要对myfd 中得对应位置进行复位
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>


int main() {

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);
//----------------------IO多路复用--------------------------------------------//
    // 初始化检测的文件描述符数组
    struct pollfd fds[1024];
    for(int i = 0; i < 1024; i++) {
        fds[i].fd = -1;
        fds[i].events = POLLIN;
    }
    fds[0].fd = lfd;
    int nfds = 0;

    while(1) {

        // 调用poll系统函数,让内核帮检测哪些文件描述符有数据
        int ret = poll(fds, nfds + 1, -1);//-1表示阻塞(当有客户端接入进来才不阻塞)
        if(ret == -1) {
            perror("poll");
            exit(-1);
        } else if(ret == 0) {
            continue;
        } else if(ret > 0) {
            // 说明检测到了有文件描述符的对应的缓冲区的数据发生了改变
            if(fds[0].revents & POLLIN) {//注意是用"&",而不用"==",因为 revents 可能 有 "|" 进行拼接多个事件,双等号不能判断。
                // 表示有新的客户端连接进来了
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

                // 将新的文件描述符加入到集合中
                for(int i = 1; i < 1024; i++) {
                    if(fds[i].fd == -1) {
                        fds[i].fd = cfd;
                        fds[i].events = POLLIN;
                        break;
                    }
                }

                // 更新最大的文件描述符的索引
                nfds = nfds > cfd ? nfds : cfd;
            }

            for(int i = 1; i <= nfds; i++) {
                if(fds[i].revents & POLLIN) {//注意是用"&",而不用"==",因为 revents 可能 有 "|" 进行拼接多个事件,双等号不能判断。
                    // 说明这个文件描述符对应的客户端发来了数据
                    char buf[1024] = {0};
                    int len = read(fds[i].fd, buf, sizeof(buf));
                    if(len == -1) {
                        perror("read");
                        exit(-1);
                    } else if(len == 0) {
                        printf("client closed...\n");
                        close(fds[i].fd);
                        fds[i].fd = -1;
                    } else if(len > 0) {
                        printf("read buf = %s\n", buf);
                        write(fds[i].fd, buf, strlen(buf) + 1);
                    }
                }
            }
        }
    }
    //----------------------IO多路复用--------------------------------------//
    close(lfd);
    return 0;
}



☼epoll



epoll多路复用图解

在这里插入图片描述

struct rb_root rbr 是红黑树,查找效率高 告诉内核需要监听的文件描述符

struct list_head rdlist 是双向链表 用于记录 文件描述符变化

相较于 select 和 poll 节省了cpu算力,提高工作效率。



epoll相关函数详解



epoll_create()

在这里插入图片描述



epoll_crtl

  • 结构体 epoll_event

    在这里插入图片描述
  • 上面结构体内的

    epoll_data_t data

    • 主要用于确定类型(这里是fd 文件描述符)

      在这里插入图片描述
  • epoll_crtl 函数详解

    在这里插入图片描述



epoll_wait

在这里插入图片描述



epoll 服务端操作案例

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
int main() {
     //创建socket
    int lfd = socket(AF_INET,SOCK_STREAM,0);
    if(lfd == -1) {
        perror("socket");
        exit(-1);
    }
    //绑定
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);
    saddr.sin_addr.s_addr = INADDR_ANY;
    int len = sizeof(saddr);
    int ret = bind(lfd,(struct sockaddr*)&saddr,len);
    if(ret == -1) {
        perror("bind");
        exit(-1);
    }
    //监听
    ret = listen(lfd,8);
    if(ret == -1) {
        perror("listen");
        exit(-1);
    }

    //-----------------------------------------------------IO多路复用-----------------------------------------------------------------------//
     // 调用epoll_create()创建一个epoll实例
    int epfd =  epoll_create(100); // 参数:size : 目前没有意义了。随便写一个数,必须大于0 - 返回值: -1 : 失败 > 0 : 文件描述符,操作epoll实例的
    if(epfd == -1) {
        perror("epollCreat");
        exit(-1);
    }

    struct epoll_event epev;
    epev.events = EPOLLIN;
    epev.data.fd = lfd;
    int ret_epc = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);
    if(ret_epc== -1) {
        perror("epoll_ctl");
        exit(-1);
    }
    struct epoll_event epevs[1024];//保存了发送了变化的文件描述符的信息
    
    while(1) {
        int ret_wat = epoll_wait(epfd,epevs,1024,-1);
            if(ret_wat== -1) {
                perror("epoll_wait");
                exit(-1);
            }

        printf("ret_wat = %d\n", ret_wat);//输出当前正在操作的客户端数
        
        //遍历查找有变化的文件描述符
        for(int i = 0 ;i < ret_wat;i++) {

            int cur_fd = epevs[i].data.fd;

            if(cur_fd == lfd) {
                //检测到客户端连接进来;
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
                if(cfd== -1) {
                    perror("accept");
                    exit(-1);
                }

                //设置对应的客户端信息
                epev.events = EPOLLIN;
                epev.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);//将新的文件描述符添加到epev。
            } else {
                
                if(epevs[i].events & EPOLLOUT) {//不同事件不同的处理 此处略;EPOLLOUT是输出事件,服务端发送给客户端。
                    continue;
                }   
                // 有数据到达,需要通信
                char buf[1024] = {0};
                int len = read(cur_fd, buf, sizeof(buf));
                if(len == -1) {
                    perror("read");
                    exit(-1);
                } else if(len == 0) {
                    printf("client closed...\n");
                    //需要在内核先删除当前文件描述符 再关闭,最后一个参数可以是NULL
                    epoll_ctl(epfd, EPOLL_CTL_DEL, cur_fd, NULL);
                    close(cur_fd);
                } else if(len > 0) {
                    printf("read buf = %s\n", buf);
                    write(cur_fd, buf, strlen(buf) + 1);
                }
            }
        }
    }
//-----------------------------------------------------IO多路复用-----------------------------------------------------------------------//
    close(epfd);
    close(lfd);
    return 0;
}



❁epoll 工作模式

  • LT

    在这里插入图片描述


    LT服务端示例代码
  • 相较于之前的epoll服务端代码 ,读缓冲区大小改为 5
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>

int main() {

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);

    // 调用epoll_create()创建一个epoll实例
    int epfd = epoll_create(100);

    // 将监听的文件描述符相关的检测信息添加到epoll实例中
    struct epoll_event epev;
    epev.events = EPOLLIN;
    epev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);

    struct epoll_event epevs[1024];

    while(1) {

        int ret = epoll_wait(epfd, epevs, 1024, -1);
        if(ret == -1) {
            perror("epoll_wait");
            exit(-1);
        }

        printf("ret = %d\n", ret);

        for(int i = 0; i < ret; i++) {

            int curfd = epevs[i].data.fd;

            if(curfd == lfd) {
                // 监听的文件描述符有数据达到,有客户端连接
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

                epev.events = EPOLLIN;
                epev.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
            } else {
                if(epevs[i].events & EPOLLOUT) {
                    continue;
                }   
                // 有数据到达,需要通信
                char buf[5] = {0};
                int len = read(curfd, buf, sizeof(buf));
                if(len == -1) {
                    perror("read");
                    exit(-1);
                } else if(len == 0) {
                    printf("client closed...\n");
                    epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
                    close(curfd);
                } else if(len > 0) {
                    printf("read buf = %s\n", buf);
                    write(curfd, buf, strlen(buf) + 1);
                }

            }

        }
    }

    close(lfd);
    close(epfd);
    return 0;
}
  • ET

    在这里插入图片描述


    ET服务端示例代码
  • 如何设置ET?
  • read函数如何设置非阻塞?使得服务端可以不因为缓冲区小而读不全
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>

int main() {

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);

    // 调用epoll_create()创建一个epoll实例
    int epfd = epoll_create(100);

    // 将监听的文件描述符相关的检测信息添加到epoll实例中
    struct epoll_event epev;
    epev.events = EPOLLIN;
    epev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);

    struct epoll_event epevs[1024];

    while(1) {

        int ret = epoll_wait(epfd, epevs, 1024, -1);
        if(ret == -1) {
            perror("epoll_wait");
            exit(-1);
        }

        printf("ret = %d\n", ret);

        for(int i = 0; i < ret; i++) {

            int curfd = epevs[i].data.fd;

            if(curfd == lfd) {
                // 监听的文件描述符有数据达到,有客户端连接
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

                // 设置cfd属性非阻塞
                int flag = fcntl(cfd, F_GETFL);
                flag |= O_NONBLOCK;
                fcntl(cfd, F_SETFL, flag);

                epev.events = EPOLLIN | EPOLLET;    // 设置边沿触发
                epev.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
            } else {
                if(epevs[i].events & EPOLLOUT) {
                    continue;
                }  

                // 循环读取出所有数据
                char buf[5];
                int len = 0;
                while( (len = read(curfd, buf, sizeof(buf))) > 0) {
                    // 打印数据
                    // printf("recv data : %s\n", buf);
                    write(STDOUT_FILENO, buf, len);
                    write(curfd, buf, len);
                }
                if(len == 0) {
                    printf("client closed....");
                }else if(len == -1) {
                    if(errno == EAGAIN) {
                        printf("data over.....");
                    }else {
                        perror("read");
                        exit(-1);
                    }
                    
                }

            }

        }
    }

    close(lfd);
    close(epfd);
    return 0;
}
  • 设置 ET模式的关键字

    在这里插入图片描述



UDP通信

在这里插入图片描述



读写函数详解

在这里插入图片描述



UDP通信案例

  • 服务端


    与TCP通信的区别


    • int fd = socket(PF_INET, SOCK_DGRAM, 0);

      这里是

      SOCK_DGRAM

      数据报格式,与tcp通信不同!

    • 没有添加监听listen
    • 通信api也不同
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

int main() {

    // 1.创建一个通信的socket(这里是SOCK_DGRAM数据报格式!!!)
    int fd = socket(PF_INET, SOCK_DGRAM, 0);
    
    if(fd == -1) {
        perror("socket");
        exit(-1);
    }   

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(9999);
    addr.sin_addr.s_addr = INADDR_ANY;

    // 2.绑定
    int ret = bind(fd, (struct sockaddr *)&addr, sizeof(addr));
    if(ret == -1) {
        perror("bind");
        exit(-1);
    }

    // 3.通信
    while(1) {
        char recvbuf[128];
        char ipbuf[16];

        struct sockaddr_in cliaddr;
        int len = sizeof(cliaddr);

        // 接收数据
        int num = recvfrom(fd, recvbuf, sizeof(recvbuf), 0, (struct sockaddr *)&cliaddr, &len);

        printf("client IP : %s, Port : %d\n", 
            inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ipbuf, sizeof(ipbuf)),
            ntohs(cliaddr.sin_port));

        printf("client say : %s\n", recvbuf);

        // 发送数据
        sendto(fd, recvbuf, strlen(recvbuf) + 1, 0, (struct sockaddr *)&cliaddr, sizeof(cliaddr));

    }

    close(fd);
    return 0;
}
  • 客户端


    与TCP通信的区别


    • int fd = socket(PF_INET, SOCK_DGRAM, 0);

      这里是

      SOCK_DGRAM

      数据报格式,与tcp通信不同!

    • 没有调用connect 函数连接服务器,只是在本地存有服务器ip和端口,通过sendto 直接发送
    • 通信API 不同
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

int main() {

    // 1.创建一个通信的socket
    int fd = socket(PF_INET, SOCK_DGRAM, 0);
    
    if(fd == -1) {
        perror("socket");
        exit(-1);
    }   

    // 服务器的地址信息
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);
    inet_pton(AF_INET, "127.0.0.1", &saddr.sin_addr.s_addr);

    int num = 0;
    // 3.通信
    while(1) {

        // 发送数据
        char sendBuf[128];
        sprintf(sendBuf, "hello , i am client %d \n", num++);
        sendto(fd, sendBuf, strlen(sendBuf) + 1, 0, (struct sockaddr *)&saddr, sizeof(saddr));

        // 接收数据
        int num = recvfrom(fd, sendBuf, sizeof(sendBuf), 0, NULL, NULL);
        printf("server say : %s\n", sendBuf);

        sleep(1);
    }

    close(fd);
    return 0;
}



广播

在这里插入图片描述



setsockapt详解
在这里插入图片描述



广播通信案例

  • 服务端


    与TCP相比


    • int fd = socket(PF_INET, SOCK_DGRAM, 0);

      这里是

      SOCK_DGRAM

      数据报格式。没有设置监听 、绑定
    • 需要设置广播属性 setsockopt,并创建一个广播地址
    • 通信API不同
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

int main() {

    //创建socket  注意参数
    int lfd = socket(PF_INET,SOCK_DGRAM,0);
    if(lfd == -1) {
        perror("socket");
        exit(-1);
    }

    //设置广播属性
    int op;
    setsockopt(lfd,SOL_SOCKET,SO_BROADCAST,&op,sizeof(op));
    
    //创建一个广播地址(不用绑定ip)
    struct sockaddr_in addr;
    //x.x.x.255为广播地址
    inet_pton(AF_INET,"172.26.4.255",&addr.sin_addr.s_addr);
    addr.sin_family = AF_INET;
    addr.sin_port = htons(9999);
   
    int num = 0;
    while (1) {

        //发送数据
        char sendbuf[128];
        sprintf(sendbuf,"Hello!Client..%d\n",num++);
        sendto(lfd,sendbuf,strlen(sendbuf) + 1,0,(struct sockaddr*)&addr,sizeof(addr));
        printf("广播数据:%s", sendbuf);
        sleep(1);
    }
    close(lfd);
    return 0;
}
  • 客户端


    与TCP相比


    • int fd = socket(PF_INET, SOCK_DGRAM, 0);

      这里是

      SOCK_DGRAM

      数据报格式。
    • 需要设置对应服务器信息并绑定

      广播使用的端口(这里是9999)

      ,而TCP客户端不用调用bind,只需设置服务器ip和端口
    • 通信API不同
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

int main() {

    //创建socket 注意参数
    int lfd = socket(PF_INET,SOCK_DGRAM,0);

    if(lfd == -1) {
        perror("socket");
        exit(-1);
    }
    
    //绑定广播地址
    struct sockaddr_in addr;
    addr.sin_addr.s_addr = INADDR_ANY;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(9999);

    int ret = bind(lfd,(struct sockaddr*)&addr,sizeof(addr));
    if(ret == -1) {
        perror("bind");
        exit(-1);
    }

    while (1) {
        
        //接收广播数据
        char revbuf[128];
        recvfrom(lfd,revbuf,sizeof(revbuf),0,NULL,NULL);
        printf("server send:%s",revbuf);
    }
    close(lfd);
    return 0;
}



多播

在这里插入图片描述

在这里插入图片描述



setsockopt 设置组播

  • 结构体imeq :保存多播地址ip 和本地ip
  • level: IPPROTO_IP (协议族)
  • optname :IP_MULTICAST_IF(表示多播)

    在这里插入图片描述



多播通信案例

  • 服务端

    • 设置多播属性,设置外出接口 ,没有绑定
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

int main() {

    // 1.创建一个通信的socket
    int fd = socket(PF_INET, SOCK_DGRAM, 0);
    if(fd == -1) {
        perror("socket");
        exit(-1);
    }   

    // 2.设置多播的属性,设置外出接口
    struct in_addr imr_multiaddr;
    // 初始化多播地址
    inet_pton(AF_INET, "239.0.0.10", &imr_multiaddr.s_addr);
    setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, &imr_multiaddr, sizeof(imr_multiaddr));
    
    // 3.初始化客户端的地址信息
    struct sockaddr_in cliaddr;
    cliaddr.sin_family = AF_INET;
    cliaddr.sin_port = htons(9999);
    inet_pton(AF_INET, "239.0.0.10", &cliaddr.sin_addr.s_addr);

    // 3.通信
    int num = 0;
    while(1) {
       
        char sendBuf[128];
        sprintf(sendBuf, "hello, client....%d\n", num++);
        // 发送数据
        sendto(fd, sendBuf, strlen(sendBuf) + 1, 0, (struct sockaddr *)&cliaddr, sizeof(cliaddr));
        printf("组播的数据:%s\n", sendBuf);
        sleep(1);
    }

    close(fd);
    return 0;
}
  • 客户端

    • 设置多播地址、加入到多播组
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

int main() {

    // 1.创建一个通信的socket
    int fd = socket(PF_INET, SOCK_DGRAM, 0);
    if(fd == -1) {
        perror("socket");
        exit(-1);
    }   

    struct in_addr in;
    // 2.客户端绑定本地的IP和端口
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(9999);
    addr.sin_addr.s_addr = INADDR_ANY;
	
    int ret = bind(fd, (struct sockaddr *)&addr, sizeof(addr));
    if(ret == -1) {
        perror("bind");
        exit(-1);
    }
	//设置多播地址属性
    struct ip_mreq op;
    inet_pton(AF_INET, "239.0.0.10", &op.imr_multiaddr.s_addr);
    op.imr_interface.s_addr = INADDR_ANY;

    // 加入到多播组
    setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &op, sizeof(op));

    // 3.通信
    while(1) {
        
        char buf[128];
        // 接收数据
        int num = recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL);
        printf("server say : %s\n", buf);

    }

    close(fd);
    return 0;
}



本地套接字通信



创建通信流程(服务端、客户端 )

  • 注意创建socket的参数设置:

    AF_LOCAL 、 SOCK_STREAM
  • 绑定成功之后,指定的sun_path中的套接字文件(server.sock)会

    自动生成。
  • 要用strcpy 给套接字文件命名

    strcpy(addr.sun_path, "server.sock")

    。因为数组名是指针常量,是不能被修改的。

    在这里插入图片描述



本地套接字通信案例

  • 服务端

    strcpy(addr.sun_path, “server.sock”);
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/un.h>

int main() {

    unlink("server.sock");

    // 1.创建监听的套接字
    int lfd = socket(AF_LOCAL, SOCK_STREAM, 0);
    if(lfd == -1) {
        perror("socket");
        exit(-1);
    }

    // 2.绑定本地套接字文件
    struct sockaddr_un addr;
    addr.sun_family = AF_LOCAL;
    strcpy(addr.sun_path, "server.sock");
    int ret = bind(lfd, (struct sockaddr *)&addr, sizeof(addr));
    if(ret == -1) {
        perror("bind");
        exit(-1);
    }

    // 3.监听
    ret = listen(lfd, 100);
    if(ret == -1) {
        perror("listen");
        exit(-1);
    }

    // 4.等待客户端连接
    struct sockaddr_un cliaddr;
    int len = sizeof(cliaddr);
    int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
    if(cfd == -1) {
        perror("accept");
        exit(-1);
    }

    printf("client socket filename: %s\n", cliaddr.sun_path);

    // 5.通信
    while(1) {

        char buf[128];
        int len = recv(cfd, buf, sizeof(buf), 0);

        if(len == -1) {
            perror("recv");
            exit(-1);
        } else if(len == 0) {
            printf("client closed....\n");
            break;
        } else if(len > 0) {
            printf("client say : %s\n", buf);
            send(cfd, buf, len, 0);
        }

    }

    close(cfd);
    close(lfd);

    return 0;
}
  • 客户端

    strcpy(addr.sun_path, “client.sock”);
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/un.h>

int main() {

    unlink("client.sock");

    // 1.创建套接字
    int cfd = socket(AF_LOCAL, SOCK_STREAM, 0);
    if(cfd == -1) {
        perror("socket");
        exit(-1);
    }

    // 2.绑定本地套接字文件
    struct sockaddr_un addr;
    addr.sun_family = AF_LOCAL;
    strcpy(addr.sun_path, "client.sock");
    int ret = bind(cfd, (struct sockaddr *)&addr, sizeof(addr));
    if(ret == -1) {
        perror("bind");
        exit(-1);
    }

    // 3.连接服务器
    struct sockaddr_un seraddr;
    seraddr.sun_family = AF_LOCAL;
    strcpy(seraddr.sun_path, "server.sock");
    ret = connect(cfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
    if(ret == -1) {
        perror("connect");
        exit(-1);
    }

    // 4.通信
    int num = 0;
    while(1) {

        // 发送数据
        char buf[128];
        sprintf(buf, "hello, i am client %d\n", num++);
        send(cfd, buf, strlen(buf) + 1, 0);
        printf("client say : %s\n", buf);

        // 接收数据
        int len = recv(cfd, buf, sizeof(buf), 0);

        if(len == -1) {
            perror("recv");
            exit(-1);
        } else if(len == 0) {
            printf("server closed....\n");
            break;
        } else if(len > 0) {
            printf("server say : %s\n", buf);
        }

        sleep(1);

    }

    close(cfd);
    return 0;
}



阻塞与非阻塞、同步与异步(网络IO)

  • 典型的一次IO的两个阶段是什么?数据就绪 和 数据读写


    数据就绪

    :根据系统IO操作的就绪状态,

    分为阻塞、非阻塞

    。如(read函数调用,非阻塞状态要通过返回值去判断)


    数据读写

    :根据应用程序和内核交互的方式,

    分为同步、异步

    。(异步api:aio_read(), aio_write())

  • 如何区分同步和异步?


    同步

    :表示A向B请求调用一个网络IO接口时(或者调用某个业务逻辑API接口时),数据的读写都是

    由请求方A自己来完成的(不管是阻塞还是非阻塞);


    异步

    :表示A向B请求调用一个网络IO接口时(或者调用某个业务逻辑API接口时),向B传入请求的事件以及事件发生时通知的方式,A就可以处理其它逻辑了,当B监听到事件处理完成后,会用事先约定好的通知方式,通知A处理结果。

    • 在处理 IO 的时候,阻塞和非阻塞都是同步 IO,只有使用了特殊的 API 才是异步 IO。
    • 或者说自己应用程序处理是同步,交给内核去处理,自己应用程序往下执行,等待内核传递对应信号是异步



Unix、Linux上的五种IO模型

阻塞、非阻塞模型、IO复用、信号驱动、异步IO模型



阻塞

调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的去检查这个函数有没有返回,必

须等这个函数返回才能进行下一步动作。

在这里插入图片描述



非阻塞

非阻塞等待,每隔一段时间就去检测IO事件是否就绪。没有就绪就可以做其他事。非阻塞I/O执行系统调

用总是立即返回,不管事件是否已经发生,若事件没有发生,则返回-1,此时可以根据

errno 区分

这两种情况,对于accept,recv 和 send,事件未发生时,errno 通常被设置成

EAGAIN



在这里插入图片描述



IO复用

Linux 用 select/poll/epoll 函数实现 IO 复用模型,这些函数也会使进程阻塞,但是和阻塞IO所不同的是

这些函数可以同时阻塞多个IO操作。而且可以同时对多个读操作、写操作的IO函数进行检测。直到有数

据可读或可写时,才真正调用IO操作函数。

在这里插入图片描述



信号驱动

Linux 用套接口进行信号驱动 IO,安装一个信号处理函数,进程继续运行并不阻塞,当IO事件就绪,进

程收到SIGIO 信号,然后处理 IO 事件。

在这里插入图片描述

内核在第一个阶段是异步,在第二个阶段是同步;

与非阻塞IO的区别

在于它提供了消息通知机制,不需

要用户进程不断的轮询检查,减少了系统API的调用次数,提高了效率。



异步

Linux中,可以调用 aio_read 函数告诉内核描述字缓冲区指针和缓冲区的大小、文件偏移及通知的方

式,然后立即返回,当内核将数据拷贝到缓冲区后,再通知应用程序。

在这里插入图片描述



版权声明:本文为yyjshang原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。