我们需要理解基本的客户端-服务端
编程模型,以及如何编写使用因特网提供的服务的客户端-服务端
程序。
最后,我们将把所有这些概念结合起来,开发一个小的但功能齐全的Web
服务器,能够为真实的Web
浏览器提供静态的和动态的文本和图形内容。
每个网络应用程序都是基于客户端 - 服务器模型
的
采用这种模型,一个应用是由一个服务器
进程
和一个或多个客户端进程
组成。
服务器
管理某种资源,并且通过操作这种资源为它的客户端提供某种服务。
WEB
服务器,代表客户端
检索,执行磁盘内容。FTP
服务器,为客户端
进行存储和检索。电子邮件
服务器,为客户端
进行读和更新。客户端-服务器
模型中的基本操作是事务(transaction)
.
客户端-服务器
事务由四步组成 事务
。响应
,并等待下一个请求。响应
并处理它。客户端
和服务端
通常运行在不同的主机上,并且通过计算机网络
的硬件和软件资源来通信。
网络
只是又一种I/O
设备,作为数据源和数据接收方。对于物理上而言,网络是一个按照地理远近组成的层次系统。
最低层是LAN(Local Area Network,局域网)
:在一个建筑或校园范围内。
扩展介绍以太网
每个以太网适配器(网卡)
都有一个全球唯一的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时,它的适配器从电缆上读到此帧,并将它传送到协议软件。系统调用
。当然,在这里,我们掩盖了许多非常艰难的问题。
帧
大小的最大值,该怎么办。帧
。虽然如此,我们也能大概了解到互联网络思想的精髓。
每台因特网主机都运行实现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)
的标示符。连接
和任何其他主机上的进程通信。一个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
函数来实现两者之间互相转换。
方便人们记忆的对于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
: 因特网应用程序通过调用 gethostbyname
和gethostbyaddr
函数,从DNS数据库中检索任意的主机条目。。
每台主机都有本地定义的域名localhost
本地送回地址(loopback address)
:127.0.0.1
。localhost
名字为引用运行在同一机器上的客户端和服务端提供了一种便利和可移植的方式。Internet
服务端和客户端通过在连接
上发送和接收字节流
来通信。
点对点
的。全双工
的。可靠
的一个套接字
是连接的
一个端点。
每个套接字都有相应的套接字地址
。
IP
地址和一个16位的整数端口
组成的,用地址:端口
来表示。当客户端发起一个连接请求时,客户端套接字地址中的端口
由内核自动分配的。
临时端口
然后,服务器套接字地址中的端口
通常是某个知名的端口,和这个服务相对应的。
例如:
80
25
在Unix
机器上,文件/etc/services
包含一张这台机器提供的服务和他们的知名端口号的综合列表。
一个连接
是由它两端的套接字地址唯一确定的。
套接字对(socket pair)
,由下列元组来表示: (cliaddr:cliport,servaddr:servport)
套接字接口(socket interface)
是一组函数,他们和Unix I/O
函数结合起来,用以创建网络应用。
给出一个典型的客户端-服务器事务的上下文中套接字接口
概述,以此导向。
不同的角度:
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
交由套接字函数使用的,两者可以直接强制转换。
客户端和服务端使用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
描述符,仅仅是部分打开,还不能用于读写。 客户端通过调用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
表示临时端口。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
函数做输入和输出。剩下的套接字函数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)
?
客户端是发起连接请求的主动实体。服务器是等待来自客户端连接请求的被动实体。
默认情况下,内核会认为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
。用socket
,bind
和listen
函数结合称open_listenfd
的包装函数。
服务器可以用它来创建一个监听描述符
。
#include<csapp.h>
int open_listenfd(int port)
返回:若成功则为描述符,若Unix出错则为-1
open_listenfd
函数打开和返回一个监听描述符
port
上接收请求。listenfd
套接字描述符。使用setsockopt
函数来配置服务器,使得它能被立即中止和重启。
接下来,初始化服务器的套接字地址结构
。
INADDR_ANY
来告诉内核这个服务器将接收任何IP地址到端口port
的请求。 INADDR_ANY
通配符地址就是指定地址为0.0.0.0
的地址blind
,listen
。将其转换为监听套接字
。CSAPP
这里介绍的十分有问题,所以特地翻了UNIX 网络编程
的原话。
用于从已完成连接队列
队头返回下一个已完成连接。
返回三个值
已连接标示符
客户端地址
客户度地址长度
监听描述符
和已连接描述符
之间的区别是很多人迷惑。
监听描述符
是作为客户端连接请求的一个端点。
已连接描述符
是客户端和服务器之间已经建立起来的连接的一个端点。
学习套接字接口的最好办法是研究示例代码。
没办法。这个代码估计也有挺多疑惑,还是过段时间啃Unix网络编程把
这块写过servlet
,就不用复述了,以后再详细补