3.1 Socket网络编程基础
3.1.1套接字概述
套接字就是网络编程的ID。网络通信,归根到底还是进程间的通信(不同计算机上的进程间的通信)。在网络中,每一个节点(计算机或路由器)都有一个网络地址,也就是IP地址,两个进程通信时,首先要确定各自所在网络节点的网络地址。但是,网络地址只能确定进程所在的计算机,而一台计算机上很可能同时运行着多个进程,所以仅凭网络地址还不能确定到底是和网络中哪一个进程通信,因此套接口中还需要有其他的信息,也就是端口号(port)。在一台计算机中,一个端口号一次只能分配给一个进程,也就是说,在一台计算机中,端口号和进程之间是一一对应的关系。所以,使用端口号和网络地址的组合就能唯一确定整个网络中的一个网络进程。
把网络地址和端口号信息放在一个结构体中,也就是套接口地址结构,大多数的套接口函数都需要一个指向套接口地址结构的指针作为参数,并以此来传递地址信息。每个协议族都定义了它自己的套接口地址结构,套接字地址结构都以"sockaddr_” 开头,并以每个协议族名中两个字母作为结尾。
下面是socket所在位置:
可以看到套接口有3种类型:
1)流式套接字(SOCK_STREAM)
流式套接字提供可靠的、面向连接的通信刘,保证数据传输的可靠性和按序收发。TCP通信使用的就是流式套接字。
2)数据包套接字(SOCK_DGRAM)
数据报套接字实现了一种不可靠、无连接的服务。数据通过相互独立的报文进行传输,是无序的,并且不保证可靠的传输。UDP通信使用的就是数据报套接字。
3)原始套接字(SOCK_RAW)
原始套接字允许对底层协议(如IP或ICMP)进行直接访问,它功能强大但使用较为不便,主要用于一些协议的开发。
3.1.2端口号
这里的端口号是逻辑意义上的端口,一般是指TCP/IP 协议中的端口,端口号的范围为0~65535,比如用于浏览网页服务(HTTP协议)的80端口,用于FTP服务的21端口等。其中, 0 到1023 一般被系统程序所使用。
那么TCP/IP协议中的端口指的是什么呢?举个例子,如果IP地址唯一指定了地球上某个地理位置的一间房子,端口号就是出入这间房子的门,只不过这个房子的门有65536个之多,端口是通过端口号来标记的,端口号是一个16位的整数,范围是从0~65535。
端口号只具有本地意义,即端口号只是为了标识本地计算机上的各个进程。在互联网中不同计算机的相同端口号是没有联系的。16bit 的端口号可允许有64K个端口号,这个数目对一个计算机来说是足够用的。
3.1.3 IP地址
1)IP地址的作用
IP地址用来表示网络中的一台主机。准确的说,IP地址是一台主机到一个网络的一个连接,因为现在一个主机中会有多个网卡。
IP地址包含两部分:网络号和主机号。其中,网络号和主机号根据子网掩码来区分。简单的说,有了源IP 和目标 IP,数据包就能在不同主机之间传输。
2)IP地址格式转换
IP地址有两种不同格式:十进制点分形式和32位二进制形式。前者是用户熟悉的形式,而后者则是网络传输中IP地址的存储方式。
这里主要介绍IPV4地址转换函数,主要有 inet_addr() 、inet_aton() 、inet_ntoa() 。前两者的功能都是将字符串转换成32位网络字节序二进制值,第三个将32位网络字节序二进制地址转换成点分十进制的字符串。
inet_addr() 函数语法如下:
所需头文件 | #include <arpa/inet.h> |
---|---|
函数原型 | in_addr_t inet_addr(const char *cp); |
函数参数 | cp:要转换的IP 地址字符串 |
函数返回值 | 成功:32位二进制的IP地址(网络字节序) 失败:-1 |
inet_pton() 函数语法如下:
所需头文件 | #include <arpa/inet.h> |
---|---|
函数原型 | int inet_pton(int af, const char src, void dst); |
函数参数 | af: AF_INET,ipv4协议,AF_INET6,ipv6协议 src:要转换的IP 地址字符串 dst:存放转换后的地址的缓冲区 |
函数返回值 | 成功:0 失败:-1 |
inet_ntop() 函数语法如下:
所需头文件 | #include <arpa/inet.h> |
---|---|
函数原型 | const char inet_ntop(int af, const void src,char *dst, socklen_t size); |
函数参数 | af:AF_INET,ipv4协议, AF_INET6,ipv6协议 src:要转换的IP 地址 dst:存放转换后的地址字符串的缓冲区 len:缓冲区的长度 |
函数返回值 | 成功:返回dst 失败:-1 |
3.1.4字节序
字节序又称为主机字节序 Host Byte Order,HBO,是指计算机中多字节整型数据的存储方式。字节序有两种:大端(高位字节存储在低位地址,低位字节存储在高位地址)和小端(和大端序相反,PC通常采用小端模式)。
为什么需要字节序?在网络通信中,发送方和接收方有可能使用不同的字节序;
为了保证数据接受后能被正确的解析处理,统一规定:数据以高位字节优先顺序在网络上传输。因此数据在发送前和接收后都需要在主机字节序和网络字节序之间转换。
1)函数说明
字节序转换涉及4个函数:htons() 、ntohs() 、htonl() 和 ntohl() 。这里的 h 代表 host , n 代表 network , s 代表 short , l 代表 long 。通常 16bit 的IP端口号用前两个函数处理,而 IP 地址用后两个函数来转换。调用这些函数只是使其得到相应的字节序,用户不需要知道该系统的主机字节序和网络字节序是否真的相等。如果两个相同不需要转换的话,该系统的这些函数会定义成空宏。
2)函数格式
所需头文件 | #include <arpa/inet.h> |
---|---|
函数原型 | uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort); |
函数参数 | hostlong:主机字节序的32bit数据 hostshort:主机子节序的16bit数据 netlong:网络字节序的32bit数据 netshort:网络字节序的16bit数据 |
函数返回值 | 成功:返回转换字节序的数值 失败:-1 |
3.1.5 TCP编程
socket()编程的基本函数有socket() 、bind()、listen()、accept()、send()、sendto()、recv()以及recvfrom()等。下面先简单介绍上述函数的功能,再结合流程图具体说明
1)socket() :该函数用于创建一个套接字,同时指定协议和类型。
2)bind() :该函数将保存在相应地址结构中的地址信息与套接字进行绑定。它主要用于服务器端,客户端创建的套接字可以不绑定地址。
3)listen() :在服务端程序成功建立套接字并与地址进行绑定以后,通过调用listen() 函数将TCP连接后,该函数会返回一个新的已连接套接字。
5)connect():客户端通过该函数向服务器端的监听套接字发送连接请求。
6)send() 和 recv():这两个函数通常在TCP通信过程中用于发送和接收数据,也可用于UDP中。
7)sendto()和recvfrom() :这两个函数一般在UDP通信过程中用于发送和接受数据。当用于TCP时,后面的几个与地址有关的参数不起作用,函数作用等同于 send() 和 recv()
服务器端和客户端使用TCP的流程如下:
可以看到通信工作的大致流程如下:
1)服务器先用socket() 函数来建立一个套接口,用这个套接口完成通信的监听及数据的收发;
2)服务器用bind() 函数来绑定一个端口号和IP地址,使套接口与制定的端口号和IP地址相关联;
3)服务器调用listen()函数,使服务器的这个端口和IP处于监听状态,等待网络中某一客户机的连接请求。
4)客户机调用socket()函数建立一个套接口,设定远程IP和端口。
5)客户机调用 connect() 函数链接远程计算机指定的端口。
6)服务器调用 accept() 函数来接受远程计算机的连接请求,建立起与客户机之间的通信连接。
7)建立连接以后,客户机用write() 函数 (或send()函数)向socket() 中写入数据,也可以用 read() 函数(或recv()函数)读取服务器发送来的数据。
8)服务器用 read()函数(或recv()函数)读取客户机发送来的数据,也可以用 write() 函数(或send()函数)来发送数据。
9)完成通信以后,使用close()函数关闭socket 连接。
函数格式:
1)创建套接口 socket() 函数
所需头文件 | #include <sys/types.h> #include <sys/socket.h> |
---|---|
函数原型 | int socket(int domain, int type, int protocol); |
函数参数 | domain:协议簇 type:套接字类型 protocol:0(原始套接字除外) |
函数返回值 | 成功:非负套接字描述符 失败:-1 |
参数domain指明协议族,取值如:
AF_INET:IPv4协议
AF_INET6:IPv6协议
AF_LOCAL:UNIX域协议
AF_ROUTE:路由套接字
AF_KEY:密钥套接字
这里“AF”代表“Adress Family”(地址族)
types指明通信字节流类型,其取值如:
SOCK_STREAM:流式套接字(TCP方式)
SOCK_DGRAM:数据包套接字(UDP方式)
SOCK_RAM:原始套接字
2)绑定端口 bind()函数
用socket() 函数创建一个套接口后,需要使用bind 函数在这个套接口上绑定一个指定的端口号和IP地址。原型如下:
所需头文件 | #include <sys/types.h> #include <sys/socket.h> |
---|---|
函数原型 | int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen); |
函数参数 | sockfd:套接字描述符 addr:绑定的地址 addrlen:地址长度 |
函数返回值 | 成功:0 失败:-1 |
这里my_addr是IPv4地址,IPv4 套接口地址数据结构以socketaddr_in 命名,定义在 <netinet/in.h>头文件中,形式如下:
struct sockaddr_in
{
short sin_family; /*Addressfamily 一般来说,AF_INET(地址族)PF_INET(协议族)*/
unsigned short sin_port; /* Portnumber (必须要采用网络数据格式,普通数字可以用htons()函数转换成网络数据格式的数字)*/
struct in_addr sin_addr; /* Internet address. */
unsigned char sin_zero[8]; /* 字符数组sin_zero[8]的存在是为了保证结构体struct sockaddr_in的大小和结构体struct sockaddr的大小相等 */
};
sin_famliy 为套接字结构协议族,如IPv4为AF_INET;
sin_port 是16位 TCP或UDP端口号,网络字节顺序;
结构体成员in_addr也是一个结构体,定义如下:
struct in_addr
{
uint32_t s_addr; /* address in network byte order */
};
这里s_addr为32位IPv地址,网络字节顺序;本地地址可以用INADDR_ANY;
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#define PORT 2345
int main()
{
int sockfd;
struct sockaddr_in addr;
int addr_len = sizeof(struct sockaddr_in);
if((sockfd = socket(AF_INET,SOCK_STREAM,0)) < 0)
{
perror("socket fail");
exit(-1);
}
else
{
printf("socket created successfully!\nsocket id is %d\n",sockfd);
}
memset(&addr,0,addr_len);
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(sockfd,(struct sockaddr *)(&addr),addr_len) < 0)
{
perror("bind error");
exit(-1);
}
else
{
printf("bind port successfully!\nlocal port:%d\n",PORT);
}
return 0;
}
执行结果如下:
3)等待监听函数
所谓监听,指的是socket 的端口一直处于等待的状态,监听网络中的所有客户机,耐心等待某一客户机发送请求。如果客户端有连接请求,端口就会接受这个连接。listen 函数用于实现服务器的监听等待功能,它的函数原型如下:
所需头文件 | #include <sys/types.h> #include <sys/socket.h> |
---|---|
函数原型 | int listen(int sockfd, int backlog); |
函数参数 | sockfd: 套接字描述符 backlog:请求队列的最大数,大多数默认为5 |
函数返回值 | 成功:0 失败:-1 |
需要注意的是listen 并未真正的接受连接,只是设置socket 的状态为监听模式,真正接受客户端连接的是accept 函数。通常情况下,listen 函数会在 socket ,bind 函数之后调用,然后才会调用 accept 函数。
listen函数只适用于SOCK_STREAM或SOCK_SEQPACKET 的socket 类型。如果socket 为 AF_INET ,则参数 backlog 最大值可设至128,即最多可以同时接受128个客户端的请求。
4)接受连接函数
服务器处于监听状态时,如果模式可获得客户机的连接请求,此时并不是立即处理这个请求,而是将这个请求放在等待队列中,当系统空闲时,再处理客户机的连接请求,接受连接请求的函数时accept,函数原型如下:
所需头文件 | #include <sys/types.h> #include <sys/socket.h> |
---|---|
函数原型 | int accept(int sockfd, struct sockaddr addr, socklen_t addrlen); |
函数参数 | sockfd: 套接字描述符 addr:用于保存客户端的地址 addrlen:地址长度 |
函数返回值 | 成功:建立连接好的套接字描述符 失败:-1 |
当 accept 函数接受一个连接时,会返回一个新的 socket 标识符,以后的数据传输与读取就是通过这个新的socket 编号来处理,原来参数中的 socket 也可以继续使用。接受连接以后,远程主机的地址和端口信息会保存在 addr 所指的结构体内。
下面是个实例,体验listen 、accept函数的使用:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 2345
int main()
{
int sockfd,newsockfd;
struct sockaddr_in addr,caddr;
int addr_len = sizeof(struct sockaddr_in);
int caddr_len = sizeof(struct sockaddr_in);
if((sockfd = socket(AF_INET,SOCK_STREAM,0)) < 0)
{
perror("socket error");
exit(-1);
}
else
{
printf("socket successfully!\n");
printf("socket id : %d\n",sockfd);
}
memset(&addr,0,addr_len);
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(sockfd,(struct sockaddr *)&addr,addr_len) == -1)
{
perror("bind error");
exit(-1);
}
else
{
printf("bind successfully!\n");
printf("local port : %d\n",PORT);
}
if(listen(sockfd,5) == -1)
{
perror("listen error");
exit(-1);
}
else
{
printf("listening...\n");
}
if((newsockfd = accept(sockfd,(struct sockaddr *)&caddr,&caddr_len)) == -1)
{
perror("accept error");
exit(-1);
}
else
{
printf("accepted a new connection ..\n");
printf("new socket id : %d\n",newsockfd);
}
return 0;
}
执行程序,得到输出结果:
程序运行到这停止,并一直在这里等待,说明本机计算机的 2345号端口正处于监听的状态,等待本机上的连接服务请求。此时打开浏览器,在浏览器地址栏中输入下列形式的地址:
这个地址是笔者个人IP地址,按"ENTER" 键,这样浏览器会请求连接本地计算机上的2345号端口。此时终端中显示如下结果:
accepted a new connection ..
new socket id : 4
表明程序已经接受了这个连接,并创建了一个新的套接口(ID为4),然后退出了程序。
3.1.6请求连接函数
所谓请求连接,是指在客户机向服务器发送信息之前,需要先发送一个连接请求,请求与服务器建立TCP通信连接。connect 函数可以完成这项功能,函数原型如下:
所需头文件 | #include <sys/types.h> #include <sys/socket.h> |
---|---|
函数原型 | int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen); |
函数参数 | sockfd: 套接字描述符 addr:服务器地址 addrlen:地址长度 |
函数返回值 | 成功:0 失败:-1 |
这里ser_addr 是一个结构体指针,指向一个sockaddr 结构体,这个结构体存储着远处服务器的IP与端口号信息。
3.1.7数据读写函数
TCP/UDP读写函数总结,注意函数要成对使用。
1)send函数
建立套接口并完成通信连接以后,可以把信息传送到远程主机上,这个过程就是信息的发送。而对于远程主机发送来的信息,本地主机需要进行接收处理。下面开始讲述这种面向连接的套接口信息发送与接收操作。
用connect 函数连接到远程计算机以后,可以用 send 函数将应答信息发送给请求服务的本地主机,通信时双向的,并且通信的双方是对等的。
send() 函数原型如下:
所需头文件 | #include <sys/types.h> #include <sys/socket.h> |
---|---|
函数原型 | ssize_t send(int sockfd, const void *buf, size_t len, int flags); |
函数参数 | sockfd:套接字描述符 buf:发送缓冲区的地址 len:地址长度 flags:一般为0 |
函数返回值 | 成功:实际发送的字节数 失败:-1 |
2)recv()函数
函数recv 可以接收远程主机发送来的数据,并将这些数据保存到一个数组中,函数原型如下:
所需头文件 | #include <sys/types.h> #include <sys/socket.h> |
---|---|
函数原型 | ssize_t recv(int sockfd, void *buf, size_t len, int flags); |
函数参数 | sockfd:套接字描述符 buf:存放数据缓冲区的地址 len:接收数据的长度 flags:一般为0 |
函数返回值 | 成功:实际接收的字节数 失败:-1 |
3.2 TCP粘包问题及解决方案
TCP是一个基于字节流的传输服务,"流"意味着TCP所传输的数据是没有边界的。这不同于UDP提供基于消息的传输服务,其传输的数据是有边界的。TCP的发送方无法保证对等方每次接收到的是一个完整的数据包。主机A向主机B发送两个数据包,主机B的接收情况可能是以下情况:
产生粘包问题的原因有以下几个:
第一 ,应用层调用write方法,将应用层的缓冲区中的数据拷贝到套接字的发送缓冲区。而发送缓冲区有一个SO_SNDBUF的限制,如果应用层的缓冲区数据大小大于套接字发送缓冲区的大小,则数据需要进行多次的发送。
第二种情况是,TCP所传输的报文段有MSS的限制,如果套接字缓冲区的大小大于MSS,也会导致消息的分割发送。
第三种情况由于链路层最大发送单元MTU,在IP层会进行数据的分片。
这些情况都会导致一个完整的应用层数据被分割成多次发送,导致接收对等方不是按完整数据包的方式来接收数据。
粘包的问题的解决思路:
粘包问题的最本质原因在与接收对等方无法分辨消息与消息之间的边界在哪。我们通过使用某种方案给出边界,例如:
- 发送定长包。如果每个消息的大小都是一样的,那么在接收对等方只要累计接收数据,直到数据等于一个定长的数值就将它作为一个消息。
- 包尾加上\r\n标记。FTP协议正是这么做的。但问题在于如果数据正文中也含有\r\n,则会误判为消息的边界。
- 包头加上包体长度。包头是定长的4个字节,说明了包体的长度。接收对等方先接收包体长度,依据包体长度来接收包体。
- 使用更加复杂的应用层协议。
粘包解决方案一:使用定长包
这里需要封装两个函数:
ssize_t readn(int fd, void *buf, size_t count)
ssize_t writen(int fd, void *buf, size_t count)
这两个函数的参数列表和返回值与read、write一致。它们的作用的读取/写入count个字节后再返回。其实现如下:
ssize_t readn(int fd, void *buf, size_t count)
{
int left = count ; //剩下的字节
char * ptr = (char*)buf ;
while(left>0)
{
int readBytes = read(fd,ptr,left);
if(readBytes< 0)//read函数小于0有两种情况:1中断 2出错
{
if(errno == EINTR)//读被中断
{
continue;
}
return -1;
}
if(readBytes == 0)//读到了EOF
{
//对方关闭呀
printf("peer close\n");
return count - left;
}
left -= readBytes;
ptr += readBytes ;
}
return count ;
}
/*
writen 函数
写入count字节的数据
*/
ssize_t writen(int fd, void *buf, size_t count)
{
int left = count ;
char * ptr = (char *)buf;
while(left >0)
{
int writeBytes = write(fd,ptr,left);
if(writeBytes<0)
{
if(errno == EINTR)
continue;
return -1;
}
else if(writeBytes == 0)
continue;
left -= writeBytes;
ptr += writeBytes;
}
return count;
}
有了这两个函数之后,我们就可以使用定长包来发送数据了,我抽取其关键代码来讲诉:
char readbuf[512];
readn(conn,readbuf,sizeof(readbuf)); //每次读取512个字节
同理的,写入的时候也写入512个字节
char writebuf[512];
fgets(writebuf,sizeof(writebuf),stdin);
writen(conn,writebuf,sizeof(writebuf);
每个消息都以固定的512字节(或其他数字,看你的应用层的缓冲区大小)来发送,以此区分每一个信息,这便是以固定长度解决粘包问题的思路。定长包解决方案的缺点在于会导致增加网络的负担,无论每次发送的有效数据是多大,都得按照定长的数据长度进行发送。
粘包解决方案二:使用结构体,显式说明数据部分的长度
在这个方案中,我们需要定义一个‘struct packet’包结构,结构中指明数据部分的长度,用四个字节来表示。发送端的对等方接收报文时,先读取前四个字节,获取数据的长度,由长度来进行数据的读取。定义一个结构体
struct packet
{
unsigned int msgLen ; //4个字节字段,说明数据部分的大小
char data[512] ; //数据部分
}
读写过程如下所示,这里抽取关键代码进行说明:
//发送数据过程
struct packet writebuf;
memset(&writebuf,0,sizeof(writebuf));
while(fgets(writebuf.data,sizeof(writebuf.data),stdin)!=NULL)
{
int n = strlen(writebuf.data); //计算要发送的数据的字节数
writebuf.msgLen =htonl(n); //将该字节数保存在msgLen字段,注意字节序的转换
writen(conn,&writebuf,4+n); //发送数据,数据长度为4个字节的msgLen 加上data长度
memset(&writebuf,0,sizeof(writebuf));
}
下面是读取数据的过程,先读取msgLen字段,该字段指示了有效数据data的长度。依据该字段再读出data。
memset(&readbuf,0,sizeof(readbuf));
int ret = readn(conn,&readbuf.msgLen,4); //先读取四个字节,确定后续数据的长度
if(ret == -1)
{
err_exit("readn");
}
else if(ret == 0)
{
printf("peer close\n");
break;
}
int dataBytes = ntohl(readbuf.msgLen); //字节序的转换
int readBytes = readn(conn,readbuf.data,dataBytes); //读取出后续的数据
if(readBytes == 0)
{
printf("peer close\n");
break;
}
if(readBytes<0)
{
err_exit("read");
}
粘包解决方案三:按行读取
ftp协议采用/r/n来识别一个消息的边界,我们在这里实现一个按行读取的功能,该功能能够按/n来识别消息的边界。这里介绍一个函数:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
与read函数相比,recv函数的区别在于两点:
- recv函数只能够用于套接口IO。
- recv函数含有flags参数,可以指定一些选项。
recv函数的flags参数常用的选项是:
- MSG_OOB 接收带外数据,即通过紧急指针发送的数据
- MSG_PEEK 从缓冲区中读取数据,但并不从缓冲区中清除所读数据
为了实现按行读取,我们需要使用recv函数的MSG_PEEK选项。PEEK的意思是"偷看",我们可以理解为窥视,看看socket的缓冲区内是否有某种内容,而清除缓冲区。
/*
* 封装了recv函数
返回值说明:-1 读取出错
*/
ssize_t read_peek(int sockfd,void *buf ,size_t len)
{
while(1)
{
//从缓冲区中读取,但不清除缓冲区
int ret = recv(sockfd,buf,len,MSG_PEEK);
if(ret == -1 && errno == EINTR)//文件读取中断
continue;
return ret;
}
}
下面是按行读取的代码:
/*
*读取一行内容
* 返回值说明:
== 0 :对端关闭
== -1 : 读取错误
其他:一行的字节数,包含\n
*
**/
ssize_t readLine(int sockfd ,void * buf ,size_t maxline)
{
int ret ;
int nRead = 0;
int left = maxline ;
char * pbuf = (char *) buf;
int count = 0;
while(true)
{
//从socket缓冲区中读取指定长度的内容,但并不删除
ret = read_peek(sockfd,pbuf,left);
// ret = recv(sockfd , pbuf , left , MSG_PEEK);
if(ret<= 0)
return ret;
nRead = ret ;
for(int i = 0 ;i< nRead ; ++i)
{
if(pbuf[i]=='\n') //探测到有\n
{
ret = readn (sockfd , pbuf, i+1);
if(ret != i+1)
exit(EXIT_FAILURE);
return ret + returnCount;
}
}
//如果嗅探到没有\n
//那么先将这一段没有\n的读取出来
ret = readn(sockfd , pbuf , nRead);
if(ret != nRead)
exit(EXIT_FAILURE);
pbuf += nRead ;
left -= nRead ;
count += nRead;
}
return -1;
}