CSAPP--第十一章:网络编程

CSAPP--第十一章:网络编程

客户端-服务器模型

  • 网络应用都是基于客户端-服务器模型。
    一个应用由一个服务器进程和多个客户端进程构成。

    事务:模型中的基本操作,包含四步:

    ①客户端进程发送请求

    ②服务器进程接收请求,并与其存储设备交互进行处理。

    ③服务器进程发送响应。

    ④客户端进程处理响应。

    事务

    网络

客户端和服务端通常运行在不同主机上,通过计算机网络的硬件和软件来通信。

  • LAN(Local Area Network)

    局域网,其包含的主机位置范围最小,通常在一个建筑内。

    以太网(Ethernet): 现在最流行的局域网技术。

    局域网内,各主机通过集线器进行连接。如下图:


    网络硬件组成
以太网段
  • 网桥(bridge):用于连接多个以太网段,形成较大的局域网(桥接以太网)。

    如下图:

桥接以太网
  • WAN(Wide-Area network):

    广域网,其将不同局域网连接在一起,因为地理范围比局域网大,取名广域网。

    如下图:

小型互联网络

网络协议

不同的主机,其采用的局域网和广域网技术可能不兼容,所以提出一个协议,消除主机间的通信障碍。

  • 协议功能:

    ①命名机制:采取同一套地址命名机制。

    ②传送机制:定义一种通用的数据包:包头 + 有效荷载。包头记载了源和目的主机的地址、及包大小等信息。

全球IP因特网

互联网络客户端-服务器交互

  • TCP/IP协议(Transmission Control Protocol,传输控制协议 / Internet Protocol,互联网络协议)

    TCP/IP是一个协议簇,每个协议提供不同功能。TCP比IP更复杂。

    • IP协议:提供了基本命名方式和传送机制。

      实现从一个主机,往另一个主机传送包,也称数据报

      其对丢包不会试图恢复。

    • UDP协议(Unreliable Datagram Protocol,不可靠数据报协议)

    • TCP协议:构建在IP协议之上的复杂协议,提供进程间可靠的双向连接。

      因特网应用程序的软件和硬件

  • IPv4、IPv6

    因特网协议版本4 (32位地址)

    1996年提出的因特网版本协议6 (128位地址):如今用户还不多。

  • IP地址 //以下针对 IPv4

ip地址是32位无符号整数。网络程序将ip地址存放在:ip地址结构中。

ps:其导致了麻烦的操作,每次使用要建立结构体,再取其中的值,很麻烦!!!

struct in_addr{
    unit32_t  s_addr; /* 网络字节顺序中统一为大端顺序 */
};

网络字节顺序(network byte order)为大端字节顺序。

所以对于小端机器中,会进行字节顺序(32位)转换。

Unix提供的转换函数:

#inlcude<arpa/inet.h>
uint32_t  htonl(uint32_t  hostlong );   //host to network
uint16_t  htonl(uint16_t  hostshort ); 

uint32_t  ntohl(uint32_t  netlong );    //network to host
uint16_t  ntohl(uint16_t  netshort ); 

ps:没有对应的64位的函数。

  • 点分十进制表示法

    用来表示ip地址的一种方法。

    如0x00000000 -> 0.0.0.0

    ​ 二进制 点分十进制

    应用程序通过inet_pton 、inet_ntop函数来实现转换。

    #include<arpa/inet.h>
    int inet_pton(AF_INET, const char *src, void *dst);
                                //成功返回1,src非法时点分十进制地址为0,出错为-1;
    const char *inet_ntop(AF_INET, const void *src, char *dst, socklen_t size);
                              //成功返回指向点分十进制字符串的指针,错误为NULL
                          
    

    说明:以上两个函数中,网络地址的值是在结构体中,所以要先设置结构体,然后取其中的值。

    编写中出错了蛮多。

    具体看linux 中的网络编程文件夹下的ntop.c 、 pton.c。

  • 因特网域名

    域名:一串用句号分隔的单词(字母、数字、破折号)。例如:www.baidu.com

    域名层次结构(树状):

因特网域名层次结构的部分

分为多层域名--

第一层为ICANN协会定义的,包括com、edu、gov、org、net等。

第二层为先到先分配的。一个组织得到了二级域名后,就可以在子域(节点的子树)中创建任何新的域名。如:cmu.edu cs.cmu.edu

  • 域名、IP地址映射

    1988年之前,都是用HOSTS.TXT的 文本文件来手工维护。

    1988年后,通过世界范围内的数据库DNS(Domain Name System:域名系统)来维护。

    DNS数据库:有上百万条 主机条目结构 组成,每条主机条目结构定义了一组域名和IP地址之间的映射。

  • 映射关系:

    一对一 映射

    多个域名映射一个IP地址

    一个域名映射多个IP地址(同一组)

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

  • 因特网连接

    因特网中,客户端和服务器通过在连接上发送和接收字节流来通信。

    从连接一对进程而言,其是:点对点的。

    从数据双向流动而言,其是:全双工的。

    • 套接字(socket):

      连接的一个端点。

      其内容为IP地址加上16位的整数端口组成。(非硬件而是软件端口)

      如:127.0.0.1:51212

    • 临时端口(ephemeral port):

      当客户端发起连接请求时,客户端套接字地址中的端口,是由内核自动分配的。称为临时端口。

    • 知名端口:

      服务器中,套接字地址的端口常常是和服务绑定的,如web服务器通常使用端口80,电子邮件服务器通常使用端口25等。称为知名端口。

    • 套接字对(socket pair):

      由连接两端的套接字,唯一确定。(cliaddr : cliport,servaddr:servport)


      网络套接字
  • 套接字接口(socket inerface)

    其是一组函数,与Unix I/O结合起来,用以创建网络应用。

    套接字地址结构

    ①从内核角度看,套接字是通信的一个端点。

    ②从linux程序看,套接字是一个有相应描述符的打开文件。

    因特网的套接字地址存放在一个结构体中:sockaddr_in (16字节)

    /* IP socket addr structure */
    struct sockaddr_in{
      uint16_t  sin_family; /*socket family(always AF_INET)*/
      uint16_t  sin_port;   /*port number in network byte order*/
      struct in_addr  sin_addr;/*IP address in network byte order*/
      unsigned char   sin_zeor[8];/*pad to sizeof(struct sockaddr)*/
    };
    
    /*generic socket addr structure(for connect,bind,accept)*/
    struct sockaddr{
      uint16_t  sa_family;  /*protocol family*/
      char      sa_data[14];/*address data*/
    };
    

实际客户端-服务器的访问流程如下图:


基于套接字接口的网络应用
  • 相关函数

    • socket函数

      客户端和服务器使用socket函数来创建一个套接字描述符(socket descriptor)

      客户端和服务器都首先要用这个函数。

      #include<sys/types.h>
      #include<sys/socket.h>
      
      int socket(int domain,int type,int protocol);
                          //成功返回非负数描述符,出错返回-1;
      /* 如:clientFd = socket(AF_INET, SOCK_STREAM,0);AF_INET:指明32位IP地址。SOCK_STREAM:这个套接字是连接的一个端点。*/ 
      

      此时仅是部分打开,并不能进行读写,根据客户端和服务器使用不同的方式完全打开。

      这几个函数最好用getaddrinfo函数来赋予参数。

    • connect函数

      客户端用来与套接字地址为addr 的服务器建立网络连接。

      #incldue<sys/sokcet.h>
      
      int connect(int clientfd, const struct sockaddr  *addr, socklen_t addrlen);
                      //成功返回0,出错返回-1. 描述符保存在clientfd中。
                  //addrlen=sizeof(sockaddr_in)
      

      当成功时,clientfd就可以读写了。此时套接字对为:

      (x:y , addr.sin_addr : addr.sin_port)

      x:客户端IP。y:客户端临时端口。其唯一确定了客户端主机的某个进程。

    • bind函数

      socket、connect函数:客户端用来与服务器建立连接。(依次)

      socket、bind、listen、accpet:服务器用来与客户端建立连接。(依次)

      bind函数将一个套接字地址和一个套接字描述符绑定起来(如socket刚刚建立的)。

      #include<sys/socket.h>
      
      int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
              //成功范围0,出错-1.
      
    • listen函数

      listen函数将一个主动套接字描述符(如bind转化后的sockfd)转化为监听套接字(listening socket),其可以接收客户端的连接请求。

      说明:无论客户段服务器,内核默认套接字为主动套接字。所以服务器要将其转换为监听套接字,说明自己是服务器端。

      #include<sys/socket.h>
    
      int listen(int sockfd, int backlog);
                      //backlog的确切含义要求对TCP\IP协议有理解。CSAPP将其设置为较                    //大的值如:1024
    
    • accept函数

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

      #include<sys/socket.h>
    
      int accept(int listenfd, struct sockaddr *addr, int *addrlen);
                      //成功返回已连接描述符,出错返回-1
                      //结构 addr用来安置客户端过来的套接字地址(在结构中一一替换)
    

会返回一个已连接描述符,用这个描述符,可以与客户端进行Unix I/O读写。

注意:监听描述符和已连接描述符都会存在于描述符列表中。

实际步骤:

①服务器调用accept,等待连接请求到达监听描述符。(假设=3)

②客户端调用connect函数,请求一个连接。

③accept函数打开一个新的已连接描述符(connect descriptor)connfd(假设=4),在clientfd和connfd之间建立连接,并随后返回connfd给应用程序。

客户端也从connect返回,然后,客户端和服务器都可以通过读写clientfd和connfd来传送数据。


说明:监听描述符是存在于服务器的生命周期内,只会创建一次。而connfd则是客户端和服务器已经建立起来的一个端点。服务器每次接受连接请求都会创建一次,其只存在于服务器为一个客户端服务的过程中。

ps:有利于并发编程。

主机和服务的转换

  • getaddrinfo函数

    用处:主机IP地址和服务(或端口)→ 套接字地址

    #include<sys/types.h>
    #include<sys/socket.h>
    #include<netdb.h>
    
    int getaddrinfo(const char *host, const char *service, const struct addrinfo *hints, struct addrinfo **result);
                      //成功返回0,错误返回非0的错误代码,其可转化为字符串。
                      
    void freeaddrinfo(struct addrinfo *result);
    
    const char *gai_strerror(int error);    //将错误代码翻译成字符串。
    

    //hints是一个addrinfo结构,提供对于返回的套接字地址列表的更好的控制。只能设置下列字段:ai_family , si_socktype , ai_protocol , ai_flasg。结构中其他字段为0(NULL).

    struct addrinfo{
      int              ai_flags;
      int              ai_family;
      int              ai_socktype;
      int              ai_protocol;
      char            *ai_cannoname;
      size_t           ai_addrlen;
        struct sockaddr *ai_addr;
        struct addrinfo *ai_next;
    }
    
  • getnameinfo函数

    用处:套接字地址结构 → 主机IP地址和服务名字符串

    #include<sys/socket.h>
    #include<netdb.h>
    
    int getnameinfo(const struct sockaddr *sa, socklen_t salen,
                  char *host, size_t hostlen,
                  char *service, size_t servlen, int flags);
              //成功返回0,错误返回非零错误代码。
              //flags 位掩码。
    

    getnameinfo函数将套接字地址结构sa,转换成对应的主机和服务名字符串,将他们复制到host和service缓冲区。

    如果返回非零,可以用gai_strerror(int errorcode);转化为字符串。

以上两个函数,如果主机名和服务名可以空白一个(NULL代替)。不能两个都NULL。

实验代码在CSAPP p659,或者我的linux 主机上的网络编程。

  • 小结:两个关于套接字地址的结构体:

    sockaddr:包含套接字的IP地址、端口地址、协议类型。

    addrinfo:对sockaddr结构体的再包装,附加了一些额外信息。

用包装过的函数创建连接

说明:将上述的客户端和服务器中建立连接的函数,包装起来成为一个简洁实用的函数。

  • open_clientfd函数

    客户端调用此函数,建立与服务器的连接,其返回一个打开的套接字描述符。

    服务器运行在hostname主机上的,并监听了port端口。

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

int open_clientfd(char *hostname, char *port)   //针对客户端。
{
    int clientfd;
    struct addrinfo  hints, *listp, *p;
    
    /* get alist of server addresses */
    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_socktype = SOCK_STREAM; /*open a connection*/
    hints.ai_flags = AI_NUMERICSERV; /*using a numeric(数字) port argument*/
    hints.ai_flags |= AI_ADDRCONFIG; /*recommended for connections*/
    getaddrinfo(hostname, port, &hints, &listp);
    
    /*walk(traverl) the list for one that we can successfully connect to */
    for(p = listp; p ; p = p->ai_next ){
        
        /*create a socket descriptor */
        if((clientfd = socket(p->ai_family,p->ai_socktype,p->ai_protocol))<0)
            continue;  /*socket failed, try the next one */
        
        /*connect to the server */
        if(connect(clientfd,p->ai_addr,p->ai_addrlen)!=-1)
            break;          /*success*/
        close(clientfd);    /*connect failed, try another*/
    }
    
    /* clean up */
    freeaddrinfo(listp);
    if(!p)  return -1;   /* all connects failed */
    else    return clientfd;
}

注意:代码具有协议无关性。因为socket和connect的参数都是getaddrinfo自动生成的。

  • open_listenfd

    服务器调用此函数,打开并返回一个监听描述符,准备好在端口port接收请求。

    #include<sys/socket.h>
    #include<netdb.h>
    #include<sys/types.h>
    #include<stdio.h>
    
    int open_listenfd(char *port)
    {
        struct addrinfo hints, *listp, *p;
        int listenfd, optval=1;
        
        /* 得到一张服务地址表 */
        memset(&hints,0 , sizeof(struct addrinfo));
        hints.ai_socktype = SOCK_STREAM;             /*类型为接收连接*/
        hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG;   /*针对任何ip地址*/
        hints.ai_flags |= AI_NUMRICSERV;                  /*使用端口数字*/
        getaddrinfo(NULL, port, &hints, &listp);
        
        /* 遍历地址表,找到一个可以用来绑定的 */
        for(p = listp ; p ; p = p.ai_next){
            /* 增加一个套接字描述符 */
            if((listenfd = socket(p->ai_family,p->ai_socktype,p->ai_protocol))<0)       continue;   /* 套接字不可用,试下一个 */
            
            /* 为bind中,消除“地址早已被使用”的错误可能 */
            setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, 
                       (const void*)&optval, sizeof(int));
            
            /* 绑定描述符和套接字地址 */
            if(bind(listenfd, p->ai_addr, p->ai_addrlen) == 0)
                break;  /*成功*/
            close(listenfd);  /*绑定失败,关闭描述符,试下一个。*/
        }
        
        /* 释放 */
        freeaddrinfo(listp);
        if(!p) return -1;   /*没有地址可用*/
        
        /* 使一个监听套接字,准备好去接收一个连接请求 */
        if(listen(listenfd, LISTENQ)<0){
            close(listenfd);
            return -1;
        }
        return listenfd;
    }
    
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,142评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,298评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,068评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,081评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,099评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,071评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,990评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,832评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,274评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,488评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,649评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,378评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,979评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,625评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,643评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,545评论 2 352

推荐阅读更多精彩内容