CSAPP--第十一章:网络编程
客户端-服务器模型
-
网络应用都是基于客户端-服务器模型。
一个应用由一个服务器进程和多个客户端进程构成。事务:模型中的基本操作,包含四步:
①客户端进程发送请求
②服务器进程接收请求,并与其存储设备交互进行处理。
③服务器进程发送响应。
④客户端进程处理响应。
网络
客户端和服务端通常运行在不同主机上,通过计算机网络的硬件和软件来通信。
-
LAN(Local Area Network)
局域网,其包含的主机位置范围最小,通常在一个建筑内。
以太网(Ethernet): 现在最流行的局域网技术。
局域网内,各主机通过集线器进行连接。如下图:
-
网桥(bridge):用于连接多个以太网段,形成较大的局域网(桥接以太网)。
如下图:
-
WAN(Wide-Area network):
广域网,其将不同局域网连接在一起,因为地理范围比局域网大,取名广域网。
如下图:
网络协议
不同的主机,其采用的局域网和广域网技术可能不兼容,所以提出一个协议,消除主机间的通信障碍。
-
协议功能:
①命名机制:采取同一套地址命名机制。
②传送机制:定义一种通用的数据包:包头 + 有效荷载。包头记载了源和目的主机的地址、及包大小等信息。
全球IP因特网
-
TCP/IP协议(Transmission Control Protocol,传输控制协议 / Internet Protocol,互联网络协议)
TCP/IP是一个协议簇,每个协议提供不同功能。TCP比IP更复杂。
-
IP协议:提供了基本命名方式和传送机制。
实现从一个主机,往另一个主机传送包,也称数据报。
其对丢包不会试图恢复。
UDP协议(Unreliable Datagram Protocol,不可靠数据报协议)
-
TCP协议:构建在IP协议之上的复杂协议,提供进程间可靠的双向连接。
-
-
IPv4、IPv6
因特网协议版本4 (32位地址)
1996年提出的因特网版本协议6 (128位地址):如今用户还不多。
IP地址 //以下针对 IPv4
ip地址是32位无符号整数。网络程序将ip地址存放在:ip地址结构中。
ps:其导致了麻烦的操作,每次使用要建立结构体,再取其中的值,很麻烦!!!
struct in_addr{
unit32_t s_addr; /* 网络字节顺序中统一为大端顺序 */
};
网络字节顺序(network byte order)为大端字节顺序。
所以对于小端机器中,会进行字节顺序(32位)转换。
Unix提供的转换函数:
#inlcude<arpa/inet.h>
uint32_t htonl(uint32_t hostlong ); //host to network
uint16_t htonl(uint16_t hostshort );
uint32_t ntohl(uint32_t netlong ); //network to host
uint16_t ntohl(uint16_t netshort );
ps:没有对应的64位的函数。
-
点分十进制表示法
用来表示ip地址的一种方法。
如0x00000000 -> 0.0.0.0
二进制 点分十进制
应用程序通过inet_pton 、inet_ntop函数来实现转换。
#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
说明:以上两个函数中,网络地址的值是在结构体中,所以要先设置结构体,然后取其中的值。
编写中出错了蛮多。
具体看linux 中的网络编程文件夹下的ntop.c 、 pton.c。
-
因特网域名
域名:一串用句号分隔的单词(字母、数字、破折号)。例如:www.baidu.com
域名层次结构(树状):
分为多层域名--
第一层为ICANN协会定义的,包括com、edu、gov、org、net等。
第二层为先到先分配的。一个组织得到了二级域名后,就可以在子域(节点的子树)中创建任何新的域名。如:cmu.edu cs.cmu.edu
-
域名、IP地址映射
1988年之前,都是用HOSTS.TXT的 文本文件来手工维护。
1988年后,通过世界范围内的数据库DNS(Domain Name System:域名系统)来维护。
DNS数据库:有上百万条 主机条目结构 组成,每条主机条目结构定义了一组域名和IP地址之间的映射。
-
映射关系:
一对一 映射
多个域名映射一个IP地址
一个域名映射多个IP地址(同一组)
多个域名映射同一组多个IP地址
-
因特网连接
因特网中,客户端和服务器通过在连接上发送和接收字节流来通信。
从连接一对进程而言,其是:点对点的。
从数据双向流动而言,其是:全双工的。
-
套接字(socket):
连接的一个端点。
其内容为IP地址加上16位的整数端口组成。(非硬件而是软件端口)
如:127.0.0.1:51212
-
临时端口(ephemeral port):
当客户端发起连接请求时,客户端套接字地址中的端口,是由内核自动分配的。称为临时端口。
-
知名端口:
服务器中,套接字地址的端口常常是和服务绑定的,如web服务器通常使用端口80,电子邮件服务器通常使用端口25等。称为知名端口。
-
套接字对(socket pair):
由连接两端的套接字,唯一确定。(cliaddr : cliport,servaddr:servport)
-
-
套接字接口(socket inerface)
其是一组函数,与Unix I/O结合起来,用以创建网络应用。
套接字地址结构
①从内核角度看,套接字是通信的一个端点。
②从linux程序看,套接字是一个有相应描述符的打开文件。
因特网的套接字地址存放在一个结构体中:sockaddr_in (16字节)
/* IP socket addr structure */ struct sockaddr_in{ uint16_t sin_family; /*socket 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_zeor[8];/*pad to sizeof(struct sockaddr)*/ }; /*generic socket addr structure(for connect,bind,accept)*/ struct sockaddr{ uint16_t sa_family; /*protocol family*/ char sa_data[14];/*address data*/ };
实际客户端-服务器的访问流程如下图:
-
相关函数
-
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:这个套接字是连接的一个端点。*/
此时仅是部分打开,并不能进行读写,根据客户端和服务器使用不同的方式完全打开。
这几个函数最好用getaddrinfo函数来赋予参数。
-
connect函数
客户端用来与套接字地址为addr 的服务器建立网络连接。
#incldue<sys/sokcet.h> int connect(int clientfd, const struct sockaddr *addr, socklen_t addrlen); //成功返回0,出错返回-1. 描述符保存在clientfd中。 //addrlen=sizeof(sockaddr_in)
当成功时,clientfd就可以读写了。此时套接字对为:
(x:y , addr.sin_addr : addr.sin_port)
x:客户端IP。y:客户端临时端口。其唯一确定了客户端主机的某个进程。
-
bind函数
socket、connect函数:客户端用来与服务器建立连接。(依次)
socket、bind、listen、accpet:服务器用来与客户端建立连接。(依次)
bind函数将一个套接字地址和一个套接字描述符绑定起来(如socket刚刚建立的)。
#include<sys/socket.h> int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen); //成功范围0,出错-1.
-
listen函数
listen函数将一个主动套接字描述符(如bind转化后的sockfd)转化为监听套接字(listening socket),其可以接收客户端的连接请求。
说明:无论客户段服务器,内核默认套接字为主动套接字。所以服务器要将其转换为监听套接字,说明自己是服务器端。
#include<sys/socket.h> int listen(int sockfd, int backlog); //backlog的确切含义要求对TCP\IP协议有理解。CSAPP将其设置为较 //大的值如:1024
-
accept函数
服务器通过调用accept函数来等待来自客户端的连接请求(connect)
#include<sys/socket.h> int accept(int listenfd, struct sockaddr *addr, int *addrlen); //成功返回已连接描述符,出错返回-1 //结构 addr用来安置客户端过来的套接字地址(在结构中一一替换)
-
会返回一个已连接描述符,用这个描述符,可以与客户端进行Unix I/O读写。
注意:监听描述符和已连接描述符都会存在于描述符列表中。
实际步骤:
①服务器调用accept,等待连接请求到达监听描述符。(假设=3)
②客户端调用connect函数,请求一个连接。
③accept函数打开一个新的已连接描述符(connect descriptor)connfd(假设=4),在clientfd和connfd之间建立连接,并随后返回connfd给应用程序。
客户端也从connect返回,然后,客户端和服务器都可以通过读写clientfd和connfd来传送数据。
说明:监听描述符是存在于服务器的生命周期内,只会创建一次。而connfd则是客户端和服务器已经建立起来的一个端点。服务器每次接受连接请求都会创建一次,其只存在于服务器为一个客户端服务的过程中。
ps:有利于并发编程。
主机和服务的转换
-
getaddrinfo函数
用处:主机IP地址和服务(或端口)→ 套接字地址
#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,错误返回非0的错误代码,其可转化为字符串。 void freeaddrinfo(struct addrinfo *result); const char *gai_strerror(int error); //将错误代码翻译成字符串。
//hints是一个addrinfo结构,提供对于返回的套接字地址列表的更好的控制。只能设置下列字段:ai_family , si_socktype , ai_protocol , ai_flasg。结构中其他字段为0(NULL).
struct addrinfo{ int ai_flags; int ai_family; int ai_socktype; int ai_protocol; char *ai_cannoname; size_t ai_addrlen; struct sockaddr *ai_addr; struct addrinfo *ai_next; }
-
getnameinfo函数
用处:套接字地址结构 → 主机IP地址和服务名字符串
#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,错误返回非零错误代码。 //flags 位掩码。
getnameinfo函数将套接字地址结构sa,转换成对应的主机和服务名字符串,将他们复制到host和service缓冲区。
如果返回非零,可以用gai_strerror(int errorcode);转化为字符串。
以上两个函数,如果主机名和服务名可以空白一个(NULL代替)。不能两个都NULL。
实验代码在CSAPP p659,或者我的linux 主机上的网络编程。
-
小结:两个关于套接字地址的结构体:
sockaddr:包含套接字的IP地址、端口地址、协议类型。
addrinfo:对sockaddr结构体的再包装,附加了一些额外信息。
用包装过的函数创建连接
说明:将上述的客户端和服务器中建立连接的函数,包装起来成为一个简洁实用的函数。
-
open_clientfd函数
客户端调用此函数,建立与服务器的连接,其返回一个打开的套接字描述符。
服务器运行在hostname主机上的,并监听了port端口。
#include<sys/socket.h>
#include<netdb.h>
#include<sys/types.h>
#include<stdio.h>
int open_clientfd(char *hostname, char *port) //针对客户端。
{
int clientfd;
struct addrinfo hints, *listp, *p;
/* get alist of 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 argument*/
hints.ai_flags |= AI_ADDRCONFIG; /*recommended for connections*/
getaddrinfo(hostname, port, &hints, &listp);
/*walk(traverl) 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 one */
/*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) return -1; /* all connects failed */
else return clientfd;
}
注意:代码具有协议无关性。因为socket和connect的参数都是getaddrinfo自动生成的。
-
open_listenfd
服务器调用此函数,打开并返回一个监听描述符,准备好在端口port接收请求。
#include<sys/socket.h> #include<netdb.h> #include<sys/types.h> #include<stdio.h> int open_listenfd(char *port) { struct addrinfo hints, *listp, *p; int listenfd, optval=1; /* 得到一张服务地址表 */ memset(&hints,0 , sizeof(struct addrinfo)); hints.ai_socktype = SOCK_STREAM; /*类型为接收连接*/ hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG; /*针对任何ip地址*/ hints.ai_flags |= AI_NUMRICSERV; /*使用端口数字*/ getaddrinfo(NULL, port, &hints, &listp); /* 遍历地址表,找到一个可以用来绑定的 */ for(p = listp ; p ; p = p.ai_next){ /* 增加一个套接字描述符 */ if((listenfd = socket(p->ai_family,p->ai_socktype,p->ai_protocol))<0) continue; /* 套接字不可用,试下一个 */ /* 为bind中,消除“地址早已被使用”的错误可能 */ setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (const void*)&optval, sizeof(int)); /* 绑定描述符和套接字地址 */ if(bind(listenfd, p->ai_addr, p->ai_addrlen) == 0) break; /*成功*/ close(listenfd); /*绑定失败,关闭描述符,试下一个。*/ } /* 释放 */ freeaddrinfo(listp); if(!p) return -1; /*没有地址可用*/ /* 使一个监听套接字,准备好去接收一个连接请求 */ if(listen(listenfd, LISTENQ)<0){ close(listenfd); return -1; } return listenfd; }