这是篇文章是在看了今日头条后,回想起来自己曾今遇到过的一个关于socket套接字发送数据的问题,当时解决后并没有记录下来,现在正好看到了与之息息相关的知识,于是打算简单的谈一下~
一、头条之问题引出
就是说腾讯面试过程中,被面试官问到了一个问题:“一个tcp服务端和一个tcp客户端,客户端和服务端建立连接后,如果服务端一直sleep,然后客户端一直发送数据会是什么现象“。(当然我遇到的不是这个问题,但与之息息相关)
要回答这个问题,需要我们清楚tcp协议的特点和tcp发送数据的大体过程。
二、tcp发送数据过程
恐怕接触过网络的同学都知道tcp是面向连接的可靠传输协议,意味着客户端发送的数据服务端是一定能够收到的,那么对于上面的问题就不可能存在数据的丢弃。下面我们分析一下tcp的传输过程。
如图所示,tcp数据包的传输过程主要有如下几个步骤:
- 应用程序调用write系列函数发送数据 ,数据首先由应用程序缓冲区复制到发送端的内核中的 套接字发送缓冲区,然后write成功返回;需要特别注意的是write成功返回只是说明数据成功的由应用进程缓冲区复制到了套接字发送缓冲区,并不代表数据发送到了对端主机(这里就是我遇到的问题的关键所在了)。
- 内核协议栈将套接字发送缓冲区中的数据发送到对端主机(一开始我的程序在这里造成了数据的丢失),这个过程不受应用程序控制,而是发送端内核协议栈完成;
- 数据到达接收端主机的套接字接收缓冲区,注意这个接收过程也不受应用程序控制,而是由接收端内核协议栈完成;
- 数据由套接字接收缓冲区复制到接收端应用程序缓冲区,注意这个过程是由类似read等函数来完成。
三、我的问题解决(判断socket是否断开)
清楚了tcp的传输过程,现在我就来讲一下当时遇到的问题具体是什么吧
先来简单描述一下场景,就是客户端向服务器端每隔一段时间上报数据,就比如说蔬菜大棚上报温湿度等信息给服务器,但是凡事总有意外发生,如果服务器发生故障或网络断开,二者之间建立的连接也随之断开了,那么数据将何去何从,当然是保存到数据库中了,如果write失败就保存到数据库中,待重连成功后再继续发送。
但是接下来问题就出现了,在我手动关闭服务器端程序(造成故障)的时候,接下来在发生故障之后发送的第一条数据显示的是write成功的,这个原因在上面给到了很好的解释(write成功返回只是说明数据成功的由应用进程缓冲区复制到了套接字发送缓冲区),也正如我当时百度了解的一般无二。
这种情况就导致了发送的数据会有数据丢失的情况发生~~~
后来是怎么解决的呢
一开始我自己的解决办法是,每次发送数据之前都read()、write()一次无关紧要的数据,这样想要发送的数据就能得到保障了,因为如果网络异常或服务器故障的话,read、write会反馈回来。但是这个方法无疑大大增加了数据传输的开销。
在之后,我又了解到了getsockopt()这个函数,如下参数设置可以判断网络是否断开
/*
* =====================================================================================
* Name: SocketConnected
* Description: To judge socket disconnected or not.
* Input args: socket file description
* return value: 1 (connected) 0 (disconnected)
* =====================================================================================
*/
//函数封装,单独使用(没有使用read、write、select时),判读网络是否断开。
int SocketConnected(int sock)
{
if (sock <= 0)
return 0;
struct tcp_info info;
int len = sizeof(info);
getsockopt(sock, IPPROTO_TCP, TCP_INFO, &info, (socklen_t *) & len);
if ((info.tcpi_state == 1)) //TCP_ESTABLISHED这个宏我这好像用不了,没有找到它的定义,这里用1就可以了
{
return 1; //myprintf("socket connected\n");
}
else
{
return 0; //myprintf("socket disconnected\n");
}
}
了解更多判断方法:https://www.likecs.com/show-693624.html
四、头条之问题回答
write系列函数的工作方式默认是阻塞方式:调用write函数时,内核从应用进程的缓冲区到套接字的发送缓冲区复制数据。如果其发送缓冲区中没有空间,进程将进入睡眠,直到有空间为止。
因此,阻塞方式下,如果服务端一直sleep不接收数据,而客户端一直write,也就是只能执行上述过程中的前三步。
- 应用程序调用write函数,应用程序缓冲区复制到发送端的内核中的套接字发送缓冲区
- 内核协议栈将套接字发送缓冲区中的数据发送到对端主机
- 数据到达接收端主机的套接字接收缓冲区
然后就到此为止了
这样最后接收端的套接字接收缓冲区和发送端套接字发送缓冲区都被填满,这样write就无法继续将数据从应用程序复制到发送端的套接字发送缓冲区了,从而发送端进程也进入睡眠。
如果想要自己验证的话,头条里有代码参考~~~
头条链接:https://m.toutiao.com/article/7120566000780608034/?app=news_article×tamp=1657891935&use_new_style=1&req_id=20220715213214010150220081070EBC9A&group_id=7120566000780608034&tt_from=mobile_qq&utm_source=mobile_qq&utm_medium=toutiao_android&utm_campaign=client_share&share_token=22410336-e9ed-4b79-bf27-10202f99cb50