第13章学习笔记
一、概述
本章论述了TCP/IP和网络编程,分为两个部分。第一部分论述了TCP/IP协议及其应用,具体包括TCP/IP栈、IP地址、主机名、DNS、IP数据包和路由器;介绍了TCP/P网络中的UDP和TCP协议、端口号和数据流;阐述了服务器-客户机计算模型和套接字编程接口;通过使用UDP和TCP套接字的示例演示了网络编程。第一个编程项目可实现一对通过互联网执行文件操作的TCP服务器–客户机,可让用户定义其他通信协议来可靠地传输文件内容。
本章的第二部分介绍了Web和CGI编程,解释了HTTP编程模型、Web页面和Web浏览器;展示了如何配置Linux HTTPD服务器来支持用户Web页面、PHP和CGI编程;阐释了客户机和服务器端动态Web页面;演示了如何使用PHP和CGI创建服务器端动态Web页面。
二、计算机网络知识
- 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网络中的数据流
三、网络编程
- 套接字协议及其数据传输特性
int socket(int domain, int type, int protocol); // domain:采取的协议族,一般为 PF_INET;type:数据传输方式,一般为 SOCK_STREAM;protocol:使用的协议,一般设为 0 即可。
//成功时返回文件描述符,失败时返回 -1
- 创建套接字的函数 socket 的三个参数的含义:
domain:使用的协议族。一般只会用到 PF_INET,即 IPv4 协议族。
type:套接字类型,即套接字的数据传输方式。主要是两种:SOCK_STREAM(即 TCP)和 SOCK_(即 UDP)。
protocol:选择的协议。一般情况前两个参数确定后,protocol 也就确定了,所以设为 0 即可。 - 套接字类型
同一个协议族可能有多种数据传输方式,因此在指定了 socket 的第一个参数后,还要指定第二个参数 type。 - SOCK_STREAM 代表的是 TCP 协议,会创建面向连接的套接字,有如下特点:
1.可靠传输,传输的数据不会消失。
2.按序传输。
3.传输的数据没有边界:从面向连接的字节流角度理解。接收方收到数据后放到接收缓存中,用户使用 read 函数像读取字节流一样从中读取数据,因此发送方 write 的次数和接收方 read 的次数可以不一样。
int tcp_socket = socket(PF_INET, SOCK_STREAM, 0);
- SOCK_DGRAM 代表的是 UDP 协议,会创建面向消息的套接字,有如下特点:
1.快速传输。
2.传输的数据可能丢失、损坏。
3.传输的数据有数据边界:这意味着接收数据的次数要和传输次数相同,一方调用了多少次 write(send),另一方就应该调用多少次 read(recv)。
4.限制每次传输的数据大小。
int udp_socket = socket(PF_INET, SOCK_DGRAM, 0);
- 套接字地址结构
IPv4套接字结构
IPv4套接字地址结构通常称为“网际套接字地址结构”,它以sockaddr_in命名,定义在<netinnet/in.h>头文件中。
以下是网际(IPv4)套接字地址结构:sockaddr_in
struct in_addr;
{
int_addr_t s_addr; /*32位IPv4的地址*/
/*网络字节命令*/
};
struct sockaddr_in
{
uint8_t sin_len;
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
POIX规范只需要这个结构中的3个字段:sin_family、sin_add、sin_port。对于符合POIX的实现来说,定义额外的结构字段是可以接收的,这对于网际套接字地址结构来说也是正常的。几乎所有的实现都增加了sin_zero字段,所以所有的套接字地址结构大小都至少是16字节。
数据类型 说明 头文件
int8_t 带符号的8位整数 <sys/typcs.h>
uint8_t 无符号的8位整数 <sys/typcs.h>
int16_t 带符号的16位整数 <sys/typcs.h>
uint16_t 无符号的16位整数 <sys/typcs.h>
int32_t 带符号的32位整数 <sys/typcs.h>
uint32_t 无符号的32位整数 <sys/typcs.h>
sa_family_t 套接字地址结构的地址族 <sys/socket.h>
socklen_t 套接字地址结构的长度,一般为uint32_t <sys/socket.h>
in_addr_t
IPv4地址,一般为unit32_t
<netine/in.h>
in_port_t TCP或UDP端口,一般为uint16_t <netine/in.h>
(扩展)POSIX(Portable Operating System Interface,可移植操作系统接口)
POIX是接口。
POSIX标准定义了操作系统应该为应用程序提供的接口标准,是IEEE为要在各种UNIX操作系统上运行的软件而定义的一系列API标准的总称,其正式称呼为IEEE 1003,而国际标准名称为ISO/IEC 9945。
(一些常见的缩写)
addr(address,地址)
info(information,信息)
- 通用套接字地址结构
通用套接字地址结构:sockaddr
struct sockaddr
{
uint8_t sa_len;
sa_family_t sa_family;
char sa_data[14];
};
IPv6套接字地址结构
IPv6套接字地址结构在<netinet/in.h>头文件中定义
struct in6_addr
{
unit8_t s6_add[16];
};
#define SIN6_LEN
struct sockaddr_in6
{
uint8_t sin6_len;
sa_family_t sin6_family;
in_port_t sin6_port;
uint32_t sin6_flowinfo;
struct in6_addr sin6_addr;
uint32_t sin6_scope_id;
};
新的struct sockaddr_storage足以容纳系统所支持的任何套接字地址结构。sockaddr_storage结构在<netinet/in.h>头文件中定义
struct sockaddr_storage
{
uint8_t ss_len;
sa_family_t ss_family;
};
不同套接字地址结构的比较
- 值-结果参数
传递方式取决于该结构的传递方向:是从进程到内核,还是从内核到进程、
1)从进程到内核传递套接字地址结构的函数有3个:bind,connect和sendto
这些函数的一个参数时指向某个套接字地址结构的指针,另一个参数时该结构的整数大小。
!!套接字地址结构大小的数据类型是socklen_t。(POIX建议将socklen_t定义为uint32_t)
2)从内核到进程传递套接字地址结构的函数有4个:accept,recvfrom,getsocknamo和getpeername。
这4个函数的其中两个参数是指向某个套接字地址结构的指针和指向表示该结构大小的整数变量的指针。
值-结果参数:把套接字地址结构大小这个参数从一个整数改为指向某个整数变量的指针,其原因在于,当函数被调用时,结构大小是一个值,它告诉内核该结构的大小,这样内核在写该结构时不至于越界;
当函数返回时,结构大小又是一个结果,它告诉进程内核在该结构中究竟储存了多少信息。
- 字节排序函数
小端和大端(内存中存储两个字节有两种方法)
小端(little-endian):将低序字节存储在起始地址
大端(big-endian):将高序字节存储在起始地址
主机字节序:某个给定系统所用的字节序
输出字节序的程序:
#iclude"unp.h"
int main(int argc,char **argv)
{
union{
short s;
char c[sizeof(short)];
}un;
un.s=0x0102;
printf("%s:",CUP_VENDOR_OS);
if(sizeof(short)==2){
if(un.c[0]==1&&un.c[1]==2)
printf("big-endian\n");
else if (un.c[0]==2&&un.c[1]==1)
printf("little-endian\n");
else
printf("unknown\n");
}else
printf("sizeof(short)=%d\n",sizeof(short));
exit(0);
}
- 字节操纵函数
bzero:bzero把目标字节串指定数目的字节置为0。我们常用该函数把一个套接字地址结构初始化为0.
bocpy:指定数目的字节从源字节串移动到目标字节串。
bcmp:比较两个任意的字节串,若相同返回值为0,否则返回值为非0.
memset:把目标字节串指定数目的字节置为c。
mencmp:比较两个任意的字符串,若相同为0,否则返回一个非0值,是大于0还是小于0则取决于第一个不等的字节。
5.inet_aton、net_addr和inet_ntoa函数(地址转换函数)
6.inet_pton和inet_ntop函数
函数中的p代表表达(presentation),n代表数值(numeric)
地址的表达格式通常是ASCII字符串,数值格式则是存放到套接字地址结构中的二进制。
1)只支持IPv4的inet_pton函数的简单定义
int inet_pton(int family,const char *strptr,void *addrptr)
{
if(family==AF_INET)
{
struct in_addr in_val;
if(inet_aton(strptr,&in_val))
{
memcpy(addrptr,&in_val,sizeof(struct int_addr));
return(1);
}
return(0);
}
errno=EAFNOSUPPROT;
return(-1);
}
2)只支持IPv4的inet_ntop函数的简化版本
const char*
inet_ntop(int family,const void *addrptr,char *strptr,size_t len)
{
const u_char *p=(const u_char *)addrptr;
if(family==AF_INET)
{char temp[INET_ADDRSTRLEN];
snprintf(temp,sizeof(temp),"%d,%d,%d,%d",p[0],p[1],p[2],p[3]);
if(strlen(temp)>=len)
{ errno=ENOSPC;
return (NULL);
}
strcpy(strptr,temp);
return (strptr);
}
errno =EAFNOSUPPORT;
return (NULL);
}
- sock_ntop和相关函数
作用:它以指向某个套接字地址结构的指针为参数,查看该结构的内部,然后调用适当的函数返回该地址的表达格式。
#include"unp.h"
char *sock_ntop(const struct sockaddr *sockaddr,socklen_t addrlen);
//若成功则为非空指针,若出错为NULL
四、实践内容与截图
#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;
}
五、问题与解决
问题:在网络编程过程中,服务器端和客户端的套接字有什么区别?
解决:据网上博客的答案,两种方式差别不大,并没有本质的区别,建议主控机器作为服务器,其它作为客户端。原因在于,监听需要一个固定的端口,相比之下在一台机器上保证一个端口不冲突,远比在众多机器当中保证端口不冲突来得容易。