深入理解计算机系统--网络编程

一、客户端-服务器编程模型

每个网络应用都是基于客户端-服务器模型的。客户端-服务器模型中的操作是事物(transaction)。一个客户端-服务器事物由以下四步组成。

1、当一个客户端需要服务时,它向服务器发送一个请求,发起一个事务。例如,当Web浏览器需要一个文件时,它就发送一个请求给Web服务器。

2、服务器收到请求后,解释它,并以适当的方式操作它的资源。例如,当Web服务器收到浏览器发出的请求后,它就读一个磁盘文件。

3、服务器给客户端发送一个相应,并等待下一个请求。例如,Web服务器将文件发送回客户端。

4、客户端收到响应并处理它。例如,当Web浏览器收到来自服务器的一页后,就在屏幕上显示此页。

注:客户端和服务器是进程,而不是常提到的机器或者主机。

 

二、网络

客户端和服务端通常运行在不同的主机上,并通过计算机网络的硬件和软件资源来通信。网络是很复杂的系统。对主机而言,网络只是又一种I/O设备,是数据源和数据接收方,如图。一个插到I/O总线扩展槽的适配器提供了到网络的物理接口。从网络上收到的数据从适配器经过I/O和内存总线复制到内存,通常是通过DMA传送。相似地,数据也能从内存复制到网络。

注:我们总是用小写字母的internet描述一般概念,而用大写字母的Internet来描述一种具体的实现,也就是所谓的全球
IP因特网。

 

上面的示例由两个局域网通过一台路由器连接而成。一个客户端运行在主机A上,主机A与LAN1相连,它发送一串数据字节到运行在主机B上的服务器端,主机B则连接在LAN2上。这个过程有8个基本步骤:

(1)运行在主机A上的客户端进行一个系统调用,从客户端的虚拟地址空间复制数据到内核缓冲区中。

(2)主机A上的协议软件通过在数据前附加互联网络包头和LAN1帧头,创建了一个LAN1的帧。互联网络包头寻址到互联网络主机B。LAN1帧头寻址到路由器。然后它传送此帧到适配器。注意,LAN1帧的有效载荷是一个互联网络包,而互联网包的有效载荷是实际的用户数据。这种封装是基本的网络互连方法之一。

(3)LAN1适配器复制该帧到网络上。

(4)当此帧到达路由器时,路由器的LAN1适配器从电缆上读取它,并把它传送到协议软件。

(5)路由器从互联网络包头中提取出目的互联网络地址,并用它作为路由表的索引,确定向哪里发这个包,在本例中是LAN2。路由器剥落旧的LAN1的帧头,加上寻址到主机B的新的LAN2帧头,并把得到的传送到适配器。

(6)路由器的LAN2适配器复制该帧到网络上。

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

(8)最后,主机B上的协议软件剥落包头和帧头。当服务器进行一个读取这些数据的系统调用时,协议软件最终将得到的数据复制到服务器的虚拟地址空间。

 

三、全球IP因特网

1、IP地址

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

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

把一个标量地址存放在结构体中,是套接字接口早期实现的不幸产物为IP地址定义一个标量类型应该更有意义,但是现在更改已经太迟了,因为已经有大量应用是基于此的

因为因特网主机可以有不同的主机字节序列,TCP/IP为任意整数数据项定义了统一的网路字节序列(大端字节序列),例如IP地址,它放在包头中跨过网络被携带。在IP地址结构中存放的地址总是以(大端法)网络字节顺序存放的,即使主机字节顺序是小端法。Unix提供了下面这样的函数在网络和主机字节顺序间实现转换。

//
#include <aropa/inet.h>

uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
返回:按照网络字节顺序的值
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
返回:按照主机字节顺序的值
//

注意:没有对应的处理64位值的函数。

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.
//

在函数名中,“n”代表网络,“p”代表表示。它们可以处理32位的IPv4地址或者64位的IPv6地址。
 

2、因特网域名

大多数IP地址难记,为了更好的使用网络,推出了域名机制。将一组域名映射到一组IP地址的机制。起初映射是通过叫做HOSTS.TXT文本文件来手动维护的。之后这个映射是通过分布世界范围内的数据库(DNS)来维护的。

 

每台因特网的主机都有本地定义的域名localhost,这个域名总是映射为回送地址(loopback address)127.0.0.1;localhost名字为引用运行在同一台机器上的客户端和服务器提供了一种便利和可移植的方式,这对调试相当有用。

(1)最简单的情况中,一个域名和一个IP地址之间是一一映射;

(2)然而有些情况是,多个域名可以映射为同一个IP地址;

(3)在最通常的情况下,多个域名可以映射到同一组的多个IP地址;

(4)当然有的合法域名没有映射到任何IP地址。

3、因特网连接

因特网客户端和服务端通过在连接上发送和接收字节流来通信。从连接一对进程的意义上而言,连接时点对点的。从数据可以同时双向流动的角度来说,它是全双工的。一个套接字是连接的一个端点。每个套接字都有相应的套接字地址,是由一个因特网地址和一个16位的整数端口组成的,用“地址:端口”来表示。当客户端发起一个连接请求时,客户端套接字地址中的端口是由内核自动分配的,称为临时端口。然而,服务器套接字的地址中的端口通常是某个知名端口,是和这个服务相对应的。例如,Web服务器通常使用端口是80,而电子邮件的服务是25.每个知名的端口的服务都有一个知名的服务名。例如Web服务的知名名字是http,email的知名名字是smtp。文件/etc/services包含一张这台机器提供的知名名字和知名端口之间的映射。一个连接是由它两端的套接字地址唯一确定的。这对套接字地址叫做套接字对(socket pair),由下列元组表示:

(cliaddr:cliport, servaddr:servport)

 

四、套接字接口

1、套接字介绍

(1)套接字接口是一组函数,它们和Unix I/O函数结合起来,用以创建网络应用。大多数现代系统上都实现套接字接口,包括所有的Unix变种、Windows和Macintosh系统。下图是典型的客户端-服务器事务的上下文中的套接字接口概述。

 

2、套接字地址结构

从Linux内核的角度来看,一个套接字就是通信的一个端点。从Linux程序的角度来看,套接字就是一个有相应描述符的打开文件。因特网的套接字地址存放在下图所示的类型为sockaddr_in的16字节结构中。对于因特网应用,sin_family成员是AF_INET,sin_port成员是一个16位的端口号,而sin_addr成员就是一个32位的IP地址。IP地址和端口号总是以网络字节顺序(大端法)存放的。

//
/* 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 */
};
//

注意:_in后缀是互联网络(internet)的缩写,而不是输入(input)的缩写

connect、bind和accept函数要求一个指向与协议相关的套接字地址结构的指针。套接字接口的设计者面临的问题是,如何定义这些函数,使之能接受各种类型的套接字地址结构。今天我们可以使用void *指针,但是那时在C中并不存在这种类的指针。解决办法是定义套接字函数要求一个指向通用sockaddr结构的指针,然后要求应用程序将与协议特定的结构的指针强制转换成这个通用结构。为了简化代码示例。我们跟随Steven的指导,定义下面的类型:

//
typedef struct sockaddr SA;
//

然后无论何时需要将sockaddr_in结构强制转换成通用sockaddr结构时,我们都使用这个类型。

3、函数介绍

(1)socket函数

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

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

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

如果想要使套接字成为连接的一个端点,就用如下编码的参数来调用socket函数:

//
clientfd = socket(AF_INET, SOCK_STREAM, 0);
//

其中,AF_INET表明我们正在使用32位IP地址,而SOCK_STREAM表示这个套接字是连接的一个端点。不过最好的方法是用getaddrinfo函数来自动生成这些参数,这样代码就与协议无关了。socket返回的clienfd描述符仅是部分打开的,还不能用于读写。如何完成打开套接字的工作,取决于我们是客户端还是服务器。

(2)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表示临时端口,它唯一地确定了客户端主机上的客户端进程。对于socket,最好的方法是用getaddrinfo来为connect提供参数。

(3)bind函数

剩下的套接字函数-bind、listen和accept,服务器用它们来和客户端建立连接。

//
#include <sys/socket.h>

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

bind函数告诉内核将addr中的服务器套接字地址和套接字描述符sockfd联系起来。参数addrlen就是sizeof(sockaddr_in)。对于socket和connect,最好的方法是用getaddrinfo来为bind提供参数。

(4)listen函数
客户端是发起连接请求的主体实体。服务器是等待来自客户端的连接请求的被动实体。默认情况下,内核会认为socket函数创建的描述符对应于主动套接字,

它存在于一个连接的客户端。服务器调用listen函数告诉内核,描述符是被服务器而不是客户端使用的。

//
#include <sys/socket.h>

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

listen函数将sockfd从一个主动套接字转化为一个监听套接字,该套接字可以接受来自客户端的连接请求。backlog参数

暗示了内核在开始拒绝连接请求之前,队列中要排队的未完成的连接请求的数量。通常我们把它设置为一个较大的值,比如1024.

(5)accept函数

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

//
#include <sys/socket.h>

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

accept函数等待来自客户端的连接请求到达侦听描述符listenfd,然后在addr中填写客户端的套接字地址,并返回一个已连接描述符,这个描述符可被用来利用Unix I/O函数于客户端通信。监听描述符和已连接描述符之间的区别使很多人感到困惑。监听描述符是作为客户端连接请求的一个端点它通常被创建一次,并存在于服务器的整个生命周期已连接描述符是客户端和服务器之间已经建立起来了的连接的一个端点。服务器每次接受连接请求时都会创建一次,它只存在于服务器为一个客户端服务器的过程中。下图描绘了监听描述符和已连接描述符的角色。

(1)服务器调用accept,等待连接请求到达监听描述符,具体地我们设定为描述符3.(描述符0~2是预留给标准文件的)

(2)客户端调用connect函数,发送一个连接请求到listenfd。

(3)accept函数打开了一个新的已连接描述符connfd(假设描述符是4),在clientfd和connfd之间建立连接,并且随后返回connfd给应用程序。客户端也从connect返回,在这以后,客户端和服务器就可以分别通过读和写clienfd和connfd来回传送数据了。

 

4、主机和服务转换

 

Linux提供了一些强大的函数(getaddrinfo和getnameinfo)实现二进制套接字地址结构和主机名、主机地址、服务名和端口号的字符串表示之间的相互转化。当和套接字接口一起使用时,这些函数能使我们编写独立于任何特定版本的IP协议的网络程序

(1)getaddrinfo函数getaddrinfo函数将主机名、主机地址、服务名和端口号的字符串表示转化成套接字地址结构。它使已弃用的gethostbyname和getservbyname函数的新的替代品。和以前的那些函数不同,这个函数是可重入的,适用于任何协议。

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

int getaddrinfo(const char *host, const char * service, struct addrinfo **result);
返回:成功则为0,错位则为非0的错误代码。
void freeaddrinfo(struct addrinfo **result);
const char *gai_strerror(int errcode);
返回错误信息。
//

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

在客户端调用getaddrinfo之后,会遍历这个列表,依次尝试每个套接字地址,直到调用socket和connect成功,建立起连接。
类似地,服务器会尝试遍历列表中的每个套接字地址,直到调用socket和bind成功,描述符会被绑定到一个合法的套接字地址。
为了避免内存泄漏,应用程序必须调用freeaddrinfo,释放该链表。如果getaddrinfo返回非零的错误代码,应用程序可以调用

gai_streeror,将该代码换成消息字符串。getaddrinfo的host参数可以是域名,也可以是数字地址(如点分十进制IP地址)。service参数可以是服务名(如http),也可以是十进制端口号。如果不想把主机名转换为地址,可以把host设置为NULL。对service来说也是一样的。但是必须指定两者至少一个。可选的参数hints是一个addrinfo结构,它提供对getaddrinfo返回的套接字地址列表的更好的控制。如果传递hints参数,只能设置下列字段:ai_family、ai_socktype、ai_protocol和ai_flags字段。其他字段设置为0(或NULL)。实际中,我们用memset将整个结构清零,然后有选择地设置一些字段:

(1)getaddrinfo默认可以返回IPv4和IPv6套接字地址。ai_family设置为AF_INET会将列表限制为IPv4地址;设置AF_INET6则限制为IPv6地址。

(2)对于host关联的每个地址,getaddrinfo函数默认最多返回三个addrinfo结构,每个的ai_socktype字段不同:一个是连接,一个是数据报,一个是原始套接字。ai_socktype设置为SOCK_STREAM将列表限制为对每个地址最多一个addrinfo结构,该结构的套接字地址可以作为连接的一个端点。这是所有示例程序所期待的行为。

(3)ai_flags字段是一个位掩码,可以进一步修改默认行为。可以把各种值用OR组合起来得到该掩码。

//
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 */
};
//

getaddrinfo一个很好的方面是addrinfo结构中的字段是不透明的,即它们可以直接传递给套接字接口中的函数,应用程序待,代码无需再做任何处理。例如,ai_family、ai_socktype和ai_protocol可以直接传递给socket。类似的,ai_addr和ai_addrlen可以直接传递给connect和bind.这个强大的属性使得我们编写的客户端和服务器能够独立于某个特殊版本的IP地址。

2、getnameinfo函数

getnameinfo函数和getaddrinfo是相反的,将一个套接字地址结构转换为相应的主机和服务名字符串。它是弃用的gethostbyaddr和getservbyport函数的新的替代品。它是可重入和于协议无关的。

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

5、套接字接口的辅助函数

初学时,getnameinfo函数和套接字接口看上去难一些。用高级的辅助函数包装会方便一些,称为open_clientfd和open_listenfd,客户端和服务器互相通信时可以使用这些函数。

(1)open_clientfd函数

客户端调用open_clientfd建立与服务器的连接。

//
#include “csapp.h”

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

注意所有代码都是与任何版本的IP无关。socket和connect的参数都是用getaddrinfo自动产生的,这使得我们的代码干净可移植。

//
int open_clientfd(char *hostname, char *port) 
{
	int clientfd;
	struct addrinfo hints, *listp, *p;
	
	/* Get a list of potential 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 arg. */
	hints.ai_flags |= AI_ADDRCONFIG;	/* Recommended  for connection */
	Getaddrinfo(hostnamem, port, &hints, &listp);
	
	/* Walk 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 */
		
		/* 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)	/* All last connects failed */
		return -1;
	else	/* The last connect succeeded */
		return clientfd;
}
//

(2)open_listenfd函数

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

//
#include ”csapp.h“

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

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

//
int open_listenfd(char *port)
{
	struct addrinfo hints, *listp, *p;
	int listenfd, optval = 1;
	
	/* Get a list of potential server addresses */
	memset(&hints, 0, sizeof(struct addrinfo));
	hints.ai_socktype = SOCK_STREAM;				/* Accept a connection */
	hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG;	/* ...on any IP addresses */
	hints.ai_flags |= AI_NUMERICSERV;				/* ...using port number */
	Getaddrinfo(NULL, port, &hints, &listp);


	/* Walk the list for one that we can bind to */
	for (p = listp; p; p = p->ai_next) {
		/* Create a socket descriptor */
		if ((listenfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) 
			< 0) continue;	/* Socket failed, try the next */
		
		/* Eliminates "Address already in use" error from bind */
		Setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR,
					(const void *)&optval, sizeof(int));
		
		/* Bind the descriptor to the address */
		if (bind(listenfd, p->ai_addr, p->ai_addrlen) == 0)
			break;	/* Success */
		Close(clientfd);	/* Bind failed, try the next */
	}
	
	/* Clean up */
	Freeaddrinfo(listp);
	if (!p)	/* All last connects failed */
		return -1;
		
	/* Make it a listening socket ready to accept connection requests */
	if (listen(listenfd, LISTENQ) < 0) {
		Close(listenfd);
		return -1;
	}
	return listenfd;
}
//

6、echo客户端和服务器的示例

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

(1)echo客户端的代码

在和服务器建立连接之后,客户端进入一个循环,反复从标准输入读取文本行,发送文本行给服务器,从服务器读取会送的行,并输出结果到标准输出。当fgets在标准输入上遇到EOF时,或者因为用户在键盘上输入Ctrl+D,或者因为一个重定向的输入文件中用尽了所有的文本行时,循环就终止。

//
#include <csapp.h>

int main(int argc, char **argv)
{
	int clientfd;
	char *host, *port, buf[MAXLINE];
	rio_t rio;
	
	if (argc != 3) {
		fprintf(stdeer, "usge:%s <host> <port>\n", argv[0]);
		exit(0);
	}
	host = argv[1];
	port = argv[2];
	
	clientfd = open_clientfd(host, port);
	rio_readinitb(&rio, clientfd);
	
	while (fgets(buf, MAXLINE, stdin) != NULL) {
		rio_write(clientfd, buf, strlen(buf));
		rio_readlineb(&rio, buf, MAXLINE);
		fputs(buf, stdout);
	}
	close(clientfd);
	exit(0);
}
//

循环终止之后,客户端关闭描述符。这会导致发送一个EOF通知到服务器,当服务器从它视为reo_readlineb函数收到一个为0的
返回码时,就会检测到这个结果。在关闭它的描述符后,客户端就终止了。既然客户端内核在一个进程终止时会自动关闭所有

打开的描述符,末尾的close函数就没有必要了。不过,显式地关闭已经打开的任何描述符式一个良好的编程习惯。

//
#include "csapp.h"

void echo(int connfd);

int main(int argc, int **argv)
{
	int listenfd, connfd;
	socklen_t clientlen;
	struct sockaddr_storage clientaddr;	/* Enough space for any address */
	char client_hostname[MAXLINE], client_port[MAXLINE];
	
	if (argc != 3) {
		fprintf(stdeer, "usge:%s <host> <port>\n", argv[0]);
		exit(0);
	}
	
	listenfd = open_listenfd(argv[1]);
	while (1) {
		clientlen = sizeof(struct sockaddr_storage);
		connfd = accept(listenfd, (SA *)&clientaddr, &clientlen);
		getnameinfo((SA *)&clientaddr, clientlen, client_hostname, MAXLINE,
					client_port, MAXLINE, 0);
		printf("connected to (%s, %s)\n", client_hostname,client_port);
		echo(connfd);
		close(connfd);
	}
	exit(0);
} 
//

这是echo服务器的主程序。在打开监听描述符后,它进入一个无线循环。每次循环都等待一饿来自客户端的连接请求,输出已连接客户端的域名和Ip地址,并调用echo函数为这些客户端服务。在echo程序返回后,主程序关闭已连接描述符。一旦客户端和服务器关闭了它们各自的描述符,连接也就终止了。第九行的clientaddr变量是一个套接字地址结构,被传递给accept。在accept返回之前,会在clientadde中填上连接另一个客户端的套接字地址。注意,我们将clientaddr声明为struct sockaddr_storage类型,而不是struct sockaddr_in类型。根据定义,sockaddr_storage结构足够大能够装下任何类型的套接字地址,以保持代码的协议无关性。

//
#include ”csapp.h“

void echo(int connfd)
{
	size_t n;
	char buf[MAXLINE];
	rio_t rio;
	
	rio_reainitb(&rio, connfd);
	while ((n = rio_realineb(&rio, buf, MAXLINE)) != 0) {
		printf("server received %d bytes\n", (int)n);
		rio_writen(connfd, buf, n);
	}
}
//

简单echo服务器一次只能处理一个客户端。这种类型的服务器一次一个地在客户端间迭代,称为迭代服务器。该程序反复读写文本行,知道rio_readlineb函数遇到EOF。

 

在连接中EOF意味着什么?

应用程序在它接收到一个由read函数返回的零返回码时,它就会发现出EOF条件。对于磁盘文件,当前文件位置超出文件
长度时,会发生EOF。对于因特网连接,当一个进程关闭连接它的那一端时,会发生EOF。连接另一端的进程在试图
读取流中最后一个字节之后的字节时,会检测到EOF。

 

五、Web服务

web客户端和服务器之间的交互用的是一个基于文本的应用级协议,叫做HTTP(超文本传输协议)。HTTP是一个简单的协议。
一个Web客户端(浏览器)打开一个到服务器的因特网连接,并且请求某些内容。服务器响应所请求的内容,然后关闭连接。
浏览器读取这些内容,并把它显示在屏幕上。Web服务和常规的文件检索服务(如FTP)主要的区别是,Web内容可以用一种叫做HTML(超文本标记语言)的语言来编写。一个HTML程序(页)包含指令(标记),它们告诉网页浏览器如何显示这页中的各种文本和图形对象。

例如,代码<b>Make me bold! </b> 告诉浏览器用粗体字类型输出<b>和</b>标记之间的文本。

然而,HTML真正强大之处在于一个页面可以包含指针(超连接)。

例如 <a href="http://www.worthsen.com/index.html">王世恩的主页</a> 告诉浏览器高亮显示文本对象”王世恩的主页“,并创建一个超连接。

 

致谢

1、《深入理解计算机系统》[第3版],作者 Randal E.Bryant, David R.O`Hallaron 译者 龚奕利 贺莲

2、http://csapp.cs.cmu.edu/public/code.html

 

 

 

展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 技术黑板 设计师: CSDN官方博客
应支付0元
点击重新获取
扫码支付

支付成功即可阅读