【深入理解计算机系统】第11章 网络编程

【深入理解计算机系统】第11章 网络编程

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

每个网络应用都是基于客户端-服务器模型的。

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

1)当一个客户端需要服务时,它向服务器发送一个请求,发起一个事务。

2)服务器收到请求后,解释它,并以适当的方式操作它的资源。

3)服务器给客户端发送一个响应,并等待下一个请求。

4)客户端收到响应并处理它

注意:客户端和服务器是进程,而不是机器或主机,因为一台主机可以同时运行不同的客户端和服务器

2.网络

客户端和服务器通常运行在不同的主机上,并通过计算机网络的硬件和软件资源来通信。

对主机而言,网络是一种I/O设备,是数据源和数据接收方

从物理上而言,网络是一个按照地理远近组成的层次系统。最低层是LAN(局域网),最流行的局域网技术是以太网。

基本概念:

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

多个以太网段通过网桥连接成较大的局域网,称为桥接以太网。

多个不兼容的局域网通过路由器连接组成internet。

路由器连接高速点到点电话连接,称WAN(广域网)

3.全球IP因特网

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

3.1IP地址

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

struct in_addr{
    uint32_t s_addr;
}

主机字节顺序与网络字节顺序互换

因为因特网主机可以有不同的主机字节顺序,TCP/IP为任意整数数据项定义了统一的网络字节顺序(大端字节顺序)。

#include<arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint26_t hostshort);//返回:网络字节顺序值
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);//返回:主机字节顺序值

htonl函数讲32位整数由主机字节顺序转为网络字节顺序。ntohl函数将32位网络字节顺序转为主机字节顺序。同理,htons和ntohs为16位转换

IP地址通常以点分十进制表示法表示,如127.0.0.1

点分十进制与二进制的网络字节序转换

#include<arpa/inet.h>
int inet_pton(AF_INET,const char *src,void *dst);//返回:成功则为1,若src为非法点分十进制地址则为0,出错为-1
int char *inet_ntop(AF_INET,const void *src,char *dst);//返回:成功则指向点分十进制字符串的指针,出错为NULL
const char *inet_ntop(AF_INET,const void *src,char *dst);

inet_pton函数将点分十进制字符串转换为二进制的网络字节顺序的IP地址,inet_ntop函数将一个二进制的网络字节序的IP地址转换为点分十进制。

3.2因特网域名

因特网客户端和服务器互相通信时使用的是IP地址,由于太难记,因特网定义了一种将域名映射到IP地址的机制。域名集合形成了一个层次结构,如图:

如:www.amazon.com

3.3因特网连接

因特网客户端和服务器通过在连接上发送和接收字节流来通信。

一个套接字是连接的一个端点。每个套接字都有相应的套接字地址,由一个因特网地址和一个16位的整数端口组成,用“地址:端口”表示

当客户端发起一个连接请求时,客户端套接字地址中的端口是由内核自动分配的,称为临时端口。而服务器套接字地址中的端口通常是某个知名端口,如Web服务器常使用端口80,电子邮件服务器使用端口25等。

例:web客户端和web服务器之间的连接

4.套接字接口

套接字接口是一组函数,它们和Unix I/O函数结合起来,用以创建网络应用。

客户端-服务器事务的上下文中的套接字向导图

4.1套接字地址结构

从Linux内核看,一个套接字就是通信的一个端点,从Linux程序看,套接字就是一个有相应描述符的打开文件

因特网的套接字地址存放在sockaddr_in的16字节结构中,对于IPV4来说,sin_family为AF_INET,sin_port为一个16位的端口号,sin_addr为一个32位的IP地址。注意:IP地址和端口号总是以网络字节序存放

struct sockaddr_in{
    uint16_t sin_family;
    uint16_t sin_port;
    struct in_addr sin_addr;
    unsigned char sin_zero[8];
}

4.2 socket函数

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

#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain,int type,int protocol);//返回:成功返回非负描述符,出错-1
  • domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。

  • type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等(socket的类型有哪些?)。

(1)SOCK_STREAM(流套接字) 提供面向连接、可靠的数据传输服务。使用了TCP协议

(2)SOCK_DGRAM(数据报套接字)提供无连接的服务,使用UDP协议

(3)SOCK_RAW(原始套接字) 提供单一的网络访问,这个socket类型使用ICMP公共协议

  • protocol:故名思意,就是指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议

注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。

例:对于ipv4创建socket,其中使用tcp协议

sock = socket(AF_INET,SOCK_STREAM,0);

4.3 connect函数

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

#include<sys/socket.h>
int connect(int sockfd,const struct sockaddr *addr,socklen_t addrlen);//返回:成功则为0,出错-1
  • 第一个参数即为客户端的socket描述字

  • 第二参数为服务器的socket地址

  • 第三个参数为socket地址的长度,一般为sizeof(sockaddr_in)。

connect函数回阻塞,一直到连接成功建立或发生错误。

4.4bind函数

bind函数告诉内核将addr中的服务器套接字地址和套接字描述符sockfd联系起来

#include<sys/socket.h>
int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);//返回:成功0,出错-1
  • 第一个参数sockfd是由socket()调用返回的套接口文件描述符。

  • 第二个参数addr是指向数据结构sockaddr的指针。数据结构sockaddr中包括了关于你的地址、端口和IP地址的信息。

  • 第三个参数addrlen可以设置成sizeof(struct sockaddr_in)。

4.5listen函数

listen函数将sockfd从一个主动套接字转化为一个监听套接字。

如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。

#include<sys/socket.h>
int listen(int sockfd,int backlog);//成功0,出错-1
  • 第一个参数即为要监听的socket描述符

  • 第二个参数为相应socket可以排队的最大连接个数。

4.6accept函数

TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就向TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。

#include<sys/socket.h>
int accept(int listenfd,struct sockaddr *addr,int *addrlen);//返回:成功为非负连接描述符,出错-1
  • 第一个参数为服务器的socket描述字

  • 第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址

  • 第三个参数为协议地址的长度

成功后返回一个已连接描述符,用来利用Unix I/O函数与客户端通信

4.7主机和服务的转换

4.7.1getaddrinfo函数

getaddrinfo函数将主机名、主机地址、服务名和端口号得字符串表示转化成套接字地址结构,这个函数可重入的,适用于任何协议。

#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,出错为非零得错误代码
void freeaddrinfo(struct addrinfo *result);//释放链表,返回无
const char *gai_strerror(int errcode);//返回错误信息
struct addrinfo{
    int ai_flags;
    int ai_family;
    int ai_socktype;
    int ai_protocol;
    char *ai_canonname;
    size_t ai_addrlen;
    struct sockaddr *ai_addr;
    struct addrinfo *ai_next;
}

getaddrinfo返回的数据结构:

为了避免内存泄漏,应用程序必须在最后调用freeaddrinfo,释放该链表,如果getaddrinfo返回非零的错误代码,应用程序可以调用gai_strerror,该代码转换成消息字符串。

  • host一个主机名或者地址串(IPv4的点分十进制串或者IPv6的16进制串)

  • service:服务名可以是十进制的端口号,也可以是已定义的服务名称,如ftp、http等

  • hints:可以是一个空指针,也可以是一个指向某个addrinfo结构体的指针,调用者在这个结构中填入关于期望返回的信息类型的暗示。举例来说:如果指定的服务既支持TCP也支持UDP,那么调用者可以把hints结构中的ai_socktype成员设置成SOCK_DGRAM使得返回的仅仅是适用于数据报套接口的信息。

  • result:本函数通过result指针参数返回一个指向addrinfo结构体链表的指针。

4.7.2 getnameinfo 函数

将一个套接字地址结构转换成相应得主机和服务名字符串。

#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,出错为非零错误代码

例程:

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <string.h>
int main(int argc, char **argv)
{
    if (argc != 2) {
        fprintf(stderr, "Usage: %s hostname/n",argv[1]);
        exit(0);  
    }
    
    struct addrinfo *answer, hint, *curr;
    char buf[MAXLINE];  
    int flags;
    memset(&hint,0,sizeof(struct addrinfo));
    hint.ai_family = AF_INET;
    hint.ai_socktype = SOCK_STREAM;
​
    int ret = getaddrinfo(argv[1], NULL, &hint, &answer);
    if (ret != 0) {
        fprintf(stderr,"getaddrinfo: &s/n",gai_strerror(ret));
        exit(1);
    }
    flags = NI_NUMERICHOST;
    for(p=answer;p;p=p->ai_next)
    {
        getnameinfo(p->ai_addr,p_>ai_addrlen,buf,MAXLINE,NULL,0,flags);
        printf("%s\n",buf);
    }
    freeaddrinfo(answer);
    exit(0);
        
    
}

猜你喜欢

转载自blog.csdn.net/zhangxiafll/article/details/81119531
今日推荐