目录
五、P2P应用
P2P(Peer-to-peer)架构:没有服务器;任意端系统之间直接通信;节点阶段性接入Internet;节点可能更换IP地址。
1、文件分发
(1)客户机/服务器 vs. P2P
从一个服务器向N个节点分发一个文件(大小是F)需要多长时间?
对于客户机/服务器架构,服务器串行地发送N个副本(NF/us),客户机i需要F/di时间下载。
对于P2P架构,服务器必须至少发送一个副本(F/us),客户机i需要F/di时间下载,总共需要下载NF比特,最快的可能上传速率us+∑ui(任何节点只要持有文件的任何部分,就可以分享给其它节点,如果其它节点也都同时上传)。节点数目增大,文件最小分发时间趋于稳定。
(2)BitTorrent
文件划分为256KB的chunk。
节点加入torrent,没有chunk,但是会逐渐积累;向tracker注册以获得节点清单,与某些节点(“邻居”)建立连接。
下载的同时,节点需要向其他节点上传chunk。
节点可能加入或离开,一旦节点获得完整的文件,它可能离开或留下。
获取chunk:给定任一时刻,不同的节点持有文件的不同chunk集合;节点(Alice)定期查询每个邻居所持有的chunk列表;节点发送请求,请求获取缺失的chunk,稀缺优先,因为拥有者可能会离开。
发送chunk:tit-for-tat“一报还一报”。Alice向4个邻居发送chunk,选择正在向其发送Chunk且速率最快的4个,每10秒重新评估top 4。每30秒随机选择一个其它节点,向其发送chunk,新选择节点可能加入top 4,“optimistically unchoke”。
2、索引技术
P2P搜索信息,P2P系统的索引是指信息到节点位置(IP地址+端口号)的映射。
文件共享(BT、电驴),利用索引动态跟踪节点所共享的文件的位置。节点需要告诉索引它拥有哪些文件;节点搜索索引,从而获知能够得到哪些文件。
即时消息(QQ):索引负责将用户名映射到位置。当用户开启IM应用时,需要通知索引它的位置;节点检索索引,确定用户的IP地址。
(1)集中式索引
Napster最早采用这种设计。1)节点加入时,通知中央服务器(IP地址、内容);2)Alice查找“Hey Jude”;3) Alice从Bob处请求文件。
内容和文件传输是分布式的,但内容定位是高度集中式的。存在单点失效问题(若集中的目录服务器挂掉)、性能瓶颈、版权问题。
(2)洪泛式查询Query flooding
完全分布式架构。Gnutella采用这种架构。每个节点对它共享的文件进行索引,且只对它共享的文件进行索引。
覆盖网络(overlay network):Graph。节点X与Y之间如果有TCP连接,那么构成一个边;所有的活动节点和边构成覆盖网络(虚拟网络),边(虚拟链路);节点一般邻居数少于10个。
查询消息通过已有的TCP连接发送,节点转发查询消息,如果查询命中,则利用反向路径发回查询节点。
给网络造成负担。节点刚加入的时候,需要一些处理。
(3)层次式覆盖网络
介于集中式索引(局部)和洪泛查询(超级节点间)之间的方法。每个节点或者是一个超级节点,或者被分配一个超级节点。节点和超级节点间维持TCP连接,某些超级节点对之间维持TCP连接。超级节点负责跟踪子节点的内容,为子节点提供索引服务。
案例应用:Skype。本质上是P2P的,传输时用户/节点对之间直接通信。私有应用层协议,不明。采用层次式覆盖网络架构。索引负责维护用户名与IP地址间的映射。
六、Socket编程
应用层通常是应用进程控制,应用进程间的信息交互,需要利用底层进行传输,传输层之下通常由操作系统控制。应用编程接口 API(Application Programming Interface)主要指应用层与其相邻层(如传输层)间的接口问题,是应用进程的控制权和操作系统的控制权进行转换的一个系统调用接口。
几种典型的应用编程接口。Berkeley UNIX操作系统定义了一种 API,称为套接字接口(socket interface),简称套接字(socket)。微软公司在其操作系统中采用了套接字接口 API,形成了一个稍有不同的 API,并称之为Windows Socket Interface,WINSOCK。AT&T为其UNIX 系统V定义了一种 API,简写为 TLI(Transport Layer Interface)。
1、Socket API概述
Socket最初设计面向BSD UNIX-Berkley,面向TCP/IP协议栈接口。目前是很多网络应用程序通信的事实上的工业标准,绝大多数操作系统都支持。Socket是Internet网络应用最典型的API接口,提供了开发客户/服务器(C/S)应用的典型手段,提供了应用进程间通信的抽象机制。
服务器和客户应用进程要进行通信,首先创建套接字,套接字像连接在传输层上的端口,类似插头。一个进程或多个进程都可能创建多个套接字,传输层协议为了区分不同套接字,给它们绑定端口号。标识通信端点(对外)通过“IP地址+端口号”,操作系统/进程管理套接字(对内)则通过“套接字描述符(socket descriptor)”,小整数Socket抽象。
类似于文件的抽象,如UNIX操作系统对文件和套接字的管理相同。当应用进程创建套接字时,操作系统分配一个数据结构存储该套接字相关信息。返回套接字描述符,是指向数据结构中套接字信息的指针,通过套接字描述符访问套接字,套接字信息包括地址信息(本地的、远程的)。
套接字地址结构:已定义结构sockaddr_in。
struct sockaddr_in{
u_char sin_len; /*地址长度*/
u_char sin_family; /*地址族(TCP/IP:AF_INET)(各种不同的协议栈)*/
u_short sin_port; /*端口号*/
struct in_addr sin_addr; /*IP地址*/
char sin_zero[8]; /*未用(置0) */
}
使用TCP/IP协议簇的网络应用程序声明端点地址变量时,使用结构sockaddr_in。
2、Socket API函数
WinSock对Berkley Sockets进行了扩展,扩展出的函数以WSA开头。WinSock中,网络应用程序的函数调用顺序:WSAStartup(初始化Windows Sockets API)->应用程序->WSACleanup(释放所使用的Windows Sockets DLL)。因为WinSock的实现机制是Windows操作系统的动态链接库,需要初始化和释放动态。
下面除了WSAStartup和WSACleanup,其它函数在WinSock、UNIX或Linux操作系统使用的Berkley Sockets都可以使用,函数名及参数基本一致。
(1)WSAStartup
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
使用Socket的应用程序在使用Socket之前必须首先调用WSAStartup函数。两个参数, 第一个参数指明程序请求使用的WinSock版本,其中高位字节指明副版本、低位字节指明主版本,是十六进制整数,例如0x102表示2.1版;第二个参数是指向WSADATA结构的指针,返回实际的WinSock的版本信息。例:
VersionRequested = MAKEWORD( 2, 1 );
err = WSAStartup( wVersionRequested, &wsaData );
(2)WSACleanup
int WSACleanup (void);
应用程序在完成对请求的Socket库的使用,最后要调用WSACleanup函数。解除与Socket库的绑定,释放Socket库所占用的系统资源。
(3)Socket
sd = socket(protofamily,type,proto);
创建套接字,操作系统返回套接字描述符(sd)。第一个参数协议族protofamily=PF_INET(TCP/IP),第二个参数套接字类型type=SOCK_STREAM,SOCK_DGRAM or SOCK_RAW(TCP/IP),第三个参数协议号,0为默认。例:
struct protoent *p;
p=getprotobyname("tcp");
SOCKET sd=socket(PF_INET,SOCK_STREAM,p->p_proto);
TCP可靠、面向连接、字节流传输、点对点。UDP不可靠、无连接、数据报传输。
(4)Closesocket
int closesocket(SOCKET sd);
关闭一个描述符为sd的套接字,在Berkley Sockets中是close。如果多个进程共享一个套接字,调用closesocket将套接字引用计数减1,减至0才关闭。一个进程中的多线程对一个套接字的使用无计数,如果进程中的一个线程调用closesocket将一个套接字关闭,该进程中的其他线程也将不能访问该套接字。
返回值0,成功;SOCKET_ERROR,失败。
(5)bind
int bind(sd,localaddr,addrlen);
绑定套接字的本地端点地址,IP地址+端口号。参数:套接字描述符(sd)、端点地址(localaddr)结构sockaddr_in。
客户程序一般不必调用bind函数,操作系统会设置。服务器端,熟知端口号(80、25等),IP地址。服务器主机的IP地址?如果主机通过两个网络接口(如网卡)分别连接到两个网上,有IP1和IP2地址(如路由器),如果在该主机上运行一个服务器,服务器绑定哪个地址?通过地址通配符INADDR_ANY,任意一个有效IP地址都可以。
(6)listen
int listen(sd,queuesize);
置服务器端的流式套接字处于监听状态,仅服务器端调用,仅用于面向连接的流式套接字。设置连接请求队列大小(queuesize),先进先出。
返回值0,成功;SOCKET_ERROR,失败。
(7)connect
connect(sd,saddr,saddrlen);
客户程序调用connect函数来使客户套接字(sd)与特定计算机的特定端口(saddr)的套接字(服务)进行连接。仅用于客户端,可用于TCP客户端也可以用于UDP客户端。TCP客户端,建立TCP连接,调用成功则可与服务器通信;UDP客户端,仅指定服务器端点地址,不意味着连接成功,不意味着可以通信。
(8)accept
newsock = accept(sd,caddr,caddrlen);
服务程序调用accept函数从处于监听状态的流套接字sd的客户连接请求队列中取出排在最前的一个客户请求,并且创建一个新的套接字来与客户套接字创建连接通道。仅用于TCP套接字,仅用于服务器。
利用新创建的套接字(newsock)与客户通信。TCP连接是点对点,只能连接服务器端与客户端两个套接字。如果不创建新套接字,TCP服务器某一时刻只能为某一客户提供服务,不能实现并发的TCP服务器。
(9)send, sendto
send(sd,*buf,len,flags);
sendto(sd,*buf,len,flags,destaddr,addrlen);
send函数未指定目的端点地址,用于TCP套接字(客户与服务器)或调用了connect函数的UDP客户端套接字。
sendto函数用于UDP服务器端套接字与未调用connect函数的UDP客户端套接字
(10)recv, recvfrom
recv(sd,*buffer,len,flags);
recvfrom(sd,*buf,len,flags,senderaddr,saddrlen);
recv函数用于从TCP连接的另一端接收数据,或者从调用了connect函数的UDP客户端套接字接收服务器发来的数据。
recvfrom函数用于从UDP服务器端套接字与未调用connect函数的UDP客户端套接字接收对端数据。
(11)setsockopt, getsockopt
int setsockopt(int sd, int level, int optname, *optval, int optlen);
int getsockopt(int sd, int level, int optname, *optval, socklen_t *optlen);
setsockopt函数用来设置套接字sd的选项参数。getsockopt函数用于获取任意类型、任意状态套接口的选项当前值,并把结果存入optval。
(12)Socket API函数小结
WSAStartup:初始化socket库(仅对WinSock);
WSACleanup:清除/终止socket库的使用(仅对WinSock);
socket:创建套接字;
connect:“连接”远端服务器(仅用于客户端);
closesocket:释放/关闭套接字;
bind:绑定套接字的本地IP地址和端口号(通常客户端不需要);
listen:置服务器端TCP套接字为监听模式,并设置队列大小(仅用于服务器端TCP套接字);
accept:接受/提取一个连接请求,创建新套接字,通过新套接(仅用于服务器端的TCP套接字);
recv:接收数据(用于TCP套接字或连接模式的客户端 UDP套接字);
recvfrom:接收数据报(用于非连接模式的UDP套接字);
send:发送数据(用于TCP套接字或连接模式的客户端UDP套接字);
sendto:发送数据报(用于非连接模式的UDP套接字);
setsockopt:设置套接字选项参数;
getsockopt:获取套接字选项参数。
(13)网络字节顺序
TCP/IP定义了标准的用于协议头中的二进制整数表示(如大端数、小端数):网络字节顺序(network byte order)。某些Socket API函数的参数需要存储为网络字节顺序(如IP地址、端口号等),可以实现本地字节顺序与网络字节顺序间转换的函数:
htons:本地字节顺序→网络字节顺序(16bits);
ntohs:网络字节顺序→本地字节顺序(16bits);
htonl:本地字节顺序→网络字节顺序(32bits);
ntohl:网络字节顺序→本地字节顺序(32bits)。
(14)网络应用的Socket API(TCP)调用基本流程
3、客户端软件设计
(1)解析服务器IP地址
客户端可能使用域名(如study.163.com)或IP地址(如123.58.180.121)标识服务器。 IP协议需要使用32位二进制IP地址,需要将域名或IP地址转换为32位IP地址。下面两个函数解析的IP地址是网络字节顺序,可以直接传递给Socket API函数。
inet_addr()实现点分十进制IP地址到32位IP地址转换;
gethostbyname()实现域名到32位IP地址转换,返回一个指向结构hostent 的指针。
struct hostent{
char FAR* h_name;/*official host name*/
char FAR* FAR* h_aliases;/*other aliases*/
short h_addrtype;/*address type*/
short h_length;/*address length */
char FAR* FAR* h_addr_list;/*list of address*/
};
#define h_addr h_addr_list[0]
(2)解析服务器(熟知)端口号
客户端还可能使用服务名(如HTTP)标识服务器端口,需要将服务名转换为熟知端口号。getservbyname()返回一个指向结构servent的指针。
struct servent{
char FAR* s_name;/*official service name*/
char FAR* FAR* s_aliases;/*other aliases*/
short s_port;/*port for this service*/
char FAR* s_proto;/*protocol to use*/
};
(3)解析协议号
客户端可能使用协议名(如TCP)指定协议,需要将协议名转换为协议号(如6)。getprotobyname()实现协议名到协议号的转换,返回一个指向结构protoent的指针。
struct protoent{
char FAR* p_name;/*official protocol name*/
char FAR* FAR* p_aliases;/*list of aliases allowed*/
short p_proto;/*official protocol number*/
};
(4)TCP客户端软件流程
确定服务器IP地址与端口号;
创建套接字,分配本地端点地址(IP地址+端口号);
连接服务器(套接字);
遵循应用层协议进行通信;
关闭/释放连接。
(5)UDP客户端软件流程
确定服务器IP地址与端口号;
创建套接字,分配本地端点地址(IP地址+端口号);
指定服务器端点地址,构造UDP数据报;
遵循应用层协议进行通信;
关闭/释放套接字。
(6)客户端软件的实现(代码见下一篇)
设计一个connectsock过程封装底层代码(UDP和TCP共用)。
设计connectUDP过程,用于创建连接模式客户端UDP套接字。
设计connectTCP过程,用于创建客户端TCP套接字。
异常处理。
(7)【例】访问DAYTIME服务的客户端(代码见下一篇)
DAYTIME服务,获取日期和时间。双协议服务(TCP、UDP),端口号13。TCP版利用TCP连接请求触发服务,UDP版需要客户端发送一个请求。
对于UDP版,前面的代码与TCP几乎相同。
4、服务器软件设计
有4种基本服务器。
(1)循环无连接(Iterative connectionless)服务器
基于UDP,无连接。循环指服务器一次只处理一个客户的服务请求,处理完再处理下一个。基本流程:创建套接字;绑定端点地址(INADDR_ANY+端口号);反复接收来自客户端的请求;遵循应用层协议,构造响应报文,发送给客户,再继续接收。
数据发送:服务器端不能使用connect()函数,无连接服务器使用sendto()函数发送数据报。
获取客户端点地址:服务器端调用recvfrom()函数接收数据时,自动提取。
(2)循环面向连接(Iterative connection-oriented)服务器
创建(主)套接字,并绑定熟知端口号;设置(主)套接字为被动监听模式,准备用于服务器;调用accept()函数接收下一个连接请求(通过主套接字),创建新套接字用于与该客户建立连接;遵循应用层协议,反复接收客户请求,构造并发送响应(通过新套接字);完成为特定客户服务后,关闭与该客户之间的连接,返回步骤3。
(3)并发无连接(Concurrent connectionless)服务器
主线程:创建套接字,并绑定熟知端口号;反复调用recvfrom()函数,接收下一个客户请求,并创建新线程处理该客户响应。
子线程:接收一个特定请求;依据应用层协议构造响应报文,并调用sendto()发送;退出(一个子线程处理一个请求后即终止)。
主线程和多个子线程同时工作。
(4)并发面向连接(Concurrent connection-oriented)服务器
主线程:创建(主)套接字,并绑定熟知端口号;设置(主)套接字为被动监听模式,准备用于服务器;反复调用accept()函数接收下一个连接请求(通过主套接字),并创建一个新的子线程处理该客户响应;
子线程:接收一个客户的服务请求(通过新创建的套接字);遵循应用层协议与特定客户进行交互;关闭/释放连接并退出(线程终止)。
主线程和多个子线程同时工作。
(5)服务器的实现(代码略)
设计一个底层过程隐藏底层代码passivesock(),两个高层过程分别用于创建服务器端UDP套接字和TCP套接字(调用passivesock()函数)passiveUDP()、passiveTCP()。
【例】无连接循环DAYTIME服务器。面向连接并发DAYTIME服务器。