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

文章目录

  • 网络结构模式
  • MAC地址、IP地址、端口
  • 网络七层模型
  • TCP/IP 四层模型
  • 协议
  • 封装与分用(传输用到的)
  • Socket通信
    • 概述
    • 字节序
      • 字节序转换函数
    • Socket地址
      • 专有的Socket地址
    • IP地址转换(字符串ip-整数 ,主机、网络字节序的转换)
    • TCP通信流程
      • TCP和UDP对比
      • 通信流程
    • socket(套接字)函数
    • TCP三次握手、四次挥手
      • 三次握手
      • 四次挥手
    • TCP滑动窗口
    • 多进程实现并发服务器
    • 多线程实现并发服务器
    • TCP状态转换
    • 端口复用
      • setsockopt 设置端口复用(还可以设置其他功能)
      • 设置端口复用的位置
    • IO多路复用(select、poll、epoll)
      • select
        • 概念
        • select函数详解
        • 操作fd_set函数
        • select 服务端应用案例
      • poll
        • poll函数详解
        • poll函数操作案例
    • ☼epoll
      • epoll多路复用图解
      • epoll相关函数详解
        • epoll_create()
        • epoll_crtl
        • epoll_wait
      • epoll 服务端操作案例
      • ❁epoll 工作模式
    • UDP通信
      • 读写函数详解
      • UDP通信案例
    • 广播
      • setsockapt详解![在这里插入图片描述](https://img-blog.csdnimg.cn/1662fab3f73c4785a08171418b815db9.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAeXlqc2hhbmc=,size_20,color_FFFFFF,t_70,g_se,x_16)
      • 广播通信案例
    • 多播
      • setsockopt 设置组播
      • 多播通信案例
    • 本地套接字通信
      • 创建通信流程(服务端、客户端 )
      • 本地套接字通信案例
  • 阻塞与非阻塞、同步与异步(网络IO)
  • Unix、Linux上的五种IO模型
    • 阻塞
    • 非阻塞
    • IO复用
    • 信号驱动
    • 异步

网络结构模式

  • 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是因为有 一个网关地址和一个广播地址Linux下C/C++开发之网络编程_第1张图片

  • 子网掩码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)

网络七层模型

Linux下C/C++开发之网络编程_第2张图片

TCP/IP 四层模型

Linux下C/C++开发之网络编程_第3张图片

协议

Linux下C/C++开发之网络编程_第4张图片

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

Linux下C/C++开发之网络编程_第5张图片

  • 网络通信过程

Linux下C/C++开发之网络编程_第6张图片Linux下C/C++开发之网络编程_第7张图片
Linux下C/C++开发之网络编程_第8张图片

  • ARP请求例子
    简述:整个过程就是当前A主机向整个局域网的主机发送ARP请求,只有ARP请求所包含的目标IP地址与B主机IP地址相同,B主机返回一个ARP应答。
    Linux下C/C++开发之网络编程_第9张图片

Socket通信

概述

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

字节序

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

Linux下C/C++开发之网络编程_第10张图片
Linux下C/C++开发之网络编程_第11张图片

字节序转换函数

  • 主机字节序转网络字节序:htons、htonl函数(host to net 无符号short/int)
  • 网络字节序转主机字节序:ntohs、ntohl(net to host 无符号short/int)
    端口一般用 htons 、ntohs
#include  
// 转换端口 
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  
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,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。
Linux下C/C++开发之网络编程_第12张图片

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

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

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

  1. 点分十进制字符串表示 IPv4 地址,以及用十六进制字符串表示 IPv6 地址
  2. 编程中我们需要先把它们转化为整数(二进制数)方能使用,而记录日志时则相反。
  3. 3 个函数可用于用点分十进制字符串表示的 IPv4或者6 地址和用 网络字节序整数表示的 IPv4或者6 地址之间的转换
#include  
p:点分十进制的IP字符串,n:表示network,网络字节序的整数
 int inet_pton(int af, const char *src, void *dst); 
  • af:地址族: AF_INET AF_INET6
  • src:需要转换的点分十进制的IP字符串
  • dst:转换后的结果保存在这个里面
  • 返回值 为0 表示成功!
#include  
将网络字节序的整数,转换成点分十进制的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对比

Linux下C/C++开发之网络编程_第13张图片

通信流程

Linux下C/C++开发之网络编程_第14张图片

  • 服务器端 (被动接受连接的角色)
  1. 创建一个用于监听的套接字
    监听:监听有客户端的连接 - 套接字:这个套接字其实就是一个文件描述符
  2. 将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息)
    客户端连接服务器的时候使用的就是这个IP和端口
  3. 设置监听,监听的fd开始工作
  4. 阻塞等待,当有客户端发起连接,解除阻塞,接受客户端的连接,会得到一个和客户端通信的套接字 (fd)
  5. 通信
    接收数据
    发送数据
  6. 通信结束,断开连接
  • 客户端
  1. 创建一个用于通信的套接字(fd)
  2. 连接服务器,需要指定连接的服务器的 IP 和 端口
  3. 连接成功了,客户端可以直接和服务器通信
    接收数据
    发送数据
  4. 通信结束,断开连接

socket(套接字)函数

Linux下C/C++开发之网络编程_第15张图片

TCP三次握手、四次挥手

三次握手

TCP 提供了一种可靠、面向连接、字节流、传输层的服务,采用三次握手建立一个连接。采用 四次挥手
来关闭一个连接。
Linux下C/C++开发之网络编程_第16张图片

  • 第一次握手
    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连接的时候,采用三次握手建立的的连接是双向的,在断开的时候需要双向断开。

Linux下C/C++开发之网络编程_第17张图片

TCP滑动窗口

  • 滑动窗口是 TCP 中实现诸如 ACK 确认、流量控制、拥塞控制的承载结构
    Linux下C/C++开发之网络编程_第18张图片
  • 图解
    Linux下C/C++开发之网络编程_第19张图片

多进程实现并发服务器

  • 服务端案例代码
    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
//字节序
#include
//socket通信
#include          
#include 
//exit
#include
#include
#include
//信号捕捉,子进程回收
#include
#include 
#include 

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
#include
#include          
#include 
#include
#include
#include



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
//字节序
#include
//socket通信
#include          
#include 
//exit
#include
#include
#include
#include
#include


//创建结构体的原因:  线程处理需要获取多个参数,那么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状态转换

Linux下C/C++开发之网络编程_第20张图片
Linux下C/C++开发之网络编程_第21张图片
绿线:服务端
红线:客户端

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

  • 2MSL(Maximum Segment Lifetime)
    1. 主动断开连接的一方, 最后进入一个 TIME_WAIT状态, 这个状态会持续: 2msl
    2. msl: 官方建议: 2分钟, 实际是30s
    3. 等待2msl的原因Linux下C/C++开发之网络编程_第22张图片

端口复用

端口复用最常用的用途:

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

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

#include  
#include  
// 设置套接字的属性(不仅仅能设置端口复用) 
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

概念

Linux下C/C++开发之网络编程_第23张图片

select函数详解

Linux下C/C++开发之网络编程_第24张图片

操作fd_set函数

fd_set 是一个文件描述符表 ,有1024位 。前0 1 2三位默认占用。
Linux下C/C++开发之网络编程_第25张图片

select 服务端应用案例

重点看:IO多路复用部分

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

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函数详解

Linux下C/C++开发之网络编程_第26张图片

poll函数操作案例

  • 注意:
  1. if(fds[i].revents & POLLIN) {//注意是用"&",而不用"==",因为 revents 可能 有 “|” 进行拼接多个事件,双等号不能判断。
  2. 关闭客户端文件描述符后要对myfd 中得对应位置进行复位
#include 
#include 
#include 
#include 
#include 
#include 


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多路复用图解

Linux下C/C++开发之网络编程_第27张图片
struct rb_root rbr 是红黑树,查找效率高 告诉内核需要监听的文件描述符
struct list_head rdlist 是双向链表 用于记录 文件描述符变化
相较于 select 和 poll 节省了cpu算力,提高工作效率。

epoll相关函数详解

epoll_create()

Linux下C/C++开发之网络编程_第28张图片

epoll_crtl

  • 结构体 epoll_event
    Linux下C/C++开发之网络编程_第29张图片
  • 上面结构体内的 epoll_data_t data
    • 主要用于确定类型(这里是fd 文件描述符)
      Linux下C/C++开发之网络编程_第30张图片
  • epoll_crtl 函数详解
    Linux下C/C++开发之网络编程_第31张图片

epoll_wait

Linux下C/C++开发之网络编程_第32张图片

epoll 服务端操作案例

#include 
#include 
#include 
#include 
#include 
#include 
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
    Linux下C/C++开发之网络编程_第33张图片
    LT服务端示例代码
  • 相较于之前的epoll服务端代码 ,读缓冲区大小改为 5
#include 
#include 
#include 
#include 
#include 
#include 

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
    Linux下C/C++开发之网络编程_第34张图片
    ET服务端示例代码
  • 如何设置ET?
  • read函数如何设置非阻塞?使得服务端可以不因为缓冲区小而读不全
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

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模式的关键字
    Linux下C/C++开发之网络编程_第35张图片

UDP通信

Linux下C/C++开发之网络编程_第36张图片

读写函数详解

Linux下C/C++开发之网络编程_第37张图片

UDP通信案例

  • 服务端
    与TCP通信的区别
    • int fd = socket(PF_INET, SOCK_DGRAM, 0); 这里是SOCK_DGRAM数据报格式,与tcp通信不同!
    • 没有添加监听listen
    • 通信api也不同
#include 
#include 
#include 
#include 
#include 

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 
#include 
#include 
#include 
#include 

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

广播

Linux下C/C++开发之网络编程_第38张图片

setsockapt详解Linux下C/C++开发之网络编程_第39张图片

广播通信案例

  • 服务端
    与TCP相比
    • int fd = socket(PF_INET, SOCK_DGRAM, 0); 这里是SOCK_DGRAM数据报格式。没有设置监听 、绑定
    • 需要设置广播属性 setsockopt,并创建一个广播地址
    • 通信API不同
#include 
#include 
#include 
#include 
#include 

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 
#include 
#include 
#include 
#include 

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

多播

Linux下C/C++开发之网络编程_第40张图片
在这里插入图片描述

setsockopt 设置组播

  • 结构体imeq :保存多播地址ip 和本地ip
  • level: IPPROTO_IP (协议族)
  • optname :IP_MULTICAST_IF(表示多播)
    Linux下C/C++开发之网络编程_第41张图片

多播通信案例

  • 服务端
    • 设置多播属性,设置外出接口 ,没有绑定
#include 
#include 
#include 
#include 
#include 

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 
#include 
#include 
#include 
#include 

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") 。因为数组名是指针常量,是不能被修改的。
    Linux下C/C++开发之网络编程_第42张图片

本地套接字通信案例

  • 服务端
    strcpy(addr.sun_path, “server.sock”);
#include 
#include 
#include 
#include 
#include 
#include 

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 
#include 
#include 
#include 
#include 
#include 

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模型

阻塞

调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的去检查这个函数有没有返回,必
须等这个函数返回才能进行下一步动作。
Linux下C/C++开发之网络编程_第43张图片

非阻塞

非阻塞等待,每隔一段时间就去检测IO事件是否就绪。没有就绪就可以做其他事。非阻塞I/O执行系统调
用总是立即返回,不管事件是否已经发生,若事件没有发生,则返回-1,此时可以根据 errno 区分这两种情况,对于accept,recv 和 send,事件未发生时,errno 通常被设置成 EAGAIN
Linux下C/C++开发之网络编程_第44张图片

IO复用

Linux 用 select/poll/epoll 函数实现 IO 复用模型,这些函数也会使进程阻塞,但是和阻塞IO所不同的是
这些函数可以同时阻塞多个IO操作。而且可以同时对多个读操作、写操作的IO函数进行检测。直到有数
据可读或可写时,才真正调用IO操作函数。
Linux下C/C++开发之网络编程_第45张图片

信号驱动

Linux 用套接口进行信号驱动 IO,安装一个信号处理函数,进程继续运行并不阻塞,当IO事件就绪,进
程收到SIGIO 信号,然后处理 IO 事件。
Linux下C/C++开发之网络编程_第46张图片
内核在第一个阶段是异步,在第二个阶段是同步;与非阻塞IO的区别在于它提供了消息通知机制,不需
要用户进程不断的轮询检查,减少了系统API的调用次数,提高了效率。

异步

Linux中,可以调用 aio_read 函数告诉内核描述字缓冲区指针和缓冲区的大小、文件偏移及通知的方
式,然后立即返回,当内核将数据拷贝到缓冲区后,再通知应用程序。
Linux下C/C++开发之网络编程_第47张图片

你可能感兴趣的:(网络,linux,c语言)