Linux下文件I/O
"一切皆文件"是 Unix/Linux 的基本哲学之一。不仅普通的文件,目录、字符设备、块设备、 套接字等在 Unix/Linux 中都是以文件被对待;它们虽然类型不同,但是对其提供的却是同一套操作界面。Linux有7中类型文件:普通文件-、目录(dierectory)文件、符号(link)链接、字符(character) 设备文件、块(block)设备文件、管道(pipe)文件、套接字(socket)文件。其中文件、目录、符号链接会占用磁盘空间来存储,而 块设备、字符设备、套接字、管道是伪文件,并不占用磁盘空间。
文件描述符
(file descriptor, fd)是Linux为了高效管理打开的文件创建的索引。程序在开始运行时,系统会自动打开三个文件描述符,0是标准输入,1是标准输出,2是标准错误。
文件I/O操作函数
读写文件中的内容一般为三步:打开(open())->读(read())->写(write())->关闭(close())。如果文件不存在可用creat()系统创建
creat()
int creat(const char *pathname, mode_t mode);
此函数用来创建一个新文件并返回其fd。它等价于 open(path, O_WRONLY|O_CREAT|O_TRUNC, mode);
open()
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
open()系统调用用来打开指定路径下的一个文件,并返回一个文件描述符(file description), 并且该文件描述符是当前进程最小、未使用的文件描述符数值。 flags指定打开模式,当flags = O_CREAT(创建一个文件并打开 )时,第三个参数指定创建文件的权限模式
creat()和open()详细链接
read()
ssize_t read(int fd, void *buf, size_t count);
read()函数用来从打开的文件描述符对应的文件中读取数据放到buf指向的内存空间中去,最多不要超过count个字节,这里的 count一般是buf剩余的空间大小。如read成功,则返回实际读到的字节数(由nbytes或读到文件尾决定,其中EOF宏用来判断 是否到了文件尾),如果返回值小于0则表示出错,如尝试读一个没有权限读的文件时就会抛错。 若buf中数据无法一次性读完,那么第二次读buf中数据时,其读位置指针(也就是第二个参数buf)不会自动移动,需要程序员来控制,而不是简单的将buf首地址填入第二参数即可。如可按如下格式实现读位置移动:write(fp, p1+len, (strlen(p1)-len)。
write()
ssize_t write(int fd, const void *buf, size_t count);
write()函数用来往打开的文件描述符fd指向的文件中写入buf指向的数据,其中count指定要写入的数据大小。如果返回值<0则 说明写入出错,譬如尝试往一个只读的文件中写入则会抛错,错误的原因系统会保存到errno变量中去。如果>0则为实际写入的 数据大小。
read()和write()详解链接
close()
int close(int fd);
该函数用来关闭一个打开的文件描述符,直接传入要关闭的fd,关闭一个文件时还会释放该进程加在该文件上的所有记录锁。当一个进程终止时,内核 将会自动关闭它所有打开的文件。
Linux下文件夹操作函数

DIR *opendir(const char *path)参数path指向目录流的下个目录进入点,将信息保存到struct dirent结构体中。
下面的这段代码是从树莓派的文件中读取传感器采集到温度,路径为 /sys/bus/w1/devices/28-xxxx/w1_slave
第一步:要读取 /sys/bus/w1/devices/28-xxxx/w1_slave 下的内容,就需要确定这个绝对路径。然而由于devices/28-xxxx这个是根据具体芯片型号而定,但是以28-开头。所以就先需要找到这个文件夹,用opendir()打开并在返回的结构体.d_name 中搜素那个文件夹
第二步:打开open()打开,read读取内容,read()会将读到的内容放入传入的buf中。
第三步:用strstr()在buf中找到温度的位置,由于在文件中找到的内容是字符串,需要用atof()转换成浮点数。最后关闭文件
float get_parameter()
{
char path1[50]= "/sys/bus/w1/devices/";/* get DS18B20 path: /sys/bus/w1/devices/28-xxxx/w1_slave */
char path2[10]="/w1_slave";
char buff[256];
DIR *dirp;
int fd=-1;
int rv=-1;
char *ptr;
char temparr[20];
float wendu;
struct dirent * p_dirrent;/*d_name(文件名)和d_type(文件类型),%d显示的值4代表目录,8代表文件*/
if((dirp=opendir(path1))==NULL)
printf("opendir failure:%s\n",strerror(errno));//DIR *opendir(const char *path)
while((p_dirrent=readdir(dirp))!=NULL)
{
if(strstr(p_dirrent->d_name,"28-"))
{
strncpy(temparr,p_dirrent->d_name,sizeof(temparr));
break;
}
}
closedir(dirp);
strncat(path1,temparr,strlen(temparr));//src所指向的前n个字符添加到dest结尾处(覆盖原dest结尾处的'\0'添
strncat(path1,path2,strlen(path2));
if((fd=open(path1,O_RDONLY))<0)
{
printf("open failure:%s\n",strerror(errno));
}
memset(buff,0,sizeof(buff));
if((rv=read(fd,buff,sizeof(buff)))<0)
{
printf("read failure:%s\n",strerror(errno));
}
ptr=strstr(buff,"t=");
ptr+=2;
wendu=atof(ptr)/1000;
/* memset(buff,0,sizeof(buff));
size_t sr;
FILE *fp;
fp=fopen(path1,"r");
sr=fread(buff,10,10,fp);//size_t fread(void *buffer, size_t size, size_t count, FILE *stream);
ptr=strstr(buff,"t=");
ptr+=2;
printf("the ptr-> is %c\n",*ptr);
wendu=atof(ptr)/1000;
printf("the temprature is :%f\n",wendu);*/
return wendu;
用到的字符串处理函数strncat()(字符串连接)和strstr()(字符串搜素)
strncat()
extern char *strncat(char *dest,char *src,int n)
把src所指字符串的前n个字符添加到dest结尾处,覆盖dest结尾处的’/0’,实现字符串连接。
strstr()
extern char *strstr(char *str1, const char *str2);
若str2是str1的子串,则返回str2在str1的首次出现的地址;如果str2不是str1的子串,则返回NULL。
由TCP/IP到网络socket
1. TCP/IP与OSI
TCP/IP(Transmission Control Protocol/Internet Protocol,传输控制协议/网际协议)并不是单纯的一个协议,而是指的一个协议族。它按照层次由上到下,层层包装。让从而使不同类型的终端和网络间能够进行有效通信。说到TCP/IP不得不提一下OSI。OSI由ISO(国际标准化组织)制定了一个国际标准OSI(开放式通信系统互联参考模型)。那为什么由国际标准化组织制定的标准没什么人用呢?原因是它制定得比较晚,错过了互联网刚刚兴起的时候,而且分为七层结构,比较复杂。当时大部分的机器已经用TCP/IP协议通信,也就没有人愿意为了这个标准而承担放弃已经生产的产品带来巨额损失。所以从此以后TCP/IP被作为一种必须遵守的规则被肯定和应用。下面看一下它们两个结构的区别

具体每一层的作用可以参考链接
IP层有一个笔试面试的重点就是IP地址的分类(列出来没事瞅瞅):
A类IP地址: 0.0.0.0~127.255.255.255
B类IP地址:128.0.0.0~191.255.255.255
C类IP地址:192.0.0.0~239.255.255.255
2. TCP连接的建立与断开(三次握手与四次握手)!!!很重要
3. socket编程
Socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”。在许多操作系统中,套接字API初是作为UNIX操作 系统的一部分而开发的,所以套接字API与系统的其他I/O设备集成在一起。应用程序要为因特网通信而创建一个套接字 (socket)时,操作系统就返回一个小整数作为描述符(descriptor)来标识这个套接字。然后应用程序以该描述符作为传递参数,通过调用相应函数(如read、write、close等)来完成某种操作(如从套接字中读取或写入数据)。

(1)socket:
首先调用系统函数socket()建立socket套接字,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO(read(),write())、打开(open())、关闭(close()))。Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
注意:其实socket也没有层的概念,它只是一个facade设计模式的应用,让编程变的更简单。是一个软件抽象层。在网络编程中,我们大量用的都是通过socket实现的。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
返回值为一个文件描述符fd
domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX, Unix域socket)(命名socket可用于进程间通信)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了 要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、 SOCK_SEQPACKET等等(socket的类型有哪些?)。
protocol:故名思意,就是指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、 IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议,type和protocol并不 是可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默 认协议。
(2)connect:
TCP客户端程序调用socket()创建socket fd之后,就可以调用connect()函数来连接服务器。如果客户端这时调用connect()发 出连接请求,服务器端就会接收到这个请求并使accept()返回,accept()返回的新的文件描述符就是对应到该客户的TCP连接, 通过这两个文件描述符(客户端connect的fd和服务器端accept返回的fd)就可以实现客户端和服务器端的相互通信。
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
struct sockaddr {
sa_family_t sin_family;//地址族
char sa_data[14]; //14字节,包含套接字中的目标地址和端口信息
};
struct sockaddr_in {
short int sin_family; /* 地址族 */
unsigned short int sin_port; /*16位 端口号 */
struct in_addr sin_addr; /* 32位Internet地址 */
unsigned char sin_zero[8]; /* 为了保持与struct sockaddr一样的长度 ,不使用*/
};
sockfd: 客户端的socket()创建的描述字
addr: 要连接的服务器的socket地址信息,这里面包含有服务器的IP地址和端口等信息 。放在结构体struct sockaddr或者struct sockaddr_in中。
addrlen: socket地址的长度
struct sockaddr和struct sockaddr_in的区别:从定义可以看出,两个结构体大小相同是为了相互转换。sockaddr在头文件#include <sys/socket.h>中定义,sockaddr的缺陷是:sa_data把目标地址和端口信息混在一起了。sockaddr_in在头文件#include<netinet/in.h>或#include <arpa/inet.h>中定义,该结构体解决了sockaddr的缺陷,把port和addr 分开储存在两个变量中。sockaddr常用于bind、connect、recvfrom、sendto等函数的参数,指明地址信息,是一种通用的套接字地址。
sockaddr_in 是internet环境下套接字的地址形式。所以在网络编程中我们会对sockaddr_in结构体进行操作,使用sockaddr_in来建立所需的信息,最后使用类型转化就可以了。一般先把sockaddr_in变量赋值后,强制类型转换后传入用sockaddr做参数的函数:sockaddr_in用于socket定义和赋值;sockaddr用于函数参数。
初始化结构体时问题又来了,由于socket是字节流传输,网络和本机有可能字节序不一样。就涉及到了本地字节序和网络字节序的相互转换,以下列出几个函数的用法方便查阅:
ipv4:
htons()作用是将端口号由主机字节序转换为网络字节序的整数值。(host to net)
htonl()作用和htons()一样,不过它针对的是32位的(long),而htons()针对的是两个字节,16位的(short)。与htonl()和htons()作用相反的两个函数是:ntohl()和ntohs()。
inet_addr()作用是将一个IP字符串转化为一个网络字节序的整数值,用于sockaddr_in.sin_addr.s_addr。
inet_ntoa()作用是将一个sin_addr结构体输出成IP字符串(network to ascii)。
具体用法可见代码或者看其他大神解析(码字太痛苦了,哈哈哈哈哈)
ipv6/ipv4都适用的:
将点分十进制的ip地址转化为用于网络传输的数值格式,返回值:若成功则为1,若输入不是有效的表达式则为0,若出错则为-1 int inet_pton(int family, const char *strptr, void *addrptr);
将数值格式转化为点分十进制的ip地址格式,返回值:若成功则为指向结构的指针,若出错则为NULL
const char * inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
附:大端小端字节序
1. Little endian:将低序字节存储在起始地址
2. Big endian:将高序字节存储在起始地址
(3)read,write:
除read,write之外还有两个函数也可以用来发送和读取数据,recv和send。它们的区别是:
recv和send函数提供了和read和write差不多的功能.不过它们提供了第四个参数来控制读写操作。
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
int send(int sockfd,void *buf,int len,int flags);
read()是负责从fd中读取内容。当读成功时read返回实际所读的字节数;如果返回的值是0表示已经读到文件的结束了,如果 是网络socke fd也就意味着TCP 链接断开了;小于0表示出现了错误并设置错误标志到errno全局变量中,如果错误为EINTR说明 读是由中断引起的,如果是ECONNREST表示网络连接出了问题;(read函数是从内核中的缓冲区读数据,并拷贝到自己定义的buf当中。当服务器未发送数据时,read函数会阻塞等待数据到来)
write()将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节数。失败时返回-1,并设置errno变量。在网络程序 中,当我们向套接字文件描述符写时有俩种可能。write的返回值大于0,表示写了部分或者全部的数据。返回的值小于0,此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。 如果为EPIPE表示 网络连接出现了问题(对方已经关闭了连接)。
client代码(每个函数最好都加if判断):
int client_socketfd;
socklen_t addrlen;
struct sockaddr_in serveraddr;
serveraddr.sin_family=AF_INET;//协议族
serveraddr.sin_port=htons(SERVERPORT); //端口SERVERPORT (自己定义的宏服务器端口)
serveraddr.sin_addr.s_addr=inet_addr(SERVER_IP); //SERVER_IP(自己定义的宏服务器地址)
//IP INADDR_ANY 可以不用htonl()转换成网络字节序
/*1.把ip地址转化为用于网络传输的二进制数值
* int inet_aton(const char *cp, struct in_addr *inp);
* 2.将网络传输的二进制数值转化为成点分十进制的ip地址
* char *inet_ntoa(struct in_addr in); */
bzero(&(serveraddr.sin_zero), 8);
client_socketfd = socket(AF_INET,SOCK_STREAM,0);
if(client_socketfd < 0)
{
printf("creat socket failure:%s\n",strerror(errno));
}
addrlen=sizeof(serveraddr);
if((connect(client_socketfd,(struct sockaddr *)&serveraddr,addrlen))<0)
{
printf("connect to server failure:%s\n",strerror(errno));
goto closefd;
}// !!!
/*int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);*/
printf("%s will be send to server\n",dest_str);
if(write(client_socketfd,dest_str,strlen(dest_str))<0)
{
printf("write data to server failure:%s\n",strerror(errno));
goto closefd;
}
closefd:
close(client_socketfd);
(4)bind:
当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个 具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数。通常服务器在启动的时候都会绑定一个众所周知的地址(如 ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,由系统自动分配一个端口号和自身 的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。当然客户端也可以在调用connect()之前bind一个地址和端口,这样就能使用特定的IP和端口来连服务器了。
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
sockfd:即创建socket()函数时返回的文件描述符,创建了唯一标识一个socket。bind()函数就是将给这个描述字绑定一 个名字。
addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,但终都会强制转换后赋值给sockaddr这种类型的指针传给内核。当指定端口号为0,那么内核就在bind被调用时选择一个临时端口。然而如果指定IP地址为通配地址,那么内核将在套接字已连接或已在套接字上发出数据报时才选择一个本地IP地址。
addrlen:对应的是地址的长度。一般用sizeof()
(5)listen:
listen函数使用主动连接套接口变为被连接套接口,使得一个进程可以接受其它进程的请求,从而成为一个服务器进程。在TCP服务器编程中listen函数把进程变为一个服务器,并指定相应的套接字变为被动连接,等待客户的连接请求。socket()函数创建的socket默认是一个主动类型的,如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听 这个socket。
int listen(int sockfd, int backlog);
sockefd: socket()系统调用创建的要监听的socket描述字
backlog: 相应socket可以在内核里排队的大连接个数。这个参数涉及到一些网络的细节。在进程正处理理一个一个连接请求的时候,可能还存在其它的连接请求。因为TCP连接是一个过程要三次握手才能建立连接,就是一种半连接的状态(服务器处于Listen状态时收到客户端SYN报文时放入半连接队列中,即SYN queue(服务器端口状态 为:SYN_RCVD)。)。如果同时有很多的连接请求,使得服务器进程无法快速地完成连接请求。如果这个情况出现了,服务器进程希望内核如何处理呢?内核会在自己的进程空间里维护一个队列以跟踪这些完成的连接但服务器进程还没有接手处理或正在进行的连接,这样的一个队列内核不可能让其任意大,所以必须有一个大小的上限。这个backlog告诉内核使用这个数值作为上限。经验值为 13
(6)accept:
TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。服务器之后就会调用accpet()接受来自客户端的连接请求,这个函数默认是一个阻塞函数,这也意味着如果没有客户端连接服务器的话该程序将一直阻塞着不会返回,直到有一个客户端连过来为止。一旦客户端调用connect()函数就会触发服务器的accept()返回,这时整个TCP链接就建立好 了。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd: 服务器开始调用socket()函数生成的,称为监听socket描述字;
struct sockaddr *addr: 用于返回客户端的协议地址,这个地址里包含有客户端的IP和端口信息等;
addrlen: 返回客户端协议地址的长度。
accept函数的返回值是由内核自动生成的一个全新的描述字(fd),代表与返回客户的TCP连接。如果想发送数据给该客户端, 则我们可以调用write()等函数往该fd里写内容即可;而如果想从该客户端读内容则调用read()等函数从该fd里读数据即可。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个新的socket描述字,当服务器完成了对某个客户的服务,就应当把该客户端相应的的socket描述字关闭。
int listen_fd;
struct sockaddr_in serveraddr;
serveraddr.sin_family=AF_INET;//协议族
serveraddr.sin_port=htons(LISTENPORT); //端口绑定
serveraddr.sin_addr.s_addr=INADDR_ANY;
bzero(&(serveraddr.sin_zero), 8);
int new_fd=-1;
struct sockaddr_in client_addr;
socklen_t addrlen;
listen_fd = socket(AF_INET,SOCK_STREAM,0);
if(listen_fd < 0)
{
printf("creat socket failure:%s\n",strerror(errno));
return -1;
}
if(bind(listen_fd,(struct sockaddr *)&serveraddr,sizeof(serveraddr)) < 0)
//int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
{
printf("bind failure:%s\n",strerror(errno));
return -2;
}
if(listen(listen_fd,BACKLOG) < 0)//int listen(int sockfd, int backlog);
{
printf("listen failure:%s\n",strerror(errno));
return -3;
}
while(!g_cilentstop)
{
printf("Waitting for client connection...\n");
new_fd=accept(listen_fd,(struct sockaddr *)&client_addr,&addrlen);
//int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
if(new_fd<0)
{
printf("accept a new client failure;%s\n",strerror(errno));
continue;
//return -4;
}
printf("accept a new cilent:fd[%d]\n",new_fd);
//new_fd,inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));
太多了分开写。。。。。

