Linux系统第十三章学习笔记
前言
本章涵盖TCP/IP和网络编程。该章分为两部分。第一部分涵盖TCP/IP协议及其应用。这包括TCP/IP堆栈、IP地址、主机名、DNS、IP数据包和路由器。它描述了UDP和TCP协议、端口号以及TCP/IP网络中的数据流。解释了服务器-客户端计算模型和套接字编程接口。通过示例演示了使用UDP和TCP套接字的网络编程。第一个编程项目是实现一对TCP服务器-客户端,以在互联网上执行文件操作。它允许用户定义可靠传输文件内容的附加通信协议。
该章的第二部分涵盖Web和CGI编程。解释了HTTP编程模型、Web页面和Web浏览器。展示了如何配置Linux HTTPD服务器以支持用户的Web页面、PHP和CGI编程。解释了客户端和服务器端的动态Web页面。展示了如何通过PHP和CGI创建服务器端的动态Web页面。第二个编程项目是让读者在Linux HTTPD服务器上通过CGI编程实现服务器端的动态Web页面。
第十三章
13.1 网络编程介绍
如今,访问互联网已经成为日常生活的必需品。尽管大多数人可能只将互联网用作信息收集、在线购物和社交媒体等工具,但计算机科学学生必须对互联网技术有一些了解,以及具备一些网络编程技能。在本章中,我们将介绍TCP/IP网络和网络编程的基础知识。这包括TCP/IP协议、UDP和TCP协议、服务器-客户端计算、HTTP和Web页面,以及用于动态Web页面的PHP和CGI编程。
13.2 TCP/IP协议
TCP/IP(Comer 1988, 2001; RFC1180 1991)是互联网的骨干。TCP代表传输控制协议。IP代表互联网协议。目前有两个IP版本,称为IPv4和IPv6。IPv4使用32位地址,IPv6使用128位地址。这里的讨论基于IPv4,它仍然是IP的主导版本。TCP/IP组织成几个层,通常称为TCP/IP堆栈。图13.1显示了TCP/IP层、每个层中的代表性组件及其功能。
在顶层是使用TCP/IP的应用程序。诸如用于远程主机登录的ssh、用于交换电子邮件的邮件、用于Web页面的http等应用程序需要可靠的数据传输。这类应用通常在传输层使用TCP。另一方面,一些应用程序,例如用于查询其他主机的ping命令,不需要可靠性。这类应用程序可以在传输层使用UDP(RFC 768 1980; Comer 1988)以提高效率。传输层负责将应用程序数据作为数据包发送/接收到/从IP主机。在传输层及以上的进程和主机之间的数据传输仅是逻辑的。实际的数据传输发生在互联网(IP)和链路层,它们将数据包划分为数据帧,以在物理网络上进行传输。图13.2显示了TCP/IP网络中的数据流路径。
13.3 IP主机和IP地址
主机是支持TCP/IP协议的计算机或设备。每个主机都由一个称为IP地址的32位数字标识。为方便起见,32位IP地址通常用点分表示法表示,例如134.121.64.1
,其中每个字节之间用点分隔。主机还通过主机名(host name)来标识,例如dns1.eecs.wsu.edu
。在实践中,应用程序通常使用主机名而不是IP地址。主机名和IP地址在某种意义上是等效的,即给定一个,我们可以通过DNS(域名系统)(RFC 134 1987; RFC 1035 1987)服务器找到另一个,这些服务器将IP地址转换为主机名,反之亦然。
IP地址分为两部分:NetworkID字段和HostID字段。根据这种划分,IP地址被分为A到E类。例如,类B的IP地址被划分为一个16位的NetworkID,其中前两位是10,后面是一个16位的HostID字段。针对IP地址的数据包首先被发送到具有相同NetworkID的路由器。路由器将将数据包转发到该网络中的特定主机,通过HostID确定。每个主机都有一个本地主机名localhost
,具有默认IP地址127.0.0.1
。localhost
的链路层是一个环回虚拟设备,将每个数据包路由回同一台localhost
。这一特殊特性使我们能够在同一台计算机上运行TCP/IP应用程序,而无需实际连接到互联网。
13.4 IP协议
IP是在IP主机之间发送/接收数据包的协议。IP以最大努力方式运行。IP主机只是将数据包发送到接收主机,但它不保证数据包将被传递到目的地,也不保证顺序。这意味着IP不是可靠的协议。如果需要可靠性,必须在IP层以上实施。
13.5 IP数据包格式
IP数据包由IP头部、发送方和接收方IP地址,以及后跟的数据组成。每个IP数据包的最大大小为64 KB。IP头部包含有关数据包的更多信息,例如总长度、数据包是否使用TCP或UDP、生存时间(TTL)计数、用于错误检测的校验和等。图13.3显示了IP头部。
13.6 路由器
IP主机可能相距甚远。通常不可能直接从一个主机发送数据包到另一个主机。路由器是特殊的IP主机,它们接收并转发数据包。一个IP数据包可能经过许多路由器或跳跃,才可能到达目的地。图13.4显示了TCP/IP网络的拓扑结构。
13.7 UDP 用户数据报协议
每个IP数据包在IP头部都有一个8位的生存时间(TTL)计数,最大值为255。在每个路由器处,TTL减小1。如果TTL减小为0且数据包仍未到达目的地,则数据包将被简单丢弃。这防止了在IP网络中任何数据包无休止地循环。
13.7.1 UDP概述
UDP(RFC 768 1980; Comer 1988)在IP之上运行,用于发送/接收数据报。与IP一样,UDP不保证可靠性,但它快速而高效。它在不需要可靠性的情况下使用。例如,用户可以使用ping命令探测目标主机,如ping 主机名 或 ping IP地址
Ping是一个应用程序,它向目标主机发送一个带有时间戳的UDP数据包。在接收到ping数据包后,目标主机会将带有时间戳的UDP数据包回显给发送者,从而允许发送者计算并显示往返时间。如果目标主机不存在或关闭,当其TTL减小到0时,ping的UDP数据包将被路由器丢弃。在这种情况下,用户将注意到来自目标主机的任何响应的缺失。用户可以尝试再次ping它,或者得出目标主机已关闭的结论。在这种情况下,使用UDP是首选的,因为可靠性并非必要。
13.8 TCP 传输控制协议
TCP是一种用于发送/接收数据流的面向连接的协议。TCP也在IP之上运行,但它保证可靠的数据传输。一个常见的类比是,UDP类似于发送邮件的美国邮政服务(USPS),而TCP类似于电话连接。
13.9 端口号
在每个主机上,许多应用程序(进程)可以同时使用TCP/UDP。每个应用程序由三元组唯一标识:
Application = (HostIP, Protocol, PortNumber)
其中Protocol为TCP或UDP,PortNumber是分配给该应用程序的唯一无符号短整数。为了使用UDP或TCP,应用程序(进程)必须首先选择或获取一个PortNumber。前1024个端口号被保留。其他端口号可供通用使用。应用程序可以选择一个可用的端口号,也可以让操作系统内核分配一个端口号。图13.5显示了在传输层使用TCP的一些应用程序及其默认端口号。
13.10 网络和主机字节顺序
计算机可能使用大端序或小端序字节顺序。在互联网上,数据始终以网络字节顺序(大端序)存在。对于小端序计算机(例如Intel x86架构的个人电脑),提供了一组库函数,如htons()
、htonl()
、ntohs()
、ntohl()
,用于在主机字节顺序和网络字节顺序之间转换数据。例如,PC上的端口号1234是主机(小端序)字节顺序中的无符号短整数。在使用之前,它必须通过htons(1234)
转换为网络字节顺序。反之,从互联网接收到的端口号必须通过ntohs(port)
转换为主机字节顺序。
13.11 TCP/IP网络中的数据流
图13.6显示了TCP/IP网络中各层的数据格式。它还展示了不同层之间的数据流路径。
在图13.6中,应用层的数据传递到传输层,传输层添加TCP或UDP头来标识所使用的传输协议。组合数据传递到IP互联网层,该层添加包含IP地址以标识发送和接收主机的IP头。组合数据然后传递到网络链路层,该层将数据分成帧,为在物理网络上传输添加了发送和接收网络的地址。IP地址到网络地址的映射由地址解析协议(ARP)(ARP 1982)执行。在接收端,数据编码过程被反转。每个层都通过剥离头部、重新组装头部来解包接收到的数据,并将数据传递给上面的一层。在发送主机上,应用程序的原始数据最终传递到接收主机上相应应用程序。
13.12 网络编程
所有Unix/Linux系统都为网络编程提供TCP/IP支持。在这一部分,我们将解释网络编程的平台以及服务器-客户端计算模型。
13.12.1 网络编程平台
为了进行网络编程,读者必须能够访问支持网络编程的平台。有多种方式提供这样的平台。
-
在服务器机器上的用户帐户: 如今,几乎所有的教育机构都提供网络访问,通常以无线连接的形式提供给他们的教职员工和学生。机构的每个成员都应该能够登录到服务器机器以访问互联网。服务器机器是否允许一般的网络编程取决于本地网络管理的政策。在这里,我们描述了作者所在机构(华盛顿州立大学的EECS)的网络编程平台设置。作者维护了一台私有服务器
cs360.eecs.wsu.edu
该服务器运行Slackware Linux版本14.2,配备了完整的网络编程支持。服务器已在EECS of WSU的DNS服务器中注册。当服务器启动时,它使用DHCP(动态主机配置协议)从DHCP服务器(RFC 2131 1997)获取私有IP地址。虽然不是公共IP地址,但可以通过NAT(网络地址转换)在互联网上访问。然后,作者为CS360班级的学生创建用户帐户,供其登录使用。学生通常通过WSU无线网络将他们的笔记本电脑连接到互联网。一旦在互联网上,他们可以登录到cs360服务器。
-
独立的个人电脑或笔记本电脑: 即使读者没有访问服务器机器,仍然可以在独立计算机上进行网络编程,方法是使用计算机的本地主机。在这种情况下,读者可能需要下载并安装一些网络组件。例如,Ubuntu Linux用户可能需要安装和配置Apache服务器进行HTTP和CGI编程,这将在第13.17节中描述。
13.12.2 服务器-客户端计算模型
大多数网络编程任务都基于服务器-客户端计算模型。在服务器-客户端计算模型中,首先在服务器主机上运行服务器进程。然后在客户端主机上运行客户端。在UDP中,服务器等待来自客户端的数据报,处理数据报并生成响应给客户端。在TCP中,服务器等待客户端连接。客户端首先连接到服务器以建立客户端和服务器之间的虚拟电路。连接建立后,服务器和客户端可以交换连续的数据流。接下来,我们将展示如何使用UDP和TCP进行网络编程。
13.13 套接字编程
在网络编程中,TCP/IP的用户界面是通过一组C库函数和系统调用实现的,这被统称为套接字API(Rago 1993; Stevens et al. 2004)。为了使用套接字API,我们需要套接字地址结构,用于标识服务器和客户端。套接字地址结构在netdb.h
和sys/socket.h
中定义。
13.13.1 套接字地址
struct sockaddr_in {
sa_family_t sin_family; // AF_INET for TCP/IP
in_port_t sin_port; // port number
struct in_addr sin_addr; // IP address
};
struct in_addr { // internet address
uint32_t s_addr; // IP address in network byte order
};
在套接字地址结构中:
sin_family
总是设置为AF_INET
以用于TCP/IP网络。sin_port
包含网络字节顺序中的端口号。sin_addr
是以网络字节顺序表示的主机IP地址。
13.13.2 套接字API
-
创建套接字:
int socket(int domain, int type, int protocol);
示例:
int udp_sock = socket(AF_INET, SOCK_DGRAM, 0); // 创建用于发送/接收UDP数据报的套接字 int tcp_sock = socket(AF_INET, SOCK_STREAM, 0); // 创建用于发送/接收数据流的连接导向TCP套接字
新创建的套接字没有关联的地址。必须使用
bind()
系统调用将其与主机地址和端口号绑定,以标识接收主机或发送主机。 -
绑定套接字:
int bind(int sockfd, struct sockaddr *addr, socklen_t addrlen);
bind()
系统调用将由addr
指定的地址分配给由文件描述符sockfd
引用的套接字,addrlen
指定了addr
指向的地址结构的字节数。对于用于联系其他UDP服务器主机的UDP套接字,必须将其绑定到客户端的地址,以便服务器可以回复。对于用于接受客户端连接的TCP套接字,必须首先将其绑定到服务器主机地址。 -
UDP套接字:
- 使用
sendto()
/recvfrom()
发送/接收数据报。
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
sendto()
将buf
中的len
字节数据发送到由dest_addr
指定的目标主机,其中包含目标主机的IP和端口号。recvfrom()
从客户端主机接收数据。除了数据之外,它还使用客户端的IP和端口号填充src_addr
,允许服务器回复客户端。 - 使用
-
TCP套接字:
- 创建套接字并绑定到服务器地址后,TCP服务器使用
listen()
和accept()
来接受来自客户端的连接。
int listen(int sockfd, int backlog); int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
listen()
将由sockfd
引用的套接字标记为将用于接受传入连接的套接字。backlog
参数定义了挂起连接队列的最大长度。
accept()
系统调用与基于连接的套接字一起使用。它从监听套接字的挂起连接队列中提取第一个连接请求,创建一个新的连接套接字,并返回引用该套接字的新文件描述符,该套接字与客 - 创建套接字并绑定到服务器地址后,TCP服务器使用
户端主机连接。执行 accept()
系统调用时,TCP服务器将阻塞,直到客户端通过 connect()
进行了连接。
- 连接到服务器。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
connect()
系统调用将由文件描述符 sockfd
引用的套接字连接到由 addr
指定的地址,addrlen
参数指定了 addr
的大小。addr
中的地址格式由套接字 sockfd
的地址空间确定。
如果套接字 sockfd
的类型是 SOCK_DGRAM
,即UDP套接字,则 addr
是默认发送数据报的地址,也是接收数据报的唯一地址。这限制了UDP套接字与特定UDP主机通信,但在实践中很少使用。因此,对于UDP套接字,连接是可选的或不必要的。如果套接字的类型是 SOCK_STREAM
,即TCP套接字,connect()
调用尝试与由 addr
指定的地址绑定的套接字建立连接。
- 发送/接收数据:
- 建立连接后,TCP主机可以使用
send()
/write()
发送数据,使用recv()
/read()
接收数据。
它们之间的唯一区别在于ssize_t send(int sockfd, const void *buf, size_t len, int flags); ssize_t write(int sockfd, void *buf, size_t len); ssize_t recv(int sockfd, void *buf, size_t len, int flags); ssize_t read(int sockfd, void *buf, size_t len);
send()
和recv()
中的flags
参数,对于简单的情况可以将其设置为0。 - 建立连接后,TCP主机可以使用
13.14 UDP回显服务器-客户端程序
在本节中,我们展示了使用UDP的简单回显服务器/客户端程序。为了方便参考,该程序被标记为C13.1。以下表格列出了服务器和客户端的算法。
UDP服务器 | UDP客户端 |
---|---|
1. 创建UDP套接字 | 1. 创建UDP套接字 |
2. 设置addr = 服务器[IP,端口] | 2. 设置addr = 服务器[IP,端口] |
3. 将套接字绑定到addr | while(1){ |
while(1){ | 3. 请求用户输入一行 |
4. 从客户端recvfrom() | 4. 将行通过sendto()发送到服务器 |
5. 将回复通过sendto()发送到客户端 | 5. 从服务器recvfrom()接收回复 |
} | } |
为简单起见,我们假设服务器和客户端都在同一台计算机上运行。服务器在默认的localhost上运行,IP为127.0.0.1,它使用固定的端口号1234。这简化了程序代码。它还允许读者在同一台计算机的不同xterms上测试运行服务器和客户端程序。以下是使用UDP的服务器和客户端程序代码。
UDP Server
/* C13.1.a: UDP server.c file */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/ip.h>
#define BUFLEN 256 // 缓冲区的最大长度
#define PORT 1234 // 固定的服务器端口号
char line[BUFLEN];
struct sockaddr_in me, client;
int sock, rlen, clen = sizeof(client);
int main() {
printf("1. 创建UDP套接字\n");
sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
printf("2. 用服务器地址和端口号填充me\n");
memset((char *)&me, 0, sizeof(me));
me.sin_family = AF_INET;
me.sin_port = htons(PORT);
me.sin_addr.s_addr = htonl(INADDR_ANY); // 使用localhost
printf("3. 将套接字绑定到服务器IP和端口\n");
bind(sock, (struct sockaddr*)&me, sizeof(me));
printf("4. 等待数据报\n");
while(1) {
memset(line, 0, BUFLEN);
printf("UDP服务器:等待数据报\n");
// recvfrom() 获取客户端IP,sockaddr_in中的端口
rlen = recvfrom(sock, line, BUFLEN, 0, (struct sockaddr *)&client, &clen);
printf("从[主机:端口] = [%s:%d]接收到一个数据报\n",
inet_ntoa(client.sin_addr), ntohs(client.sin_port));
printf("rlen=%d: line=%s\n", rlen, line);
printf("发送回复\n");
sendto(sock, line, rlen, 0, (struct sockaddr*)&client, clen);
}
}
UDP Client
/***** C13.1.b: UDP client.c file *****/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/ip.h>
#define SERVER_HOST "127.0.0.1" // 默认服务器IP: localhost
#define SERVER_PORT 1234 // 固定的服务器端口号
#define BUFLEN 256 // 缓冲区的最大长度
char line[BUFLEN];
struct sockaddr_in server;
int sock, rlen, slen = sizeof(server);
int main() {
printf("1. 创建UDP套接字\n");
sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
printf("2. 填写服务器地址和端口号\n");
memset((char *)&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(SERVER_PORT);
inet_aton(SERVER_HOST, &server.sin_addr);
while(1) {
printf("输入一行:");
fgets(line, BUFLEN, stdin);
line[strlen(line) - 1] = 0;
printf("将行发送到服务器\n");
sendto(sock, line, strlen(line), 0, (struct sockaddr *)&server, slen);
memset(line, 0, BUFLEN);
printf("尝试从服务器接收一行\n");
rlen = recvfrom(sock, line, BUFLEN, 0, (struct sockaddr *)&server, &slen);
printf("rlen=%d: line=%s\n", rlen, line);
}
}
图13.7显示了运行UDP服务器-客户端程序C13.1的示例输出。
13.15 TCP回显服务器-客户端程序
本节介绍了使用TCP的简单回显服务器-客户端程序,程序标记为C13.2。为简单起见,我们假设服务器和客户端都在同一台计算机上运行,并且服务器端口号被硬编码为1234。以下图表显示了TCP服务器和客户端的算法和操作顺序。
TCP服务器 | TCP客户端 |
---|---|
1. 创建TCP套接字 | 1. 创建TCP套接字sock |
2. 填充server_addr = [IP,端口] | 2. 填充server_addr = [IP,端口] |
3. 将套接字绑定到server_addr | |
4. 通过listen()在套接字上监听 | |
5. int csock = accept() <== | = 3. 通过sock连接到服务器 |
------------------------------ | ------------------------------ |
4. while(gets()从csock读取行){ | 5. 使用sock写入行 |
写入回复到csock -- | ---> 从sock读取回复 |
} | 6. 退出 |
7. 关闭newsock; | |
8. 循环至5以接受新客户端 | |
------------------------------ | ------------------------------ |
以下是使用TCP的服务器和客户端程序代码。
TCP服务器
/******** C13.2.a: TCP server.c file ********/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netdb.h>
#define MAX 256
#define SERVER_HOST "localhost"
#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 1234
struct sockaddr_in server_addr, client_addr;
int mysock, csock; // 套接字描述符
int r, len, n; // 辅助变量
int server_init() {
printf("================== 服务器初始化 ======================\n");
// 通过socket()系统调用创建一个TCP套接字
printf("1:创建一个TCP流套接字\n");
mysock = socket(AF_INET, SOCK_STREAM, 0);
if (mysock < 0) {
printf("套接字调用失败\n");
exit(1);
}
printf("2:用主机IP和端口号填充server_addr\n");
// 初始化server_addr结构
server_addr.sin_family = AF_INET; // 用于TCP/IP
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 本地主机IP
server_addr.sin_port = htons(SERVER_PORT); // 端口号1234
printf("3:将套接字绑定到服务器地址\n");
r = bind(mysock, (struct sockaddr*)&server_addr, sizeof(server_addr));
if (r < 0) {
printf("绑定失败\n");
exit(3);
}
printf(" 主机名 = %s 端口 = %d\n", SERVER_HOST, SERVER_PORT);
printf("4:服务器正在监听....\n");
listen(mysock, 5); // 队列长度 = 5
printf("=================== 初始化完成 =======================\n");
}
int main() {
char line[MAX];
server_init();
while (1) { // 尝试接受客户端请求
printf("服务器:正在接受新连接....\n");
// 尝试接受一个客户端连接,作为描述符newsock
len = sizeof(client_addr);
csock = accept(mysock, (struct sockaddr *)&client_addr, &len);
if (csock < 0) {
printf("服务器:接受错误\n");
exit(1);
}
printf("服务器:接受了一个来自的客户端连接\n");
printf("-----------------------------------------------\n");
printf("客户端:IP=%s 端口=%d\n",
inet_ntoa(client_addr.sin_addr.s_addr),
ntohs(client_addr.sin_port));
printf("-----------------------------------------------\n");
// 处理循环:client_sock <== 数据 ==> client
while (1) {
n = read(csock, line, MAX);
if (n == 0) {
printf("服务器:客户端断开连接,服务器循环\n");
close(csock);
break;
}
// 显示行字符串
printf("服务器:读取 n=%d 字节; line=%
s\n", n, line);
// 将行回显给客户端
n = write(csock, line, MAX);
printf("服务器:写入 n=%d 字节; ECHO=%s\n", n, line);
printf("服务器:准备处理下一个请求\n");
}
}
}
TCP客户端
/******** C13.2.b: TCP client.c file ********/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netdb.h>
#define MAX 256
#define SERVER_HOST "localhost"
#define SERVER_PORT 1234
struct sockaddr_in server_addr;
int sock, r;
int client_init() {
printf("======= 客户端初始化 ==========\n");
printf("1:创建一个TCP套接字\n");
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
printf("套接字调用失败\n");
exit(1);
}
printf("2:用服务器的IP和端口号填充server_addr\n");
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 本地主机
server_addr.sin_port = htons(SERVER_PORT); // 服务器端口号
printf("3:连接到服务器....\n");
r = connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr));
if (r < 0) {
printf("连接失败\n");
exit(3);
}
printf("4:成功连接到\n");
printf("-------------------------------------------------------\n");
printf("服务器主机名=%s PORT=%d\n", SERVER_HOST, SERVER_PORT);
printf("-------------------------------------------------------\n");
printf("========= 初始化完成 ==========\n");
}
int main() {
int n;
char line[MAX], ans[MAX];
client_init();
printf("******** 处理循环 *********\n");
while (1) {
printf("输入一行:");
bzero(line, MAX); // 将line[]置零
fgets(line, MAX, stdin); // 从stdin获取一行
line[strlen(line) - 1] = 0; // 去除末尾的\n
if (line[0] == 0) // 如果行为空,则退出
exit(0);
// 将行发送到服务器
n = write(sock, line, MAX);
printf("客户端:写入 n=%d 字节; line=%s\n", n, line);
// 从套接字读取一行并显示
n = read(sock, ans, MAX);
printf("客户端:读取 n=%d 字节; 回显=%s\n", n, ans);
}
}
图13.8显示了运行TCP服务器-客户端程序C13.2的示例输出。
13.16主机名和IP地址
迄今为止,我们假设服务器和客户端在同一台计算机上运行,使用localhost或IP=127.0.0.1,并且服务器使用固定的端口号。如果读者打算在具有由操作系统内核分配的服务器端口号的不同主机上运行服务器和客户端,则需要知道服务器的主机名或IP地址及其端口号。如果计算机运行TCP/IP,其主机名通常记录在/etc/hosts文件中。库函数 gethostname(char *name, sizeof(name))
返回机器的主机名字符串在名字数组中,但可能不是它的点分表示法的完整官方名称,也不包含其IP地址。库函数 struct hostent *gethostbyname(void *addr, socklen_t len, int typo)
可用于获取机器的完整名称以及其IP地址。它返回<netdb.h>
中的hostent
结构的指针,如下:
struct hostent {
char *h_name; // 主机的官方名称
char **h_aliases; // 别名列表
int h_addrtype; // 主机地址类型
int h_length; // 地址的长度
char **h_addr_list; // 地址列表
}
#define h_addr h_addr_list[0] // 为了向后兼容
请注意,h_addr
被定义为 char *
,但它指向一个以网络字节顺序表示的4字节IP地址。可以通过以下方式访问 h_addr
的内容:
u32 NIP = *(u32 *)h_addr
是以网络字节顺序表示的主机IP地址。u32 HIP = ntohl(NIP)
是主机字节顺序中的NIP
。inet_ntoa(NIP)
将NIP
转换为DOT表示法中的字符串。
以下代码片段展示了如何使用 gethostbyname()
和 getsockname()
动态获取服务器IP地址和端口号。服务器必须发布其主机名或IP地址和端口号,以便客户端连接。
TCP服务器代码:
char myname[64];
struct sockaddr_in server_addr, sock_addr;
// 1. 获取主机名
gethostname(myname, 64);
// 2. 获取有关本地主机的信息,包括其IP地址
struct hostent *hp = gethostbyname(myname);
if (hp == 0) {
printf("unknown host %s\n", myname);
exit(1);
}
// 3. 初始化server_addr结构并让内核分配一个端口号
server_addr.sin_family = AF_INET; // for TCP/IP
server_addr.sin_addr.s_addr = *(long *)hp->h_addr;
server_addr.sin_port = 0; // 让内核分配端口号
// 4. 创建一个TCP套接字
int mysock = socket(AF_INET, SOCK_STREAM, 0);
// 5. 将套接字与server_addr绑定
bind(mysock, (struct sockaddr *)&server_addr, sizeof(server_addr));
// 6. 获取套接字地址以显示内核分配的端口号
getsockname(mysock, (struct sockaddr *)&name_addr, &length);
// 7. 显示服务器主机名和端口号
printf("hostname=%s IP=%s port=%d\n", hp->h_name,
inet_ntoa(*(long *)hp->h_addr), ntohs(name_addr.sin_port));
TCP客户端代码:
// 1. 通过名称获取服务器IP
struct sockaddr_in server_addr, sock_addr;
struct hostent *hp = gethostbyname(argv[1]);
SERVER_IP = *(long *)hp->h_addr;
SERVER_PORT = atoi(argv[2]);
// 2. 创建TCP套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
// 3. 填充server_addr结构与服务器IP和端口号
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = SERVER_IP;
server_addr.sin_port = htons(SERVER_PORT);
// 4. 连接到服务器
connect(sock, (struct sockaddr *)&server_addr, sizeof(server_addr));
13.17.1 项目规格
设计并实现一个TCP服务器和一个TCP客户端,以在互联网上执行文件操作。以下图表描述了服务器和客户端的算法。
服务器
- 将虚拟根目录设置为当前工作目录(CWD)。
- 广播服务器主机名和端口号。
- 接受来自客户端的连接。
- 从客户端获取命令行 = cmd pathname。
- 对pathname执行cmd。
- 将结果发送给客户端。
- 重复步骤4直到客户端断开连接。
- 重复步骤3以接受新的客户端连接。
客户端
- 连接到服务器,使用服务器主机名和端口号。
- 提示用户输入命令行 = cmd pathname。
- 将命令行发送到服务器。
- 从服务器接收结果。
- 重复步骤2直到命令行为NULL或退出命令。
在命令行中,取决于命令,pathname 可能是文件或目录。有效命令包括:
mkdir
: 使用路径名创建目录。rmdir
: 删除名为路径名的目录。rm
: 删除名为路径名的文件。cd
: 将当前工作目录(CWD)更改为路径名。pwd
: 显示CWD的绝对路径名。ls
: 以与Linux的ls –l
相同的格式列出CWD或路径名。get
: 从服务器下载路径名文件。put
: 将路径名文件上传到服务器。
13.17.2 提示与帮助
-
在Internet主机上运行时,服务器必须发布其主机名或IP地址和端口号,以允许客户端连接。出于安全原因,服务器应将虚拟根目录设置为服务器进程的CWD,以防止客户端访问虚拟根目录上方的文件。
-
大多数命令都很容易实现。例如,前5个命令中的每个只需要一个Linux系统调用,如下所示。
mkdir pathname
:int r = mkdir(pathname, 0755);
// 默认权限rmdir pathname
:int r = rmdir(pathname);
rm pathname
:int r = unlink(pathname);
cd pathname
:int r = chdir(pathname);
pwd
:char buf[SIZE]; char *getcwd(buf, SIZE);
对于ls命令,读者可以参考第8章的第8.6.7节中的ls.c程序。对于get filename命令,它基本上与文件复制程序拆分成两部分相同。服务器打开文件以进行读取,读取文件内容并将其发送到客户端。客户端打开一个文件进行写入,从服务器接收数据,并将接收到的数据写入文件。对于put filename命令,只需颠倒服务器和客户端的角色。
-
在使用TCP时,数据是连续的流。为确保客户端和服务器可以发送/接收命令和简单回复,最好让双方写入/读取固定大小的行,例如256字节。
-
读者必须设计用户级协议以确保在服务器和客户端之间正确传输数据。以下描述了需要用户级数据传输协议的命令。
-
对于ls命令,服务器有两个选项。在第一种选择中,对于目录中的每个条目,服务器生成以下形式的一行:
-rw-r--r-- link gid uid size date name
并将该行保存到临时文件中。在累积所有行之后,服务器将整个临时文件发送给客户端。在这种情况下,服务器必须能够告诉客户端文件从哪里开始和结束。由于ls命令只生成文本行,因此读者可以使用特殊的ASCII字符作为文件的开始和结束标记。
在第二个选项中,服务器可以生成一行并在生成和发送下一行之前立即将其发送到客户端。在这种情况下,服务器必须能够告诉客户端何时开始发送行,以及何时不再有更多的行。
-
对于传输文件内容的get/put命令,无法使用特殊的ASCII字符作为文件的开始和结束标记。这是因为二进制文件可能包含任何ASCII代码。解决此问题的标准方法是通过比特填充[SDLC,IBM,1979]。在此方案中,发送数据时,发送者在每个5个或更多连续的1位序列之后插入一个额外的0位,以便传输的数据中永远不包含任何6个或更多连续的1。在接收端,接收器在每个5个连续的1之后去掉额外的0位。这允许双方都使用特殊的标志位模式01111110作为文件的开始和结束标记。在没有硬件帮助的情况下,比特填充效率低下。读者必须考虑其他同步服务器和客户端的方法。
提示:通过文件大小。
-
-
该项目适合2人团队合作。在开发过程中,团队成员可以讨论如何设计用户级协议,并在它们之间分配实施工作。在测试期间,一名成员可以在Internet主机上运行服务器,而另一名成员可以在不同的主机上运行客户端。
图13.9显示了运行项目程序的示例输出。
如图所示,服务器在主机 wang.eecs.wsu.edu
上运行,端口号为 56809
。客户端在不同的主机上运行,IP 为 172.19.160.221
,端口号为 48774
。除了指定的命令之外,客户端还实现了本地文件操作命令,由客户端直接执行。
13.17.3 多线程TCP服务器
在TCP服务器客户端编程项目中,服务器一次只能为一个客户端提供服务。在多线程服务器中,它可以接受来自多个客户端的连接并同时为它们提供服务,或者并发服务。这可以通过fork-exec创建新的服务器进程或通过同一服务器进程中的多个线程来完成。我们将这个扩展作为另一个可能的编程项目留下。
13.18 Web 和 CGI 编程
万维网(WWW)或Web是利用超文本传输协议(HTTP)(RFC 2616 1999)在Internet上使用的资源和用户的组合,用于信息交换。自90年代初首次亮相以来,伴随着互联网不断扩展的能力,Web已经成为全球人们日常生活中不可或缺的一部分。因此,计算机科学专业的学生有必要对这项技术有一些了解。在本节中,我们将介绍HTTP和Web编程的基础知识。总的来说,Web编程包括Web开发中涉及的编写、标记和编码,包括Web内容、Web客户端和服务器脚本以及网络安全。在较狭义的意义上,Web编程是指创建和维护Web页面。在Web编程中最常用的语言有HTML、XHTML、JavaScript、Perl 5和PHP。
13.18.1 HTTP编程模型
HTTP是基于Internet的应用程序的服务器-客户端协议。它运行在TCP之上,因为它需要可靠的文件传输。图13.10显示了HTTP编程模型。
HTTP编程模型
在HTTP编程模型中,HTTP服务器在Web服务器主机上运行。它等待来自HTTP客户端的请求,通常是一个Web浏览器。在HTTP客户端端,用户输入一个URL(统一资源定位符)的形式
http://hostname[/filename]
以向HTTP服务器发送文件请求。在URL中,http标识HTTP协议,hostname是HTTP服务器的主机名,filename是请求的文件。如果未指定filename,则默认文件为index.html。客户端首先连接到服务器以发送请求。收到请求后,服务器将请求的文件发送回客户端。请求的文件通常是用HTML语言编写的Web页面文件,供浏览器解释和显示,但也可能是其他格式的文件,如视频、音频或二进制文件。
在HTTP中,客户端可以发出URL以向不同的HTTP服务器发送请求。对于客户端来说,与特定服务器保持永久连接是不必要的,也是不可取的。客户端仅连接到服务器以发送请求,然后关闭连接。同样,服务器仅连接到客户端以发送回复,然后再次关闭连接。每个请求或回复都需要一个单独的连接。这意味着HTTP是一个无状态协议,因为在连续的请求或回复之间没有维护任何信息。当然,这将导致很多开销和低效。为了弥补这个缺乏状态信息的问题,HTTP服务器和客户端可以使用Cookies,这是嵌入请求和回复中的小数据片段,以在它们之间提供和维护一些状态信息。
13.18.2 Web页面
Web页面是用HTML标记语言编写的文件。一个Web文件通过一系列HTML元素为Web浏览器指定Web页面的布局,以便解释和显示。流行的Web浏览器包括Internet Explorer、Firefox、Google Chrome等。创建一个Web页面相当于使用HTML元素作为构建块创建一个文本文件。这更像是一种文书工作而不是编程。因此,我们不讨论如何创建Web页面。相反,我们将仅使用一个示例HTML文件来说明Web页面的本质。以下是一个简单的HTML文件示例:
<html>
<body>
<h1>H1 heading: A Simple Web Page</h1>
<P>This is a paragraph of text</P>
<!---- this is a comment line ---->
<P><img src="firefox.jpg" width=16></P>
<a href="http://www.eecs.wsu.edu/~cs360">link to cs360 web page</a>
<P>
<font color="red">red</font>
<font color="blue">blue</font>
<font color="green">green</font>
</P>
<!--- a table ---->
<table>
<tr>
<th>name</th>
<th>ID</th>
</tr>
<tr>
<th>kwang</th>
<th>12345</th>
</tr>
</table>
<!---- a FORM ---->
<FORM>
Enter command: <INPUT NAME="command"><P>
Submit command: <INPUT TYPE="submit" VALUE="Click to Submit">
</FORM>
</body>
</html>
HTML文件内容解释:
-
一个HTML文件包含HTML元素。每个HTML元素由一对匹配的开放和关闭标签指定。
<tag>contents</tag>
实际上,一个HTML文件本身可以被看作是由一对匹配的
<html>
标签指定的HTML元素。<html>HTML file</html>
-
行1到行26指定了一个HTML文件。HTML文件包括一个由一对匹配的
<body>
标签指定的body。<body>body of HTML file</body>
行2到行25指定了HTML文件的body。
-
HTML文件可以使用标签
<H1>
到<H7>
来显示不同字体大小的标题。 -
行3指定了一个
<H1>
标题。 -
每一对匹配的
<P>
标签指定一个段落,将显示在新行上。 -
行4指定了一个文本段落。
-
行5指定了一个注释行,浏览器将
忽略该注释。
-
行6指定了一个图像文件,将以每行宽度像素显示。
-
行7指定了一个链接元素
<a HREF=”link_URL”>link</a>
其中属性HREF指定一个link_URL和一个描述链接的文本字符串。浏览器通常以深蓝色显示链接文本。如果用户单击链接,它将将请求定向到由link_URL标识的Web服务器。这可能是Web页面的最强大功能。它允许用户通过跟随链接在Web中的任何地方导航。
-
行8到行10使用
<font>
元素以不同的颜色显示文本。<font>
元素还可以指定不同字体大小和样式的文本。 -
行11到行20指定了一个表格,其中
<tr>
是每行,<th>
是每行中的列。 -
行21到行24指定了一个用于收集用户输入并将其提交给Web服务器进行处理的表单。我们将在下一节关于CGI编程的部分中更详细地解释和演示HTML表单。
图13.11显示了上述HTML文件的Web页面。
13.18.3 托管Web页面
现在我们有了一个HTML文件。它必须放在一个Web服务器上。当Web客户端通过URL请求HTML文件时,Web服务器必须能够找到文件并将其发送回客户端以供显示。有几种托管Web页面的方法。
-
与商业Web托管服务提供商签约,支付月费。 对于大多数普通用户来说,这可能根本不是一个选择。
-
在机构或部门服务器上使用用户帐户。 如果读者在运行Linux的服务器机器上有用户帐户,可以通过以下步骤在用户的主目录中创建一个私有网站:
- 登录到服务器机器上的用户帐户。
- 在用户的主目录中,以权限0755创建一个public_html目录。
- 在public_html目录中,创建一个index.html文件和其他HTML文件。
例如,从互联网上的Web浏览器中输入URL
http://cs360.eecs.wsu.edu/~kcw
将访问作者在服务器机器cs360.eecs.wsu.edu上的网站。
-
独立的个人计算机或笔记本电脑。 这里描述的步骤适用于运行标准Linux的独立PC或笔记本电脑,但也适用于其他Unix平台。由于某种原因,Ubuntu Linux选择以与Linux标准设置不同的方式进行操作。Ubuntu用户可以查阅Official Ubuntu Documentation中的HTTPD-Apache2 Web Server页面获取详细信息。
13.18.4 为Web页面配置HTTPD
-
下载并安装Apache Web服务器。 大多数Linux发行版(例如Slackware Linux 14.2)都预装了Apache Web服务器,称为HTTPD。
-
输入
ps –x | grep httpd
查看httpd是否正在运行。 如果没有运行,请输入以下命令:sudo chmod +x /etc/rc.d/rc.httpd
以使rc.httpd文件可执行。这将在下一次启动时启动httpd。或者,也可以通过输入以下命令手动启动httpd:
sudo /usr/sbin/httpd –kstart
-
配置httpd.conf文件: HTTPD服务器的操作受
/etc/httpd/
目录中的httpd.conf文件的管理。为了允许个别用户的网站,请按照以下方式编辑httpd.conf文件。- 如果这些行被注释了,请取消注释这些行:
Loadmodule dir_module MODULE_PATH Include /etc/httpd/extra/httpd-userdir.conf
- 在第一个Directory块中,将以下行:
更改为:Require all denied
Require all granted
- 所有用户主目录都在/home目录中。更改以下行:
为:DocumentRoot /srv/httpd/htdocs
DocumentRoot /home
- 所有HTML文件的默认目录是htdocs。更改以下行:
为:<Directory /srv/httpd/htdocs>
<Directory /home>
编辑完httpd.conf文件后,重新启动httpd服务器或输入以下命令:
ps –x | grep httpd # 查看httpd PID sudo kill –s 1 httpdPID
kill命令向httpd发送1号信号,使其读取更新的httpd.conf文件,而无需重新启动httpd服务器。
- 如果这些行被注释了,请取消注释这些行:
-
通过
adduser user_name
创建用户帐户。 通过以下命令登录到用户帐户:ssh user_name@localhost
如前所述,创建public_html目录和HTML文件。
然后打开Web浏览器并输入
http://localhost/~user_name
以访问用户的Web页面。
13.18.5 动态Web页面
用标准HTML编写的Web页面都是静态的。当从服务器获取并由浏览器显示Web页面内容时,Web页面的内容不会更改。要显示具有不同内容的Web页面,必须再次从服务器获取不同的Web页面文件。动态Web页面是其内容可以变化的Web页面。有两种类型的动态Web页面,分别称为客户端动态Web页面和服务器端动态Web页面。客户端动态Web页面文件包含使用JavaScript编写的代码,由客户端机器上的JavaScript解释器执行。它可以响应用户输入、时间事件等,以在本地修改Web页面,而无需与服务器进行任何交互。服务器端动态Web页面在与URL请求中的用户输入相一致的情况下动态生成。服务器端动态Web页面的核心在于服务器能够在HTML文件中执行PHP代码或使用CGI程序生成由用户输入的HTML文件。
13.18.6 PHP
PHP(Hypertext Preprocessor)(PHP 2017)是用于创建服务器端动态Web页面的脚本语言。PHP文件的标识符是.php
后缀。它们本质上是包含Web服务器要执行的PHP代码的HTML文件。当Web客户端请求PHP文件时,Web服务器将首先处理PHP语句以生成HTML文件,然后将其发送到请求的客户端。所有运行Apache HTTPD服务器的Linux系统都支持PHP,但可能需要启用。要启用PHP,只需要对httpd.conf文件进行一些修改,如下所示。
-
DirectoryIndex index.php
:# 默认Web页面是index.php DirectoryIndex index.php
-
AddType application/x-httpd-php .php
:# 添加.php扩展类型 AddType application/x-httpd-php .php
-
Include /etc/httpd/mod_php.conf
:# 加载php5模
块
Include /etc/httpd/mod_php.conf
```
启用PHP后,重新启动httpd服务器,这将将PHP模块加载到Linux内核中。当Web客户端请求.php
文件时,httpd服务器将fork一个子进程来执行.php
文件中的PHP语句。由于子进程在其映像中加载了PHP模块,因此它可以快速高效地执行PHP代码。此外,httpd服务器还可以配置为以CGI方式执行PHP,但这样会更慢,因为它必须使用fork-exec调用PHP解释器。为了提高效率,我们假设.php
文件由PHP模块处理。接下来,我们将通过示例展示基本的PHP编程。
PHP在HTML文件中的语句
在.php
文件中,PHP语句包含在一对PHP标记中:
<?php
// PHP语句
?>
下面是一个简单的PHP文件p1.php
的示例:
<html>
<body>
<?php
echo "hello world<br>"; // hello world<br>
print "see you later<br>"; // see you later<br>
?>
</body>
</html>
与C程序类似,每个PHP语句都必须以分号结束。可以使用成对的/*
和*/
注释块,或使用//
,#
进行单行注释。对于输出,PHP可以使用echo
或print
。在echo
或print
语句中,多个项目必须用点(字符串连接)运算符分隔,而不是用空格,如:
echo "hello world<br>" . "see you later<br>";
当Web客户端请求p1.php
文件时,httpd服务器的PHP预处理器将首先执行PHP语句以生成HTML行(显示在PHP行的右侧),然后将生成的HTML文件发送给客户端。
PHP变量
在PHP中,变量以$
符号开头,后跟变量名。PHP变量的值可以是字符串、整数或浮点数。与C不同,PHP是一种弱类型语言。用户不需要定义带有类型的变量。与C类似,PHP允许进行类型转换以更改变量类型。在大多数情况下,PHP还可以自动将变量转换为不同的类型。
<?php
$PID = getmypid(); // 返回整数
echo "pid = $PID <br>"; // pid = PHP进程PID
$STR = "hello world!"; // 一个字符串
$A = 123; $B = "456"; // 整数123,字符串"456"
$C = $A + $B; // PHP进行的类型转换
echo "$STR Sum=$C<br>"; // hello world! Sum=579<br>
?>
与C或shell脚本中的变量一样,PHP变量可以是局部的、全局的或静态的。
PHP运算符
在PHP中,可以使用各种运算符对变量和值进行操作:
- 算术运算符
- 赋值运算符
- 比较运算符
- 增量/减量运算符
- 逻辑运算符
- 字符串运算符
- 数组运算符
大多数PHP运算符与C中的运算符相似。PHP中的特殊字符串和数组运算符包括:
字符串操作
大多数字符串操作(例如strlen()
,strcmp()
)与C中的相同。PHP还支持许多其他字符串操作,通常在稍微不同的语法形式中。例如,PHP使用点运算符而不是strcat()
进行字符串连接,如"string1" . "string2"
。
PHP数组
PHP数组由array()
关键字定义。PHP支持索引数组和多维数组。可以通过数组索引步进索引数组。
<?php
$name = array('name0', 'name1', 'name2', 'name3');
$value = array(1, 2, 3, 4); // 值数组
$n = count($name); // 数组元素的数量
for ($i = 0; $i < $n; $i++) { // 按索引打印数组
echo $name[$i];
echo " = ";
echo $value[$i];
}
?>
此外,PHP数组还可以通过运算符(例如并集(+
)和比较)作为集合进行操作,或作为列表进行操作,可以按不同的顺序进行排序等。
关联数组
关联数组由名称-值对组成。
$A = array('name' => 1, 'name1' => 2, 'name2' => 3, 'name' => 4);
关联数组允许按名称而不是按索引访问元素值。
echo "value of name1 = " . $A['name1'];
PHP条件语句
PHP支持通过if
,if-else
,if-elseif-else
和switch-case
语句进行条件和测试条件。这与C中的语句完全相同,但语法略有不同。
<?php
if (123 < 456) { // 测试条件
echo "true<br>"; // 在匹配的大括号中
} else {
echo "not true<br>"; // 在匹配的大括号中
}
?>
PHP循环语句
PHP支持while
,do-while
,for
循环语句,这与C中的相同。foreach
语句可用于在没有显式索引变量的情况下遍历数组。
<?php
$A = array(1, 2, 3, 4);
for ($i = 0; $i < 4; $i++) { // 使用一个索引变量
echo "A[$i] = $A[$i]<br>";
}
foreach ($A as $value) { // 遍历数组元素
echo "$value<br>";
}
?>
PHP函数
在PHP中,使用function
关键字定义函数。它们的格式和用法与C中的函数相似。
<?php
function nameValue($name, $
value) {
echo "$name . “ has value ” . $value <br>";
}
nameValue("abc", 123); // 用2个参数调用函数
nameValue("xyz", 456);
?>
PHP日期和时间函数
PHP具有许多内置函数,例如date()
和time()
。
<?php
echo date("y-m-d"); // 以年-月-日格式显示时间
echo date("h:i:sa); // 以hh:mm:ss格式显示时间
?>
PHP中的文件操作
PHP的一个巨大优势是其对文件操作的集成支持。PHP中的文件操作包括系统调用的功能,例如mkdir()
,link()
,unlink()
,stat()
等,以及C中的标准库I/O函数,例如fopen()
,fread()
,fwrite()
和fclose()
等。这些函数的语法可能与C中的I/O库函数不同。大多数函数不需要特定的数据缓冲区,因为它们要么接受字符串参数,要么直接返回字符串。通常,对于写操作,Apache进程必须具有对用户目录的写权限。读者可以查阅PHP文件操作手册以获取详细信息。以下是如何显示文件内容和通过fopen()
,fread()
和fwrite()
复制文件的PHP代码段。
<?php
readfile("filename"); // 与cat filename相同
$fp = fopen("src.txt", "r"); // 为READ打开文件
$gp = fopen("dest.txt", "w"); // 为WRITE打开文件
while (!feof($fp)) { // feof()与C中相同
$s = fread($fp, 1024); // 从$fp读取nbytes
fwrite($gp, $s, strlen($s)); // 将字符串写入$gp
}
?>
PHP中的表单
在PHP中,表单和表单提交与HTML中的相同。表单处理是通过包含PHP代码的PHP文件进行的,该文件由PHP预处理器执行。PHP代码可以从提交的表单中获取输入,并以通常的方式处理它们。我们通过一个示例来说明PHP中的表单处理。
表单.php文件
此.php
文件显示一个表单,收集用户输入,并通过METHOD="post"
和ACTION="action.php"
将表单提交到httpd服务器。图13.12显示了form.php
文件的Web页面。当用户单击“Submit”时,它会将表单输入发送到HPPTD服务器进行处理。
<!------- form.php 文件 -------->
<html><body>
<H1>Submit a Form</H1>
<form METHOD="post" ACTION ="action.php" >
command: <input type="text" name="command"><br>
filename: <input type="text" name="filename"><br>
parameter:<input type="text" name="parameter"><br>
<input type="submit">
</form>
</body></html>
action.php
文件
action.php
文件包含处理用户输入的PHP代码。表单输入通过关键字从全局_POST
关联数组中提取。为简单起见,我们只是回显用户提交的输入名称-值对。图13.13显示了action.php
的返回Web页面。正如图中所示,它是由服务器端具有PID=30256的Apache进程执行的。
<!------- action.php 文件 -------->
<html><body>
<?php
echo "process PID = " . getmypid() . "<br>";
echo "user_name = " . get_current_user() . "<br>";
$command = $_POST["command"];
$filename = $_POST["filename"];
$parameter= $_POST["parameter"];
echo "you submitted the following name-value pairs<br>";
echo "command = " . $command . "<br>";
echo "filename = " . $filename . "<br>";
echo "parameter= " . $parameter . " <br>";
?>
</body></html>
PHP 概述
PHP是一种多功能的脚本语言,用于在互联网上开发应用程序。从技术角度来看,PHP可能没有什么新的东西,但它代表了几十年Web编程努力的演进。PHP不是一种单一的语言,而是许多其他语言的集成。它包括许多早期脚本语言的特性,如sh和Perl。它包含了C语言的大多数标准特性和函数,并且还在C语言的标准I/O库中提供文件操作。在实践中,PHP通常用作网站的前端,与后端的数据库引擎进行交互,通过动态Web页面在线存储和检索数据。第14章将涵盖PHP与MySQL数据库的接口。
13.18.7CGI 编程
CGI代表通用网关接口(RFC 3875 2004)。这是一种协议,允许Web服务器根据用户输入动态执行程序生成Web页面。使用CGI,Web服务器无需维护数百万个静态Web页面文件来满足客户端请求。相反,它通过动态生成Web页面来满足客户端请求。图13.14显示了CGI编程模型。
在CGI编程模型中,客户端发送一个请求,通常是一个包含输入和要由服务器执行的CGI程序名称的HTML表单。收到请求后,httpd服务器分叉出一个子进程来执行CGI程序。CGI程序可以使用用户输入查询数据库系统,如MySQL,根据用户输入生成HTML文件。当子进程完成时,httpd服务器将生成的HTML文件发送回客户端。CGI程序可以用任何编程语言编写,比如C,sh脚本和Perl。
13.18.8配置HTTPD以支持CGI
在HTTPD中,CGI程序的默认目录是/srv/httpd/cgi-bin
。这允许网络管理员控制和监视哪些用户被允许执行CGI程序。出于安全原因,在许多机构中,通常禁用用户级CGI程序。为了允许用户级CGI编程,必须配置httpd服务器以启用用户级CGI。编辑/etc/httpd/httpd.conf
文件,并更改CGI目录的设置如下:
<Directory "/home/*/public_html/cgi-bin">
Options +ExecCGI
AddHandler cgi-script .cgi .sh .bin .pl
Order allow,deny
Allow from all
</Directory>
修改后的CGI目录块将CGI目录设置为用户主目录中的public_html/cgi-bin/
。cgi-script
设置指定带有后缀.cgi
、.sh
、.bin
和.pl
(用于Perl脚本)的文件作为可执行的CGI程序。
13.19 CGI 编程项目:通过CGI创建动态网页
此编程项目旨在让读者练习CGI编程。它将远程文件操作、CGI编程和服务器端动态Web页面结合到一个单一的包中。项目的组织如下:
-
用户网站:在
cs360.eecs.wsu.edu
服务器上,每个用户都有一个用于登录的帐户。用户的主目录中有一个public_html
目录,其中包含一个index.html
文件,可以通过Internet上的URL访问:
http://cs360.eecs.wsu.edu/~username
下面显示了用户
kcw
的index.html
文件:<!---------- index.html file -----------> <html> <body bgcolor="#00FFFF" <H1>Welcome to KCW's Web Page</H1><P> <img src="kcw.jpg" width=100><P> <FORM METHOD="POST" ACTION=\ "http://cs360.eecs.wsu.edu/~kcw/cgi-bin/mycgi.bin"> Enter command: <INPUT NAME="command"> (mkdir|rmdir|rm|cat|cp|ls)<P> Enter filename1: <INPUT NAME="filename1"> <P> Enter filename2: <INPUT NAME="filename2"> <P> Submit command: <INPUT TYPE="submit" VALUE="Click to Submit"> <P> </FORM> </body> </html>
图13.15显示了上述
index.html
文件对应的Web页面。 -
HTML表单:
index.html
文件包含一个HTML表单。<FORM METHOD="POST" ACTION=\ "http://cs360.eecs.wsu.edu/~kcw/cgi-bin/mycgi.bin"> Enter command: <INPUT NAME="command"> (mkdir|rmdir|rm|cat|cp|ls)<P> Enter filename1: <INPUT NAME="filename1"> <P> Enter filename2: <INPUT NAME="filename2"> <P> Submit command: <INPUT TYPE="submit" VALUE="Click to Submit"> <P> </FORM>
在HTML表单中,
METHOD
指定如何提交表单输入,ACTION
指定要执行的Web服务器和CGI程序。有两种表单提交方法。在GET
方法中,用户输入包含在提交的URL中,使它们直接可见,输入数据的数量也受到限制。出于这些原因,很少使用GET
方法。在POST
方法中,用户输入被(URL)编码并通过数据流传输,这更安全,输入数据的数量也是无限的。因此,大多数HTML表单使用POST
方法。HTML表单允许用户在提示框中输入内容。当用户单击“提交”按钮时,输入将被发送到Web服务器进行处理。在示例HTML文件中,表单输入将发送到cs360.eecs.wsu.edu
上的HTTP服务器,该服务器将执行用户cgi-bin
目录中的mycgi.bin
CGI程序。 -
CGI目录和CGI程序:HTTPD服务器配置为允许用户级CGI。下图显示了用户级CGI的设置。
/home/username: public_html | ---- index.html | ---- cgi-bin | ---- mycgi.c | ---- util.o | ---- mycgi.bin, sample.bin
在
cgi-bin
目录中,mycgi.c
是一个C程序,它获取并显示提交的HTML表单中的用户输入。它会回显用户输入并生成一个包含FORM的HTML文件,将其发送回Web客户端以显示。下面显示了mycgi.c
程序代码。/************** mycgi.c file *************/ #include <stdio.h> #include <stdlib.h> #include <string.h> #define MAX 1000 typedef struct{ char *name; char *value; }ENTRY; ENTRY entry[MAX]; extern int getinputs(); // in util.o int main(int argc, char *argv[]) { int i, n; char cwd[128]; n = getinputs(); // get user inputs name=value into entry[ ] getcwd(cwd, 128); // get CWD pathname // generate a HTML file containing a HTML FORM printf("Content-type: text/html\n\n"); // NOTE: 2 new line chars printf("<html>"); printf("<body bgcolor=\"#FFFF00\"); // background color=YELLOW printf("<p>pid=%d uid=%d cwd=%s\n", getpid(), getuid(), cwd); printf("<H2>Echo Your Inputs</H2>"); printf("You submitted the following name/value pairs:<p>"); for(i=0; i<=n; i++) printf("%s = %s<P>", entry[i].name, entry[i].value); printf("<p>"); // create a FORM webpage for user to submit again printf("---------- Send Back a Form Again ------------<P>"); printf("<FORM METHOD=\"POST\" ACTION=\"http://cs360.eecs.wsu.edu/~kcw/cgi-bin/mycgi.bin\">"); printf("<font color=\"RED\">"); printf("Enter command : <INPUT NAME=\"command\"> <P>"); printf("Enter filename1: <INPUT NAME=\"filename1\"> <P>"); printf("Enter filename2: <INPUT NAME=\"filename2\"> <P>"); printf("Submit command: <INPUT TYPE=\"submit\" VALUE=\"Click to \ Submit\"> <P>"); printf("</form>"); printf("</font>"); printf("----------------------------------------------<p>"); printf("</body>"); printf("</html>"); }
图13.16显示了上述CGI程序生成的Web页面。
CGI程序
当HTTPD服务器接收到CGI请求时,它分叉一个带有UID 80的子进程来执行CGI程序。表单提交方法、输入编码和输入数据长度在进程的环境变量REQUEST_METHOD
、CONTENT_TYPE
和CONTENT_LENGTH
中,而输入数据则在stdin中。输入数据通常是URL编码的。将输入解码为名称-值对是直截了当但相当繁琐的。因此,提供了一个预编译的util.o
文件。它包含一个函数
int getinputs()
该函数将用户输入解码为名称-值字符串对。通过以下Linux命令生成CGI程序:
gcc -o mycgi.bin mycgi.c util.o
动态网页
在获取用户输入后,CGI程序可以处理用户输入以生成输出数据。在示例程序中,它只是回显用户输入。然后,通过将HTML语句作为行写入stdout,生成一个HTML文件。为了将这些行标识为HTML文件,第一行必须是
printf("Content-type: text/html\n\n");
带有2个换行字符。其余的行可以是任何HTML语句。在示例程序中,它生成一个与提交的表单相同的FORM,用于在下一次提交中获取新的用户输入。
SETUID程序
通常,CGI程序只使用用户输入从服务器端的数据库中读取以生成HTML文件。出于安全原因,它可能不允许CGI程序修改服务器端的数据库。在编程项目中,我们将允许用户请求执行文件操作,例如mkdir、rmdir、cp文件等,这需要写入用户目录的权限。由于CGI进程具有UID=80,因此它不应能够写入用户的目录。有两种选项允许CGI进程写入用户的目录。在第一种选项中,用户可以将cgi-bin
目录权限设置为0777,但这是不希望的,因为这将允许任何人都能够在其目录中写入。第二种选项是通过以下方式将CGI程序设置为SETUID程序:
chmod u+s mycgi.bin
当进程执行SETUID程序时,它会临时假定程序所有者的UID,从而允许它写入用户的目录。
用户请求文件操作
由于项目的目标是CGI编程,我们仅假设以下简单的文件操作作为用户请求:
ls [directory]
:以Linux的ls –l
格式列出目录mkdir dirname permission
:创建一个目录rmdir dirname
:删除目录unlink filename
:删除文件cat filename
:显示文件内容cp file1 file2
:将file1复制到file2
示例解决方案
在cgi-bin
目录中,sample.bin
是该项目的示例解决方案。读者可以将index.html
文件中的mycgi.bin
替换为sample.bin
,以测试用户请求并观察结果。