CSAPP-网络编程

编程/技术 2019-05-22 @ 07:45:43 浏览数: 185 净访问: 160 By: skyrover

本博客采用创作共用版权协议, 要求署名、非商业用途和保持一致. 转载本博客文章必须也遵循署名-非商业用途-保持一致的创作共用协议


这一章读完感觉没有上一章痛快,可能涉及网络这里只是浅尝辄止,所以想继续深入,还是要读读计算机网络方面的。后面就只是介绍各种函数,但是具体深入的原理没有。最后是这些函数组成的一个web服务器。

11.1 客户端-服务器变成模型

  • 客户端发送请求
  • 服务器接收请求,处理
  • 服务器发送响应
  • 客户端接收响应,处理

11.2 网络

对于主机来说,网络是一种IO设备,作为数据源和数据接收方。一个插到IO总线扩展槽的适配器提供了到网络的物理接口,从网络上接收的数据从适配器经过IO和存储器总线拷贝到存储器,一般是通过DMA传送。

以太网段:电缆+集线器,集线器会将一个端口上接收的位复制到其他所有端口上,所以每台主机都能看到每个位。

以太网适配器都有全球唯一的48位地址,一台主机可以发送一段位,为帧,包含头部和有效载荷。

通过网桥来实现更大的局域网-桥接以太网,网桥比集线器更充分利用电缆带宽,并且有选择性的复制数据。

再往高,多个不兼容的局域网可以通过路由器的特殊计算机联结起来,形成一个internet

11.3 全球IP因特网

因特网的客户端和服务器混合使用套接字接口函数和Unix IO函数来进行通信,套接字函数典型的是作为会陷入内核的系统调用来实现的,并调用各种内核模式的TCP/IP函数。

11.3.1 IP地址

IP地址是一个32位无符号整数:

struct in_addr{
    unsigned int s_addr;
}

TCP/IP定义了统一的网络字节顺序——大端字节顺序。关于大端和小端的一点:

  • 计算机电路先处理低位字节,效率比较高,因为计算都是从低位开始的。所以,计算机的内部处理都是小端字节序。
  • 人类还是习惯读写大端字节序。所以,除了计算机的内部处理,其他的场合几乎都是大端字节序,比如网络传输和文件储存。

所以如果是大端字节序,先读到的就是高位字节,后读到的就是低位字节。小端字节序正好相反。只有读取的时候,才必须区分字节序,其他情况都不用考虑。理解了这个,就很容易知道大端就是高位在低地址,方便阅读;小端就是低位在低地址,方便计算电路处理。

Unix 提供了下面函数在网络和主机字节顺序中实现转换:

#include <netinet/in.h>
// 返回按照网络字节顺序的值
unsigned long int htonl(unsigned long int hostlong);
unsigned short int htons(unsigned short int hostshort);

// 返回按照主机字节顺序的值
unsigned long int ntohl(unsigned long int netlong);
unsigned short int ntohs(unsigned short int netshort);

点分十进制,每个字节由它的十进制表示。Linux上使用hostname -i来查看。通过inet_atoninet_ntoa来实现IP地址和十进制串的转换:

#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);

char *inet_ntoa(struct in_addr in);

ntoa-> network to application

11.3.2 因特网域名

一开始域名集合和IP地址的集合之间的映射是通过HOSTS.TXT来手工维护的,后来才通过分布世界范围内的数据库(DNS)来维护的,DNS由下面的主机条目结构组成,每条定义了一组域名和一组IP地址之间的映射。

struct hostent{
    char *h_name;
    char **h_aliases;
    int h_addrtype;
    int h_length;
    char **h_addr_list;
}

通过gethostbynamegethostbyaddr,来从DNS中检索任意主机条目。每台主机都有本地定义的域名localhost,映射为本地回送地址127.0.0.1,localhost为引用运行在同一台机器上的客户端和服务器提供了便利的方式。

11.3.3 因特网连接

因特网客户端和服务器通过在连接上发送和接收字节流来通信,点对点,全双工,可靠的。

套接字是连接的端点,套接字地址是因特网地址和16位整数端口组成,客户端发起请求是,端口是内核自动分配的,称为临时端口。/etc/services包含一张机器提供的复核以及端口号的列表。

一个套接字对(连接):(cliaddr:cliport, servaddr:servport)

11.4 套接字接口

套接字接口是一组函数:

11.4.1 套接字地址结构

从内核角度来看,套接字是通信端点,程序角度看,套接字就是一个有相应描述符的打开文件。

struct sockaddr{
    unsigned short sa_family;
    char sa_data[14];
}

struct sockaddr_in{
    unsigned short sin_family;  //AF_INET
    unsigned short sin_port;
    struct in_addr sin_addr;
    unsigned char sin_zero[8];
}

11.4.2 socket函数

创建套接字描述符

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

int socket(int domain, int type, int protocol);

比如这样:clientfd = socket(AF_INET, SOCK_STREAM, 0);

11.4.3 connect函数

客户端调用 connect 函数来建立和服务器的连接

#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);

连接成功后sockfd描述符就准备好读写了

11.4.4 open_clientfd函数

该函数就是socket和connect包装在一起

#include "csapp.h"
int open_clientfd(char *hostname, int port);

下面是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;
    }

    // 查询DNS 域名对应的ip地址
    if ((hp = gethostbyname(hostname)) == NULL){
        return -2;
    }

    // 将前几位置为0
    bzero((char *) &serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    // 拷贝数据
    bcopy((char *)hp->h_addr_list[0], (char *)&serveraddr.sin_addr.s_addr, hp->length);

    // IP地址已经是按照网络字节顺序了,但是端口不是
    serveraddr.sin_port = htons(port);

    if (connect(clientfd, (SA *)&serveraddr, sizeof(serveraddr)) < 0){
        return -1;
    }
    return clientfd;
}

11.4.5 bind 函数

bind,listen和accept函数被服务器用来和客户端建立连接。

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

bind告诉内核将my_addr中的服务器套接字地址和套接字描述符sockfd联系起来。

11.4.6 listen函数

默认socket函数创建的描述符对应于主动套接字,存在于一个连接的客户端,而listen函数将sockfd从主动套接字转换为监听套接字。backlog表示内核在开始拒绝连接请求之前,应该仿佛队列中等待的未完成连接请求的数量,比如1024

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

11.4.7 open_listenfd函数

将socket,bind和listen函数结合成open_listenfd,服务器用它来创建一个监听描述符。另外使用setsockopt函数来配置服务器,使得可以立即终止和重启,一般,一个重启的服务器将在大约30秒内拒绝客户端的连接请求,会报Address already in user错误

int open_listenfd(int port){
    int listenfd, optval = 1;
    struct sockaddr_in serveraddr;

    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
        return -1;
    }

    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (const void *)&optval, sizeof(int)) < 0){
        return -1;
    }

    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;
    }
    if (listen(listenfd, LISTENQ) < 0){
        return -1;
    }
    return listenfd;
}

11.4.8 accept函数

服务器调用accept函数来等待来自客户端的连接请求

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

accept函数等待来自客户端的连接请求到达侦听描述符listenfd,然后在addr中填写客户端的套接字地址,返回一个已连接描述符。这里有两个描述符,监听描述符和已连接描述符:监听描述符是作为客户端连接请求的端点,只创建一次,而已连接描述符在客户端每次发起连接请求的时候都会创建一次。

11.4.9 echo客户端和服务器示例

客户端关闭描述符,导致发送一个EOF通知到服务器,当服务器从rio_readlineb函数收到一个为0的返回码时候,就会检测到。

#include "csapp.h"
int main(int argc, char **argv){
    int clientfd, port;
    char *host, buf[MAXLINE];
    rio_t rio;

    if (argc != 3){
        fprintf(stderr, "usage: %s", argv[0]);
        exit(0);
    }

    host = argv[1];
    port = atoi(argv[2]);

    clientfd = open_clientfd(host, port);
    rio_readinitb(&rio, clientfd);

    while (fgets(buf, MAXLINE, stdin) != NULL){
        rio_writen(clientfd, buf, strlen(buf));
        rio_readlineb(&rio, buf, MAXLINE);
        fputs(buf, stdout);
    }
    close(clientfd);
    exit(0);
}

这个echo服务器一次只能处理一个客户端,叫做迭代服务器。反复读写文本行,直到rio_readlineb函数在第10行遇到EOF

#include "csapp.h"

void echo(int connfd);

int main(int argc, char **argv){
    int listenfd, connfd, port, clientlen;

    // 套接字结构
    struct sockaddr_in clientaddr;
    // DNS结构
    struct hostent *hp;
    char *haddrp;
    if (argc != 2){
        fprintf(stderr, "usage: %s <port>\n", argv[0]);
        exit(0);
    }
    // 将字符串转换为整数
    port = atoi(argv[1]);

    listenfd = open_listenfd(port);
    while (1){
        clientlen = sizeof(clientaddr);
        connfd = accept(listenfd, (SA *)&clientaddr, &clientlen);

        hp = gethostbyaddr((const char *)&clientaddr.sin_addr.s_addr, sizeof(clientaddr.sin_addr.s_addr), AF_INET);
        haddrp = inet_ntoa(clientaddr.sin_addr);
        printf("server connected to %s (%s)\n", hp->h_name, haddrp);

        echo(connfd);
        close(connfd);
    }
    exit(0);
}

void echo(int connfd){
    size_t n;
    char buf[MAXLINE];
    rio_t rio;

    rio_readinitb(&rio, connfd);
    while((n = rio_readlineb(&rio, buf, MAXLINE)) != 0){
        printf("server received %d bytes\n", n);
        rio_written(connfd, buf, n);
    }
}

11.5 Web服务器

HTTP基于文本的应用级协议

11.5.2 Web内容

内容是一个MIME(multipurpose Internet Mail Extensions)类型相关的字节序列,常用的:text/html, text/plain, image/gif, image/jpeg

11.5.3 HTTP事务

  • HTTP请求

HTTP请求包含 请求行+多个请求报头+空文本行,请求行的形式<method> <uri> <version>,比如GET / HTTP/1.1,请求报头格式:<header_name>: <header_data>,其中在HTTP1.1中Host报头是必须的,因为中间可能有代理服务器,所以用来指示原始服务器的域名,使得代理链中的代理能够判断它是否可以在本地缓存中拥有一个被请求内容的副本。

  • HTTP响应

一个响应行+多个响应报头+终止报头的空行+响应主体,响应行:<version> <status_code> <status_message>比如HTTP/1.0 200 OK,响应报头中比较重要的是Content-Type告诉客户端响应主体内容的MIME类型,Content-Length用来指示响应主体的字节大小。

11.5.4 服务动态内容

使用CGI(Common Gateway Interface,通用网关接口)解决下面问题

  • 客户端如何将程序参数传递给服务器

在URI或者在请求主体中(POST请求),URI中的特殊字符需要编码

  • 服务器如何将参数传递给子进程

服务器接收请求后,fork创建一个子进程,使用execve在子进程上下文执行对应的动态程序,在执行之前,将环境变量QUERY_STRING设置,然后程序可以在运行时候使用getenv函数来引用。

  • 服务器如何将其他信息传递给子进程

还有其他的环境变量,比如SERVER_PORT, REMOTE_ADDRCONTENT_TYPE, CONTENT_LENGTH

  • 子进程将它的输出发送到哪里

CGI程序将动态内容发送到标准输出,子进程加载CGI程序之前使用dup2函数将标准输出重定向到和客户端相关联的已连接描述符,所以CGI程序写到标准输出的东西会直接送到客户端。

POST请求,子进程需要重定向标准输入到已连接描述符,这样CGI就可以从标准输入读取请求主体的参数。

#include "csapp.h"

int main(void){
    char *buf, *p;
    char arg1[MAXLINE], arg2[MAXLINE], content[MAXLINE];
    int n1=0, n2=0;

    if ((buf = getenv("QUERY_STRING")) != NULL){
        p = strchr(buf, '&');
        *p = '\0';
        strcpy(arg1, buf);
        strcpy(arg2, p+1);
        n1 = atoi(arg1);
        n2 = atoi(arg2);
    }

    // make the reponse body and write it to stdout
}

11.6 综合:TINY Web服务器

TINY 的main程序

#include "csapp.h"

int main(int argc, char **argv){
    int listenfd, connfd, port, clientlen;
    struct sockaddr_in clientaddr;

    if (argc != 2){
        fprintf(stderr, "usage");
        exit(1);
    }

    port = atoi(argv[i]);
    listenfd = open_listenfd(port);
    while(1){
        clientlen = sizeof(clientaddr);
        connfd = accept(listenfd, (SA *)&clientaddr, &clientlen);
        doit(connfd);
        close(connfd);
    }
}
  • doit函数

用来处理一个http事务,首先读和解析请求行

void doit(int fd){
    int is_static;
    struct stat sbuf;
    char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
    char filename[MAXLINE], cgiargs[MAXLINE];

    rio_t rio;

    rio_readinitb(&rio, fd);
    rio_readlineb(&rio, buf, MAXLINE);
    sscanf(buf, "%s %s %s", method, uri, version);
    // strcasecmp 忽略大小写比较,如果一样返回0
    if (strcasecmp(method, "GET")){
        clienterror(fd, method, "501", "Not Implementd", "");
        return;
    }
    read_requesthdrs(&rio);

    is_static = parse_uri(uri, filename, cgiargs);
    if (stat(filename, &sbuf) < 0){
        clienterror(fd, filename, "404", "Not Found", "");
        return;
    }

    if (is_static){
        if (!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode)){
            clienterror(fd, filename, "403", "Forbidden", "");
            return;
        }
        serve_static(fd, filename, sbuf.st_size);
    }else{
        if(!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode)){
            clienterror(fd, filename, "403", "Forbidden", "");
            return;
        }
        serve_dynamic(fd, filename, cgiargs);
    }
}
  • read_requesthdrs函数
void read_requesthdrs(rio_t *rp){
    char buf[MAXLINE];
    rio_readlineb(rp, buf, MAXLINE);
    while(strcmp(buf, "\r\n")){
        rio_readlineb(rp, buf, MAXLINE);
        printf("%s", buf);
    }
    return;
}
  • parse_uri函数
int parse_uri(char *uri, char *filename, char *cgiargs){
    char *ptr;
    // 查找cgi-bin是不是uri的子串
    if (!strstr(uri, "cgi-bin")){
        strcpy(cgiargs, "");
        strcpy(filename, ".");
        strcat(filename, uri);
        if (uri[strlen(uri) - 1] == '/'){
            strcat(filename, "home.html");
        }
        return 1;
    }else{
        ptr = index(uri, '?');
        if (ptr){
            strcpy(cgiargs, ptr+1);
            *ptr = '\0';
        }else{
            strcpy(cgiargs, "");
        }
        strcpy(filename, ".");
        strcat(filename, uri);
        return 0;
    }
}
  • clienterror函数
void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg){
    char buf[MAXLINE], body[MAXBUF];

    sprintf(body, "<html><title>Tiny Error</title>");

    sprintf(buf, "HTTP/1.0 %s %s\r\n", errnum, shortmsg);
    rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "Content-Type: text/html\r\n");
    rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "Content-Length: %d\r\n\r\n", (int)strlen(body));
    rio_writen(fd, buf, strlen(buf));
    rio_writen(fd, body, strlen(body));
}
  • serve_dynamic函数
void serve_dynamic(int fd, char *filename, char *cgiargs){
    char buf[MAXLINE], *emptylist[] = {NULL};

    sprintf(buf, "HTTP/1.0 200 OK\r\n");
    rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "Server: Tiny Web Server\r\n");
    rio_writen(fd, buf, strlen(buf));

    if (fork() == 0){
        setenv("QUERY_STRING", cgiargs, 1);
        dup2(fd, STDOUT_FILENO);
        execve(filename, emptylist, environ);
    }
    wait(NULL);
}


点赞走一波😏


评论

提交评论