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

网络结构模式

  • 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是因为(第一,IP地址中的全0是个保留地址,意思是“本网络”。第二,网络号为127(即01111111)保留作为本地软件环回测试本主机的进程之间的通信之用。)在这里插入图片描述

  • 子网掩码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_insockaddr_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 函数告诉内核描述字缓冲区指针和缓冲区的大小、文件偏移及通知的方
式,然后立即返回,当内核将数据拷贝到缓冲区后,再通知应用程序。
在这里插入图片描述

  • 11
    点赞
  • 76
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
教程非常不错,价值280元,绝对是干货 Linux网络编程(总共41集) 讲解Linux网络编程知识,分以下四个篇章。 Linux网络编程之TCP/IP基础篇 Linux网络编程之socket编程篇 Linux网络编程之进程间通信篇 Linux网络编程之线程篇 Linux网络编程之TCP/IP基础篇 01TCPIP基础(一) ISO/OSI参考模型 TCP/IP四层模型 基本概念(对等通信、封装、分用、端口) 02TCPIP基础(二) 最大传输单元(MTU)/路径MTU 以太网帧格式 ICMP ARP RARP 03TCPIP基础(三) IP数据报格式 网际校验和 路由 04TCPIP基础(四) TCP特点 TCP报文格式 连接建立三次握手 连接终止四次握手 TCP如何保证可靠性 05TCPIP基础(五) 滑动窗口协议 UDP特点 UDP报文格式 Linux网络编程之socket编程篇 06socket编程(一) 什么是socket IPv4套接口地址结构 网络字节序 字节序转换函数 地址转换函数 套接字类型 07socket编程(二) TCP客户/服务器模型 回射客户/服务器 socket、bind、listen、accept、connect 08socket编程(三) SO_REUSEADDR 处理多客户连接(process-per-conection) 点对点聊天程序实现 09socket编程(四) 流协议与粘包 粘包产生的原因 粘包处理方案 readn writen 回射客户/服务器 10socket编程(五) read、write与recv、send readline实现 用readline实现回射客户/服务器 getsockname、getpeername gethostname、gethostbyname、gethostbyaddr 11socket编程(六) TCP回射客户/服务器 TCP是个流协议 僵进程与SIGCHLD信号 12socket编程(七) TCP 11种状态 连接建立三次握手、连接终止四次握手 TIME_WAIT与SO_REUSEADDR SIGPIPE 13socket编程(八) 五种I/O模型 select 用select改进回射客户端程序 14socket编程(九) select 读、写、异常事件发生条件 用select改进回射服务器程序。 15socket编程(十) 用select改进第八章点对点聊天程序 16socket编程(十一) 套接字I/O超时设置方法 用select实现超时 read_timeout函数封装 write_timeout函数封装 accept_timeout函数封装 connect_timeout函数封装 17socket编程(十二) select限制 poll 18socket编程(十三) epoll使用 epoll与select、poll区别 epoll LT/ET模式 19socket编程(十四) UDP特点 UDP客户/服务基本模型 UDP回射客户/服务器 UDP注意点 20socket编程(十五) udp聊天室实现 21socket编程(十六) UNIX域协议特点 UNIX域地址结构 UNIX域字节流回射客户/服务 UNIX域套接字编程注意点 22socket编程(十七) socketpair sendmsg/recvmsg UNIX域套接字传递描述符字 Linux网络编程之进程间通信篇 23进程间通信介绍(一) 进程同步与进程互斥 进程间通信目的 进程间通信发展 进程间通信分类 进程间共享信息的三种方式 IPC对象的持续性 24进程间通信介绍(二) 死锁 信号量 PV原语 用PV原语解决司机与售票员问题 用PV原语解决民航售票问题 用PV原语解决汽车租赁问题 25System V消息队列(一) 消息队列 IPC对象数据结构 消息队列结构 消息队列在内核中的表示 消息队列函数 26System V消息队列(二) msgsnd函数 msgrcv函数 27System V消息队列(三) 消息队列实现回射客户/服务器 28共享内存介绍 共享内存 共享内存示意图 管道、消息队列与共享内存传递数据对比 mmap函数 munmap函数 msync函数 29System V共享内存 共享内存数据结构 共享内存函数 共享内存示例 30System V信号量(一) 信号量 信号量集结构 信号量集函数 信号量示例 31System V信号量(二) 用信号量实现进程互斥示例 32System V信号量(三) 用信号集解决哲学家就餐问题 33System V共享内存与信号量综合 用信号量解决生产者消费者问题 实现shmfifo 34POSIX消息队列 POSIX消息队列相关函数 POSIX消息队列示例 35POSIX共享内存 POSIX共享内存相关函数 POSIX共享内存示例 Linux网络编程之线程篇 36线程介绍 什么是线程 进程与线程 线程优缺点 线程模型 N:1用户线程模型 1:1核心线程模型 N:M混合线程模型 37POSIX线程(一) POSIX线程库相关函数 用线程实现回射客户/服务器 38POSIX线程(二) 线程属性 线程特定数据 39POSIX信号量与互斥锁 POSIX信号量相关函数 POSIX互斥锁相关函数 生产者消费者问题 自旋锁与读写锁介绍 40POSIX条件变量 条件变量 条件变量函数 条件变量使用规范 使用条件变量解决生产者消费者问题 41一个简单的线程池实现 线程池性能分析 线程池实现 网络编程, Linux, 密码
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值