CSAPP Note chap11

CSAPP Note chap111

CSAPP 读书笔记系列chap11

chap 11 网络编程

这一次说的是网络编程,是一个比较广泛的话题.之前也有看过王达的<<深入理解计算机网络>> 和宋敬彬的 《Linux网络编程》,但也是了解个皮毛.这一次当作简述吧.

客户端-服务器模型

客户端-服务器模型是网络应用最广泛使用的模型,其中客户端和服务器都是进程而不是机器或主机!!!
客户端-服务器模型的基本操作是事务,一个事务由四步组成

  • 客户端发送一个请求给服务器进程,表示需要某个服务
  • 服务器收到并解释请求,并与适当的方式处理所需资源
  • 服务端发送一个响应给客户端并等待下一个请求
  • 客户端收到响应后处理它

    如下图: cs模型

    cs模型.png

计算机网络

可以把网络抽象成一个比较慢的IO设备

为了在不同机器中传输传输数据,需要的是大家都遵循一个协议。协议负责做的事情有:

  • 提供命名机制 naming scheme
    • 定义 host address 格式
    • 每个主机和路由器都至少有一个独立的 internet 地址
  • 提供传输机制 delivery mechanism
    • 也即提供一个封装分层机制,上一层可以忽略下一层的不同
    • 不同层之间定义了标准的传输单元
      下面是不同层的传输单元
传输层 TCP/IP传输单元
应用层消息 message
传输层段segment
网络IP层包packet
物理层帧frame

每个传输单元的格式不一样,发送的时候下一层会把上一层给封装起来,加上自己的头部Header.
接收的时候就反过来去掉底层header

计算机网络网络结构分为:
- OSI/RM(Open System Interconnection ReferenceModel, 开放系统互连参考模型)
- TCP/IP 协议体系结构 (又称 TCP/IP 协议参考模型)
如下图
OSI&TCP.png

现在都是使用TCP/IP层,计算机网络涉及的东西太多这个详细以后再谈.

TCP/IP是一种面向连接(连接导向)的、可靠的、基于字节流的网络传输协议.

网络编程基本概念

IP地址

IPV4中主机有 32 位的 IP 地址 ,IPv6 为 128 位地址

IP 地址是以 network byte order(也就是大端)来进行存储的

// Internet address structure
struct in_addr {
   uint32_t s_addr;    // network byte order (big-endian)
}

IP 地址:0x8002C2F2 = 128.2.194.242

具体的转换可以使用 getaddrinfo 和 getnameinfo 函数

为了忽略不同机器的字节顺序,Linux提供了网络和主机顺序的转换函数

#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

IP 地址到域名的映射为 Domain Naming System(DNS) ,可以用 nslookup 命令来查看

 -> nslookup www.bing.cn
Server:         127.0.1.1
Address:        127.0.1.1#53

Non-authoritative answer:
www.bing.cn     canonical name = fw.ename.net.
Name:   fw.ename.net
Address: 23.234.4.151
Name:   fw.ename.net
Address: 23.234.4.153

域名和地址之间的关系可以是:

  • 一一映射
  • 多个域名映射到同一个IP地址
  • 多个域名映射到同一组IP地址

Internet 连接

客户端和服务器通过连接(connection)来发送字节流,特点是:
- 点对点: 连接一对进程
- 全双工: 数据同时可以在两个方向流动
- 可靠: 字节的发送的顺序和收到的一致

Socket套接字 可以认为是 connection 的 端点endpoint,socket 地址是一个 IPaddress:port 对。
Port(端口)是一个 16 位的整数,用来标识不同的进程,利用不同的端口来连接不同的服务( linux 系统上可以在 /etc/services 中查看具体的信息))

而一个连接是两端的套接字地址唯一确定的,由下面元组表示:
(clientAddr:port, serverAddr:port)

网络编程架构

主要为下面的流程

Socket架构

其中分为服务器和客户端两个part

开启服务器(open_listenfd )
  • getaddrinfo: 设置服务器的相关信息

    • 将主机名,地址等转为套接字地址结构,其为可重入的,适用于任何协议
    • 若成功返回0,否则为nerror代码; res 指向addrinfo结构的链表,

      int getaddrinfo(const char *node, const char *service,
                         const struct addrinfo *hints,
                         struct addrinfo **res);
    • 更加具体看书657页 chap11.4.7

  • socket: 创建 socket descriptor,也就是之后用来读写的 file descriptor

    • int socket(int domain, int type, int protocol)
    • 例如 int clientfd = socket(AF_INET, SOCK_STREAM, 0);
    • AF_INET 表示在使用 32 位 IPv4 地址
      -SOCK_STREAM 表示这个 socket 将是 connection 的 endpoint
    • 前面这种写法是协议相关的,建议使用 getaddrinfo 生成的参数来进行配置,这样就是协议无关的了
  • bind: 请求 kernel 把 socket address 和 socket descriptor 绑定

    • int bind(int sockfd, SA *addr, socklen_t addrlen);
    • 最好是用 getaddrinfo 生成的参数作为 addr 和 addrlen,,从result链表中
  • listen: 默认来说,从 socket 函数中得到的 descriptor 默认是 active socket(也就是客户端的连接),调用 listen 函数告诉 kernel 这个 socket 是被服务器使用的

    • int listen(int sockfd, int backlog);
    • 把 sockfd 从 active socket 转换成 listening socket,用来接收客户端的请求
    • backlog 的数值表示 kernel 在接收多少个请求之后(队列缓存起来)开始拒绝请求
  • accept: 调用 accept 函数,开始等待客户端请求

    • int accept(int listenfd, SA *addr, int *addrlen);
    • 等待绑定到 listenfd 的连接接收到请求,然后把客户端的 socket address 写入到 addr,大小写入到 addrlen
    • 返回一个 connected fd 用来进行信息传输(类似 Unix I/O)

注意 listenfd 和 connected fd, 其设定不同状态是为了连接和处理分开做,从而建立并发服务器

  • listenfd 是作为客户端请求连接的一个端点,通常被创建一次,并存在整个server的生命周期
  • connectfd 是已经连接后的一个端点,server每次接受连接都会创建一次,存在于server为一个客户端服务的一次过程
client开启客户端(open_clientfd 函数,设定访问地址,尝试连接)
  • getaddrinfo: 设置客户端的相关信息

  • socket: 创建 socket descriptor,也就是之后用来读写的 file descriptor

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

    • int connect(int clientfd, SA *addr, socklen_t addrlen);
    • 尝试与在 socker address addr 的服务器建立连接
    • 如果成功 clientfd 可以进行读写
    • connection 由 socket 对描述 (x:y, addr.sin_addr:addr.sin_port)
    • x 是客户端地址,y 是客户端临时端口,后面的两个是服务器的地址和端口
    • 最好是用 getaddrinfo 生成的参数作为 addr 和 addrlen
交换数据

主要是一个流程循环,客户端向服务器写入,就是发送请求;服务器向客户端写入,就是发送响应)
- [Client]rio_writen: 写入数据,相当于向服务器发送请求
- [Client]rio_readlineb: 读取数据,相当于从服务器接收响应
- [Server]rio_readlineb: 读取数据,相当于从客户端接收请求
- [Server]rio_writen: 写入数据,相当于向客户端发送响应

关闭客户端

客户端读到EOF,发起 close
- [Client]close: 关闭连接

断开客户端

服务接收到客户端发来的 EOF 消息之后,断开已有的和客户端的连接
- [Server]rio_readlineb: 收到客户端发来的关闭连接请求
- [Server]close: 关闭与客户端的连接
- 然后返回accept,继续监听下一个client

一个echo程序

书上给了一个echo程序的代码,包括客户端和服务端两部分的主函数和辅助函数

echo客户端
echo客户端主函数

主函数为打开一个socket,然后建立一个循环,把一段用户输入的文字发送到服务器,然后再把从服务器接收到的内容显示到输出中

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

    host = argv[1];
    port = 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客户端open_clientfd

用来建立和服务器的连接,协议无关

int open_clientfd(char *hostname, char *port) {
    int clientfd;
    struct addrinfo hints, *listp, *p;

    // Get a list of potential server address
    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_socktype = SOCK_STREAM; // Open a connection
    hints.ai_flags = AI_NUMERICSERV; // using numeric port arguments
    hints.ai_flags |= AI_ADDRCONFIG; // Recommended for connections
    getaddrinfo(hostname, port, &hints, &listp);

    // Walk the list for one that we can successfully connect to
    // 如果全部都失败,才最终返回失败(可能有多个地址)
    for (p = listp; p; p = p->ai_next) {
        // Create a socket descriptor
        // 这里使用从 getaddrinfo 中得到的参数,实现协议无关
        if ((clientfd = socket(p->ai_family, p->ai_socktype,
                               p->ai_protocol)) < 0)
            continue; // Socket failed, try the next

        // Connect to the server
        // 这里使用从 getaddrinfo 中得到的参数,实现协议无关
        if (connect(clientfd, p->ai_addr, p->ai_addrlen) != -1)
            break; // Success

        close(clientfd); // Connect failed, try another
    }

    // Clean up
    freeaddrinfo(listp);
    if (!p) // All connections failed
        return -1;
    else // The last connect succeeded
        return clientfd;
}
echo 服务端
echo 服务端主函数和echo处理函数

也就是打开和绑定,listen一个socket,然后使用一个循环处理 1.client建立链接,2.调用处理函数echo

// echoserveri.c
#include "csapp.h"
void echo(int connfd);
int main(int argc, char **argv){
    int listenfd, connfd;
    socklen_t clientlen;
    struct sockaddr_storage clientaddr; // Enough room for any addr
    char client_hostname[MAXLINE], client_port[MAXLINE];

    // 开启监听端口,注意只开这么一次
    listenfd = Open_listenfd(argv[1]);
    while (1) {
        // 需要具体的大小
        clientlen = sizeof(struct sockaddr_storage); // Important!
        // 等待连接
        connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
        // 获取客户端相关信息
        Getnameinfo((SA *) &clientaddr, clientlen, client_hostname,
                     MAXLINE, client_port, MAXLINE, 0);
        printf("Connected to (%s, %s)\n", client_hostname, client_port);
        // 服务器具体完成的工作
        echo(coonfd);
        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", (int)n);
        // 把从 client 接收到的信息再写回去
        Rio_writen(connfd, buf, n);
    }
}
echo 服务端open_listenfd

创建 listening descriptor,用来接收来自客户端的请求,协议无关

int open_listenfd(char *port){
    struct addrinfo hints, *listp, *p;
    int listenfd, optval=1;

    // Get a list of potential server addresses
    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_socktype = SOCK_STREAM; // Accept connection
    hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG; // on any IP address
    hints.ai_flags |= AI_NUMERICSERV; // using port number
    // 因为服务器不需要连接,所以原来填写地址的地方直接是 NULL
    getaddrinfo(NULL, port, &hints, &listp);

    // Walk the list for one that we can successfully connect to
    // 如果全部都失败,才最终返回失败(可能有多个地址)
    for (p = listp; p; p = p->ai_next) {
        // Create a socket descriptor
        // 这里使用从 getaddrinfo 中得到的参数,实现协议无关
        if ((listenfd = socket(p->ai_family, p->ai_socktype,
                               p->ai_protocol)) < 0)
            continue; // Socket failed, try the next

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

        // Bind the descriptor to the address
        if (bind(listenfd, p->ai_addr, p->ai_addrlen) == 0)
            break; // Success

        close(listenfd); // Bind failed, try another
    }

    // Clean up
    freeaddrinfo(listp);
    if (!p) // No address worked
        return -1;

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

web服务器

这里实现一个HTTP服务器,关于HTTP协议的一些简单介绍可以看一些之前的笔记HTTP协议介绍

这是一个实现CGI等基本功能的Tiny Server,使用telnet测试。但模型是一个循环服务器,没有并发,所以等chap12看完再做proxy lab

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值