您的位置:首页 > 理论基础 > 计算机网络

[置顶] [CSAPP笔记][第十一章网络编程]

2016-05-28 21:57 543 查看

第十一章 网络编程

我们需要理解基本的
客户端-服务端
编程模型,以及如何编写使用因特网提供的服务的
客户端-服务端
程序。

最后,我们将把所有这些概念结合起来,开发一个小的但功能齐全的
Web
服务器,能够为真实的
Web
浏览器提供静态的和动态的文本和图形内容。

11.1 客户端 - 服务器编程模型

每个网络应用程序都是基于
客户端 - 服务器模型


采用这种模型,一个应用是由一个
服务器
进程

和一个或多个客户端
进程
组成。

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

WEB
服务器,代表
客户端
检索,执行磁盘内容。

FTP
服务器,为
客户端
进行存储和检索。

电子邮件
服务器,为
客户端
进行读和更新。

客户端-服务器
模型中的基本操作是
事务(transaction)
.

一个
客户端-服务器
事务由四步组成

客户端需要服务的时候,向服务器发送请求,发送一个
事务


服务器收到请求后,解释它,并以适当方式操作它的资源。

服务器给客户端发送一个
响应
,并等待下一个请求。

客户端收到
响应
并处理它。

11.2 网络

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

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



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

最低层是
LAN(Local Area Network,局域网)
:在一个建筑或校园范围内。

迄今为止,最流行的
LAN
技术是
以太网(Ethernet)
.

Xerox PARC
公司在20世纪70年代中期提出。

以太网
被证明是适应力极强的,从
3 MB/s
10 GB/s


一个
以太网段(Ethernet segment)




包括一些
电缆(通常是双绞线)
和一个叫做
集线器
的小盒子。

每根
电缆
都有相同的最大位带宽

典型的是
100MB/s
或者
1GB/S
.

一端连接在主机的
适配器
,一端连接到集线器的
一个端口


集线器
不加分辨地将从一个端口收到的每个位复制到其他所有端口上。

因此每台主机都能看到每个位。

以太网段
通常跨越一些小的区域。

例如某建筑物的一个房间或一个楼层。

扩展介绍
以太网


每个
以太网适配器(网卡)
都有一个全球唯一的
48
位地址,它存储在这个适配器的
ROM
上(
MAC
)。

一台主机可以发送一段
,称为
帧(frame)
,到这个
网段
内其他任何主机。

每个
包括

一些固定数量的
头部(header)


用于表示此
的源,和目的地址以及此
的长度。

此后就是数据位的
有效载荷


每个主机适配器都能看到这个
,但是只有目的主机实际读取它。

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




一些
电缆
连接网桥与网桥,或者 网桥与集线器。

这些电缆的带宽可以是不同的。



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


Internet和internet

我们总是用小写字母的
internet
表示一般概念,大写的
Internet
表示一种具体实现,如全球IP因特网。

WAN
(
Wide-Area Network
,广域网)



互联网至关重要的特性是:

它能由采用完全不同和不兼容技术的各种局域网和广域网组成。

Q
:如何能够让某台
源主机
跨过所有这些不兼容的网络发送数据位到另一台
目的主机
呢?

A
:解决办法是一层运行在每台主机和路由器上的
协议软件
,消除不同网络之间的差异。

这个软件实现一种
协议
:控制主机和路由器如何协调工作来实现数据传输。

必须提供两种基本能力:

命名机制


每台主机会被分配至少一个
互联网地址(internet address)
,这个地址唯一标识了这台主机。

传送机制


协议
通过定义一种把数据位捆扎成不连续的片(称为
)的方式。

一个
是由
包头
有效载荷
组成的。

包头


包的大小


源主机
目的主机
地址

有效载荷
包括从源主机发出的数据位



一个
客户端
运行在主机
A
上,主机
A
LAN1
相连,它发送了一串数据字节到运行在主机B上的服务器端,主机B则连接在
LAN2
上。有如下8个步骤。

运行在主机
A
上的客户端进行系统调用,从客户端的
虚拟地址空间
拷贝到
内核缓冲区


主机
A
上的
协议软件
通过在数据前附加互联网络
包头
LAN1
帧头,创建了一个
LAN1
的帧。

互联网包头
寻址到互联网主机B。(最终目的)

LAN1帧头
寻址到
路由器
。(中转站)

封装

LAN1帧
有效载荷
互联网络包


互联网络包
有效载荷是实际的用户数据。

这种
封装
是基本的网络互联方法之一。

LAN1
适配器拷贝该
到网络上。

到达路由器,路由器的
LAN1适配器
从电缆上读取它,并传送到协议软件中。

路由器从
互联网包头中
提取处目的互联网络地址,用它作为路由器的索引,确定向哪里转发这个包。

路由器剥落旧的
LAN1
的帧头,加上寻址到主机
B
的新的
LAN2
帧头,并把得到的帧传送到适配器。

路由器的
LAN2适配器
拷贝该
到网络

到达主机B时,它的适配器从电缆上读到此帧,并将它传送到协议软件。

最后,主机B上的协议软件剥落包头和帧头。服务器进行一个读取这些数据的
系统调用


当然,在这里,我们掩盖了许多非常艰难的问题。

如果不同的网络有不同
大小的最大值,该怎么办。

路由器如何知道往哪里转发


网络拓扑变化的时候,如何通知路由器。

包丢失了,会如何?

虽然如此,我们也能大概了解到互联网络思想的精髓。

11.3 全球IP 因特网



每台因特网主机都运行实现
TCP/IP
协议 (Transmission Control Protocol/Intelnet Protocol,传输控制协议/互联网络协议)的软件,几乎所有计算机系统都支持这个
协议


因特网的客户端和服务端混合使用
套接字接口
函数和
Unix I/O
函数来进行通信。

套接字函数
典型地是作为会陷入内核的
系统调用
来实现的,并调用各种内核模式的
TCP/IP
函数。

TCP/IP
协议实际上一个
协议族
,每一个协议提供不同的功能。



IP
协议提供基本的命名方法,和传递机制。

这种
传递机制
能够从一台因特网主机往其他主机发送包,也叫做
数据报(datagram)


IP
机制从某种意义上是不可靠的,如果数据报在网络丢失或重复,并不会试图恢复。

UDP(Unreliable Datagram Protocol,不可靠数据报协议)
稍微扩展了
IP
协议。

这样,
可以在
进程
间,而不是
主机
间传送。

TCP
是一个构建在
IP
之上的复杂协议,提供了进程间可靠地
全双工(双向)
的连接。

为了简化讨论

我们将
TCP/IP
看作是一个单独的整体协议。

不讨论它的内部工作,只讨论
TCP
IP
为应用程序提供的基本功能。

不讨论
UDP


从程序员的角度,我们可以把因特网看作世界范围内主机的集合,满足一下特性。

主机集合被映射为一组
32
位的
IP
地址。

这组
IP
地址可以被映射为一组称为
因特网域名(Internet domain name)
的标示符。

因特网主机上的进程能够通过
连接
和任何其他主机上的进程通信。

11.3.1 IP地址

一个
IP
地址就是一个32位无符号整数。网络程序将
IP
地址存放在一个
IP地址结构
中。

/* Internet address structure */
struct in_addr{
unsigned int s_addr;
}


为什么要用结构来存放标量
IP
地址

是早期的不幸产物,但是现在更改太迟了。

主机字节序,和网络字节序

因为因特网主机可以有不同的
主机字节顺序


TCP/IP
为任意整数数据项定义了统一的
网络字节顺序(network byte order)
(大端,x86是小端)。

Unix
提供下面这样的函数实现转换。



IP地址通常是以一种称为
点分十进制表示法
来表示的

这里,每个
字节
(8位)都是由它的十进制表示(0~255),并且用句点和其他字节间分开。

在Linux系统上,你能够使用
hostname
命令来确定你自己主机的点分十进制:

linux> hostname -i
10.174.204.145


可以使用
inet_aton
inet_ntoa
函数来实现两者之间互相转换。



11.3.2 因特网域名

方便人们记忆的对于
IP
的映射就是
域名


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



叶子结点反向到根的
路径
就是
域名


层次结构第一层 : 未命名的根结点

层次结构第二层 :
一级域名(first-level domain name)


由非盈利组织
ICANN
(Internet Corporation for Assigned Names and Numbers,因特尔分配名字数字协会)定义。

常见的一级域名:
com
,
edu
,
gov
,
org
net


层次结构第三层:
二级域名(second-level)


例如:
cmu.edu


这些域名是由
ICANN
的各个授权代理按照先到先服务的基础分配的。

一旦一个组织得到一个二级域名,那么它就可以在这个子域中创建任何新的域名了。

因特网定义了
域名集合
IP
地址直接的映射。

HOSTS.TXT


直到
1988
年,这个映射都是通过一个叫做
HOSTS.TXT
的文本文件来手工维护的。

DNS
:

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

DNS
数据库由上百万的
主机条目结构(host entry structure)
组成的。

定义了一组
域名
(一个官方名字和一个别名)和一组
IP
地址之间的映射。



因特网应用程序通过调用
gethostbyname
gethostbyaddr
函数,从DNS数据库中检索任意的主机条目。。



每台主机都有本地定义的域名
localhost


这个域名总是映射
本地送回地址(loopback address)
:
127.0.0.1


localhost
名字为引用运行在同一机器上的客户端和服务端提供了一种便利和可移植的方式。



11.3.3 因特网连接

Internet
服务端和客户端通过在
连接
上发送和接收
字节流
来通信。

从连接一对进程的意义上而言,连接是
点对点
的。

从数据可以同时双向流动的角度来说,它是
全双工
的。

并且从由源进程发出的字节流最终被目的进程按照发送的数据接收来说,它是
可靠


一个
套接字
连接的
一个端点。

每个套接字都有相应的
套接字地址


是由一个
IP
地址和一个16位的整数
端口
组成的,用
地址:端口
来表示。

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

称为
临时端口


然后,服务器套接字地址中的
端口
通常是某个知名的端口,和这个服务相对应的。

例如:

Web服务器通常使用端口
80


电子邮件服务器使用端口
25


Unix
机器上,文件
/etc/services
包含一张这台机器提供的服务和他们的知名端口号的综合列表。

一个
连接
是由它两端的套接字地址唯一确定的。

这对套接字地址叫做
套接字对(socket pair)
,由下列元组来表示:

(cliaddr:cliport,servaddr:servport)




11.4 套接字接口

套接字接口(socket interface)
是一组函数,他们和
Unix I/O
函数结合起来,用以创建网络应用。

给出一个典型的客户端-服务器事务的上下文中
套接字接口
概述,以此导向。





11.4.1 套接字地址结构

不同的角度:

Unix
内核角度来看,一个套接字就是通信的一个
端点


Unix
程序来看,套接字就是一个有相应描述符的打开文件。



Internet
的套接字地址(
Internet-sytle
)存放在上图所示的类型为
sockaddr_in
的16字节结构中。

sin_family
成员是
AF_INET
,ipv4还是ipv6。

sin_port
成员是一个16位的端口号。

sin_addr
成员就是一个32位的
IP
地址。

IP
地址和端口号总是以网络字节顺序(大端法)存放的。

sin_zero
是填充,使得
sockaddr_in
sockaddr
一样大。

sockaddr_in
给程序员操作的,
sockaddr
交由套接字函数使用的,两者可以直接强制转换。



11.4.2 socket函数

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


open
差不多

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

int socket(int domain, int type ,int protocol);
返回:若成功则为非负描述符,出错为-1


我们总是带这样的参数调用
socket
函数:

clientfd = Socket(AF_INET,SOCK_STREAM,0);


AF_INET
表面我们在使用IPV4协议。

SOCK_STREAM
表示这个套接字是Internet
连接
的一个端点。

socket
返回的
clientfd
描述符,仅仅是部分打开,还不能用于读写。

如何完成打开套接字的工作,取决于我们是客户端还是服务器。

下一节描述我们是客户端时如何打开套接字。

11.4.3 connect函数

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

#include<sys/socket.h>

int connect(int sockfd,struct sockaddr *serv_addr,int addrlen);
返回:若成功则为0,若出错则为-1


connect
函数试图于套接字地址为
serv_addr
的服务器建立一个因特网连接.

其中
addrlen
sizeof(sockaddr_in)
.

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

如果成功,
sockfd
描述符就可以读写了。

并且得到链接是由套接字对
(x:y,serv_addr.sin_addr,serv_addr.sin_port)
刻画的。

其中
x
是客户端IP地址,而
y
表示临时端口。

它唯一地确立了客户端主机上的客户端进程。

11.4.4 open_clientfd函数

open_cilentfd
socket
connect
的包装函数(不是系统自带)

#include <csapp.h>

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


open_clientfd
函数和运行在
hostname
上的服务器建立一个连接,并在知名端口
port
上监听连接请求。

它返回一个打开的套接字描述符。

该描述符准备好了,可以用
Unix I/O
函数做输入和输出。



11.4.5 bind函数

剩下的套接字函数
bind
listen
accept
被服务器用来和客户端建立链接。

#include<sys/socket.h>

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


bind
函数告诉内核将
my_addr
中的服务器套接字地址和套接字描述符
sockfd
联系起来。

参数
addrlen
就是
sizeof(sockaddr_in)


?

11.4.6 listen函数(主动套接字->监听套接字)

客户端是发起连接请求的主动实体。服务器是等待来自客户端连接请求的被动实体。

默认情况下,内核会认为
socket
函数创建的描述符对应于
主动套接字(active socket)
.

它存在于一个连接的客户端。

服务器调用
listen
告诉内核,描述符是被服务器而不是客户端使用的

#include<sys/socket.h>

int listen(int sockfd,int backlog);

返回:若成功则为0,若出错则为-1


listen
函数将
sockfd
从一个
主动套接字
转化为一个
监听套接字(listenning socket)


该套接字可以接收来自客户端的连接请求。

backlog
参数暗示了内核在开始拒绝连接请求之前,应该放入队列中等待的未完成连接请求的数量。

backlog
参数的确切含义要求对
TCP/IP
协议的理解,这超出了我们的讨论的范围。

通常我们会把它设置成一个较大的值,比如
1024


11.4.7 open_listenfd函数

socket
,
bind
listen
函数结合称
open_listenfd
的包装函数。

服务器可以用它来创建一个
监听描述符


#include<csapp.h>

int open_listenfd(int port)

返回:若成功则为描述符,若Unix出错则为-1


open_listenfd
函数打开和返回一个监听描述符

这个描述符准备好在知名端口
port
上接收请求。



创建
listenfd
套接字描述符。

使用
setsockopt
函数来配置服务器,使得它能被立即中止和重启。

默认地,一个重启的服务器将在大约30秒内拒绝客户端的连接请求,严重阻碍调试。

接下来,初始化服务器的
套接字地址结构


INADDR_ANY
来告诉内核这个服务器将接收任何IP地址到端口
port
的请求。

INADDR_ANY
通配符地址就是指定地址为
0.0.0.0
的地址

调用
blind
listen
。将其转换为
监听套接字


11.4.8 accept函数

CSAPP
这里介绍的十分有问题,所以特地翻了
UNIX 网络编程
的原话。



用于从已完成连接
队列
队头返回下一个已完成连接。

如果已完成连接为空,那么进程进入阻塞(假定套接字为默认的阻塞方式)

返回三个值

已连接标示符


客户端地址


客户度地址长度


监听描述符
已连接描述符
之间的区别是很多人迷惑。

监听描述符
是作为客户端连接请求的一个端点。

它被创建一次,并存在于服务器的整个生命周期。

已连接描述符
是客户端和服务器之间已经建立起来的连接的一个端点。

服务器每次接收连接请求时都会创建一次。

它只存在于服务器为一个客户端服务的过程中。

11.4.9 echo客户端和服务器的示例

学习套接字接口的最好办法是研究示例代码。

没办法。这个代码估计也有挺多疑惑,还是过段时间啃
Unix网络编程把


11.5 WEB服务器

这块写过
servlet
,就不用复述了,以后再详细补
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: