GuangchaoSun's Blog

第11章 网络编程

学习目标:

  • 了解网络的基本架构
  • 了解常见的网络协议
  • 学会编写web服务器

网络架构

每个网络应用都是基于客户端-服务器模型。对于一个主机而言,网络只是一种I/O设备,作为数据源和数据接收方。

根据网络的应用范围和架构层级:

  • LAN-Local Area Network
    • 以太网(Ethernet)
  • WAN-Wide Area Network
    • High speed point-to-point phone lines

最底层Ethernet segment

几个host连接到hub上,通常范围是一个房间或一个楼层

  • 每个以太网适配器都有一个全球唯一的48位地址(也就是MAC地址)
  • 不同主机间传送数据的称为帧,由header和有效载荷组成

桥接以太网bridge Ethernet

通常是一层楼的范围,Bridge知道从某端口可达的主机,并有选择在端口间复制数据。

下一层-internet

互联网络重要的特性是:由采用完全不同和不兼容技术的各种局域网和广域网组成,关键在于协议软件

网络协议

这种协议控制主机和路由器协同工作来实现数据传输。在不同的 LAN 和 WAN 中传输数据,就要守规矩,这个规矩就是协议。协议负责做的事情有:

  • 命名机制
    • 定义 host address 格式
    • 每个主机和路由器至少有一个internet address
  • 传送机制
    • 标准传输单元-package(把数据位捆扎成不连续的片)
    • 包由包头和有效载荷组成
      • 包头:包的大小、源主机和目的主机的地址
      • 有效载荷:源主机发出的数据位

在网络协议下,具体的数据传输如下图所示,PH = Internet pachage header, FH = LAN frame header.

TCP/IP 实际上是一个协议族。

  • IP 协议提供基本的命名方法和递送机制,这种递送机制能够从一台因特网主机往其他主机发送包,也叫做数据报(datagram)。
  • TCP 是一个构建在 IP 之上的复杂协议,提供子进程间可靠的全双工(双向的)连接。

Internet 是 internet 最著名的例子。主要基于 TCP/IP 协议族:

  • IP(Internet Protocal)
    • Provides basic naming scheme and unreliable delivery capability of packages(datagrams) from host-to-host
  • UDP(Unreliable Datagram Protocal)
    • Use IP to provide unreliable datagram delivery from process-to-process(进程间)
  • TCP(Transmission Control Protocal)
    • Uses IP to provide reliable byte streams from process-to-process over connection

Accessed via a mix of Unix file I/O and functions from sockets interface

  • 主机集合被映射为一组32位的IP地址
  • IP地址被映射到域名
  • 不同主机之间通信,可以通过 connection 来交换数据
    Internet域名

  • 因特网定义了域名集合和IP地址集合之间的映射。
  • 这个映射是通过分布世界范围内的数据库DNS(Domain Name System,域名系统)来维护的。
  • DNS数据库是由数以百万计的主机条目结构(host entry structure)组成的。

因特网应用通过gethostnamegethostbyaddr函数,从DNS数据库检索任意的主机条目。

Internet连接

客户端和服务器通过连接(connection)来发送字节流,特点是:

  • 点对点:连接一对进程
  • 全双工:数据可以同时双向流动
  • 可靠:字节的发送循序和收到的一致

Socket 则可以被认为是 connection 的 endpoint,socket 地址是一个IPaddress:port 对。
Port是一个16位的整数,用来标识不同的进程,利用不同的端口来连接不同的服务:

  • Ephemeral port(临时端口):Assigned automatically by client kernel when client makes a connection request
  • Well-known port:Associated with some service provided by a server
    • echo server:7/echo
    • ssh server:22/ssh
    • email server:25/smtp
    • web servers:80/http

套接字接口

套接字接口(socket interface)是一组函数,它们和Unix I/O函数结合起来,用以创建应用。
从Unix内核的角度来看,一个套接字就是通信的一个端点;从Unix程序的角度来看,套接字就是一个有相应描述符的打开文件。
connect、bind和accept函数要求一个指向与协议相关的套接字地址结构的指针。

简单服务器实现

架构总览
根据上面的流程图,总共有5个步骤:

  1. 开启服务器(open_listenfd函数)
    • getaddrinfo:设置服务器的相关信息
    • socket:创建socket descriptor, 也就是后来用来读写的file descriptor
      • int socket(int domain, int type, int protocol);
      • 例如int clientfd = socket(AF_INET, SOCK_STREAM, 0);
      • AF_INET表示在用32位IPv4地址
      • SOCK_STREAM表示这个 socket 将是 connection 的 endpoint
      • 前面这种写法是协议相关的,建议使用getaddrinfo生成的参数来进行配置,这样就与协议无关
    • bind:请求kernel把socket address和socket desctiptor绑定
      • int bind(int sockfd, SA *addr, socklen_t addrlen);
      • The process can read bytes that arrive on the connection whose endpoint is addr by reading from descriptor sockfd
      • Similarly,writes to sockfd are transferred along connection whose endpoint is addr
      • 最好用getaddrinfo生成的参数作为addraddrlen
    • listen: 默认来说,我们从socket函数中得到的descriptor默认是active socket(也就是客户端的连接),调用listen函数告诉kernel这个socket是被服务器使用的
      • int listen(int sockfd, int backlog);
      • sockfd从active socket转换成listening socket,用来接收客户端的请求
      • backlog的数值表示kernel在接收多少个请求之后(队列缓存起来)开始拒绝请求
    • accept:调用accept函数,开始等待客户端请求
      • int accept(int listenfd, SA *addr, int *addrlen);
      • 等待绑定到listenfd的连接接收到请求,然后把客户端的socket address写入到addr,大小写入到addrlen
      • 返回一个connected descriptor用来进行信息传输(类似Unix I/O)
  2. 开启客户端(open_clientfd 函数,设定访问地址,尝试连接)
    • getaddrinfo:设置客户端的相关信息
    • socket:创建socket descriptor,也就是之后用来读写的file descriptor
    • connect:客户端调用connect来建立和服务器的连接
      • int connect(int clientfd, SA *addr, socklen_t addrlen);
      • 尝试与在socket address addr的服务器建立连接
      • 如果成功clientfd可以进行读写
      • connection由socket对描述(x:y, addr.sin_addr:addr.sin_port)
      • x是客户端地址,y是客户端临时端口,后面两个是服务器的地址和端口
      • 最好用getaddrinfo生成的参数作为addraddrlen
  3. 交换数据(主要是一个流程循环,客户端向服务器写入,就是发送请求;服务器向客户端写入,就是发送响应)
    • [Client]rio_writen:写入数据,相当于向服务器发送请求
    • [Client]rio_readlineb:读取数据,相当于从服务器接收响应
    • [Server]rio_readlineb:读取数据,相当于从客户端接收请求
    • [Server]rio_writen:写入数据,相当于向客户端发送响应
  4. 关闭客户端(主要是close
    • [Client]close:关闭连接
  5. 断开客户端
    • [Server]rio_readlined:收到客户端发来的关闭连接请求
    • [Server]close:关闭与客户端的连接

Client:open_clientfd

socketconnect函数包装成一个叫做open_clientfd的辅助函数。
open_clientfd函数和运行在主机hostname上的服务器建立一个连接,并在知名端口port上监听连接请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int open_clientfd(char *hostname, int port)
{
int clientfd;
struct hostent *hp;
struct sockaddr_in serveraddr;
if((clientfd = socket(AF_INET,SOCK_STREAM, 0)) < 0)
return -1;
if((hp = gethostbyname(hostname)) == NULL)
return -2;
bzero((char *) &serveraddr, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
bcopy((char*)hp->h_addr_list[0],(char *)&serveraddr.sin_addr.s_addr, hp->h_length);
serveraddr.sin_port = htons(port);
/*Establish a connection with the server*/
if(connect(clientfd,(SA *) &serveraddr, sizeof(serveraddr)) < 0)
return -1;
return clientfd;
}

在这里用文字描述一下流程:在创建了套接字描述符(第7行)后,我们检索服务器的DNS主机条目,并拷贝主机条目中的第一个IP地址到服务器的套接字地址结构(10~14行)。在用按照网络字节顺序的服务器的知名端口号初始化套接字地址结构(15行)之后,我们发起了一个到服务器的连接请求(18行)。当connect函数返回时,我们返回套接字描述符给客户端,客户端就可以立即开始用Unix I/O和服务器通信了。

Server:open_listenfd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
nt open_listenfd(int port)
{
int listenfd, optval = 1;
struct sockaddr_in serveraddr;
/*create a socket descriptor*/
if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
return -1;
if(setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (const void *)&optval, sizeof(int)) < 0)
return -1;
/*Listenfd will be an end point for all request to port on any IP address for this host*/
bzero((char *) &serveraddr, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons((unsigned short)port);
if(bind(listenfd, (SA *)&serveraddr, sizeof(serveraddr)) < 0)
return -1;
if(listen(listenfd, LISTENQ) < 0)
return -1;
return listenfd;
}

简单的socket服务器实例

参考