深入理解计算机系统 第11章 笔记整理
这次回顾深入理解计算机系统第11章网络编程。
电子书地址:
参考资料:
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,若出错则为-1。
const 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服务器
略过,见练习课本。