九 网络io (course21-22)

网络架构

http://www.cs.cmu.edu/afs/cs/academic/class/15213-f17/www/lectures/21-netprog1.pdf

客户端-服务器模型是网络应用最广泛使用的模型,客户端进程发送请求给服务器进程,服务器进程获取所需资源并响应客户端进程的请求,客户端进程收到响应后展示给用户。网络相关的处理,都是通过网络适配器来完成的,具体在硬件上为(右下角):

image

根据网络的应用范围和架构层级,可以分成三个部分:

  • SAN - System Area Network
    • Switched Ethernet, Quadrics QSW, …
  • LAN - Local Area Network
    • Ethernet, ..
  • WAN - Wide Area Network
    • High speed point-to-point phone lines

最底层 - Ethernet Segment

由若干主机(hosts)通过交换机(hub)连接,通常范围是房间或一层楼,如下图所示:

image
  • 每个 Ethernet 适配器有一个唯一的 48 位的地址(也就是 MAC 地址),例如 00:16:ea:e3:54:e6
  • 不同主机间发送的数据称为帧(frame)
  • Hub 会把每个端口发来的所有数据复制到其他的端口
    • 所有的主机都可以看到所有的数据(注意安全问题)

下一层 - Bridged Ethernet Segment

通常范围是一层楼,通过不同的 bridge 来连接不同的 ethernet segment。Bridge 知道从某端口出发可达的主机,并有选择的在端口间复制数据。

image

为了从概念上简化,我们可以认为,所有的 hub, bridge 可以抽象为一条线,如下图所示:

image

下一层 - internets

不同的(也许不兼容)的 LAN 可以通过 router 来进行物理上的连接,这样连接起来的网络称为 internet(注意是小写,大写的 Internet 可以认为是最著名的 internet)

image

internet 的逻辑结构为:

  • Ad hoc interconnection of networks
    • 没有特定的拓扑结构
    • 不同的 router 和 link 差异可能很大
  • 通过在不同的网络间跳转来传递 packet
    • Router 是不同网络间的连接
    • 不同的 packet 可能会走不同的路线

网络协议

在不同的 LAN 和 WAN 中传输数据,就要守规矩,这个规矩就是协议。协议负责做的事情有:

  • 提供 naming scheme
    • 定义 host address 格式
    • 每个主机和路由器都至少有一个独立的 internet 地址
  • 提供 delivery mechanism
    • 定义了标准的传输单元 - packet
    • Packet 包含 header 和 payload
      • header 包括 packet size, source 和 destination address
      • payload 包括需要传输的数据

在这样的协议下,具体的数据传输如下图所示,这里 PH = Internet packet header, FH = LAN frame header(具体名词解释可见参考文末参考资料):

image

Internet 是 internet 最为著名的例子。主要基于 TCP/IP 协议族:

  • IP (Internet Protocal)
    • Provides basic naming scheme and unreliable delivery capability of packets (datagrams) from host-to-host
  • UDP (Unreliable Datagram Protocol)
    • Uses IP to provide unreliable datagram delivery from process-to-process
  • TCP (Transmission Control Protocol)
    • Uses IP to provide reliable byte streams from process-to-process over connections

Accessed via a mix of Unix file I/O and functions from sockets interface.

  • 主机有 32 位的 IP 地址 - 23.235.46.133

    • IPv4 - 32 位地址,IPv6 - 128 位地址
  • 不同主机之间的进程,可以通过 connection 来交换数据

IP 地址

我们会用一个叫做 IP address struct 的东西来存储,并且 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

具体的转换可以使用 getaddrinfogetnameinfo 函数

Internet 域名

这里主要需要了解的就是 Domain Naming System(DNS) 的概念,用来做 IP 地址到域名的映射。具体可以用 nslookup 命令来查看,下面是一些例子


$ nslookup www.twitter.com

Server:     8.8.8.8

Address:    8.8.8.8#53

Non-authoritative answer:

www.twitter.com canonical name = twitter.com.

Name:   twitter.com

Address: 199.16.156.6

Name:   twitter.com

Address: 199.16.156.198

Name:   twitter.com

Address: 199.16.156.230

Name:   twitter.com

Address: 199.16.156.70

Internet 连接

客户端和服务器通过连接(connection)来发送字节流,特点是:

  • 点对点: 连接一对进程
  • 全双工: 数据同时可以在两个方向流动
  • 可靠: 字节的发送的顺序和收到的一致

Socket 则可以认为是 connection 的 endpoint,socket 地址是一个 IPaddress:port 对。

Port(端口)是一个 16 位的整数,用来标识不同的进程,利用不同的端口来连接不同的服务:

  • Ephemeral port: Assigned automatically by client kernel when client makes a connection request
  • Well-known port: Associated with some service provided by a server(在 linux 系统上可以在 /etc/services 中查看具体的信息)
    • echo server: 7/echo
    • ssh server: 22/ssh
    • email server: 25/smtp
    • web servers: 80/http

Socket 接口

http://www.cs.cmu.edu/afs/cs/academic/class/15213-f17/www/lectures/22-netprog2.pdf

一系列系统级的函数,和 Unix I/O 配合构造网络应用(在所有的现代操作系统上都可用)。

对于 kernel 来说,socket 是 endpoint of communication;对于应用程序来说,socket 是 file descriptor,用来读写(回忆一下,STDIN 和 STDOUT 也是 file descriptor)。客户端和服务器通过读写对应的 socket descriptor 来进行。

image

套接字地址结构:

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

因特网的套接字地址存放在类型为sockaddr_in的16字节结构中,因特网应用,sin_family成员是AF_INET。Sin_port成员是一个16位端口号,sin_addr成员是一个32位IP地址。其中connect, bind, accept函数要求一个指向与协议相关的套接字地址结构的指针。

socket函数:

int socket(intdomain, int type, int protocol);客户端和服务器使用socket函数来创建一个socket descriptor。可以使用clientfd = Socket(AF_INET, SOCK_STREAM, 0);使套接字成为连接的一个端点。

connect函数:

intconnect(int clientfd, const struct sockaddr *addr, sockel_t addrlen); 客户端通过调用connect函数来建立和服务器的连接。Connect函数试图与套接字地址为addr的服务器建立一个因特网连接,addrlen是sizeof(sockaddr_in)。connect函数会阻塞,直到连接成功或者发生错误。若成功,clientfd描述符就准备好可以读写了,且得到的链接是套接字对(x:y, addr.sin_addr:addr.sin_port)。X表示客户端的IP地址,y表示临时端口,对唯一确定了客户端主机上的客户端进程。对于socket最好使用getaddrinfo来为connect提供参数。

bind,listen,accept函数是服务器用来跟客户端建立连接的。

int bind(int sockfd, const struct sockaddr*addr, socklen_t addrlen);bind函数告诉内核将addr中的服务器套接字地址和套接字描述符sockfd联系起来,getaddrinfo为bind提供参数比较好。

listen函数:

客户端是发起连接请求的主动实体,服务器是等待来自客户端的连接请求的被动实体。默认情况下,内核会认为socket函数创建的描述符对应于active socket.它存在于一个连接的客户端。服务器调用listen函数告诉内核,描述符是被服务器使用的。

int listen(int sockfd, int backlog);函数将sockfd从一个主动socket转化为一个listeningsocket,该socket可以接受来自客户端的连接请求;backlog参数暗示内核在开始拒绝连接请求前,队列中要排队的未完成的连接请求数量。

accept函数,

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

int accept(int listenfd, struct sockaddr *addr, int *addrlen);该函数等待来自客户端的连接请求到达侦听描述符listenfd,之后再addr中填写客户端的socket地址,返回一个已连接描述符,这个descriptor可以用来Unix I/O函数与客户端通信。

其中,listening descriptor是作为客户端连接请求的一个端点,通常被创建一次,并存在于服务器的整个生命周期,而已连接描述符是客户端和服务器间已经建立起来的连接的一个端点。服务器每次接受连接请求时都会创建一次,只存在于服务器为一个客户端服务的过程中。

主机和服务的转换:

Linux提供一些强大的函数getaddrinfo,getnameinfo实现二进制套接字地址结构和主机名,主机地址,服务名和端口号的字符串表示之间的相互转化。和套接字接口一起使用时,这些函数能使我们编写独立于任何特定版本的IP协议的网络程序。

简单服务器实现

架构总览

写服务器,最重要的就是理清思路,上节课我们介绍了诸多概念,尤其是最后提到的 getaddrinfogetnameinfo,都是我们在搭建过程中必不可少的工具。参考上面的流程图,整个的工作流程有 5 步:

  1. 开启服务器(open_listenfd 函数,做好接收请求的准备)
    • getaddrinfo: 设置服务器的相关信息,具体可以参见 图1&2
    • 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);
      • The process can read bytes that arrive on the connection whose endpoint is addr by reading from descriptor sockfd
      • Similarly, writes to sockfd are transferred along connection whose endpoint is addr
      • 最好是用 getaddrinfo 生成的参数作为 addraddrlen
    • 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 descriptor 用来进行信息传输(类似 Unix I/O)
      • 具体的过程可以参考 图3
  2. 开启客户端(open_clientfd 函数,设定访问地址,尝试连接)
    • getaddrinfo: 设置客户端的相关信息,具体可以参见 图1&2
    • 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 生成的参数作为 addraddrlen
  3. 交换数据(主要是一个流程循环,客户端向服务器写入,就是发送请求;服务器向客户端写入,就是发送响应)
    • [Client]rio_writen: 写入数据,相当于向服务器发送请求
    • [Client]rio_readlineb: 读取数据,相当于从服务器接收响应
    • [Server]rio_readlineb: 读取数据,相当于从客户端接收请求
    • [Server]rio_writen: 写入数据,相当于向客户端发送响应
  4. 关闭客户端(主要是 close
    • [Client]close: 关闭连接
  5. 断开客户端(服务接收到客户端发来的 EOF 消息之后,断开已有的和客户端的连接)
    • [Server]rio_readlineb: 收到客户端发来的关闭连接请求
    • [Server]close: 关闭与客户端的连接

Client 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;

}

Server 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;

}

简单的 socket 服务器实例

客户端 Echo Client

这个客户端做的事情很简单,就是把一段用户输入的文字发送到服务器,然后再把从服务器接收到的内容显示到输出中,具体可以参见注释


// 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);

}

服务器 Iterative Echo Server

服务器做的工作也很简单,接收到从客户端发送的信息,然后返回一个一模一样的。具体参加注释。


// 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);

    }

}

HTTP REQUEST RESPONSE

image.png

image.png
image.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容

  • “鲁迅说,世界上本没有路,走的人多了,也就成了路。对于每个个人来说,人生刚开始的时候面对这世上,是千万条充满未知的...
    雪梨和雪柔阅读 464评论 0 7
  • 初冬 自从离职一心带娃后,每天的生活,除了宝宝的生活起居,就是一日三餐的吃食了。除此之外,就是待风和日丽之时,带着...
    薄暮归村阅读 182评论 0 0
  • 【基本案情】 李某某年幼时,父母就因外出打工,常年将其留在家中与爷爷和奶奶、姥姥 和姥爷一起生活,其父亲在家里经常...
    宫明路阅读 389评论 1 0
  • 我研究JavaScript闭包(closure)已经有一段时间了。我之前只是学会了如何使用它们,而没有透彻地了解它...
    流动码文阅读 249评论 1 0
  • 时间:2+10+10+30+10+3 感觉: 汗,依旧 关键词: 关你屁事 关我屁事 目标 计划 行动 换位思考 ...
    倩倩打怪记阅读 163评论 0 0