Unix域协议

Unix域协议
    socket套接字也可以实现一台主机上的不同进程间通信。
    socket--->IPC手段
    
    socket实现进程间通信用到了另外一个协议:
        Unix域协议,简称AF_UNIX或AF_LOCAL
        
        Unix与协议提供了两类套接字:
            SOCK_STREAM        字节流套接字(类似TCP)
            SOCK_DGRAM        数据报套接字(类似UDP)
        
        虽然UDP协议是不可靠的,但是Unix域数据报服务是可靠的。
        不会丢失消息,也不会传递出错,因为本质上就是在同一台主机上面进行通信。
        
        Unix域协议的区别:
            1.    和TCP/UDP相比,速度更快,数据不需要传递到主机外,也不需要封包和拆包的过程。
            2.    相对于IP协议来讲,IP协议是通过IP地址+端口号标识客户端和服务器
                Unix域协议是使用普通文件系统中的路径名标识客户端和服务器。
                
        Unix域协议大部分的编程流程和函数API结构都与TCP类似,
        只不过Unix域协议使用路径名描述一个地址
        
        //
        #include <sys/un.h>
        define UNIX_PATH_MAX 108
        
        //Unix域协议的地址结构体
        struct sockaddr_un
        {
            sa_familt_t sun_family;//AF_UNIX/AF_LOCAL
            char sun_path[UNIX_PATH_MAX];//Unix域协议地址,是以'\0'为结尾的本地文件系统
                                //以绝对路径形式存在。“/home/china/long”
        };
        
        Unix域协议字节流套接字编程方法(类似于TCP编程):
            Server:先创建套接字---> bind ---> 监听 ---> 接收客户端的连接请求 ---> 连接成功
                    ---> read/write ---> close
            
            Client:    先创建套接字 ---> connect ---> read/write ---> close
            
            
        做一个DNS客户端向DNS服务器提交请求解析某一个某一个域名对应的IP地址。
        

五、IO模型
    IO模型---->输入输出模型
    
    可以把IO(输入输出)理解为两个步骤:
    1.    等待IO事件就绪。
        如果可以读,我们等待缓冲区内有数据就可以读,如果可以写,我们就等到缓冲区内有空间
        可以写。
    2.    第二步才是真正意义上的数据的迁移。
        涉及到内核态和用户态的切换。
        
    
    Linux中五种IO模型:
    1)    阻塞IO
        读:如果有数据(即使数据少于你要读取的字节数),直接返回数据
            如果没有数据,则阻塞(等待IO事件就绪),直到有数据或者出错
        
        写:如果有空间(即使空间少于你要写的字节数)可以写,直接写入
            如果没有空间让你写,则阻塞直到有空间写或者出错。
            
        这种模型是最常见的,最简单的,效率最低的一种IO模型,是默认的IO模型
        
            read/write/sendto/recvform....都是属于阻塞IO。
    
    2)    非阻塞IO
        能读(有数据)就读,不能读(没有数据)就立即返回一个错误码。
        能写(有空间)就写,不能写(没有空间)就立即返回一个错误码。
        
        虽然这种不会阻塞,但是有一个缺点,这种方式相对于阻塞IO可能会更加效率低。
        会浪费掉大量的CPU。
        
        非阻塞IO采用的方式有点类似于轮询,所以效率可能会更低。
    
    3)    IO多路复用
        允许同时对多个IO事件进行控制,同时监控多个“文件描述符”。
        
        IO多路复用实现的机制是通过select/poll/epoll函数来实现的。
        
        
    4)    信号驱动IO
        如果有IO事件就绪了,就可以发一个信号给应用程序,进行数据的处理。
        相当于注册了一个信号处理函数,当数据准备好了,就给我发一个信号
        当我捕捉到这个信号之后,就去我的信号处理函数中去获取数据即可。
        所以信号驱动IO是一种非阻塞IO。
        
    5)    异步IO
        应用的较少。
        
        进程仅仅是发起对数据的请求,而数据的监控和迁移由别的进程来完成。
        
        从阻塞程度上来说:
            阻塞IO > 非阻塞IO > 多路复用IO > 信号驱动IO > 异步IO
            
    1.    文件的读写方式(阻塞/非阻塞)的改变
        文件的默认读写方式是阻塞。
        
        在open的时候加上O_NOBLOCK选项,则可以以非阻塞的形式打开文件。
        
        通过fcntl函数去改变一个已经打开的文件的文件性质的。
        
        NAME
            fcntl - manipulate file descriptor

        SYNOPSIS
            #include <unistd.h>
            #include <fcntl.h>

            int fcntl(int fd, int cmd, ... /* arg */ );

            fcntl改变文件的状态,具体的操作由命令号cmd来决定。
            
            fcntl有五种功能:
            1.    复制一个现有的文件描述符(cmd == F_DUPFD)
                让多个文件描述符指向同一个文件。
                复制一个现有的文件描述符fd,新的文件描述符作为函数的返回值返回。
                比如:
                    int new_fd = fcntl(old_fd,F_DUPFD,4);
                    第三个参数表示新的文件描述符的最小值。
                    如果如果4被占用了,那么新的文件描述符就+1,直到找到一个没有被占用。
                    
            2.    设置/获取文件描述符标志(cmd == F_GETFD/F_SETFD)
                cmd == F_GETFD,第三个参数不需要设置
                    作用是取得与文件描述符关联的状态标志,目前与文件描述符关联的
                    状态标志只有一个close_on_exec(FD_CLOEXEC).
                    
                    FD_CLOEXEC的作用是:
                        是一个进程所有的文件描述符的位图标志,每一个比特代表一个打开的
                        文件描述符,用于确定在调用exec时需要关闭的文件描述符。
     
                        如果对应的标志位置为1,则子进程在执行exec时该描述符将会被关闭
                        如果对应的标志位置为0,则子进程在执行exec时该描述符将会维持被打开的状态
                        
                cmd == F_SETFD,第三个参数为新的文件描述符的状态标记
                    
            3.    获取/设置文件状态标志(cmd == F_GETFL/F_SETFL)    
                F_GETFL    :
                    第三个参数不需要设置
                    
                    文件所有的状态标记作为返回值返回
                    文件的状态标记有:
                        O_RDONLY/O_WRONLY/O_RDWR/O_APPEND/O_NOBLOCK.....
                    
                    这些标记,保存在一个struct file结构体的一个成员变量中:
                        unsigned long f_flags;
                        
                    是通过位域来实现,各个状态只是变量中的某一位。
                    
                    
                F_SETFL:新的文件状态标志按照第三个参数设置
                    例子:
                        判断一个打开的文件,是否为非阻塞
                        unsigned long f_flags = fcntl(fd,F_GETFL);
                        if(f_flags & O_NOBLOCK)
                        {
                            //文件是阻塞的
                        }
                        else
                        {
                            //文件是非阻塞的
                        }
                    
                        设置文件的阻塞方式
                        unsigned long f_flags = fcntl(fd,F_GETFL);
                        f_flags &= ~O_NOBLOCK;//去掉非阻塞标志,其他标志不动
                        fcntl(fd,F_SETFL,f_flags);
                        
            4.    获取/设置文件的属主进程
            5.    获取/设置文件记录锁
                当一个进程正在修改或者读取文件的时候,可以阻止另一个进程去访问文件。
            
    3)    多路复用IO
        用来实现对多个文件描述符进行IO“监听”。
        一般用在网络服务器中,可以并发的处理多个客户端的连接和请求。
        
        1.    select
            实现原理:
            1.    将需要监听的文件描述符放在一个监听集合中,将这个集合拷贝到内核中去。
            2.    在内核中创建一个内核线程,有这个线程去轮询所有的文件描述符,这个线程
                是处于内核空间,所有这个CPU占用的是内核的执行时间,而不是占用用户的
                执行时间,当一个或者多个文件描述符就绪(可读了,可写了,出错了)时,
                就将就绪的文件描述符集合拷贝到用户空间中去。
            3.    用户去处理就绪的文件描述符。
    
               /* According to earlier standards */
                #include <sys/time.h>
                #include <sys/types.h>
                #include <unistd.h>

                int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);

                void FD_CLR(int fd, fd_set *set);
                    将fd指定的文件描述符从set指定的集合中移除。
                
                int  FD_ISSET(int fd, fd_set *set);
                    判断fd指定的文件描述符是否存在于set指定的集合中
                
                void FD_SET(int fd, fd_set *set);
                    把fd指定的文件描述符加入到set指定的集合中
                
                void FD_ZERO(fd_set *set);
                    把set指向的文件描述符集合清空

                select用类型fd_set来表示一个文件描述符的集合,因为你需要监听多个文件描述符
                需要一个fd_set类型的集合来保存你所有需要监听的文件描述符。
    
                但是可能有的文件描述符是监听可读,有的文件描述符是监听可写,
                有的监听是否出错,所有,就有三个文件描述符集合分别存储你要监听的
                文件描述符。
    
                readfds:监听是否可读的文件描述符集合
                writefds:监听是否可写的文件描述符集合
                exceptfds:监听是否出错的文件描述符集合
                
                参数:
                    nfds:指这个集合中所有文件描述符的范围
                        或者说监听的文件描述符的个数。
                        你需要监听的所有文件描述符中的最大值+1
                        内核是从0文件描述符开始轮询,到你指定的最大的文件描述符。
                
                    readfds:监听可读的文件描述符集合
                        返回的时候,里面保存的是所有的已经可读的文件描述符
                
                    writefds:监听可写的文件描述符集合
                        返回的时候,里面保存的是所有的已经可写的文件描述符    

                    exceptfds:监听出错的文件描述符集合
                        返回的时候,里面保存的是所有的已经出错的文件描述符


                        上面三个集合,如果不需要监听则可指定全部为NULL
                    
                        select(max_fd,NULL,NULL,NULL,2);//sleep(2)

                        select(nfds,&readfds,NULL,NULL,&timeout);

                    timeout:超时时间,在指定的时间内还没有文件描述符就绪,也返回
                        struct timeval
                        {
                            long tv_sec;    //秒
                            long tv_usec;    //毫秒
                        };
                        
                        等待分为三种情况:
                            1.    如果把这个参数设置为NULL,表示一直等待下去,“死等”
                                直到至少有一个文件描述符继续。
                            
                            2.    等待一段固定的时间(有timeval参数指定等待多久)。
                                在等待的时间内,正常等待,一旦超时,还没有文件描述符就绪的
                                话,就立即返回。同时超时时间全部清0。

                                struct timeval timeout;
                                timeout.tv_sec = 2;
                                timeout.tv_usec = 0;
                                select(nfds,&readfds,NULL,NULL,&timeout);//只监听是否可读集合2秒
                                //tv_sec == 0,tv_usec == 0
                            
                            3.    根本不等待。
                                轮询一次后立即返回。
                                该参数必须指定struct timeval中的成员变量的值都为0。
                            
                    返回值:
                        > 0     表示已经就绪的文件描述符的个数
                                由于你监听了多个文件描述符,至于你到底是哪些文件描述符就绪了
                                你必须在select返回后,使用FD_ISSET一个一个的去测试。

                        == 0     超时了,等待的指定的时间到了。
                    
                        < 0     表示select函数出错了,errno被设置。
                        
            简单的伪代码逻辑:
                
                fd_set readfds;//需要监听可读的文件描述符的集合
                int max_fd = 0;
                struct timeval timeout;
                
                while(1)
                {
                    //把所有需要监听的文件描述符加入到集合
                    //select每次返回的时候,集合里面只保存了就绪的文件描述符所以你每次
                    //重新监听前需要重新将你想要监听的文件描述符加入到集合中去
                    //将文件描述符集合清空
                    FD_ZERO(&readfds);
                    while(...)//不断的把你想要监听的文件描述符加入到集合中去
                    {
                        FD_SET(fd,&readfds);
                        max_fd = (fd >= max_fd) ? fd : max_fd;//记录所有加入集合的文件描述符的最大值
                    }
                    
                    timeout.tv_sec = 10;//等待10秒
                    timeout.tv_usec = 0;
                    
                    //使用select监听指定的集合
                    int r = select(max_fd + 1,&readfds,NULL,NULL,&timeout);
                    if(r == 0)//超时了
                    {
                        continue;
                    }
                    else if(r < 0)
                    {
                        perror("select error");
                        return -1;//continue;
                    }
                    
                    //r > 0 有就绪的文件描述符啦
                    while(...)//所有加入集合的文件描述符都需要遍历
                    {
                        if(FD_ISSET(fd,&readfds))
                        {
                            //fd是可读
                            //对fd进行读
                        }
                    }
                }

        2.    poll
            poll的原理基本上和select类似,poll是在内核中使用链表来存储文件描述符集合
            而select是使用数组去存储(数组空间有限,所以最多监听1024个文件描述符)。
            poll监听的文件描述符个数不限。

            NAME
                poll, ppoll - wait for some event on a file descriptor
                    在文件描述符上等待一些事件

            SYNOPSIS
                #include <poll.h>

                int poll(struct pollfd *fds, nfds_t nfds, int timeout);
                        poll与select类似,但是poll是用结构体struct pollfd描述
                        你要监听的文件描述符以及你要监听的事件。
                    struct pollfd {
                        int   fd;         /* file descriptor */
                            //你要监听的文件描述符
                        
                        short events;     /* requested events */
                            //你要监听的文件描述符的事件
                            //你所期待文件描述符发生的事件,事件码使用位域来实现。
                            
                            //POLLIN:期待文件描述符可读
                            //POLLOUT:期待文件描述符可写
                            //POLLERR:期待文件描述符出错
                            
                            如:
                                POLLIN | POLLOUT 监听是否可读可写
                            
                        short revents;    /* returned events */
                            //已经就绪的事件
                            如:是否已经可读
                            if(revents & POLLIN)
                            {
                                //可读啦
                            }
                    };
                    
                    监听一个文件描述符的需要一个这样的结构体。
                    如果需要监听多个文件描述符的话就需要多个这样的结构体。
                    
                    fds:struct pollfd *的指针,指向你要监听的所有的事件。
                        实际上就是监听结构体struct pollfd的数组
                    
                    nfds:表示上面那个数组中的元素个数
                    
                    timeout:超时时间,单位是ms
                    
                    返回值:
                        > 0     表示就绪的文件的个数
                            由于你监听多个文件描述符,至于到底是哪些文件描述符就绪了、
                            你必须在poll返回后,一个一个的去测试每一个结构体中的
                            revents成员变量。
                        
                        = 0    超时了
                        < 0 出错了

                #define _GNU_SOURCE         /* See feature_test_macros(7) */
                #include <signal.h>
                #include <poll.h>

                int ppoll(struct pollfd *fds, nfds_t nfds,
                   const struct timespec *tmo_p, const sigset_t *sigmask);
                    与poll类似,时间更加精确(微妙级),去屏蔽一些信号。
                    
            简单的伪代码:
                while(1)
                {
                    struct pollfd fds[100];//需要监听的事件集合数组
                    for(i = 0;i < 你需要监听的文件描述符个数;i++)
                    {
                        fds[i].fd = fd;
                        fds[i].event = POLLIN | POLLOUT;//监听fd是否可读可写
                        fds[i].revents = 0;//初始化的时候成员变量没有值
                    }
                    
                    //多路复用poll
                    int r = poll(fds,100,2000);
                    if(r < 0)
                    {
                        perror("poll failed");
                        return -1;//continue;
                    }
                    else if(r == 0)
                    {
                        printf("timeout!\n");
                        continue;
                    }
                    
                    //表示有文件描述符就绪了,就绪的文件描述符就保存在fds中
                    //但是你不知道是哪一个就绪了,所以需要测试
                    for(i = 0;i < 100;i++)
                    {
                        if(fds[i].revents & POLLIN)
                        {
                            //fds[i].fd这个文件描述符可读了
                            //read
                        }
                        else if(fds[i].revents & POLLOUT)
                        {
                            //fds[i].fd这个文件描述符可写了
                            //write
                        }                        
                    }
                }
                    
        3.    epoll
            select和poll的效率都不高。
            1.    因为这些文件描述符每一次执行函数(select/poll)都会被拷贝两次。
            2.    select和poll每次需要去遍历所有的文件描述符才能确定哪些文件描述符就绪了。
            
            epoll不会随着文件描述符的数量增加而降低效率。在返回的时候,只返回一个就绪队列。
            
            epoll的实现接口函数,只有三个函数:
                1)    创建epoll的句柄
                    SYNOPSIS
                    #include <sys/epoll.h>

                    int epoll_create(int size);
                    创建/打开一个监听文件的集合,这个函数的返回值是一个文件描述符。
                    
                    size:用来告诉内核这个监听的数量一共有多大。
                        现在这个参数其实是被忽略的,意思就是你只要填>0的数
                        就意味着监听任意多个文件描述符。
                        同时epoll本身也会占用一个文件描述符,使用完毕后需要关闭掉它。
                    
                    返回值:成功返回一个epoll的实例(本质就是一个文件描述符),失败返回NULL
                        同时errno被设置。

                2)    将需要监听的文件描述符加入到epoll句柄或者从句柄中删除要监听的文件描述符
                    #include <sys/epoll.h>

                    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

                    对于指定的需要监听的文件描述符,只需要加入一次即可。
                    会记录到内核链表中去,不想select和poll每一次监听都需要重新加入。
                    
                        epfd:epoll的实例,表示你要操作哪一个epoll的文件描述符
                        op:具体对epoll实例进行何种操作
                            EPOLL_CTL_ADD:增加fd指定的文件描述符到epfd所表示的实例中去
                            EPOLL_CTL_MOD:修改fd指定的文件描述符监听的事件
                            EPOLL_CTL_DEL:从监听集合中删除fd指定的文件描述符
                            
                        fd:要操作(ADD/MOD/DEL)的文件描述符
            
                        event:要监听fd的哪一些事件
                            
                            typedef union epoll_data {
                                void        *ptr;
                                int          fd;
                                uint32_t     u32;
                                uint64_t     u64;
                            } epoll_data_t;

                            //事件描述结构体
                            struct epoll_event {
                                uint32_t  events;      /* Epoll events */
                                    //要监听的事件,使用位域来实现,不同的事件占events不同的bit位
                                    //主要要监听的事件由:
                                    //EPOLLIN : 监听的事件为可读事件
                                    //EPOLLOUT : 监听的事件为可写事件
                                    //EPOLLERR : 监听的事件是否出错
                                    //EPOLLRDHUP : 监听流式套接字(UDP)对方是否关闭
                                    //EPOLLLET : Edge-Triggered
                                    //边缘触发
                                    //这个标志表示监听的文件的数据有变化时,才会报告事件
                                    //有两种模式:
                                    //LT:Level-Triggered 只要有数据,就会不停的上报可读事件
                                    //默认行为为LT。
                                    //ET:Edge-Triggered 只有当有数据变化(数量)的时候,
                                    //才会报告可读事件
                                    //LT:不停的上报可读事件
                                    //ET:只有在数据的数量发生变化的时候才会上报事件
                                    
                                epoll_data_t data;        /* User data variable */
                                    //用户自定义的数据,用来保存用户的一些数据
                            };
            
                     
                        成功返回0,失败返回-1,同时errno被设置。
                        
                3)    等待监听事件的发生
                           #include <sys/epoll.h>

                int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
                    epfd:需要监听的epoll的实例。
                    events:指针,指向一个结构体数组,用来保存已经就绪的事件的信息。
                    maxevents:表示第二个参数结构体数组的最多可以保存多少个事件结构体。
                    timeout:超时时间,单位ms
                    
                    返回值:
                        >0     已经就绪的文件描述符的个数
                            已经就绪的事件的信息是直接保存在events指向的数组中
                            struct epoll_event结构体中有一个成员变量,可以保存用户的数据    
                            一般这个成员变量用来保存要监听的文件描述符本身。
                            当函数返回时,直接去操作事件结构体就可以了,不需要轮询所有的文件
                            描述符。
                            
                        =0     超时了
                        <0  出错,同时errno被设置。
                        
                简单的伪代码:    
                    int epollfd = epoll_create(10);
                    struct epoll_event ev;
                    //只需要添加一次即可
                    while(....)//可能有很多文件描述符需要添加
                    {
                        ev.events = EPOLLIN;//记录监听的事件
                        ev.data = fd;//记录事件本身的文件描述符
                        epoll_ctl(epollfd,EPOLL_CTL_ADD,&event);
                    }
                    
                    while(1)
                    {
                        struct epoll_event e[10];//记录监听结果的结构体数组
                        int r = epoll_wait(epollfd,e,10,2000);
                        if(r == 0)
                        {
                            printf("time out!\n");
                            continue;
                        }
                        else if(r < 0 )
                        {
                            perror("epoll failed");
                            return -1;
                        }
                        
                        //所有的就绪信息保存到了e数组中
                        //不需要轮询所有的文件描述符
                        for(i = 0;i < r;i++)
                        {
                            //e[i].events 就是表示就绪的事件
                            //e[i].data.fd 就是表示是哪一个文件描述符就绪了
                            if(e[i].events & EPOLLIN)
                            {
                                //表示e[i].data.fd文件描述符可读
                            }
                            else if(e[i].events & EPOLLOUT)
                            {
                                //表示e[i].data.fd文件描述符可写
                            }
                        }
                    }
                    
                
    项目:
        实现一个简单的FTP服务器文件传输助手
            功能:通过网络传输,实现文件的跨设备传输,包含服务器和客户端
            服务器功能:
                等待客户端的连接,支持多客户端并发,根据客户端发送过来的命令,执行
                相应的操作,并向客户端发送其所需要的数据。
            
            客户端功能:
                负责连接服务器后,发服务器发送命令(命令从键盘获取),并等待服务器的
                相应,同时处理服务器回复的数据。
                
            命令:
                1.    ls    
                    用来获取服务器目录下的文件信息(文件名)
                
                2.    get file
                    用来从服务器中获取指定的文件名的文件
                    如果服务器存在此文件,则服务器回复文件内容
                    如果服务器不存在该文件,则回复错误码
                
                3.    put file
                    用来上传文件到服务器
                    如果服务器愿意接受文件,则后续发送文件数据
                    如果服务器不愿意接受数据,则不发送
                    
                4.    bye
                    用来告诉服务器,客户端将要断开连接
                    发送bye之后,客户端关闭套接字
                    服务器收到bye之后,关闭与客户端连接的套接字,并结束该客户端处理分支

详情见ftp服务器

 

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值