CSAPP-----网络编程

版权声明: https://blog.csdn.net/zl6481033/article/details/86061417

本节目录:

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

2、网络

3、全球IP因特网

4、套接字接口

5、Web服务器

6、总结


本系列文章的观点和图片均来自《深入理解计算机系统第 3 版》仅作为学习使用

        网络应用基本都是基于相同的基本编程模型,有着相似的整体逻辑结构,并且以来相同的编程接口。网络应用依赖于前面很多概念,进程、信号、字节顺序、内存映射、以及动态内存分配。需要理解基本的客户端-服务器编程模型,以及如何编写使用因特网提供的服务的客户端-服务器程序。

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

        每个网络应用都是基于客户端-服务器模型的,采用这个模型,一个应用是由一个服务器进程和一个或多个客户端进程组成。服务器管理某种资源,并且通过操作这种资源来为它的客户端提供某种服务。比如一个WEB服务器管理着一组磁盘文件,他会代表客户端进行检索和执行。一个FTP服务器管理着一组磁盘文件,它会为客户端进行存储和检索。相似的一个电子邮件服务器管理着一些文件,他会为客户端进行读和更新。

        客户端-服务器模型中的基本操作是事务,一个客户端-服务器事务由以下四步组成:

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

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

        (3)服务器给客户端发送一个响应,并等待下一个请求。例如WEB服务器将文件发送回客户端。

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

        也就如下图所示:

        

        认识到客户端和服务器是进程,而不是常提到的机器或主机这是很重要的。一台主机可以同时运行许多不同的客户端和服务器,而且一个客户端和服务器的事务可以在同一台或不同的主机上。无论客户端和服务器是怎么样映射到主机上的,客户端-服务器模型都是相同的。客户端-服务器事务和数据库事务不同。

2、网络

        客户端和服务器通常运行在不同的主机上,并且通过计算机网络的硬件和软件资源来通信。对于主机而言网络只是一种IO设备,是数据源和数据接收方,一个插到IO总线扩展槽的适配器提供了到网络的物理接口,从网络上接受的数据从适配器经过IO和内存总线复制到内存,通常是通过DMA(数据不经过CPU)传送,同样,数据也能从内存复制到网络。

        下图为一个主机的组成:

        

        物理上,网络是一个按照地理远近组成的层次系统,最底层是LAN(局域网)。目到目前为止最流行的局域网技术是以太网。

        一个以太网段包括一些电缆和一个叫做集线器。

        

        每个以太网适配器都有一个全球唯一的48位地址(MAC地址),存储在这个适配器的非易失性存储器上,一台主机可以发送一段位(帧)到这个网段的任何主机,每个帧包含一些固定数量的头部,用来标识此帧的源和目的地址以及长度,每个主机适配器都能看到该帧但是只有目的主机实际读取它。

        使用一些电缆和网桥,可以将多个以太网段连接成较大的局域网。称为桥接以太网。一些电缆连接网桥和网桥,一些连接集线器和网桥,它们的带宽可以不一样。

        在层次更高的网络中,多个不兼容的局域网可以通过叫做路由器的特殊计算机连接起来,组成一个互联网络(internet)。每台路由器对于它所连结到每个网络都有一个适配器,路由器也能连接高速点对点电话连接,这是称为WAN(广域网)的网络示例。一般而言路由器可以用来由各种局域网和广域网构建互联网络。

        关于网络更多可参考图解TCP/IP专栏(https://blog.csdn.net/zl6481033/column/info/32119)。

3、全球IP因特网

        全球IP因特网是最著名和最成功的互联网网络实现。下图是一个因特网客户端-服务器应用程序的基本硬件和软件组织。

        每台因特网主机都运行实现着TCP/IP协议的软件,几乎每个现代计算机系统都支持这个协议,因特网的客户端和服务器混合使用套接字接口函数和Unix IO函数进行通信,通常套接字函数实现为系统调用,这些系统调用会陷入内核,并调用各种内核模式的TCP/IP函数。

        其中TCP/IP实际是一个协议组,其中每一个提供不同的功能。例如IP协议提供基本的命名方法和递送机制,这种递送机制能够从一台因特网主机往其他主机发送包,也叫做数据报。IP机制从某种意义上而言是不可靠的,因为如果数据报在网络中丢失或重复他不会试图恢复,UDP(用户数据报协议)扩展了IP协议,这样包可以在进程间传递而不仅仅是主机。TCP是建在IP之上的复杂协议,提供了进程间可靠的全双工连接。

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

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

        (2)这组IP地址被映射为一组称为因特网域名的标识符。

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

    3.1 IP地址

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

struct in_addr{
    uint32_t s_addr;
}

        把一个标量地址放在结构中,是套接字接口早期实现的不幸产物,为其定一个标量更有意义,但是现在更改已经太迟了。因为因特网主机可以有不同的主机字节序,TCP/IP为任意整数数据项定义了统一的网络字节序(大端字节序),例如IP地址,他放在包头中跨过网络被携带,在IP地址结构中存放的地址总是以网络字节顺序存放的,即使主机字节序是小端法,Unix提供下面的函数在网络和主机字节序间实现转换。

#include <arpa/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);

        hotnl函数将32位整数由主机字节序转换为网络字节序,ntohl 函数将32位整数从网络自己徐转为主机字节序。htons 和 ntohs函数为16位无符号整数执行响应的转换。

        IP地址通常是以点分十进制表示法来表示的,应用程序可以使用inet_pton和inet_ntop函数实现IP地址的点分十进制串之间的转换。

       

#include<arpa/inet.h>

int inet_pton(int af, const char *src, void *dst);

 const char *inet_ntop(int af, const void *src,
                             char *dst, socklen_t size);

        在这些函数名中 “n”代表网络,“p”代表表示,它们可以处理32位IPv4地址。inet_pton函数将一个点分十进制串(src)转换成一个二进制网络字节序的IP地址(dst),如果src没有指向一个合法的点分十进制字符串函数返回0。任何其他错误返回-1,并设置errno。相似的inet_ntop函数将一个二进制网络字节序的IP地址(src)转换成他对应的点分十进制表示,并把得到的null结尾的字符串的最多size个字节复制到dst。

    3.2 因特网域名

        因为IP地址 是比较难记的,所以定义了域名。因特网定义了域名集合和IP地址集合之间的映射。这个映射通过分布世界范围内的数据库DNS(域名系统)维持。

        每个因特网主机都有本地定义的域名localhost,这个域名总是映射为回送地址127.0.0.1。localhost这个名字为引用运行在同一台机器上的客户端和服务器提供了一种便利和可移植的方式,可以使用hostname确定本地主机的实际域名。

       

        最简单的青情况,域名与IP地址一一映射,某些情况下多个域名可以映射为同一个IP地址。最通常情况下,多个域名可以映射到同一组的多个IP地址。

    3.3 因特网互联

        因特网客户端和服务器通过在连接上发送和接收字节流来通信,从连接一对进程的意义上而言,连接是点对点的。从数据可以双向流动的角度来说,它是全双工的,并且从源进程发出的字节流最终被目的进程以及它发出的顺序收到它的角度来说是可靠的。

        一个套接字是连接的一个端点,每个套接字都有相应的套接字地址,是一个因特网地址和一个16为的整数端口组成,用“地址:端口”来表示。当客户端发起一个连接请求时,客户端套接字地址中的端口是内核自动分配的,称为临时端口。然而服务器套接字地址中的端口通常是某个知名端口和这个服务相对应的。例如Web服务器通常使用的是端口80,电子邮件服务器是25。每个知名端口的服务都有一个对应的知名的服务名。文件/etc/services包含一张这台机器提供的知名名字和知名端口之间的映射。

        一个连接是由它两端的套接字地址唯一确定的。这对套接字地址叫做套接字对,由下列元组表示:

(cliaddr:cliport,servaddr:servport)

        其中cliaddr是客户端的IP地址,cliport是客户端端口,servaddr是服务器IP地址,servport是服务器端口。如下图展示Web客户端和一个Web服务器之间的连接。

               

        这样客户端和服务器之间的连接就由这一对套接字唯一确定了。

4、套接字接口

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

         

    4.1 套接字地址结构

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

struct sockaddr_in
 
{
 
short sin_family;/*Address family一般来说AF_INET(地址族)PF_INET(协议族)*/
 
unsigned short sin_port;/*Port number(必须要采用网络数据格式,普通数字可以用htons()函数转换成网络数据格式的数字)*/
 
struct in_addr sin_addr;/*IP address in network byte order(Internet address)*/
 
unsigned char sin_zero[8];/*Same size as struct sockaddr没有实际意义,只是为了 跟SOCKADDR结构在内存中对齐*/
 
};

struct sockaddr
{
uint16_t sa_family;/*addressfamily,AF_xxx*/
char sa_data[14];/*14bytesofprotocoladdress*/
};

        connect、bind和accept函数都要求一个指向与协议相关的套接字地址结构的指针,套接字接口设计者面临的问题是,如何定义这些函数使其能接受各种类型的套接字地址结构,现在我们可以使用void *指针,但是在之前并没有这种类型的指针,解决办法就是定义套接字函数要求一个指向通用sockaddr结构的指针,然后要求应用程序将与协议特定的结构的指针强制转换成这个通用结果。无论何时需要将sockaddr_in结构强制转换成通用sockaddr结构。可以使用如下结构。

typedef struct sockaddr SA;

    4.2 socket函数

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

 #include <sys/types.h>          /* See NOTES */
 #include <sys/socket.h>

 int socket(int domain, int type, int protocol);
/* The  domain argument specifies a communication domain; this selects the protocol family which will be used for communication.  These fami‐
       lies are defined in <sys/socket.h>.  The currently understood formats include:

       Name                Purpose                          Man page
       AF_UNIX, AF_LOCAL   Local communication              unix(7)
       AF_INET             IPv4 Internet protocols          ip(7)
       AF_INET6            IPv6 Internet protocols          ipv6(7)
       AF_IPX              IPX - Novell protocols
       AF_NETLINK          Kernel user interface device     netlink(7)
       AF_X25              ITU-T X.25 / ISO-8208 protocol   x25(7)
       AF_AX25             Amateur radio AX.25 protocol
       AF_ATMPVC           Access to raw ATM PVCs
       AF_APPLETALK        AppleTalk                        ddp(7)
       AF_PACKET           Low level packet interface       packet(7)
       AF_ALG              Interface to kernel crypto API

       The socket has the indicated type, which specifies the communication semantics.  Currently defined types are:

       SOCK_STREAM     Provides sequenced, reliable, two-way, connection-based byte streams.  An out-of-band data transmission mechanism  may  be
                       supported.

       SOCK_DGRAM      Supports datagrams (connectionless, unreliable messages of a fixed maximum length).

       SOCK_SEQPACKET  Provides  a  sequenced, reliable, two-way connection-based data transmission path for datagrams of fixed maximum length; a
                       consumer is required to read an entire packet with each input system call.

       SOCK_RAW        Provides raw network protocol access.

       SOCK_RDM        Provides a reliable datagram layer that does not guarantee ordering.

       SOCK_PACKET     Obsolete and should not be used in new programs; see packet(7).

       Some socket types may not be implemented by all protocol families.

       Since Linux 2.6.27, the type argument serves a second purpose: in addition to specifying a socket type, it may include the bitwise  OR  of
       any of the following values, to modify the behavior of socket():

       SOCK_NONBLOCK   Set  the  O_NONBLOCK  file status flag on the new open file description.  Using this flag saves extra calls to fcntl(2) to
                       achieve the same result.

       SOCK_CLOEXEC    Set the close-on-exec (FD_CLOEXEC) flag on the new file descriptor.  See the description of the O_CLOEXEC flag in  open(2)
                       for reasons why this may be useful.
*/

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

        clientfd = socket(AF_INET,SOCK_STREAM,0);

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

    4.3 connect函数

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

 #include <sys/types.h>          /* See NOTES */

 #include <sys/socket.h>

int connect(int clientfd, const struct sockaddr *addr,
                   socklen_t addrlen);

        connect函数试图与套接字地址为addr的服务器建立一个因特网连接,其中addrlen是sizeof(sockaddr_in)。connect函数回阻塞,一直到连接成功建立或是发生错误,如果成功,clientfd描述符就准备好了读写,并且得到的连接是由套接字对

(x:y,addr.sin_addr : addr.sin_port)刻画的,其中x表示客户端的IP地址,y表示临时端口,它唯一的确定了客户端主机上的客户端进程,对于socket,最好使用getaddrinfo来为connect 提供参数。

    4.4 bind函数

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

 #include <sys/types.h>          /* See NOTES */

 #include <sys/socket.h>

 int bind(int sockfd, const struct sockaddr *addr,
                socklen_t addrlen);

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

    4.6 listen 函数

        客户端是发起连接请求的主动实体,服务器是等待来自客户端的连接请求的被动实体,默认情况下内核会认为socket函数创建的描述符对应主动套接字,它存在在一个连接的客户端,服务器调用listen函数告诉内核,描述符是被被服务器而不是客户端使用的。

 #include <sys/types.h>          /* See NOTES */

 #include <sys/socket.h>

 int listen(int sockfd, int backlog);

        listen函数将socketfd从一个主动套接字转为一个监听套接字,该套接字可以接受来自客户端的连接请求,backlog参数暗示内核在开始拒绝连接请求之前,队列中要排队的未完成的连接请求数量。backlog参数的确切含义要求对TCP/IP协议的理解,这里不赘述,通常设为1024。

    4.6 accept 函数

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

 #include <sys/types.h>          /* See NOTES */

 #include <sys/socket.h>

 int accept(int listenfd, struct sockaddr *addr,int *addrlen);

        accept函数等待来自客户端的请求到达侦听描述符listenfd,然后在addr中填写客户端的套接字地址,并返回一个已连接描述符,这个描述符可被利用Unix IO 函数与客户端通信。

        监听描述符与已连接描述符之间的区别是很多人感到迷惑,监听描述符是作为客户端连接请求的一个端点,它通常被创建一次,并存在于服务器整个生命周期,已连接描述符是客户端于服务器之间已经建立起来的连接的端点,服务器每次接受连接请求时都会创建一次,它只存在于服务器为一个客户端服务的过程中。

       下图描绘了监听描述符和已连接描述符的角色,在第一步,服务器调用accept,等待连接请求到达监听描述符,设为3。第二步中客户端调用connect函数,发送一个连接请求到listenfd,第三步,accept函数打开一个新的已连接描述符connfd(设为4),在clientfd和connfd之间建立连接,并随后返回connfd给应用程序,客户端也从connect返回,在这一点后,客户端和服务器就可以分别读和写clientfd和connfd来回传数据了。

        

        监听描述符和已连接描述符之间的区别:这两者的区分是很有用的,它可以使我们建立并发服务器,能够同时处理许多客户端连接,例如,每一次一个连接请求到达监听描述符时,我们可以派生fork一个新的进程,它通过已连接描述符与客户端通信。

    4.7 主机与服务的转换

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

        (1)getaddrinfo函数

         getaddrinfo函数将主机名、主机地址、服务名和端口号的字符串转化为套接字地址结构。

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

int getaddrinfo(const char *node, const char *service,
                       const struct addrinfo *hints,
                       struct addrinfo **res);

void freeaddrinfo(struct addrinfo *res);

const char *gai_strerror(int errcode);


struct addrinfo {
               int              ai_flags;
               int              ai_family;
               int              ai_socktype;
               int              ai_protocol;
               socklen_t        ai_addrlen;
               struct sockaddr *ai_addr;
               char            *ai_canonname;
               struct addrinfo *ai_next;
           };

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

        

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

        getaddrinfo的host参数可以是域名,也可以使数字地址,service可以是服务名(如http)也可以是十进制端口号,如果不想把主机名转换成地址可以把host参数设为NULL,service也一样,但必须指定两者中至少一个。

        可选参数hints是一个addrinfo结构,它提供对getaddrinfo返回的套接字地址列表更好的控制,如果要传递hints参数,只需要设置下列字段,ai_family、ai_socktype、ai_protocol和ai_flags字段,其他字段必须设置为0或NULL。实际中,使用memset将整个结构清零,然后有选择地设置一些字段。

        *getaddrinfo 默认返回IPv4和IPv6套接字地址,ai_famliy设置为AF_INET会将列表限制为IPv4地址,设置为AF_INET6会限制为IPv6地址。                                                                 

        *对于host关联地每个地址,getaddrinfo函数最多返回三个addrinfo结构。

        *ai_flags字段是一个位掩码,可以进一步修改默认行为。

        当getaddrinfo创建输出列表中地addrinfo结构时,会填写每个字段,除了ai_flags。ai_addr字段指向一个套接字地址结构,ai_addrlen字段给出这个套接字地址结构的大小,而ai_next字段指向列表中下一个addrinfo结构,其他字段描述这个套接字地址的各种属性。

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

        (2)getnameinfo函数

        getnameinfo函数和getaddrinfo是相反的,将一个套接字地址结构转换成相应的主机和服务名字符串。

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

int getnameinfo(const struct sockaddr *sa, socklen_t salen,
                       char *host, socklen_t hostlen,
                       char *serv, socklen_t servlen, int flags);


/*
**The flags argument modifies the behavior of getnameinfo() as follows:

**       NI_NAMEREQD
**              If set, then an error is returned if the hostname cannot be determined.
**
**       NI_DGRAM
**              If set, then the service is datagram (UDP) based rather than stream (TCP) **based.  This is required for the few ports (512-514) that
**              have different services for UDP and TCP.

**       NI_NOFQDN
**              If set, return only the hostname part of the fully qualified domain name **for local hosts.

**       NI_NUMERICHOST
**              If set, then the numeric form of the hostname is returned.  (When not **set, this will still happen in case the node's name cannot be
**              determined.)

**       NI_NUMERICSERV
**              If set, then the numeric form of the service address is returned.  (When **not set, this will still happen in case the service's name
**              cannot be determined.)
*/

        参数sa指向大小为salen字节的套接字结构,host指向大小为hostlen字节的缓冲区,serv 指向大小为servlen字节的缓冲区。getnameinfo函数将套接字地址结构sa转换成对应的主机和服务名字符串,并将它们复制到host和service缓冲区。如果getnameinfo返回非零的错误代码,应用程序可以调用gai_strerror把它转换成字符串。

        如果不想要主机名,可以把host设置为NULL,hostlen设置为0。对服务字段来说也是一样,不过两者最少设置一个。

        参数flag是一个位掩码,能够修改默认的行为,可以把各种值用OR组合起来得到该掩码。

        *NI_NUMERICHOST 。getnameinfo默认试图返回host中的域名,设置该标志会使该函数返回一个数字地址字符串。

        *NI_NUMERICSERV。getnameinfo默认会检查/etc/service,如果可能会返回服务名而不是端口号,设置该标志会使该函数跳过查找,简单返回端口号。更多参数可以查看上图。

        关于这两个函数的使用可参考(https://blog.csdn.net/zl6481033/article/details/86218301)。

    4.8 套接字接口的辅助函数

        一开始可能觉得getnameinfo函数和套接字接口看上去比较麻烦,可以用一些辅助函数包装一下会方便很多。称为open_clientfd和open_listenfd,客户端和服务器相互通信时可以使用这些函数。

        (1)open_clientfd函数

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

//成功返回描述符,出错返回-1
int open_clientfd(char *hostname, char *port) {
    int clientfd;
    struct addrinfo hints, *listp, *p;
    
    // Get a list of potential server address
    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_socktype = SOCK_STREAM; // Open a connection
    hints.ai_flags = AI_NUMERICSERV; // using numeric port arguments
    hints.ai_flags |= AI_ADDRCONFIG; // Recommended for connections
    getaddrinfo(hostname, 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
        // 这里使用从 getaddrinfo 中得到的参数,实现协议无关
        if ((clientfd = socket(p->ai_family, p->ai_socktype,
                               p->ai_protocol)) < 0)
            continue; // Socket failed, try the next
        
        // Connect to the server
        // 这里使用从 getaddrinfo 中得到的参数,实现协议无关
        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 connections failed
        return -1;
    else // The last connect succeeded
        return clientfd;
}

        open_clientfd函数建立与服务器的连接,该服务器运行在主机hostname上,并在端口号port监听连接请求,返回一个打开的套接字描述符,该描述符准备好了就可以用Unix IO函数作为输入和输出。上图是源码,调用getaddrinfo,返回一个addrinfo的结构列表,每个结构指向一个套接字地址结构,可以用于建立与服务器的连接,该服务器运行在hostname上并监听port端口,然后遍历该列表,依次尝试列表中的每个条目,直到调用socket和connect成功,如果connect失败,再尝试下一个条目之前要关闭套接字描述符。如果connect成功,会释放列表内存,并把套接字描述符返回给客户端,客户端可以立即开始用Unix IO与服务器通信。

        (2)open_listenfd 函数

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

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 connection
    hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG; // on any IP address
    hints.ai_flags |= AI_NUMERICSERV; // using port number
    // 因为服务器不需要连接,所以原来填写地址的地方直接是 NULL
    getaddrinfo(NULL, 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
        // 这里使用从 getaddrinfo 中得到的参数,实现协议无关
        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(listenfd); // Bind failed, try another
    }
    
    // Clean up
    freeaddrinfo(listp);
    if (!p) // No address worked
        return -1;
    
    // Make it a listening socket ready to accept connection requests
    if (listen(listenfd, LISTENQ) < 0) {
        close(listenfd);
        return -1;
    }
    return listenfd;
}

        open_listenfd函数打开和返回一个监听描述符,这个描述符准备好在端口port上,接受连接请求。和open_clientfd很像,也是调用getaddrinfo,然后遍历结果列表,直到调用socket和bind成功。在上面的程序中,使用了setsockopt函数配置服务器,使得服务器能够被终止、重启和立即开始接受连接请求。一个重启的服务器默认在大约30s内拒绝客户端的连接请求。在调用getaddrinfo时使用AI_PASSIVE标志,将host参数设置为NULL,每个套接字地址结构中的地址字段会被设置为通配符地址,告诉内核这个服务器会接收发送到本主机的所有IP地址的请求。

    4.9 echo客户端和服务器示例

        具体实现(https://blog.csdn.net/zl6481033/article/details/86242035)。

5、Web服务器

        在这一节,大致了解Web基础知识,暂时略过部分内容。

    5.1 Web基础

        Web客户端和服务器中之间的交互作用是一个基于文本的应用级协议,叫做HTTP协议(超文本传输协议)。HTTP是一个简单的协议,一个Web客户端(也就是浏览器)打开一个到服务器的因特网连接,并请求这些内容。服务器响应所请求的内容,然后关闭连接,浏览器读取这些内容,并把它显示在屏幕上。

        Web内容可以用一种叫做HTML(超文本标记语言)来编写,一个HTML程序(页)包含指令(标记)他们告诉浏览器如何显示这页中的各种文本和图形对象。例如

<b> Make me bold! </b>

        告诉浏览器用粗体字型输出<b>和</b>标记之间的文本,然而,HTML真正强大的地方再一个页面可以包含指针(超链接)这些指针可以指向存放在任何因特网主机上的内容,例如,一个格式如下的HTML行

<a href="http://www.cmu.edu/index.html">Carnegie Mwllon</a>

        告诉浏览器高亮显示文本对象Carnegie Mwllon并创建一个超链接,指向存放CMU Web服务器叫做index.html的HTML文件。如果用户单击了这个高亮文本对象,浏览器会从CMU服务器中请求相应的HTML文件并显示。 

    5.2 Web内容 

        对于Web客户端和服务器而言,内容是与一个MIME(多用途的忘记邮件扩充协议)类型相关的字节序列,下图是一些常用的MIME类型。

              

        Web服务器以两种不同的方式向客户端提供内容:

        *取一个磁盘文件,并将它的内容返回客户端,磁盘文件成为静态内容,而返回文件给客户端的过程称为服务静态内容。

        *运行一个可执行文件,并将它的输出返回客户端,运行时可执行文件的输出称为动态内容,而运行程序并返回他的输出到客户端的过程称为服务动态内容。而运行程序并返回他的输出到客户端的过程被称为服务动态内容。

        每条由Web服务器返回的内容都是和它管理的某个文件相关联的,这些文件中的每一个都有一个唯一的名字,叫做URL(通用资源定位符),例如

http://www.google.coim:80/index.html

        表示因特网主机www.goole.com上有一个称为/index.html的HTML的文件,是由一个监听端口80的Web服务器管理的,端口号是可选的,默认为知名的HTTP端口80,可执行文件的URL可以在文件名后包括程序参数。“?”字符分割文件名和参数,并且每个参数都用“&”字符分隔开。例如

http://bludfish.ics.cs.cmu.edu:8000/cgi-bin/addr?15000&213

        上面这个URL标识了一个叫做/cgi-bin/addr 的可执行文件,会带有两个参数字符串,15000和213来调用它,在事务过程中客户端和服务器使用的是URL的不同部分,例如,客户端使用前缀:http://www.google.com:80来决定与哪类服务器联系,服务器在哪以及监听的端口号是多少,服务器使用后缀/index.html来发现它文件系统中的文件,并确定请求的是静态内容还是动态内容。

6、总结

        每个网络应用都是基于客户端和服务器模型的,根据这个模型一个应用是由一个服务器和一个或多个客户端组成,服务器管理资源,以某种方式操作资源为它的客户端提供服务,客户端-服务器模型中的基本操作是客户端-服务器事务,它是由客户端请求和跟随其后的服务器相应组成的。

        客户端和服务器通过因特网这个全球网络来通信,从程序员观点上来看=,可以把因特网看成一个全球范围的主机集合,每个因特网主机都有一个唯一的32位字,称为IP地址,IP地址的集合被映射为一个因特网域名的集合,不同的因特网主机上的进程能够互相连接通信。

        客户端-服务器通过使用套接字接口建立连接,一个套接字是连接的一个端点,连接以文件描述符的形式提供给应用程序,套接字接口提供了打开和关闭套接字描述符的函数,客户端和服务器通过读写这些描述符来实现彼此之间的通信。

        本节实践:

        (1)getaddrinfo和getnameinfo展示出域名到它关联的IP地址之间的映射。相当于nslookup指令。 

                 (https://blog.csdn.net/zl6481033/article/details/86218301)

         (2)echo回声服务器和客户端。书中的程序有点小问题,需要改动。

                (https://blog.csdn.net/zl6481033/article/details/86242035

        

        

        

        

        

        

猜你喜欢

转载自blog.csdn.net/zl6481033/article/details/86061417