第十三章 TCP/IP和网络编程
一、梗概
本章论述了 TCP/IP和网络编程,分为两个部分。第一部分论述了 TCP/IP协议及其应 用,具体包括TCP/IP栈、IP地址、主机名、DNS、IP数据包和路由器;介绍了 TCP/IP网 络中的UDP和TCP协议、端口号和数据流;阐述了服务器-客户机计算模型和套接字编程 接口;通过使用UDP和TCP套接字的示例演示了网络编程。第一个编程项目可实现一对通 过互联网执行文件操作的TCP服务器-客户机,可让用户定义其他通信协议来可靠地传输 文件内容。本章的第二部分介绍了 Web和CGI编程,解释了 HTTP编程模型、Web页面和Web浏 览器;展示了如何配置Linux HTTPD服务器来支持用户Web页面、PHP和CG1编程;阐释了客户机和服务器端动态Web页面;演示了如何使用PHP和CGI创建服务器端动态Web 页面。第二个编程项目可让读者在Linux HTTPD服务器上通过CGI编程实现服务器端动态Web页面。
二、知识点归纳
1、计算机网络知识
-
TCP/IP协议包括ICMP、IP、telnet、udp等协议,是利用IP进行通信时所必须用到的协议群的统称。
-
IP主机和IP地址:主机是支持TCP/IP 协议的计算机或设备。每个主机由一个32位的IP地址来标识。为了方便起见,32位的P地址号通常用点记法表示,例如:134.121.64.1,其中各个字节用点号分开。主机也可以用主机名来表示,如dns1.eec.wsu.edu。IP地址分为两部分,即 NetworkID字段和HostID字段。根据划分,IP地址分为A~E类。例如,一个B类P地址被划分为一个16位NetworkID,其中前2位是10,然后是一个16位的HostID字段。发往P地址的数据包首先被发送到具有相同networkID 的路由器。路由器将通过HostID将数据包转发到网络中的特定主机。每个主机都有一个本地主机名localhost,默认P地址为127.0.0.1。本地主机的链路层是一个回送虚拟设备,它将每个数据包路由回同一个 localhost。
-
IP协议:用于在IP主机之间发送/接收数据包。IP尽最大努力运行。IP主机只向接收主机发送数据包,但它不能保证数据包会被发送到它们的目的地,也不能保证按顺序发送。
-
IP数据包:由IP头、发送方地址和接收方I地址以及数据组成。每个数据包的大小最大为64KB。IP头包含有关数据包的更多信息,例如数据包的总长度、数据包使用TCP还是UDP、生存时间(TTL)计数、错误检测的校验和等。
-
路由器:是接收和转发数据包的特殊IP主机。一个IP数据包可能会经过许多路由器,或者跳跃到达某个目的地。每个IP包在IP报头中都有一个8位生存时间(TTL)计数,其最大值为255。在每个路由器上,TTL会减小1。如果TTL减小到0,而包仍然没有到达目的地,则会直接丢弃它。这可以防止任何数据包在IP网络中无限循环。
-
UDP:在IP上运行,用于发送/接收数据报。与IP类似,UDP不能保证可靠性,但是快速高效。ping是一个向目标主机发送带时间戳UDP包的应用程序。接收到一个pinging数据包后,目标主机将带有时间戳的UDP包回送给发送者,让发送者可以计算和显示往返时间。如果目标主机不存在或宕机,当TTL减小为0时,路由器将会丢弃pinging UDP数据包。在这种情况下,用户会发现目标主机没有任何响应。用户可以尝试再次ping,或者断定目标主机宕机。
-
TCP:是一种面向连接的协议,用于发送/接收数据流。TCP也可在IP上运行,但它保证了可靠的数据传输。通常,UDP类似于发送邮件的USPS,而TCP类似于电话连接。
-
端口编号:端口号是分配给应用程序的唯一无符号短整数。要想使用UDP或TCP,应用程序(进程)必须先选择或获取一个端口号。前1024个端口号已被预留。其他端口号可供一般使用。应用程序可以选择一个可用端口号,也可以让操作系统内核分配端口号。
-
网络和主机字节序:计算机可以使用大端字节序,也可以使用小端字节序。在互联网上,数据始终按网络序排列,这是大端。在小端机器上,例如基于Intel x86的PC,htons()、htonl()、ntohs()、ntohl()等库函数,可在主机序和网络序之间转换数据。例如,PC中的端口号1234按主机字节序(小端)是无符号短整数。必须先通过htons(1234)把它转换成网络序,才能使用。相反,从互联网收到的端口号必须先通过ntohs(port)转换为主机序。
-
TCP/IP网络中的数据流
2、套接字编程
套接字编程
-
在网络编程中,TCP/IP的用户界面是通过一系列C语言库函数和系统调用来实现的, 这些函数和系统调用统称为套接字API (( Rago 1993; Stevens等2004 )。为了使用套接宇 API,我们需要套接字地址结构,它用于标识服务器和客户机。netdb.h和sys/socket.h中有 套接字地址结构的定义:
struct sockaddr_in ( sa_family_t sin_family; in_port_t sin_port; struct in_addr sin_addr;
);
struct in_addr {
uint32_t s_addr;
-
在套接字地址结构中,TCP/IP 网络的 sin_family 始终设置为 AF_INET,sm_port包含按网络字节顺序排列的端口号,sin_addr是按网络字节顺序排列的主机IP地址。
-
服务器必须创建一个套接字,并将其与包含服务器IP地址和端口号的套接字地址绑 定。它可以使用一个固定端口号,或者让操作系统内核选择一个端口号(如果sin_port为 0)o为了与服务器通信,客户机必须创建〜个套接字。对于UPD套接字,可以将套接字绑定到服务器地址。如果套接字没有绑定到任何特定的服务器,那么它必须在后续的sendto()/ recvfromO调用中提供一个包含服务器IP和端口号的套接字地址。下面给出了socket。系统调用,它创建一个套接字并返回一个文件描述符。
-
UDP套接字使用scndto()/recvfrom()来发送/接收数据报。
aendto(int aockfdr const void *bufr size.t len, lot flags,
const struct sockaddr •de8t_addrf socklen_t addrlen)|
asize_t recvfrora(int sock£d, void *buf, aiza_t len, int flags, struct sockaddr *Btc_addr, aocklen_t *addrlen};
-
在创建套接字并将其绑定到服务器地址之后,TCP服务器使用listen()和acccpt()来接 收来自客户机的连接
int Iistcn(int sockfd, int backlog);
listen()将sockfd引用的套接字标记为将用于接收连入连接的套接字。backlog参数定义了等待连接的最大队列长度。 -
建立连接后,两个TCP主机都可以使用send()/write()发送数据,并使用recv()/read。接收数据。它们唯一的区别是send和recv()中的nag参数不同,通常情况下可以将其设置为0。
ssize_t send(int Bockfd, const void *bufr size.t len« int flags);
write(sockfd/ void *buf, aize_t, l«n)
S0izo_t recv(int sockfd, void *buf# size_t len, int flags);
ssize_t read(sockfd, void *buf, size_t len);
-
库函数
gethostname(char *name, sizeof(name))
在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]
-
下面的代码段展示了如何使用gethostbyname()和getsockname()来获取服务器1P地址 和端口号(若是动态分配)。服务器必须发布其主机名或IP地址和端口号,以便客户机连接。
char myname[64];
struct sockaddr_in server_addr, sock_addr;
// gethostname(), gethostbyname() gethostname(myname,64);
struct hostent *hp = gethostbyname(myname); if (hp == 0)(
printf("unknown host %s\n", myname); exit(1);
// initialize the server_addr structure server_addr.sin_family = AF.INET; // for TCP/IP
server_addr.Bin_addr・ s_addr = *(long *)hp->h_addr; server_addr.sin_port = 0; // let kernel assign port number
// create a TCP socket
int mysock = socket(AF_INET, SOCK_STREAM, 0);
bind socket with server_addr bind(mysock,(struct
to show port number assigned by kernel
(struct sockaddr *)&name_addr, &length)/
// show server host name and port number
printf("hostname=%s IP=%s port=%d\n", hp->h_name,
inet_ntoa(*(long *)hp->h_addr), ntohs(name_addr.sin_port));
struct sockaddr_in server_addr, sock_addr;
// 1. get server IP by name
struct hostent *hp = gethostbyname(argv[l]);
SERVER_IP = *(long *)hp->h_addr;
SERVER_PORT = atoi(argv[2]);
// 2. create TCP socket
int sock = socket(AF_INET, SOCK_STREAM, 0);
// 3. fill server_addr with server IP and PORT#
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = SERVER_IP;
server_addr.sin_port = htons(SERVER_PORT);
// 4. connect to server
connect(sock,(struct sockaddr *)&server_addr, sizeof(server_addr));
-
在命令行中,根据命令的不同,路径名可以是文件或目录。有效命令有:
mkdir:创建一个带路径名的目录。 rmdir:删除名为路径名的冃录。 rm:删除名为路径名的文件。 cd:将当前工作目录(CWD)更改为路径名。 pwd:显示当前工作目录的绝对路径名。 Is:按照与Linux的Is -1相同的格式列出当前工作目录或路径名。 get:从服务器下载路径名文件。 put:将路径名文件上传到服务器。
-
对于传输文件内容的get/put命令,不能使用特殊ASCII字符作为文件的开始和结束标记。这是因为二进制文件可能包含ASCII代码。
-
用标准HTML编写的Web页面都是静态的。当从服务器获取并用浏览器显示时,Web 页面的内容不会变化。要显示包含不同内容的Web页面,必须再次从服务器获取不同的 Web页面文件。
-
动态Web页面有两种,分别称为 客户机端动态Web页面和服务器端动态Web页面。客户机端动态Web页面文件包含JavaScript写的代码,这些代码由JavaScript解释器在客户机上执行。它可以响应用户输入、时间事件等来对Web页面进行本地修改,而不需要与服务器进行任何交互。服务器端动态Web页面是真正的动态页面,因为它们是根据URL请求中的用户输入动态生成的。服务器 端动态Web页面的核心在于服务器在HTML文件中执行PHP代码,或CGI程序通过用户输入生成HTML文件的能力。
-
CGI代表通用网关接口,它是一种协议,允许Web服务器执行程序, 根据用户输入动态生成Web页面。使用CGI, Web服务器不必维护数百万个静态Web页面文件来满足客户机请求。相反,它通过动态生成Web页面来满足客户机请求。
三、实践
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <time.h> #include <string.h> #include <unistd.h> #define MAXLINE 256 #define PORT 7777 void sys_err(char *msg){ perror(msg); exit(-1); } int main(int argc , char **argv){ int sockFd,n; char recvLine[MAXLINE]; struct sockaddr_in servAddr; if (argc != 2) { sys_err("usage: a.out <IPaddress>"); } sockFd=socket(AF_INET,SOCK_STREAM,0); memset(&servAddr,0,sizeof(servAddr)); servAddr.sin_family = AF_INET; servAddr.sin_port = htons(PORT); if (inet_pton(AF_INET,argv[1],&servAddr.sin_addr) <= 0) { sys_err("inet_pton error"); } connect(sockFd,(struct sockaddr *)&servAddr,sizeof(servAddr)); while((n=read(sockFd,recvLine,MAXLINE)) >0 ){ recvLine[n] = '\0'; if(fputs(recvLine,stdout) == EOF){ sys_err("fputs error"); } } if(n <0){ sys_err("read error"); } return 0; }