UNP总结 Chapter 15~17 Unix域协议、非阻塞式I/O、ioctl操作

时间:2022-04-25 22:49:51

一、Unix域协议

Unix域协议并不是一个实际的协议族,它只是在同一台主机上进行客户-服务器通信时,使用与在不同主机上的客户和服务器间通信时相同的API(套接口或XTI)的一种方法

当客户和服务器在同一台主机上时,Unix域协议是IPC通信方式的一种替代品。

Unix域提供了两种类型的套接口:字节流套接口(与TCP类似)和数据报套接口(与UDP类似)。

 

1.Unix域套接口地址结构

struct sockaddr_un {
  sa_family_t sun_family;     /* AF_LOCAL */
  char        sun_path[104];  /* null-terminated pathname */
};

存放sun_path数组中的路径名必须以空字符结尾

 

2.socketpair函数

socketpair函数建立一对相互连接的套接口,这个函数支队Unix域套接口使用。

#include <sys/socket.h>
 
int socketpair(int family, int type, int protocol, int sockfd[2]);
//返回: 成功返回0,出错返回-1

family必须为AF_LOCAL,protocol必须为0,type可以是SOCK_STREAM或SOCK_DGRAM,新创建的两个套接口描述字作为sockfd[0]和sockfd[1]返回

创建的两个套接口是没有名字的,即没有涉及隐式bind。

指定type参数为SOCK_STREAM调用socketpair所得到的结果称为流管道(stream pipe),这和一般的Unix管道(由pipe函数生成)类似,但流管道是全双工的,即两个描述字都是可读写的

 

 

3.描述符传递

一般传递描述符的方法:

  • 在fork调用后,子进程共享父进程的所有打开的描述字
  • 在调用exec时所有描述字仍保持打开

第一个例子中进程打开一个描述字,调用fork,然后父进程关闭描述字,让子进程处理这个描述字。这样将一个打开的描述字从父进程传递到子进程。

两个进程之间传递描述符涉及的步骤:

1).创建一个字节流的或数据报的Unix域套接口

2).进程可以用任何返回描述字的Unix函数打开一个描述字:譬如open, pipe, mkfifo, socket或accept。

3).发送进程建立一个msghdr结构,其中包含要传递的描述字。

4).接收进程调用recvmsg在来自步骤1的Unix域套接字上接收这个描述符,传递描述字不是传递描述字的编号,而是在接收进程中创建一个新的描述字,指向内核的文件表中与发送进程发送的描述字相同的项。

 

UNP总结 Chapter 15~17 Unix域协议、非阻塞式I/O、ioctl操作

 

 

4.程序实例

#include     "unp.h"

int
my_open(const char *pathname, int mode)
{
    int     fd, sockfd[2], status;
    pid_t   childpid;
    char    c, argsockfd[10], argmode[10];

    Socketpair(AF_LOCAL, SOCK_STREAM, 0, sockfd);

    if ( (childpid = Fork()) == 0) { /* child process */
        Close(sockfd[0]);
        snprintf(argsockfd, sizeof(argsockfd), "%d", sockfd[1]);
        snprintf(argmode, sizeof(argmode), "%d", mode);
        execl("./openfile", "openfile", argsockfd, pathname, argmode,
              (char *) NULL);
         err_sys("execl error");
    }

    /* parent process - wait for the child to terminate */
    Close(sockfd[1]);           /* close the end we don't use */

    Waitpid(childpid, &status, 0);
    if (WIFEXITED(status) == 0)
        err_quit("child did not terminate");
    if ( (status = WEXITSTATUS(status)) == 0)
        Read_fd(sockfd[0], &c, 1, &fd);
    else {
        errno = status;         /* set errno value from child's status */
        fd = -1;
    }
    Close(sockfd[0]);
    return (fd);
}

 

二、非阻塞式I/O

1.概述

1).输入操作: read, readv, recv, recvfrom和recvmsg函数。

2).输出操作: write, writev, send, sendto和sendmsg函数。

3).接收外来连接: accept函数

4).初始化外出的连接: 用于TCP的connect函数

 

2.非阻塞读和写

我们维护两个缓冲区: to容纳从标准输入到服务器去的数据,fr容纳自服务器到标准输出来的数据

UNP总结 Chapter 15~17 Unix域协议、非阻塞式I/O、ioctl操作

UNP总结 Chapter 15~17 Unix域协议、非阻塞式I/O、ioctl操作

这里程序仅给出包含非阻塞的前半部分:

#include     "unp.h"

void
str_cli(FILE *fp, int sockfd)
{
    int     maxfdp1, val, stdineof;
    ssize_t n, nwritten;
    fd_set  rset, wset;
    char    to[MAXLINE], fr[MAXLINE];
    char    *toiptr, *tooptr, *friptr, *froptr;

    val = Fcntl(sockfd, F_GETFL, 0);
    Fcntl(sockfd, F_SETFL, val | O_NONBLOCK);

    val = Fcntl(STDIN_FILENO, F_GETFL, 0);
    Fcntl(STDIN_FILENO, F_SETFL, val | O_NONBLOCK);

    val = Fcntl(STDOUT_FILENO, F_GETFL, 0);
    Fcntl(STDOUT_FILENO, F_SETFL, val | O_NONBLOCK);

    toiptr = tooptr = to;       /* initialize buffer pointers */
    friptr = froptr = fr;
    stdineof = 0;

    maxfdp1 = max(max(STDIN_FILENO, STDOUT_FILENO), sockfd) + 1;
    for ( ; ; ) {
        FD_ZERO(&rset);
        FD_ZERO(&wset);
        if (stdineof == 0 && toiptr < &to[MAXLINE])
            FD_SET(STDIN_FILENO, &rset);     /* read from stdin */
        if (friptr < &fr[MAXLINE])
            FD_SET(sockfd, &rset);  /* read from socket */
        if (tooptr != toiptr)
            FD_SET(sockfd, &wset);  /* data to write to socket */
        if (froptr != friptr)
            FD_SET(STDOUT_FILENO, &wset);   /* data to write to stdout */
         Select(maxfdp1, &rset, &wset, NULL, NULL);

 

3.非阻塞connect

非阻塞的connect有三种用途:

1). 我们可以在三路握手同时做一些其他的处理。完成一个connect要花一个往返时间完成,而且可以是在任何地方,从几个毫秒的局域网到几百毫秒或几秒的广域网。

2). 可以用这种技术同时建立多个连接。这在Web浏览器中很普遍

3). 由于我们用select等待连接的完成,因此可以给select设置一个时间限制,从而缩短connect的超时时间。

 

非阻塞connect虽然听似简单,却有一些必须处理的细节

1).即使套接口是非阻塞的,如果连接的服务器在同一台主机上,那么在调用connect建立连接时,连接通常会立即建立成功.我们必须处理这种情况;

2).源自Berkeley的实现(和Posix.1g)有两条与select和非阻塞IO相关的规则:

  • 当连接建立成功时,套接字描述符变成可写;
  • 当连接出错时,套接子描述符变成既可读又可写;

注意:当一个套接口出错时,它会被select调用标记为既可读又可写;

 

程序实例:

#include     "unp.h"

int
connect_nonb(int sockfd, const SA *saptr, socklen_t salen, int nsec)
{
    int     flags, n, error;
    socklen_t len;
    fd_set rset, wset;
    struct timeval tval;

    flags = Fcntl(sockfd, F_GETFL, 0);
    Fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

    error = 0;
    if ( (n = connect(sockfd, saptr, salen)) < 0)
        if (errno != EINPROGRESS)
            return (-1);

    /* Do whatever we want while the connect is taking place. */

    if (n == 0)
        goto done;               /* connect completed immediately */

    FD_ZERO(&rset);
    FD_SET(sockfd, &rset);
    wset = rset;
    tval.tv_sec = nsec;
    tval.tv_usec = 0;

    if ( (n = Select(sockfd + 1, &rset, &wset, NULL,
                    nsec ? &tval : NULL)) == 0) {
        close(sockfd);          /* timeout */
        errno = ETIMEDOUT;
        return (-1);
    }

    if (FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset)) {
        len = sizeof(error);
        if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0)
            return (-1);     /* Solaris pending error */
    } else
        err_quit("select error: sockfd not set");

  done:
    Fcntl(sockfd, F_SETFL, flags);  /* restore file status flags */

    if (error) {
       close(sockfd);           /* just in case */
       errno = error;
       return (-1);
    }
    return (0);
}

 

4.非阻塞accept

阻塞模式下,服务器会一直阻塞在accept调用上,知道其他某个客户建立一个连接为止,但是在此期间,服务器单纯阻塞在accept调用上,无法处理任何其他已就绪的描述符

非阻塞accept模式下解决办法

1).当使用select获悉某个监听套接字上何时有已完成连接准备被accept时候,总是把这个监听套接字设置为非阻塞

2).在后续的accept调用忽略以下错误:EWOULDBLOCK(客户终止连接时)、ECONNABORTED(客户终止连接时)、EPROTO(客户终止连接时)和EINTR(如果有信号被捕获)

 

 

 

三、ioctl操作

 

网络程序中ioctl常用于在程序启动时获得主机上所有接口的信息:接口的地址,接口是否支持广播,是否支持多播,等等。

#include <unistd.h>
 
int ioctl(int fd, int request, ... /* void *arg */ );
//返回:若成功0,出错-1

其中第三个参数总是一个指针,但指针的类型依赖于request参数,我们可以把和网络相关的请求(request)划分为6类:

  • 套接口操作
  • 文件操作
  • 接口操作
  • ARP高速缓存操作
  • 路由表操作
  • 流系统

用法详见UNP