文章目录
- 1 TCP协议
- 1.1 TCP 基础
- 1.1.1 TCP 特性
- 1.2.2 TCP连接数
- 1.2 TCP 头
- 1.2.1 TCP 头格式
- 1.2.2 MTU,MSS,分片传输
- 1.3 TCP 连接三路握手
- 1.4 TCP 断开四次挥手
- 1.5 SYN攻击和防范
- 1.6 重传机制
- 1.6.1 超时重传
- 1.6.2 快速重传
- 1.6.3 SACK
- 1.7 滑动窗口
- 1.8 流量控制
- 1.9 拥塞控制
- 1.10 TCP Socket编程
1 TCP协议
1.1 TCP 基础
1.1.1 TCP 特性
TCP 是一种面向连接的,可靠的,基于字节流传输层通信协议,TCP 能确保接收端接收的网络包无损坏,无间隔,非冗余(即同一数据包只接收一次),按序的:
- 面向连接:首先,面向连接是一对一的,面向连接在通信前,需要先建立链接才能传输数据;
- 可靠性:无论网络链路中出现什么变化, TCP 都可以保证报文到达接收端;
- 字节流:数据以字节流形式进行传输,因此数据可以无限拓展(不超过MTU最大传输单元);
确定一个 TCP 连接,需要 TCP 四元组:源地址 : 源端口 + 目的地址 : 目的端口。还有如下基础概念:
- Socket套接字:套接字由 IP 地址和 Port 端口号组成;
- IP 地址:(IPv4 32位)主机之间通过 IP 地址识别报文应属于哪一个主机;
- Port 端口:(16位)端口用来识别报文属于一台主机上的哪一个进程;
- 序列号:用来解决乱序问题;
- 窗口大小:用来做流量控制;
由于 TCP 是面向连接,能保证数据是一定被交付的,因此常用于:
-
FTP 文件传输
-
HTTP / HTTPS连接
1.2.2 TCP连接数
TCP 的连接数量永远到达不了 IP x Port 数的理论上限,主要受限于以下两点:
-
内存限制,每个TCP都会占用一定的内存(2~4KB),但操作系统的内存是有限的。
-
文件描述符限制(一般是1024),因为 Socket 也是文件描述符的一种。解除文件描述符有如下方法:
# 这是临时修改文件描述符上限的方法 # (1)查看文件描述符的限制 ulimit -n # (2)修改上限,但系统重启后就不再生效 ulimit -n 100000
# 这是永久修改文件描述符上限的方法 # (1)用nano或vim打开limits.conf配置文件 sudo nano /etc/security/limits.conf sudo vim /etc/security/limits.conf # (2)添加代码,其中 * 代表所有用户,可以替换成用户名来单独取消某一用户的文件描述符上限。 * soft nofile 100000 * hard nofile 100000 # (3)重启系统
1.2 TCP 头
1.2.1 TCP 头格式
TCP 头 20~60 字节长
字段 | 长度 | 含义 |
---|---|---|
源端口 | 16bit | 源端口,标识是哪个应用程序发送的 |
目的端口 | 16bit | 目的端口,标识哪个应用程序接收 |
序列号(client_isn和server_isn) | 32bit | 序号字段,用来标识发送顺序,因为接收端不一定是按发送顺序接收到报文的。 |
首部长度 | 4bit | 首部长度指出TCP报文段的数据起始处距离TCP报文段的起始处有多远,以32比特(4字节)为计算单位。最多有60字节的首部,若无选项字段,正常为20字节。 |
保留位 | 6bit | 必须填0 |
URG | 1bit | 紧急指针有效标志位 |
ACK | 1bit | 应答位 |
PSH | 1bit | 紧急位 |
RST | 1bit | 该位置为1的时候,表示 TCP 出现了异常,必须断开连接 |
SYN | 1bit | 发起请求的信号 |
FIN | 1bit | 表示希望断开连接 |
窗口大小 | 16bit | 窗口的大小 |
校验和 | 16bit | CRC校验和 |
紧急指针 | 16bit | 用紧急指针来表明紧急数据在数据流的哪个位置 |
选项 | ||
数据 |
TCP 数据长度 = IP 总长度 - IP首部长度 - TCP 首部长度
1.2.2 MTU,MSS,分片传输
- MTU:一个网络包的最大长度,以太网中一般默认设置为 1500 字节。
- MSS:出去 IP 和 TCP 头,一个网络包中 TCP 数据的最大长度。
为什么我们有了 IP 分片之后,还需要 TCP 分片呢?
当 IP 层有一个超过 MTU 长度的报文需要发送的时候,如果一个 IP 分片丢失,那么所有的 IP 分片都需要重新传送,而有了 MSS 之后,当发现数据长度超过了 MSS 之后,就先进行分片,这样就能避免这个问题了。
1.3 TCP 连接三路握手
- 客户端和服务端同时处于
CLOSE
关闭状态。 - 服务端将 Socket 套接字设置为被动
LISTEN
状态。准备接收客户端发来的 connect() 连接。 - 客户端通过 connect() 发起请求,此时客户端会随机初始化序列号(client_isn)。同时把 SYN 位设置为1。发送报文,随后客户端进入
SYN_SENT
状态。 - 服务端接收到报文后,会初始化自己的(server_isn)序列号,同时将收到的(client_isn)+1 然后填入到确认应答号中,之后把 SYN 和 ACK 位设置为1。发送报文,随后服务端进入
SYN_RCVD
状态。 - 客户端接收到报文后,同样的将收到的(server_isn)+1 填入到确认应答号中,把 SYN 位设置为1。发送报文,随后客户端进入
ESTABLISHED
状态。 - 服务端接收到报文后,进入
ESTABLISHED
状态。
完成三次握手后,客户端服务端都处于 ESTABLISHED
状态,双方就可以相互发送数据了。值得一提的是,第一次和第二次握手是不能携带数据的,但第三次握手是可以携带数据的。
Linux中我们可以使用netstat -napt
命令查看 TCP 状态:
需要三次握手,而不是两次,四次的原因:
因为三次握手才能保证双方具有接收和发送的能力。
例如 A 和 B 相约好去打篮球:
- A 向 B 发消息:“下午五点咱去打篮球”。此时 B 收到了消息,但 A 并不知道 B 是否收到了消息;
- B 向 A 发消息:“好的没问题,记得带球”。此时 A 知道了 B 收到了自己的消息,但是 B 并不知道 A 是否收到了自己的回复;
- 所以此时还需要 A 给 B 发消息确认:“OK”。至此, A 和 B 才能确认彼此都收到了自己的消息。
即:
- 三次握手才可以阻止历史重复连接的初始化(主要原因)
- 三次握手才可以同步双方的初始序列号
- 三次握手才可以避免资源浪费
1.4 TCP 断开四次挥手
1.5 SYN攻击和防范
我们都知道 TCP 连接建立是需要三次握手,若发送大量不同 IP 地址的 SYN
报文到同一个服务器,服务端每接收到一个 SYN
报文,就进入SYN_RCVD
状态,但服务端发送出去的 ACK + SYN
报文,无法得到未知 IP 主机的 ACK
应答,久而久之就会占满服务端的 SYN 接收队列(未连接队列),使得服务器不能为正常用户服务。
避免方式:
(1)修改 Linux 内核参数
# 当网卡接收数据包的速度大于内核处理的速度时,会有一个队列保存这些数据包。控制该队列的最大值如下参数:
net.core.netdev_max_backlog# SYN_RCVD 状态连接的最大个数:
net.ipv4.tcp_max_syn_backlog# 超出处理能时,对新的 SYN 直接回 RST,丢弃连接:
net.ipv4.tcp_abort_on_overflow
(2)启动 cookie
正常流程:
- 当服务端接收到客户端的 SYN 报文时,会将其加入到内核的「 SYN 队列」;
- 接着发送 SYN + ACK 给客户端,等待客户端回应 ACK 报文;
- 服务端接收到 ACK 报文后,从「 SYN 队列」移除放入到「 Accept 队列」;
- 应用通过调用
accpet()
socket 接口,从「 Accept 队列」取出的连接。
而启动cookie后,服务端接收到客户端的应答报文时,服务器会检查这个 ACK 包的合法性。如果合法,直接放入到 Accept 队列。不合法则丢弃。
1.6 重传机制
TCP 通过序列号与确认应答实现可靠传输,TCP 中,发送端的数据到达接收主机时,接收端主机会返回一个 ACK 确认应答消息。
这是数据正常传输的情况,但若TCP数据包丢失时,TCP 会怎么办呢?没错,就是重传机制。
1.6.1 超时重传
发送数据时,发送端会设定一个定时器,当定时器超时,发送端仍未收到对方的 ACK 报文时,发送端就会重新发送该数据。
基于这个原理,下面这两种情况会导致超时重传:
- 数据包丢失
- 确认信号 ACK 丢失
那么定时器的时间,超时时间 (RTO) 我们应该如何设置呢?他与 RTO(数据从一端到达另一端的时间) 有关。
超时时间 RTO 应该略大于我们的 RTT ,过大或过小都不合适,Linux 计算 RTO 有个公式大概原理是:
- 需要 TCP 通过采样 RTT 的时间,然后进行加权平均,算出一个平滑 RTT 的值,而且这个值还是要不断变化的,因为网络状况不断地变化。
- 除了采样 RTT,还要采样 RTT 的波动范围,这样就避免如果 RTT 有一个大的波动的话,很难被发现的情况。
如果超时重发的数据,再次超时的时候,又需要重传的时候,TCP 的策略是超时间隔加倍。
也就是**每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。**这样就避免了大量重复发送的同一报文使网络变得更加拥堵。
1.6.2 快速重传
TCP 还有一种快速重传的机制,它不以时间为判断依据,是以数据为判断依据。
- 当发送端重复收到了消息 seq1 的ACK2 信号,那么就证明 seq2 没有被接收。
- 发送端接收到三个同样的ACK信号后,就知道了seq2并没有被收到。
- 于是就会在定时器超时前,重传seq2,但是因为3,4,5都被收到了,于是此时ACK会回复6。
对于上面的例子来说,假如我们现在有Seq2、Seq3、Seq4、Seq5、Seq6、Seq7、Seq8、Seq9、这么多消息,当发送端接收了三次ACK信号时,我们并不知道,这三次ACK 代表的是Seq2、Seq7、Seq9、收到触发的ACK信号,还是Seq3、Seq5、Seq6收到触发的ACK信号,因此我们并不清除这连续的三个 ACK2 代表的是谁被接收了,我们就不知道之后的这几条消息里,我们应该重传那些 TCP 报文,于是我们就有了 SACK 方法。
1.6.3 SACK
这种方式需要在 TCP 头部「选项」字段里加一个 SACK
的东西,它可以将缓存的地图发送给发送方,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据。
如下图,发送方收到了三次同样的 ACK 确认报文,于是就会触发快速重发机制,通过 SACK
信息发现只有 200~299
这段数据丢失,则重发时,就只选择了这个 TCP 段进行重复。
若要支持 SACK
。在 Linux 下,需要通过 net.ipv4.tcp_sack
参数打开这个功能(Linux 默认打开)。
还有一种技术叫做 Duplicate SACK。Duplicate SACK 又叫 D-SACK
,其主要使用了 SACK 来告诉「发送方」有哪些数据被重复接收了。
使用 D-SACK 有如下好处 :
- 可以让发送方知道,是发出去的包丢了,还是接收方回应的 ACK 包丢了;
- 可以知道是不是发送方的数据包被网络延迟了;
- 可以知道网络中是不是把发送方的数据包给复制了;
若要支持 DSACK
。在 Linux 下,需要通过 net.ipv4.tcp_dsack
参数打开这个功能(Linux 默认打开)。
1.7 滑动窗口
若 TCP 每发送一个数据,就要进行一次确认的应答,这样的模式效率低下。那么如何解决这个问题呢?使用滑动窗口:
窗口的大小就是一次可以无需等待确认应答,可以继续发送数据的最大值。窗口的实现,实际上是操作系统开辟了一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,此时数据就可以从缓存区清除。
ACK 600 确认应答报文丢失,也没关系,操作系统会在下一个确认应答进行确认,只要发送方收到了 ACK 700 确认应答,就意味着 700 之前的所有数据接收方都收到了。这个模式就叫累计确认、累计应答。通常窗口的大小是由接收方的决定的。
发送窗口构成:
接受窗口构成:
1.8 流量控制
TCP 提供一种机制可以让 发送方 根据 接收方 的实际接收能力控制发送的数据量,这就是流量控制。
其实流量控制就是通过之前的滑动窗口来实现的,例如如下例子:
这就是流量控制,但是滑动窗口的大小不代表我们系统可以接收的缓存的大小,那么他们之间是什么关系呢?实际上,窗口中存放的字节数,都是放在操作系统内存缓冲区中的,而操作系统的缓冲区,会被操作系统调整,此时,窗口也会被调整。当应用进程没办法及时读取缓冲区的内容时,也会对我们的缓冲区造成影响。
窗口糊涂综合征
想象一下,若接收端需要接受很多的数据,接收窗口越来越小,每一次只能腾出几个字节的接收窗口,但是我们的发送端也会义无反顾的发送报文,为了几个字节,我们需要多发 TCP + IP 头40个字节的数据,这会影响我们的通信速率,同时还会影响我们的系统的开销,不断地进行数据的拷贝。那么我们如何解决呢?
解决方法就是接收方不通知小窗口给发送方,发送方也不发送小数据给接收方,我们可以通过 Nagle 延时算法来避免这个问题:
- 等到窗口大小 >=
MSS
或是 数据大小 >=MSS
后再发送 - 收到之前发送数据的
ack
再发送
只要没满足上面条件中的一条,发送方一直在囤积数据,直到满足上面的发送条件。
Nagle 算法默认是打开的,如果对于一些需要小数据包交互的场景的程序,我们需要关闭这个算法:可以在 Socket 设置 TCP_NODELAY
选项来关闭这个算法。
1.9 拥塞控制
拥塞控制和流量控制是不同的,想象从水龙头用一根水管往水桶内接水,以此来模拟 TCP 数据传输的过程,我们上面提到的流量控制是因为桶太小,所以我们需要控制水龙头出水的流速,以此来保证桶中的水不会因为水龙头流速太快导致水溢出,放到 TCP 中去理解,就是发送端主动减少发送流量,以此来避免接收端接收数据的能力不够,导致接收出错或者效率减慢。
而拥塞控制是水管中已经有了很多很多的水,即网络中已经含有很多数据了,这时候水龙头的流速过大反而会使得水管中的水流更加拥挤,所以我们主动调节水龙头的水流量,放到 TCP 中去理解,就是发送端主动控制发送流量,以此来避免造成网络信道的拥挤。
拥塞控制主要靠三个算法来实现:
- 慢启动
- 拥塞避免
- 快速恢复
为了实现拥塞控制,我们定义了一个叫做拥塞窗口 (swnd) 的概念,拥塞窗口是发送方的一个窗口,他会根据网络的拥塞程度进行动态变化,可以简单理解为是网络拥塞时 TCP 发送窗口的数量。同时还有一个叫做慢启动门限(ssthresh)的概念,他的作用是判定什么时候使用慢启动算法,什么时候是使用拥塞避免算法:
- 当
cwnd < ssthresh
时,使用慢启动算法。 - 当
cwnd >= ssthresh
时,使用拥塞避免算法。
拥塞控制过程
- 一开始采用慢启动过程,这个过程是 cwnd 指数级增长的:1、2、4、8…。
- 达到 ssthresh 门限后,会进入拥塞避免算法,这个过程拥塞窗口 cwnd 是线性增加的。
- 若遇到超时情况,会重新开始慢启动过程,同时将 ssthresh 设置为之前的 1/2 。
- 进入拥塞避免算法后,若遇到三次 ACK 应答,即快速重传的情况,我们会使用快速恢复算法。
- 此时不会进入慢启动过程从零开始,而是从上一次拥塞避免的 ssthresh 的数值开始线性增长。
拥塞处理的过程:
1.10 TCP Socket编程
//服务端代码
/*********************************************************************************实现socket_server服务器,并且每隔接收一次温度********************************************************************************/#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <dirent.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <getopt.h>
#include <arpa/inet.h>#define MAX_LISTEN 13
#define LISTEN_PORT 8999int help_printf( char *program );//打印使用信息
int socket_init(int *listen_fd,int listen_port);//进行socket初始化int main( int argc,char *argv[])
{int listen_fd = -1;int client_fd = -1;int choo = -1;int daemon_var = 0;//用来决定是否后台运行int listen_port = LISTEN_PORT;int rv =-1;char buf[1024];struct option opts[] = {{"help",no_argument,NULL,'h'},{"port",required_argument,NULL,'p'},{"daemon",required_argument,NULL,'d'},{0,0,0,0}};//选项系统while((choo = getopt_long(argc,argv,"hp:d:",opts,NULL)) != -1){switch(choo){case 'h':help_printf(argv[0]);return 0;case 'p':listen_port = atoi(optarg);//optarg 不是值,而是指向选项后参数的指针break;case 'd':daemon_var = atoi(optarg);break;}}if (listen_port == LISTEN_PORT) {printf("server will listen default port:%d\n",listen_port);}else{printf("server will listen port:%d\n",listen_port);}//是否转移到后台运行if ( daemon_var ){daemon(0,0);}if (socket_init(&listen_fd,listen_port) < 0){printf("socket_init error:[%s]\n",strerror(errno));return -1;}printf("listen_fd:[%d]\n",listen_fd);while(1){//开始accept过程if ((client_fd = accept(listen_fd,NULL,NULL)) < 0){printf("accept error:[%s]\n",strerror(errno));close(listen_fd);return -2;}printf("client_fd[%d]\n",client_fd);while(1){rv = read(client_fd,buf,sizeof(buf));if(rv<0){printf("error or disconnect[%s]\n",strerror(errno));close(client_fd);return 0;}if(rv == 0){printf("client disconnect and waiting new clientfd connet\n");close(client_fd);break;}printf("client message:[%dbytes]%s\n",rv,buf);if (write(client_fd,"Receive success\n",sizeof("Receive success\n")) < 0){printf("error:[%s]\n",strerror(errno));continue;}}continue;//若客户端断开,rv=0后break,重新到accept环节去监听socketfd}return 0;
}//打印使用信息函数
int help_printf( char *program )
{if (NULL == program ){printf("help_printf arguments error[%s]\n",strerror(errno));return -1;}printf("%s usage:\n",program);printf("--help (-h) : get help menu\n");printf("--port (-p) : listen port \n");printf("--daemon(-d): [0]un_daemon use ,[1]daemon use\n");return 0;
}//socket_init初始化
int socket_init(int *listen_fd,int listen_port)
{struct sockaddr_in servaddr_in;int opt = 1; printf("start init socket...\n");if ((*listen_fd = socket(AF_INET,SOCK_STREAM,0)) < 0){printf("socket failture:%s\n",strerror(errno));return -1;}//printf("listen_fd[%d]\n",*listen_fd);setsockopt(*listen_fd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));memset(&servaddr_in,0,sizeof(servaddr_in));servaddr_in.sin_family = AF_INET;servaddr_in.sin_port = htons(listen_port);servaddr_in.sin_addr.s_addr = htonl(INADDR_ANY);if (bind(*listen_fd,(struct sockaddr *)&servaddr_in,sizeof(servaddr_in)) < 0 ){printf("bind error[%s]\n",strerror(errno));close(*listen_fd);return -2;}printf("bind success!\n");if (listen(*listen_fd,MAX_LISTEN) < 0){printf("listen error[%s]\n");close(*listen_fd);return -3;}printf("listen_fd[%d]\n",*listen_fd);printf("init_socket finish...\n");return 0;
}
//客户端代码
/*********************************************************************************每十秒上报一次温湿度的客户端********************************************************************************/
#include <stdio.h>
#include <sys/types.h>
#include <string.h>
#include <dirent.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <getopt.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>int ds18b20(float *temp);
void help_printf(char *program);
int socket_client(int *sock_fd,char *listen_ip,int *listen_port);int main(int argc, char *argv[])
{int sock_fd = -1;int listen_port = -1;char *listen_ip = NULL;int choo = -1;float temp = -1;char buf[1024];int rv = -1;struct option opts[]={{"help",no_argument,NULL,'h'},{"port",required_argument,NULL,'p'},{"ip",required_argument,NULL,'i'},{0,0,0,0}};while( (choo = getopt_long(argc,argv,"hp:i:",opts,NULL)) != -1 ) {switch(choo){case 'h':help_printf(argv[0]);return 0;case 'p':listen_port = atoi(optarg);break;case 'i':listen_ip = optarg;break;}}//printf("will connect ip:%s port:%d\n",listen_ip,listen_port);if ( (listen_port == 0) || (listen_ip == NULL) ){help_printf(argv[0]);return -1;}printf("will connect ip:%s port:%d\n",listen_ip,listen_port);//ds18b20(&temp);//printf("real-time temperature:%.2f'c\n",temp);//sock过程开始if (socket_client(&sock_fd,listen_ip,&listen_port) < 0){printf("error:%s\n",strerror(errno));return -2;}while(1){ ds18b20(&temp);printf("real-time temperature:%.2f'c\n",temp);memset(buf,0,sizeof(buf));snprintf(buf,sizeof(buf),"temp:%.2f",temp);if ((rv = write(sock_fd,buf,strlen(buf))) < 0){printf("write error\n");close(sock_fd);return -3;}if ((rv = read(sock_fd,buf,sizeof(buf))) <= 0){printf("error:%s\n",strerror(errno));close(sock_fd);return -4;}printf("server message:%s\n",buf);sleep(10);}return 0;
}int socket_client(int *sock_fd,char *listen_ip,int *listen_port)
{int client_fd = -1;struct sockaddr_in clieaddr_in;if ((*sock_fd = socket(AF_INET,SOCK_STREAM,0)) < 0){printf("socket error:%s\n",strerror(errno));return -1;}printf("client_fd %d\n",*sock_fd);memset(&clieaddr_in,0,sizeof(clieaddr_in));clieaddr_in.sin_family = AF_INET;clieaddr_in.sin_port = htons(*listen_port);inet_aton(listen_ip,&clieaddr_in.sin_addr);printf("listen_port:%d\n",*listen_port);printf("listen_ip:%s\n",listen_ip);if (connect(*sock_fd,(struct sockaddr*)&clieaddr_in,sizeof(clieaddr_in))){printf("connect error:%s\n",strerror(errno));close(*sock_fd);return -3;}printf("connect success!\n");return 0;}void help_printf(char* program)
{printf(" %s usage:\n",program);printf("--help(-h):help menu\n");printf("--port(-p):port\n");printf("--ip(-i);ip address\n");return ;}int ds18b20(float *temp)
{int fd = -1;int found_var = -1;char *w1_path = "/sys/bus/w1/devices/";char chip_path[128];char ds_path[256];DIR *dirp = NULL;struct dirent *direntp = NULL;char buf[128];char *ptr = NULL;if (NULL == temp){printf("arguments error:%s\n",strerror(errno));return -1;}if ( (dirp = opendir(w1_path)) == NULL ){printf("dirp nofound error:%s\n",strerror(errno));return -2;}printf("open dir %s success,DIR address:%p\n",w1_path,dirp);while ( (direntp = readdir(dirp)) != NULL ){if( strstr(direntp->d_name,"28-") ){strncpy(chip_path,direntp->d_name,sizeof(chip_path)); //chip_path = *direntp->d_name;不行,为什么编译会报错found_var = 1;}}if (found_var < 0){printf("nofound dir 28-...\n");closedir(dirp);return -3;}printf("success found [%s]\n",chip_path);closedir(dirp);snprintf(ds_path,sizeof(ds_path),"%s%s/w1_slave",w1_path,chip_path);printf("ds18b20 devices route:%s\n",ds_path);if ((fd = open(ds_path,O_RDONLY)) < 0){printf("open error:%s\n",strerror(errno));return -4;}memset(buf,0,sizeof(buf));if (read(fd,buf,sizeof(buf)) <= 0){printf("read error:%s\n",strerror(errno));return -5;}close(fd);if ((ptr = strstr(buf,"t=")) == NULL){printf("ptr error:%s\n",strerror(errno));close(fd);return -6;}ptr +=2;*temp = atof(ptr)/1000;return 0;
}