UNIX网络编程(UNP) 第十六章学习笔记

概述

套接字默认是阻塞的,可能阻塞的套接字调用可以分为以下四类

  1. 读取操作。包括read,reav,recv,recvfrom和recvmsg五个函数。如果进程对阻塞的TCP套接字调用这些函数,而接收缓冲区没有数据可读,那么进程就会沉睡直到有数据可读(无论是一个字节还是说一个完整的TCP分节的数据)。如果需要等到足够数目的数据可读,那么可以用MSG_WAITALL状态或者调用我们自己的readn函数

    对于UDP套接字而言,苏醒的条件是有数据报的到来。

    对于非阻塞的套接字而言,调用这些函数如果不符合条件(TCP至少有一个字节数据可读,UDP至少有一个完整的数据报可读),那么会立刻返回EWOULDBLOCK

  2. 写入操作。包括write,writev,send,sendto和sendmsg。如果进程对阻塞的TCP套接字调用这些函数,而缓冲区没有空间,则投入睡眠直到有空间位置。而如果是非阻塞的TCP套接字,如果发送缓冲区没有任何空间,则立刻返回EWOULDBLOCK,如果有一些空间(但不足),函数返回值则是内核能够复制到该缓冲区中的字节数,称为不足计数

    UDP套接字不存在真正的发送缓冲区,因此阻塞的UDP套接字不会因为和TCP套接字相同的原因而阻塞,但也可能因为其他原因而阻塞

  3. 接受外来连接,即accept函数。对于阻塞的套接字调用accept函数,如果没有新的连接到达,进程将会投入睡眠。如果是非阻塞的套接字,尚无新的连接到达,则立即返回EWOULDBLOCK错误

  4. 发起外出连接。即用于TCP的connect函数(应用于UDP的话不会建立连接,只会让内核保存对端的IP地址和端口号)。connect总会阻塞进程直到收到自己的SYN的ACK才返回(所以至少一个RTT时间)

    对于非阻塞的TCP套接字调用connect,而且连接不能立刻建立,那么连接的建立依然发起(发出第一个分组),但是会返回一个EINPROGRESS错误。注意连接如果发生在同一个主机之间,是有可能立刻建立的(所以也要准备connect成功返回的情况)

非阻塞读和写:str_cli函数

#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;
    friptr = froptr = fr;
    stdineof = 0;

    maxfdp1 = max(max(STDIN_FILENO, STDOUT_FILENO), sockfd) + 1;
    for (;;)
    {
        FD_ZERO(&rset);
        FD_ZERO(&wset);
        //如果to缓冲区未满,则打开读描述集中标准输入的位置
        if (stdineof == 0 && toiptr < &to[MAXLINE])
            FD_SET(STDIN_FILENO, &rset);
        //如果fr缓冲区未满,则打开读描述集中socket套接字的位
        if (friptr < &fr[MAXLINE])
            FD_SET(sockfd, &rset);
        //如果to缓冲区有要写到socket的内容,则打开写描述集中socket套接字的位
        if (tooptr != toiptr)
            FD_SET(sockfd, &wset);
        //如果fr缓冲区中有要写到标准输出的数据,那么就打开写描述符集中标准输出的位
        if (froptr != friptr)
            FD_SET(STDOUT_FILENO, &wset);

        Select(maxfdp1, &rset, &wset, NULL, NULL);

        if (FD_ISSET(STDIN_FILENO, &rset))
        {   
            //试图读取标准输入,toiptr就是输入缓冲区
            if ((n = read(STDIN_FILENO, toiptr, &to[MAXLINE] - toiptr)) < 0)
            {
                if (errno != EWOULDBLOCK)
                    err_sys("read error on stdin.");
            }
            //收到eof,此时检查tooptr是否等于toiptr(是否还有未发出数据),如果没有数据
            //则关闭
            else if (n == 0)
            {
                fprintf(stderr, "%s:EOF on stdin \n", gf_time());
                stdineof = 1;
                if (tooptr == toiptr)
                    Shutdown(sockfd, SHUT_WR);
            }
            //正常收到数据,移动toiptr(注意tooptr没有移动)
            else
            {
                fprintf(stderr, "%s:read %d bytes from stdin\n", gf_time());
                toiptr += n;
                FD_SET(sockfd, &wset);
            }
        }
        if (FD_ISSET(sockfd, &rset))
        {
            if ((n = read(sockfd, friptr, &fr[MAXLINE] - friptr)) < 0)
            {
                if (errno != EWOULDBLOCK)
                    err_sys("read error on socket");
            }
            else if (n == 0)
            {
                fprintf(stderr, "%s:EOF on socket\n", gf_time());
                if (stdineof)
                    return;
                else
                    err_quit("str_cli: server terminated prematurely");
            }
            else
            {
                fprintf(stderr, "%s :read %d bytesfrom socket\n", gf_time(), n);
                friptr += n;
                //这里打开了标准输出,导致下面的ISSET为真
                FD_SET(STDOUT_FILENO, &wset);
            }
        }
        
        if (FD_ISSET(STDOUT_FILENO, &wset) && ((n = friptr - froptr) > 0))
        {
            //注意由于上面会设置为真,所以此时调用write不一定不会阻塞,所以可能返回
            //EWOULDBLOCK
            if ((nwritten = write(STDOUT_FILENO, froptr, n)) < 0)
            {
                if (errno != EWOULDBLOCK)
                    err_sys("write error to stdout");
            }
            else
            {
                fprintf(stderr, "%s:wrote %d bytes to stdout\n", gf_time(), nwritten);
                froptr += nwritten;
                //如果fri==fro,那么意思是写出标准输出后,缓冲区已经没有数据要处理
                //就设置回起点
                if (froptr == friptr)
                    froptr = friptr = fr;
            }
        }

        if (FD_ISSET(sockfd, &wset) && ((n = toiptr - tooptr) > 0))
        {
            if ((nwritten = write(sockfd, tooptr, n)) < 0)
            {
                if (errno != EWOULDBLOCK)
                    err_sys("write error to socket");
            }
            else
            {
                fprintf(stderr, "%s: wrote %d bytes to socket\n", gf_time(), nwritten);
                tooptr += nwritten;
                if (tooptr == toiptr)
                {
                    toiptr = tooptr = to;
                    if (stdineof)
                        Shutdown(sockfd, SHUT_WR);
                }
            }
        }
    }
}

总结:为什么非阻塞会比select版本的更快一点?因为select版本中,我们在监听标准输入和socket,如果标准输入有信息,读取之后(这里因为select,是不会阻塞的)写入到socket(注意这里可能就出现阻塞等待)。而非阻塞的socket不会有这样的需要等待的问题。但是非阻塞的套接字不能保证自己已经能够写入,因此需要更加繁杂的检查。

fork版本的str_cli函数

非阻塞的速度更快毫无疑问,但是长度却大大增加,因此我们不妨引用第二种思路:我们用多个进程来处理多个输入输出流

#include "../unp.h"
void str_cli(FILE *fp,int sockfd)
{
    pid_t pid;
    char sendline[MAXLINE],recvline[MAXLINE];

    if ( (pid=Fork())==0){
        while (Readline(sockfd,recvline,MAXLINE)>0)
            Fputs(recvline,stdout);
        kill(getppid(),SIGTERM);//过早地终止,向父进程发送SIGTERM信号
        exit(0);
    }
    while (Fgets(sendline,MAXLINE,fp)!=NULL)
        Writen(sockfd,sendline,strlen(sendline));
    Shutdown(sockfd,SHUT_WR);
    pause();//让自己睡眠
    return;

}

非阻塞connect

非阻塞的TCP套接字调用connect之后会立刻返回EINPROGRESS错误,然后我们可以用select检测连接成功或者失败。非阻塞的connect的作用有以下几点

  1. 三次握手需要花费一个RTT时间,RTT可能从几毫秒到几秒,我们也许希望能处理其他事务
  2. 我们可以用这个技术同时建立多个连接
  3. 我们可以select指定一个时间限制,使得我们能够缩短connect的超时(75秒钟)

但是我们也需要处理一些细节:

  1. 在特定情况下(如同一个主机),connect可能会立刻建立,我们需要处理这种情形
  2. 当连接成功时候,描述符变为可写。当连接遇到错误的时候,描述符变为即可读又可写
例子
#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);
            
    //我们可以在这个过程中做我们需要完成的其他事情

    if (n == 0) //立刻完成
        goto done;
    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);	//避免已经启动的三次握手继续
        errno = ETIMEDOUT;
        return (-1);
    }
    if (FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset))
    {
        len = sizeof(error);
        //下面的处理主要是移植性问题,berkeley的实现会在error中是设置,
        //Solaris会返回-1然后设置了errno
        if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0)
            return (-1);
    }
    else
    {
        err_quit("select error:sockfd not set");
    }
done:
    Fcntl(sockfd,F_SETFL,flags);
    if (error){
        close(sockfd);
        errno=error;
        return(-1);
    }
    return 0;
}

值得注意的是,非阻塞conenct面临很多移植性问题。其一,由于在我们调用connect之前可能连接已经完成并且对端发送了数据。所以”可读且可写“不足以判断连接失败,另一个是”可写而不可读“也同样不能充当连接成功的标志,这里我们统一使用了getsockopt

被中断的connect

假设一个阻塞套接字的connect调用在完成之前被打断(如信号)且没有自动重启,此时我们是不能重新调用connect的,会触发EADDRINUSE错误。此时我们能做的,使用select监听等待连接完成,然后用”可读且可写“判断失败,”可写而不可读“判断成功

非阻塞connect:web客户端程序

#include "web.h"
int main(int argc, char **argv)
{
    int i, fd, n, maxnconn, flags, error;
    char buf[MAXLINE];
    fd_set rs, ws;
    if (argc < 5)
        err_quit("usage: web <#conns> <hostname> <homepage> <file1> ...");
    maxnconn = atoi(argv[1]);
    nfiles = min(argc - 4, MAXFILES);
    for (i = 0; i < nfiles; i++)
    {
        file[i].f_name = argv[i + 4];
        file[i].f_host = argv[2];
        file[i].f_flags = 0;
    }
    printf("nfiles = %d\n", nfiles);
    //homepage会读取主页
    home_page(argv[2], argv[3]);
    FD_ZERO(&rset);
    FD_ZERO(&wset);
    maxfd = -1;
    nlefttoread = nlefttoconn = nfiles;
    nconn = 0;

    while (nlefttoread > 0)
    {
        while (nconn < maxnconn && nlefttoconn > 0)
        {
            for (i = 0; i < nfiles; i++)
                if (file[i].f_flags == 0)
                    break;
            if (i == nfiles)
                err_quit("nlefttoconn = %d but nothing found", nlefttoconn);
            start_connect(&file[i]);
            nconn++;
            nlefttoconn--;
        }
        rs = rset;
        ws = wset;
        n = Select(maxfd + 1, &rs, &ws, NULL, NULL);
        for (i = 0; i < nfiles; i++)
        {
            flags = file[i].f_flags;
            if (flags == 0 || flags & F_DONE)
                continue;
            fd = file[i].f_fd;
            if (flags & F_CONNECTING && (FD_ISSET(fd, &rs) || FD_ISSET(fd, &ws)))
            {
                n = sizeof(error);
                if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &n) < 0 ||
                    error != 0)
                {
                    err_ret("nonblocking connect failed for %s", file[i].f_name);
                }
                printf("connection established for %s\n", file[i].f_name);
                FD_CLR(fd, &wset);
                write_get_cmd(&file[i]);
            }
            else if (flags & F_READING && FD_ISSET(fd, &rs))
            {
                if ((n = Read(fd, buf, sizeof(buf))) == 0)
                {
                    printf("end-of-file on %s\n", file[i].f_name);
                    Close(fd);
                    file[i].f_flags = F_DONE;
                    FD_CLR(fd, &rset);
                    nconn--;
                    nlefttoread--;
                }
                else
                {
                    printf("read %d bytes from %s\n",n,file[i].f_name);
                }
            }
        }
    }
    exit(0);
}

void home_page(const char *host, const char *fname)
{
    int fd, n;
    char line[MAXLINE];
    fd = Tcp_connect(host, SERV);//建立连接
    n = snprintf(line, MAXLINE, GET_CMD, fname);//发送GET请求
    Writen(fd, line, n);

    for (;;)
    {
        //读取应答
        if ((n = Read(fd, line, MAXLINE)) == 0)
            break;
        printf("read %d bytes of home page\n", n);
        //这里可以用主页获取一些信息
    }
    printf("end-of-file on home page\n");
    Close(fd);
}
void start_connect(struct file *fptr)
{
    int fd, flags, n;
    struct addrinfo *ai;
    ai = Host_serv(fptr->f_host, SERV, 0, SOCK_STREAM);

    fd = Socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
    fptr->f_fd = fd;
    printf("start_connect for %s,fd %d\n", fptr->f_name, fd);

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

    if ((n = connect(fd, ai->ai_addr, ai->ai_addrlen)) < 0)
    {
        if (errno != EINPROGRESS)
            err_sys("nonblocking connect error");
        fptr->f_flags = F_CONNECTING;
        FD_SET(fd, &rset);
        FD_SET(fd, &wset);
        if (fd > maxfd)
            maxfd = fd;
    }
    else if (n >= 0)
        write_get_cmd(fptr);
}
void write_get_cmd(struct file *fptr)
{
    int n;
    char line[MAXLINE];

    n = snprintf(line, sizeof(line), GET_CMD, fptr->f_name);
    Writen(fptr->f_fd, line, n);
    printf("wrote %d bytes for %s\n", n, fptr->f_name);

    fptr->f_flags = F_READING;

    FD_SET(fptr->f_fd, &rset);
    if (fptr->f_fd > maxfd)
        maxfd = fptr->f_fd;
}

非阻塞accept

当一个套接字已经准备好被accept的时候,select将会作为可读描述符返回该连接的监听套接字。这听起来似乎是说我们没有必要为了accept设置套接字为非阻塞,然而这里有一个陷阱:如果连接成功,select返回,然而在调用accept之前(一个很小的时间差),对端发送了RST报文,来自Berkeley的实现会选择丢弃该连接,然后我们调用accept,此时进程就(意外的)阻塞了。

解决办法是:调用select来获悉哪个连接准备好被accpet时候,把这个监听套接字设置为非阻塞。然后在accept调用时候忽略以下错误:EWOULDBLOCK(源自berkeley实现,客户中止连接时),ECONNABORTED(POSIX实现,客户中止连接时),EPTOTO(SVR4实现,客户中止连接时)和EINTR

发布了31 篇原创文章 · 获赞 32 · 访问量 738

猜你喜欢

转载自blog.csdn.net/a348752377/article/details/104186112
今日推荐