第十一章 网络编程

客户端-服务器编程模型

每个网络应用都是基于客户端-服务器模型的。采用这个模型,一个应用是由一个服务器进程和一个或多个客户端进程组成。
客户端-服务器模型中的基本操作是事务(transaction)。一个客户端-服务器事物由以下四步组成。

  1. 当一个客户端需要服务时,他就向服务器发送一个请求,发起一个事物。例如,当Web浏览器需要一个文件时,它就发送一个请求给Web服务器。
  2. 服务器收到请求后,解释它,并以适当的方式操作它的资源。例如,当Web服务器收到浏览器发出的请求后,它就读取一个磁盘文件。
  3. 服务器给客户端发送一个响应,并等待下一个请求。例如,Web服务器将文件发送回客户端。
  4. 客户端收到响应并处理它。例如,当Web浏览器收到来自服务器的一页后,就在屏幕上显示此页。

认识到客户端和服务器是进程,而不是常提到的机器或者主机,这是很重要的。

网络

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

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

全球IP因特网

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

  • 主机集合被映射为一组32位的IP地址。
  • 这组IP地址被映射为一组称为 因特网域名(Internet domain name) 的标识符。
  • 因特网主机上的进程能够通过 连接(connection) 和任何其他因特网主机上的进程通信。

    IP地址

    IP地址存放在IP地址结构中
    1
    2
    3
    struct in_addr{
    uint32_t s_addr; /*Address in network byte order (big-endian)
    }

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

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

1
2
3
4
5
6
7

#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);//返回:主机字节顺序值

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

1
2
3
4
5
6
#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);
//返回:成功则指向点分十进制字符串的指针,出错为NULL

因特网连接

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

一个连接是由它两端的套接字地址唯一确定的。这对套接字地址叫做套接字对(socket pair)

套接字的地址结构

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

1
2
3
4
5
6
7
8
9
10
11
struct sockaddr_in {
uint16_t sin_family; /*Protocol family(always AF_INET)*/
uint16_t sin port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
}
//通用套接字结构
struct sockaddr {
uint16_t sa_family;
char sa_data[14];
}

我们可以通过下图理解各个函数。
套接字接口的应用

socket函数

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

1
2
3
4
5
#include <sys/types.h>
#include <sys/socket.h>

int socket(int domain, int type, int protocol);
//返回:若成功则为非负描述符,若出错则为-1

如果想要使套接字称为连接的一个端点,就用如下硬编码的参数来调用socke函数:
clientfd = Socket(AF_INMET, SOCKET_STREAM, 0);
socket返回的clientfd描述符仅是部分打开的,还不能读写。如何完成打开套接字的工作,取决于我们是客户端还是服务器。

connect函数

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

1
2
3
4
#include <sys/socket.h>
int connect(int clientfd, const struct sockadrr *addr,
socklen_t addrlen);
//返回:若成功为0,出错为-1;

connect函数试图与套接字地址为addr的服务器建立一个因特网连接,其中addrlen是sizeof(sockaddr_in)。connect函数会阻塞,一直到成功连接或是发生错误。

bind函数

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

1
2
3
4
5
#include <sys/socket.h>

int bind(int sockefd, const struct sockaddr *addr,
socklen_t addrlen);
//返回:成功返回0, 错误返回-1。

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

listen函数

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

accept函数

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

1
2
3
4
#include <sys/socket.h>

int accept(int listenfd, struct sockaddr *addr, int *addrlen);
//若成功则返回非负连接描述符,若出错则为-1

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

-------------本文结束感谢您的阅读-------------
+ +