1.文件描述符与套接字
对 Linux 而言,socket操作与文件操作没有区别,因而有必要详细了解文件。在 Linux 世界里,socket 也被认为是文件的一种,因此在网络数据传输过程中自然可以使用文件 I/O 的相关函数。
Linux 分配给标准输入、标准输出、标准错误的文件描述符如下表所示:
文件描述符 | 对象 |
---|---|
0 | 标准输入:Standard Input |
1 | 标准输出:Standard Output |
2 | 标准错误:Standard Error |
文件和套接字一般经过创建过程才会被分配文件描述符。而上表中的 3 3 3 种输入输出对象即使未经过特殊的创建过程,程序开始运行后也会被自动分配文件描述符。
实际上,文件描述符只不过是为了方便称呼操作系统创建的文件或套接字而赋予的数而已。如果是 Windows 平台,则通常称之为“文件句柄”;如果是 Linux 平台,则通常称之为“文件描述符”。
在下面的程序 fd_seri.c 中,将同时创建文件和套接字,并用整数型态比较返回的文件描述符值。
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/socket.h>
int main(void)
{
int fd1, fd2, fd3;
// 创建1个文件和2个套接字
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;
}
编译运行:
gcc fd_seri.c -o fds
./fds
输出结果:
file descriptor 1: 3
file descriptor 2: 4
file descriptor 3: 5
从输出的文件描述符整数值可以看出,描述符从 3 3 3 开始以由小到大的顺序编号,因为 0 0 0、 1 1 1、 2 2 2 是分配给标准 I/O 的描述符。
2.打开文件(open 函数)
调用 open
函数时需传递两个参数:第一个参数是打开的目标文件名及路径信息,第二个参数是文件打开模式(文件特性信息)。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *path, int flag);
// 成功时返回文件描述符,失败时返回-1
// path:文件名的字符串地址
// flag:文件打开模式信息
下表是 open
函数第二个参数 flag
可能的常量值及含义。如需传递多个参数,则应通过位或运算符组合并传递。
打开模式 | 含义 |
---|---|
O_CREAT | 必要时创建文件 |
O_TRUNC | 删除全部现有数据 |
O_APPEND | 维持现有数据,保存到其后面 |
O_RDONLY | 只读打开 |
O_WRONLY | 只写打开 |
O_RDWR | 读写打开 |
3.关闭文件(close 函数)
使用文件后必须关闭。
#include <unistd.h>
int close(int fd);
// 成功时返回0,失败时返回-1
// fd:需要关闭的文件或套接字的文件描述符
若调用 close
函数的同时传递文件描述符参数,则关闭(终止)相应文件。另外需要注意的是,close
函数不仅可以关闭文件,还可以关闭套接字。这再次证明了“Linux操作系统不区分文件与套接字”的特点。
4.将数据写入文件(write 函数)
write
函数用于向文件输出(传输)数据。当然,Linux 中不区分文件与套接字,因此,通过套接字向其他计算机传递数据时也会用到该函数。
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t nbytes);
// 成功时返回写入的字节数,失败时返回-1
// fd:显示数据传输对象的文件描述符
// buf:保存要传输数据的缓冲地址值
// nbytes:要传输数据的字节数
在上述 write
函数的定义中,size_t
是通过 typedef
声明的 unsigned int
类型。ssize_t
前面多加的 s
代表 signed
,即 ssize_t
是通过 typedef
声明的 signed int
类型。
在下面的程序 low_open.c 中,将创建新文件并保存数据。
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
void error_handling(char* message);
int main(void)
{
int fd;
char buf[] = "Let's go!\n";
// 文件打开模式为 O_CREAT、O_WRONLY 和 O_TRUNC 的组合,因此将创建空文件,并只能写。
// 若存在data.txt文件,则清空文件的全部数据。
fd = open("data.txt", O_CREAT | O_WRONLY | O_TRUNC);
if (fd == -1)
{
error_handling("open() error!");
}
printf("file descriptor: %d\n", fd);
// 向对应于fd中保存的文件描述符的文件传输buf中保存的数据
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);
}
编译运行:
gcc low_open.c -o lopen
./lopen
运行上面的程序后,利用 Linux 的 cat
命令输出 data.txt
文件的内容为 Let's go!
,可以确认确实已向文件传输数据。
5.读取文件中的数据(read 函数)
与前面的 write
函数相对应,read
函数用来输入(接收)数据。
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t nbytes);
// 成功时返回接收的字节数(但遇到文件结尾则返回0),失败时返回-1
// fd:显示数据接收对象的文件描述符
// buf:要保存接收数据的缓冲地址值
// nbytes:要接收数据的最大字节数
在下面的程序 low_read.c 中,将通过 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(void)
{
int fd;
char buf[BUF_SIZE];
// 打开读取专用文件data.txt
fd = open("data.txt", O_RDONLY);
if (fd == -1)
{
error_handling("open() error!");
}
printf("file descriptor: %d\n", fd);
// 调用read函数向第13行中声明的数组buf保存读入的数据
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);
}
编译运行:
gcc low_read.c -o lread
./lread
输出结果:
file descriptor: 3
file data: Let's go!
基于文件描述符的 I/O 操作相关介绍到此结束。希望各位记住,该内容同样适用于套接字。