基于UNIX的网络编程(理论篇:设计迭代服务器)

这里写图片描述
注意:read的返回值为0说明读的字节数,如果为EOF则为0.

之前腾讯一面的时候,面试官看见了我的最近一篇博客《TCP和UDP详解》感觉我的理论还行。但是,问我有没实际做过TCP/UDP的项目。结果我就只能说我在大一做过的一个没有搭在服务器上面的下载器,感觉有点丢人。于是,自己马上下来做了准备一个基于UNIX的网络编程的小项目。准备分为两个理论篇和一个实践篇。
现在马上进入我们的第一个理论篇:设计迭代服务器
在这篇博客中,你将看到以下内容:
这里写图片描述

好的,好戏开始:

UNIX I/O

一.UNIX的I/O系统是怎么回事?

计算机专业的都知道,我们的计算机都是基于冯诺伊曼的架构(当然还是哈佛架构总线分开)。那么,对于冯诺伊曼架构的计算机I/O就成了很重要的一环。在我们的UNIX系统的I/O是怎么样的呢?对于I/O(输入、输出),UNIX系统做了一个非常优美的抽象,UNIX系统将所有的I/O设备,正如网络、磁盘和终端,都抽象为了我们的文件。包括我们写程序中用到的prinf,scanf,sprintf,sscanf都是我们的文件。
针对于每一个程序,内核都会自动分配出三个文件:stdin,stdout,stderr(标准输入、标准输出、标准错误流),文件描述符分别为0,1,2.
这里提一下,UNIX是怎么进行文件区分的,下面直接来放一张UNIX系统的文件头结构体:
这里写图片描述
看起来有点多,我们挑我们常用的几个来说
1.mode_t st_mode. 说明文件的具体类型,是可读的,还是可写的,还是可执行的。
我们可以利用我们的宏指令来看文件的类型

宏指令 描述
S_ISREG() 这是一个普通文件吗?
S_ISDIR() 这是一个目录文件吗?
S_ISSOCK() 这是一个网络套接字吗?

其他的相关信息:st_atime、st_mtime、st_ctime可以用来做文件的mac或者hash来进行传输,来保证文件在发送的时候没有被修改,这又涉及到安全方面的知识了,这又是另一个故事了,对信息安全感兴趣的可以看我的这篇博文:http://blog.csdn.net/github_33873969/article/details/79008664
这里还是上一段代码如何查询和处理一个文件的st_mode位吧

#include<unistd.h>
#include<sys/stat.h>

int main(int argc, char **argv)
{
    struct stat stat;
    char *type, *readok;

    stat(argv[1],&stat);
    /* Determine file type */
    if(S_ISREG(stat.st_mode))
        type="regular";
    else if(S_ISDIR(stat.st_mode))
        type="directory";
    else
        type="other";
    /* Check read access */
    if((stat.st_mode & S_IRUSR))
        readok="yes";
    else
        readok="no";

    printf("type: %s, read: %s\n",type,readok);
    exit(0);
}

接下来,我还是想说一下针对每一个进程而言,文件是如何管理的,映射到操作系统内核文件又是怎样的?
还是先上图:
这里写图片描述
从头开始:
1.对于每一个进程而言,都会独立维护一个描述符表,这个描述符表就是我刚刚所说的对于进程而言,区分不同的文件而准备的(比如:stdin、stdout、stderr)
这里说明一个打开对应文件的函数吧。

#include <sys/types.h>
#inlcude <sys/stat.h>
#include <fcntl.h>
int open(char *filename,int flags,mode_t mode);   //返回:若成功则为新文件描述符,若出错为-1.

flags说明我们的进程将以什么样的方式访问这个文件,O_RDONLY:只读、O_WRONLY:只写、O_RDWR:可读可写。
接下来说明一下mode,这个参数,这个参数指定了我们对文件的访问权限,我们可以通过调用umask函数来设置。
还是直接上表吧,这个表中说明了各种权限。

宏指令 描述
S_IRUSR 使用者(拥有者)能够读这个文件
S_IWUSR 使用者(拥有者)能够写这个文件
S_IXUSR 使用者(拥有者)能够执行这个文件
S_IRGRP 拥有者所在组的成员能够读这个文件
S_IWGRP 拥有者所在组的成员能够写这个文件
S_IXGRP 拥有者所在组的成员能够执行这个文件
S_IROTH 其他人(任何人)能够读这个文件
S_IWOTH 其他人(任何人)能够写这个文件
S_IXOTH 其他人(任何人)能够执行这个文件

注意哦,这些文件权限,关系到我们的网络编程中的动态文件(可执行的文件)还是静态文件(html、jpg)图片的发送有关哦。
2.打开文件表
打开文件表位于内核中(所有进程共享),存储着所有的打开文件的信息。存储着文件位置和引用计数,关于这个引用计数可是又一定的技术故事的啦,引用计数技术在这里简单来说,就是有哪些进程打开了这个文件,引用了这个问题,一旦进程销毁后,引用计数就会减一。直到引用计数为0,则该文件被销毁。
Reference:https://www.zhihu.com/question/21539353
这种引用计数的方法也被引用在java的垃圾回收以及C++对象中。关于java的垃圾回收,我这里还是简单的聊一下吧:
2.1.引用计数。大家都知道java是一门面向对象的语言,针对于每一个对象都有的一个引用计数,谁引用了这个对象,这个对象的引用计数+1。引用结束就-1。直到对应的引用计数为0,说明该对象被销毁,这样的垃圾内存需要被回收。但是这样的回收方式,是基于局部的引用的情况,是局部最优解,可能产生无法回收的情况,如下面的例子:由于不方便转载,这个例子还是去这个网站上看比较好:https://www.zhihu.com/question/21539353
2.2.不可达图回收:如果把每一个对象看到一个结点,相互引用的关系看作是结点与结点相连关系,就可以构成一张图,那么那些图中达不到(不可达)的结点就是垃圾结点(垃圾对象),需要进行回收。所以,我们只要从GC开始对所有结点进行bfs,达不到的结点就是垃圾结点。
题外话说多了,回到我们的正题:
3.v-node表
这个才是我们在硬盘里面的真正文件,所有进程共享。所以会出现多个文件表指向同一个v-node(利用I/O重定向dup2来实现文件描述符指向同一个,这个之后会马上 讲),如果是父子进行(fork)出来的会共享同一个文件表,如下图所示:
这里写图片描述

二.如何编写健壮的I/O函数

这里举一个非常形象的例子来说明我们的普通的I/O函数,read,write有什么缺陷。假设我们要拷贝1个G的文件,我们如果用read和write一个个字节的拷贝,我们将会重复的陷入(trap into)系统调用,这会大量消耗我们无用的性能。我们基于这样的缺陷,设计了一个带有缓冲区的Rio_read和Rio_write,它的基本设计思路是调用rio_read要求读n个字节时,读缓冲区内有rp->rio_cnt个未读字节。如果缓冲区为空,那么会通过调用read再填满它。(原则其实就是多读数据进来)。
写也是同理。

这里还是直接上代码吧:

/*
 * rio_writen - robustly write n bytes (unbuffered)
 */
/* $begin rio_writen */
ssize_t rio_writen(int fd, void *usrbuf, size_t n) 
{
    size_t nleft = n;
    ssize_t nwritten;
    char *bufp = usrbuf;

    while (nleft > 0) {
    if ((nwritten = write(fd, bufp, nleft)) <= 0) {
        if (errno == EINTR)  /* Interrupted by sig handler return */
        nwritten = 0;    /* and call write() again */
        else
        return -1;       /* errno set by write() */
    }
    nleft -= nwritten;
    bufp += nwritten;
    }
    return n;
}
/* $end rio_writen */


/* 
 * rio_read - This is a wrapper for the Unix read() function that
 *    transfers min(n, rio_cnt) bytes from an internal buffer to a user
 *    buffer, where n is the number of bytes requested by the user and
 *    rio_cnt is the number of unread bytes in the internal buffer. On
 *    entry, rio_read() refills the internal buffer via a call to
 *    read() if the internal buffer is empty.
 */
/* $begin rio_read */
static ssize_t rio_read(rio_t *rp, char *usrbuf, size_t n)
{
    int cnt;

    while (rp->rio_cnt <= 0) {  /* Refill if buf is empty */
    rp->rio_cnt = read(rp->rio_fd, rp->rio_buf, 
               sizeof(rp->rio_buf));
    if (rp->rio_cnt < 0) {
        if (errno != EINTR) /* Interrupted by sig handler return */
        return -1;
    }
    else if (rp->rio_cnt == 0)  /* EOF */
        return 0;
    else 
        rp->rio_bufptr = rp->rio_buf; /* Reset buffer ptr */
    }

    /* Copy min(n, rp->rio_cnt) bytes from internal buf to user buf */
    cnt = n;          
    if (rp->rio_cnt < n)   
    cnt = rp->rio_cnt;
    memcpy(usrbuf, rp->rio_bufptr, cnt);
    rp->rio_bufptr += cnt;
    rp->rio_cnt -= cnt;
    return cnt;
}
/* $end rio_read */

/*
 * rio_readinitb - Associate a descriptor with a read buffer and reset buffer
 */
/* $begin rio_readinitb */
void rio_readinitb(rio_t *rp, int fd) 
{
    rp->rio_fd = fd;  
    rp->rio_cnt = 0;  
    rp->rio_bufptr = rp->rio_buf;
}
/* $end rio_readinitb */

/*
 * rio_readnb - Robustly read n bytes (buffered)
 */
/* $begin rio_readnb */
ssize_t rio_readnb(rio_t *rp, void *usrbuf, size_t n) 
{
    size_t nleft = n;
    ssize_t nread;
    char *bufp = usrbuf;

    while (nleft > 0) {
    if ((nread = rio_read(rp, bufp, nleft)) < 0) 
            return -1;          /* errno set by read() */ 
    else if (nread == 0)
        break;              /* EOF */
    nleft -= nread;
    bufp += nread;
    }
    return (n - nleft);         /* return >= 0 */
}
/* $end rio_readnb */

/* 
 * rio_readlineb - robustly read a text line (buffered)
 */
/* $begin rio_readlineb */
ssize_t rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen) 
{
    int n, rc;
    char c, *bufp = usrbuf;

    for (n = 1; n < maxlen; n++) { 
        if ((rc = rio_read(rp, &c, 1)) == 1) {
        *bufp++ = c;
        if (c == '\n') {
                n++;
            break;
            }
    } else if (rc == 0) {
        if (n == 1)
        return 0; /* EOF, no data read */
        else
        break;    /* EOF, some data was read */
    } else
        return -1;    /* Error */
    }
    *bufp = 0;
    return n-1;
}

三.I/O重定向

这个就更简单了,有了我们上面懂得UNIX系统中对文件中的抽象。我们利用下面的函数

#include <unitstd.h>
int dup2(int oldfd, int newfd);   //返回:若成功则为非负的描述符,若出错则为-1.

假设我们调用dup2(3,1),即使让3也指向和文件表1中的同一个v-node,一是标准输出,那么3的输出会直接被定向到标准输出。同理,重定向到网络套接字描述符,重定向到标准输入,那么文件3的输出就变成了网络套接字的输入或者标准输入,这就是重定向I/O以及“一切都是文件”的魅力,刚刚的过程我用下面的图表示出来:
这里写图片描述

网络编程

这部分我将尽量简化网络例如TCP/IP协议以及HTTP的理论部分,着眼于代码以及相关函数部分

一.客户端与服务端的基本模式

这里写图片描述
这里我将说明一个最简单的模型,并且我们的第一个服务器也是基于这个模式。
1.客户端发送情况。2.服务器去请求资源。((1).可能是磁盘中的静态资源,html/css/jpg文件。(2)也可能是动态文件(需要fork出一个进程去execve该程序)).3.之后就将服务器的相关资源发送。4.客户端进行响应。中间的TCP的可靠传输的实现,HTTPS和SSL的加密等内容,可以看我的其他博客:http://blog.csdn.net/github_33873969/article/details/79422188
这里就不细说了。

二.UNIX有哪些函数与我们的客户端与服务端相关

说了这么多,终于开始调函数了。受首先是我们的客户端,可以想象我们发送一个消息,需要一个对应服务器主机的IP地址,了解到需要发送服从的协议。代码如下:

#include <sys/types.h>
#include <sys/socket.h>

//domain表示使用的网络(常用的是AF_INET),type常为SOCK_STREAM表示因特网连接的一个结点,
int socket(int domain, int type, int protocol);

现在只是连接起一个套接字,但是,还不能用于读写。

#include <sys/types.h>
#include <sys/socket.h>
int connect(int socketfd,struct sockaddr *serv_addr,int addslen)

emmmm,这个参数的含义我觉得直接看参数名都能看懂,值得注意的是,当connect在执行过程中,函数处于阻塞状态,意思就是说其他套接字不能进行连接状态,直到连接上或者返回报错信息,IP在sockaddr这里结构体中。
我们直接将socket函数和connect函数封装成一个函数。
如下所示:

/* $begin open_clientfd */
int open_clientfd(char *hostname, int port) 
{
    int clientfd;
    struct hostent *hp;
    struct sockaddr_in serveraddr;

    if ((clientfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    return -1; /* Check errno for cause of error */

    /* Fill in the server's IP address and port */
    if ((hp = gethostbyname(hostname)) == NULL)
    return -2; /* Check h_errno for cause of error */
    bzero((char *) &serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    bcopy((char *)hp->h_addr_list[0], 
      (char *)&serveraddr.sin_addr.s_addr, hp->h_length);
    serveraddr.sin_port = htons(port);

    /* Establish a connection with the server */
    if (connect(clientfd, (SA *) &serveraddr, sizeof(serveraddr)) < 0)
    return -1;
    return clientfd;
}

对于服务器

1.第一个仍然是我们的socket函数,这个不多说了,创建一个套接字描述符。
2.bind函数。

#include <sys/socket.h>
int bind(int socketfd,struct sockaddr *my_addr,int addrlen);

即让操作系统内核将对应的socketfd绑定到我们的my_addr结构题中。
3.listen函数

#include <sys/socket.h>
int listen(int sockfd,int backlog);

这个函数是想主动的套接字转化为监听套接字(服务器专用),这个函数可是有的说呀,这个函数足以和我们的TCP握手协议连接起来,还是先图:
Reference:https://www.cnblogs.com/chris-cp/p/4022262.html
这里写图片描述
我们都知道TCP有着三次握手和四次挥手协议。但这些协议老实说都在操作系统底层实现了。但是,其中的函数还是会影响到我们的listen函数,我们的listen函数维护着两个队列:
3.1.已完成连接队列,负责排队已经完成三次握手的客户端socket。即是已经是ESTABLISHED状态的连接。
3.2.未完成连接队列:这个更好理解了,就是还处于三次握手的连接(连接处于SYN_RCVD状态)。
这里要注意,listen函数负责三次握手的连接,但是accept函数却不负责三次握手的连接。这个还要注意一点,accept函数只首发处于已连接状态的套接字,所以当以完成连接队列为空的时候,accept会处于阻塞状态。知道已完成连接队列中有已经完成握手的套接字存在。
然后已完成连接队列+未完成连接队列=我们的参数backlog。
4.accept函数

#include <sys/socket.h>
int accept(int listenfd,struct sockaddr *addr,int *addrlen)

这时候,我们输入的listenfd监听描述符就返回出了连接描述符,记住listenfd监听描述符针对的一个服务器,而通过accept创建的连接描述符是针对服务器针对一个客户端的连接。具体的连接过程如下图所示:
这里写图片描述
图已经很清楚哒。
同时我们也将socket和listen函数封装成一个函数

/* $begin open_listenfd */
int open_listenfd(int port) 
{
    int listenfd, optval=1;
    struct sockaddr_in serveraddr;

    /* Create a socket descriptor */
    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    return -1;

    /* Eliminates "Address already in use" error from bind */
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, 
           (const void *)&optval , sizeof(int)) < 0)
    return -1;

    /* Listenfd will be an endpoint for all requests to port
       on any IP address for this host */
    bzero((char *) &serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET; 
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); 
    serveraddr.sin_port = htons((unsigned short)port); 
    if (bind(listenfd, (SA *)&serveraddr, sizeof(serveraddr)) < 0)
    return -1;

    /* Make it a listening socket ready to accept connection requests */
    if (listen(listenfd, LISTENQ) < 0)
    return -1;
    return listenfd;
}

设计我们的第一个迭代服务器

代码有点多,我就直接下地址啦:https://github.com/HBKO/tiny-server

猜你喜欢

转载自blog.csdn.net/github_33873969/article/details/79508045