这次回顾深入理解计算机系统第11章网络编程。

电子书地址:

http://eol.bnuz.edu.cn/meol/common/script/preview/download_preview.jsp?fileid=2169600&resid=242120&lid=28605

参考资料:

https://blog.csdn.net/u012294618/article/details/77979604

备注:图片和总结内容均来自于电子书。

第11章:网络编程

重要概念

  • 客户端服务器编程模型
  • 网络对计算机来说是数据源和数据接收方
  • 网络层次结构
    • 局域网
      • 以太网
    • 广域网
  • internet和Internet
  • IP地址
    • 点分十进制表示法
  • 因特网域名,DNS(Domain Name System, 域名系统)
  • localhost,回送地址(loopback address),127.0.0.1
  • 套接字:“地址:端口”
  • 连接由套接字对确定

客户端服务器编程模型

  • 许多网络应用都是基于客户端-服务器模型;

  • 采用这个模型,一个应用是由一个服务器进程一个或者多个客户端进程组成;

  • 服务器管理某种资源,并且通过操作这种资源来为它的客户端提供某种服务;

  • 客户端-服务器模型的基本操作时事物,由以下四步组成:

网络

  • 客户端和服务器通过计算机网络的硬件和软件资源来通信;

  • 对于主机而言,网络只是有一种I/O设备,是数据源数据接收方

  • 物理上而言,网络是一个按照地理远近组成的层次系统:

    • 最底层是LAN(Local Area Network,局域网)

      • 最流行的局域网技术是以太网(Ethernet)

        • 一个以太网段(Ethernet segment)包括一些电缆(通常是双绞线)和一个叫做集线器的小盒子:

        • 以太网一端连接到主机的适配器,另一端则连接到集线器的一个端口上;

        • 每个以太网适配器都有一个全球唯一的48位地址, 它存储在这个适配器的非易失性存储器上

        • 一台主机可以发送一端位(称为(frame))到这个网段内的其他任何主机,每个帧包括一些固定数量的头部位,此后紧随的就是数据位的有效载荷

        • 使用一些电缆和叫做网桥(bridge)的小盒子,多个以太网段可以连接成较大的局域网, 称为桥接以太网(bridged Ethernet),如下图所示:

        • 局域网的简化表示如下:

    • 在层次的更高级别中,多个不兼容的局域网可以通过叫做路由器(router)的特殊计算机连接起来,组成一个internet(互联网络)

      • 每台路由器对于它所连接到的每个网络都有一个适配器(端口)

      • 路由器也能连接高速点到点电话连接,这是称为WAN(Wide-Area Network,广域网)的网络示例;

      • 示例:

  • 互联网络使用协议完成网络通信,示例如下:

说明:

  • internet表示互联网络,Internet表示全球IP因特网;

全球IP因特网

全球IP因特网是最著名和最成功的互联网络实现,因特网客户端-服务器应用程序的基本硬件和软件组织如下:

IP地址

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

/* $begin inaddr */
/* IP address structure */
struct in_addr {
	uint32_t  s_addr; /* Address in network byte order (big-endian) */
};
/* $end inaddr */

说明:

  • IP地址通常是以一种称为点分十进制表示法来表示的,每个字节由它的十进制值表示,并且用句点和其他字节间分开;
  • 例如,128.2.194.242是地址0x8002c2f2的点分十进制表示;

应用程序使用inet_pton和inet_ntop函数来实现IP地址和点分十进制串之间的转换:

#include <arpa/inet.h>

int inet_pton(AF_INET, const char *src, void *dst);
返回:若成功则为1,若src为非法点分十进制地址则为0,若出错则为-1const char *inet_ntop(AF_INET, const void *src, char *dst, socklen_t size);
返回:若成功则指向点分十进制字符串的指针,若出错则为NULL

说明:

  • inet_pton函数将一个点分十进制串(src)转换为一个二进制的网络字节顺序的IP地址(dst);
    • 如果src没有指向一个合法的点分十进制字符串,那么该函数就返回0;
    • 任何其他错误会返回-1,并设置errno;
  • 相似地,inet_ntop函数将一个二进制的网络字节顺序的IP地址(src)转换为它所对应的点分十进制表示,并把得到的以null结尾的字符串的最多size个字节复制到dst;

因为因特网主机可以有不同的主机字节顺序,TCP/IP为任意整数数据项定义了统一的网络字节顺序(network byte order)(大端字节顺序),Unix提供了下面这样的函数在网络和主机字节顺序间实现转换:

#include <arpa/inet.h>

uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
返回:按照网络字节顺序的值。

uint32_t ntoh1(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
返回:按照主机字节顺序的值。

说明:

  • hotnl(s)函数将32(16)位整数由主机字节顺序转换为网络字节顺序;
  • ntohl(s)函数将32(16)位整数由网络字节顺序转换为主机字节顺序;

因特网域名

  • 因特网客户端和服务器互相通信时使用的是IP地址;

  • 然而,对于人们而言,大整数是很难记住的,所以因特网也定义了一组更加人性化的域名(domain name),以及一种将域名映射到IP地址的机制;

  • 域名是一串用句点分隔的单词(字母、数字和破折号),例如whaleshark.ics.cs.cmu.eduo;

  • 域名集合形成了一个层次结构,每个域名编码了它在这个层次中的位置:

  • 域名到IP的映射由分布式数据库DNS(Domain Name System, 域名系统)来维护;

  • 每台因特网主机都有本地定义的域名localhost,这个域名总是映射为回送地址(loopback address) 127.0.0.1;

  • 常用命令:

    nslookup domain
    
    hostname

因特网连接

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

  • 一个套接字是连接的一个端点,每个套接字都有相应的套接字地址,是由一个因特网地址和一个16位的整数端口组成的,用“地址:端口”来表示;

  • 当客户端发起一个连接请求时,客户端套接字地址中的端口是由内核自动分配的,称为临时端口(ephemeral port)。然而,服务器套接字地址中的端口通常是某个知名端口,和服务对应;

  • 一个连接是由它两端的套接字地址唯一确定的,这对套接字地址叫做套接字对(socket pair),由下列元组来表示:

    (cliaddr: cliport, servaddr: servport)
  • 示例:

套接字接口

套接字接口(socket interface)是一组函数,它们和Unix I/O函数结合起来,用以创建网络应用,下图给出了一个典型的客户端-服务器事务的上下文中的套接字接口:

套接字地址结构

代码:

/* $begin sockaddr */
/* IP socket address structure */
struct sockaddr_in  {
    uint16_t        sin_family;  /* Protocol 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_zero[8]; /* Pad to sizeof(struct sockaddr) */
};

/* Generic socket address structure (for connect, bind, and accept) */
struct sockaddr {
    uint16_t  sa_family;    /* Protocol family */
    char      sa_data[14];  /* Address data  */
};	

typedef struct sockaddr SA;
/* $end sockaddr */

说明:

  • 因特网的套接字地址存放在sockaddr_in的16字节结构中;
  • 对于因特网应用,sin_family成员是AF_INET,sin_port成员是一个16位的端口号,而sin_addr成员就是一个32 位的IP地址,IP 地址和端口号总是以网络字节顺序(大端法)存放的;

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 表示这个套接字是连接的一个端点;
  • socket返回的clientfd描述符仅是部分打开的,还不能用于读写;

connect函数

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

#include <sys/socket.h>

int connect(int clientfd, const struct sockaddr *addr, socklen_t addrlen);
返回:若成功则为0,若出错则为-1

说明:

  • connect函数试图与套接字地址为addr的服务器建立一个因特网连接,其中addrlen是sizeof(sockaddr_in);

  • connect函数会阻塞,一直到连接成功建立或是发生错误。

    • 如果成功,Clientfd描述符现在就准备好可以读写了,并且得到的连接是由套接字对

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

      刻画,其中x表示客户端的IP地址,y表示临时端口;

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

bind函数

代码:

#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
返回:若成功则为0,若出错则为-1

说明:

  • bind函数告诉内核将addr中的服务器套接字地址和套接字描述符sockfd联系起来;
  • 参数addrlen是sizeof(sockaddr_in);

listen函数

代码:

#include <sys/socket.h>

int listen(int sockfd, int backlog);
返回:若成功则为0,若出错则为-1

说明:

  • 默认情况下,内核会认为socket函数创建的描述符对应于主动套接字
  • 服务器调用listen函数告诉内核,描述符是被服务器而不是客户端使用;
  • listen函数将sockfd从一个主动套接字转化为一个监听套接字,该套接字可以接受来自客户端的连接请求;
  • backlog参数从略;

accept函数

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

#include <sys/socket.h>

int accept(int listenfd, struct sockaddr *addr, int *addrlen);
返回:若成功则为非负连接描述符,若出错则为-1

说明:

  • accept函数等待来自客户端的连接请求到达监听描述符listenfd,然后在addr中填写客户端的套接字地址,并返回一个已连接描述符(connected descriptor),这个描述符可被用来利用Unix I/O函数与客户端通信;

监听描述符和已连接描述符的关系和区别:

主机和服务的转换

getaddrinfo

getaddrinfo函数将主机名、主机地址、服务名和端口号的字符串表示转化成套接字地址结构:

#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,如果错误则为非零的错误代码。
    
void freeaddrinfo(struct addrinfo *result);
返回:无。
    
const char *gai_strerror(int errcode);
返回:错误消息。
    
/* $begin addrinfo */
struct addrinfo {
    int              ai_flags;     /* Hints argument flags */
    int              ai_family;    /* First arg to socket function */
    int              ai_socktype;  /* Second arg to socket function */
    int              ai_protocol;  /* Third arg to socket function  */
    char            *ai_canonname; /* Canonical hostname */
    size_t           ai_addrlen;   /* Size of ai_addr struct */
    struct sockaddr *ai_addr;      /* Ptr to socket address structure */
    struct addrinfo *ai_next;      /* Ptr to next item in linked list */
};
/* $end addrinfo */

说明:

  • 给定host和service(套接字地址的两个组成部分),getaddrinfo返回result,result一个指向addrinfo结构的链表,其中每个结构指向一个对应于host和service的套接字地址结构:

    • host是域名或者IP;
    • service是服务名或者端口号;
    • host和service可以设置为NULL,但是至少要指定其中一个;
    • 如果要传递hints参数,则只能设置字段ai_family,ai_socktype,ai_protocol,ai_flags;
      • 字段说明见11.4.7;
  • freeaddrinfo释放链表;

  • gai_strerror将getaddrinfo返回的错误代码转换成消息字符串;

getnameinfo

getnameinfo将一个套接字地址结构转换成相应的主机和服务名字符串:

#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,如果错误则为非零的错误代码。

说明:

  • getnameinfo函数将套接字地址结构sa转换成对应的主机和服务名字符串,并将它们复制到host和servcice缓冲区;
  • 参数sa指向大小为salen字节的套接字地址结构;
  • host指向大小为hostlen字节的缓冲区;
  • service指向大小为servlen字节的缓冲区;
  • host和service可以设置为NULL,但是至少要指定其中一个;
  • flags参数说明见11.4.7;

示例

/* $begin hostinfo-ntop */
#include "csapp.h"

int main(int argc, char **argv) 
{
    struct addrinfo *p, *listp, hints;
    struct sockaddr_in *sockp;
    char buf[MAXLINE];
    int rc;

    if (argc != 2) {
        fprintf(stderr, "usage: %s <domain name>\n", argv[0]);
        exit(0);
    }

    /* Get a list of addrinfo records */
    memset(&hints, 0, sizeof(struct addrinfo));                         
    hints.ai_family = AF_INET;       /* IPv4 only */              
    hints.ai_socktype = SOCK_STREAM; /* Connections only */   
    if ((rc = getaddrinfo(argv[1], NULL, &hints, &listp)) != 0) {
        fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(rc));
        exit(1);
    }

    /* Walk the list and display each associated IP address */
    for (p = listp; p; p = p->ai_next) {
        sockp = (struct sockaddr_in *)p->ai_addr;
        Inet_ntop(AF_INET, &(sockp->sin_addr), buf, MAXLINE);
        printf("%s\n", buf);
    } 

    /* Clean up */
    Freeaddrinfo(listp);

    exit(0);
}
/* $end hostinfo-ntop */

套接字接口的辅助函数

open_clientfd

客户端调用open_clientfd建立与服务器的连接,返回一个打开的套接字描述符:

#include "csapp.h"

int open_clientfd(char *hostname, char *port);
返回:若成功则为描述符,若出错则为-1.

open_listenfd

调用open_listenfd函数,服务器创建一个监听描述符,准备好接收连接请求:

#include "csapp.h"

int open_listenfd(char *port);
返回:若成功则为描述符,若出错则为-1

说明:

open_listenfd函数打开和返回一个监听描述符,这个描述符准备好在端口port上接收连接请求。

Web服务器

略过,见练习课本。