TCPIP网络编程_1.2 基于Linux的文件操作
讨论套接字的过程中突然谈及文件也许有些奇怪. 但对Linux而言, socket 操作与文件操作没有区别, 因此有必要详细了解文件. 在Linux 世界里, socket 也被认为是文件的一种, 因此在网络数据传输中自然可以使用文件I/O 的相关的函数, Windows 则与 Linux 不同, 是要区分 socket 和文件的. 因此在 Windows中需要调用特殊的数据传输相关函数.
底层文件访问(Low-Level File Access) 和 文件标识符 (File Descriptor)
即使看到"底层"二字, 也会有读者一测其难以理解. 实际上, "底层"这个表达可以理解为 "与标准无关的操作系统独立提供的. ". 稍后讲解的函数是由 Linux 提供的, 而非 ANSI 标准定义的函数. 如果想要使用 Linux 提供的文件 I/O 函数, 首先应该理解好文件描述符的概念.
此处的文件描述符是系统分配给文件或套接字的整数. 实际上, 学习c语言过程中用过的标准输入输出及标准错误在 Linux 中也被分配表 1-1 中的文件描述符.
文件和套接字一般经过创建过程才会被分配文件描述符. 而表 1-1 中的3种输入输出对象即使未经过特殊的创建处理, 程序开始运行后也会被自动分配文件描述符. 稍后将详细讲解其使用方法及含义.
学校附近有个服务站, 只需打个电话就能复印所需论文. 服务站有一位常客加英秀, 他每次都要求复印同一篇论文的部分内容.
"大叔你好! 请帮我复印一下<<关于随着高度信息化社会而逐步提升地位的触觉, 知觉, 思维, 性格, 智力等人类生活质量相关问题特性的人类学研究>>“这篇论文第 26页到 30页”.
这位同学每天这样打好几次电话, 更是雪上加霜的是语速还特别慢. 终于有一天大叔说话:“从现在开始, 那篇论文编为第18号! 你就说帮我复印18号论文26页到30页!”
之后英秀也是只复印超过50字标题的论文, 大叔也会给每篇论文分配无重复的新号(数字).这才不会头痛与英秀的对话, 且不影响业务.
该案例中, 大叔相当于操作系统, 英秀相当于程序员, 论文号相当于文件描述符, 论文相当于文件或套接字. 也就是说, 每当生成文件或套接字, 操作系统将返回分配给它们的整数. 这个整数将成为程序员与操作系统之间良好的沟通的渠道. 实际上, 文件描述符只不过是为了方便称呼操作系统创建的文件或套接字而赋予的数而已.
文件描述符有时也称为文件句柄, 但"句柄"主要是 Windows 中的术语. 因此, 本书中如果涉及 Windows 平台将使用 “句柄”, 如果是 Linux 平台则用"描述符".
打开文件
首先介绍打开文件以读写数据的函数. 调用此函数时需要传递两个参数: 第一个参数是打开的目标文件名及路径信息, 第二个参数是文件打开模式(文件特性信息)
表 1-2 是此函数第二个参数flag 可能的常量值及含义. 如需传递多个参数, 则应通过位或运算(OR)符组合并传递.
稍后将给出次函数的使用示例. 接下来先介绍文件和写文件时调用的函数.
关闭文件
各位学习C语言时学过, 使用文件后必须关闭. 下面介绍关闭文件时调用的函数.
若调用此函数的同时传递文件描述符参数, 则关闭(终止)相应文件. 另外需要注意的是, 此函数不仅仅可以关闭文件, 还可以关闭套接字. 这再次证明了 "Linux操作系统不区分文件与套接字"的特点.
将数据写入文件
接下来介绍write函数用于向文件输出(传输)数据. 当然, Linux 中不区分文件与套接字, 因此, 通过套接字向其他计算机传输数据时也会用到该函数. 之前的示例也调用它传递字符串"Hello word! ".
此函数定义中, size_t 是通过typedef 声明的unsigned int 类型. 对ssize_t 来说, size_t 前面多加的 s 代表signed, 即ssize_t 是通过typedef 声明的signed int 类型.
我们已经接触到 ssize_t, size_t 等陌生的数据类型. 这些都是元数据类型(primitive), 在sys/types.h 头文件中一般由typedef 声明定义, 算是给大家熟悉的基本数据起来别名. 既然已经有了基本数据类型, 为何还要声明并使用这些新的呢?
人们目前普遍认为int 是32 位的, 因为主流操作系统和计算机仍采用32位. 而在过去16 位操作系统时代, int 是16位. 根据系统的不同, 时代的变化, 数据类型的表现形式也随之改变, 需要修改程序中使用的数据类型. 如果之前已在需要声明4字节数据类型之处使用了size_t或 ssize_t ,则将大大减小代码变动, 因为只需要修改并编译size_t和ssize_t的typedeef声明即可. 在项目中, 为了给基本数据类型赋予别名, 一般会添加大量typedef声明. 而为了与程序员定义的新数据类型加以区别,操作系统定义的数据类型会添加后缀_t.
下面通过实例帮助大家更好地理解前面讨论过的函数. 此程序将创建新文件并保存数据.
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
void error_handling(char *message);
int main()
{
int fd;
char buf[] = "Let's go!\n";
fd = open("data.txt", O_CREAT|O_WRONLY|O_TRUNC);
if (fd == -1)
{
error_handling("open() error!");
}
printf("file descriptor: %d\n", fd);
if (write(fd, buf, sizeof(buf)) == -1)
{
error_handling("write() error!");
}
close(fd);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
运行结果:
读取文件中的数据
与之前的write函数相对应, read 函数用来输入(接收)数据
下列实例将通过read函数读取data.txt 中保存的数据.
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#define BUF_SIZE 100
void error_handling(char *message);
int main()
{
int fd;
char buf[BUF_SIZE];
fd = open("data.txt", O_RDONLY);
if (fd == -1)
{
error_handling("open() error!");
}
printf("file descriptor: %d \n", fd);
if (read(fd, buf, sizeof(buf)) ==-1)
{
error_handling("read() error!");
}
printf("file data: %s", buf);
close(fd);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
运行结果:
基于文件描述符的 I/O 操作相关介绍到此结束. 希望各位记住, 该内容同样适用于套接字.
文件描述符与套接字
下面将同时创建文件和套接字, 并用整数型态比较返回的文件描述值.
#include <stdio.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int fd1, fd2, fd3;
/* 创建1个文件和两个套接字 */
fd1 = socket(PF_INET, SOCK_STREAM, 0);
fd2 = open("test.dat", O_CREAT|O_WRONLY|O_TRUNC);
fd3 = socket(PF_INET, SOCK_DGRAM, 0);
/* 输出之前创建的文件描述符的整数值. */
printf("file descriptor 1: %d\n", fd1);
printf("file descriptor 2: %d\n", fd2);
printf("file descriptor 3: %d\n", fd3);
close(fd1);
close(fd2);
close(fd3);
return 0;
}
运行结果:
从输出的文件描述符整数值可以看出, 描述符从3开始以由小到大的顺序编号(numbering), 因为0 , 1, 2 是分配给标准I/O 的描述符(如图表 1- 1所示)
时间: 2020_05_21