tcp服务端一直sleep,客户端发送数据问题总结

在之前参加腾讯面试的时候,被面试官问到了一个问题:“一个tcp服务端和一个tcp客户端,客户端和服务端建立连接后,服务端一直sleep,然后客户端一直发送数据会是什么现象”。

回答这个问题前我们先想一想tcp的特征和tcp发送数据的大体过程:首先,tcp是有链接的可靠传输协议,所谓可靠也就是说保证客户端发送的数据服务端都能够收到,并且是按序收到。那么对于上面的问题就不可能存在数据的丢弃。那么客户端一直发送数据越来越多怎么办?下面我们分析一下tcp的传输过程。

图1

如图1所示当发送数据时:

(1) 数据首先由应用程序缓冲区复制到发送端的套接字发送缓冲区(位于内核),注意这个过程是用类似write功能的函数完成的。有的人通常看到write成功就以为数据发送到了对端主机,其实这是错误的,write成功仅仅表示数据成功的由应用进程缓冲区复制到了套接字发送缓冲区。

(2) 然后内核协议栈将套接字发送缓冲区中的数据发送到对端主机,注意这个过程不受应用程序控制,而是发送端内核协议栈完成,其中包括使用滑动窗口、用赛控制等功能。

(3) 数据到达接收端主机的套接字接收缓冲区,注意这个接收过程也不受应用程序控制,而是由接收端内核协议栈完成,其中包括发送ack确认等。

(4) 数据由套接字接收缓冲区复制到接收端应用程序缓冲区,注意这个过程是由类似read等函数来完成。

1.1 阻塞方式的情况

知道了这个过程,我们在看一下在默认情况下(套接字为阻塞方式)write等函数的工作方式:输出操作,包括write、writev、send、sendto和sendmsg共5个函数。对于一个TCP套接字,内核从应用进程的缓冲区到套接字的发送缓冲区复制数据。对于阻塞的套接字,如果其发送缓冲区中没有空间,进程将被投入睡眠,直到有空间为止。——UNPv1

这样我们就可以推测出了结果:阻塞方式下,如果服务端一直sleep不接收数据,而客户端一直write,也就是只能执行上述过程中的前三步,这样最终结果肯定是接收端的套接字接收缓冲区和发送端套接字发送缓冲区都被填满,这样write就无法继续将数据从应用程序复制到发送端的套接字发送缓冲区了,从而使进程进入睡眠。

验证例子如下,客户端代码:

客户端每次write成功一次,将计数器count加1,同时输出本次write成功的字节数。count保存客户端write成功的次数。

服务端代码:

首先编译运行服务端,然后启动客户端,运行结果如图2所示。

图2

可以看到客户端write成功377次后就陷入了阻塞,注意这个时候不能说明发送端的套接字发送缓冲区一点是满的,只能说明套接字发送缓冲区的可用空间小于write请求写的自己数——1024。

补充:当服务端sleep到1000后,会关闭当前连接,此时客户端处于阻塞中的write会返回错误,效果如图3。

图3

1.2 非阻塞方式的情况

下面看一下非阻塞套接字情况下,write的工作方式:对于一个非阻塞的TCP套接字,如果发送缓冲区中根本没有空间,输出函数将立即返回一个EWOULDBLOCK错误。如果发送缓冲区中有一些空间,返回值将是内核能够复制到该缓冲区的字节数。这个字节数也成为“不足计数”。——UNPv1

这样就可以知道非阻塞情况下服务端一直sleep,客户端一直write数据的效果了:开始客户端write成功,随着客户端write,接收端的套接字接收缓冲区和发送端的套接字发送缓冲区会被填满。当发送端的套接字发送缓冲区的可用空间小于write请求写的字节数时,write立即返回-1,并将errno置为EWOULDBLOCK。

验证例子代码如下。

l 服务端同阻塞情况(略)。

客户端(非阻塞模式):

首先编译运行服务端,然后启动客户端,运行结果如下图4所示。

图4

可以看到客户端成功write 185次后就发生套接字发送缓冲区空间不足,从而返回EWOULDBLOCK错误。我们注意到每次write同样的字节数(1024)阻塞模式下能write成功377次,为什么非阻塞情况下要少呢?

这是因为阻塞模式下一直write到接收端的套接字接收缓冲区和发送端的套接字发送缓冲区都满的情况才会阻塞。而非阻塞模式情况下有可能是发送端发送过程的第二步较慢,造成发送端的套接字发送缓冲区很快写满。而接收端的套接字接收缓冲区还没有满,这样write就会仅仅因为发送端的套接字发送缓冲区满而返回错误(准确的说的套接字发送缓冲区的可用空间小于write请求写的字节数)。

对比一下377正好是185的二倍左右,所以可以推测由于发送过程第二步的延迟,很可能发送端的套接字发送缓冲区已经满了,而接收端的套接字接收缓冲区还是空的。

1.3 UDP情况补充

对于UDP套接字不存在真正的发送缓冲区。内核只是复制应用进程数据并把它沿协议栈向下传送,渐次冠以UDP首部和IP首部。因此对于一个阻塞的UDP套接字(默认设置),输出函数调用将不会因为与TCP套接字一样的原因而阻塞,不过有可能会因为其他原因而阻塞。——UNPv1

扩展阅读

Java网络编程之TCP协议

关于Http协议,你必须要知道的

Java网络编程之UDP协议

来源:http://blog.chinaunix.net/uid-28541347-id-4730278.html

微信公众号:javafirst

扫码关注免费获取更多资源 

<think>我们正在开发一个Android Studio中的TCP客户端应用,需要实现自动连接到TCP服务端的功能。根据引用内容,我们可以总结出以下关键点: 1. 使用Socket编程实现TCP连接(引用[1][2][3]) 2. 网络操作不能在主线程中执行(引用[2]),需要使用AsyncTask、Thread、Handler或Coroutine等 3. 客户端建立步骤(引用[3]): - 创建Socket对象,指定服务器IP和口 - 获取输入输出流进行数据收发 4. 客户端发送消息时,如果服务端使用readLine(),则需要在消息结尾加上换行符(引用[4]) 5. 需要处理连接失败的情况,实现自动重连机制 实现自动连接的核心思路:在连接失败时,不是立即退出,而是等待一段时间后重新尝试连接,直到连接成功或达到最大重试次数。 具体步骤: 1. 创建一个后台任务(如Thread)来执行连接操作 2. 在连接失败时,进行重试(可设置重试间隔和最大重试次数) 3. 使用循环实现自动重连逻辑 下面是一个简单的实现示例:</think>### Android Studio TCP客户端自动连接服务端实现方法 在Android Studio中实现TCP客户端自动连接服务端,需要遵循以下步骤: #### 1. **基础连接实现** ```java // 在后台线程中执行网络操作 private class ConnectTask extends AsyncTask<Void, Void, Void> { private Socket socket; private DataOutputStream out; @Override protected Void doInBackground(Void... params) { try { // 创建Socket连接(示例IP和口) socket = new Socket("192.168.1.100", 8080); out = new DataOutputStream(socket.getOutputStream()); // 发送测试消息(注意结尾加\n) out.writeUTF("Hello Server!\n"); out.flush(); } catch (IOException e) { e.printStackTrace(); } return null; } } ``` > 关键点:使用`AsyncTask`避免主线程阻塞[^2],消息结尾必须加`\n`确保服务端正确接收[^4] #### 2. **自动重连机制** ```java // 自动重连实现 private void autoConnect() { new Thread(() -> { while (!Thread.interrupted()) { try (Socket socket = new Socket(SERVER_IP, SERVER_PORT)) { // 连接成功后持续监听 DataInputStream in = new DataInputStream(socket.getInputStream()); while (true) { String msg = in.readUTF(); // 处理接收到的消息 } } catch (IOException e) { // 连接失败时等待重试 try { Thread.sleep(5000); // 5秒后重试 } catch (InterruptedException ie) { ie.printStackTrace(); } } } }).start(); } ``` > 特性: > - 循环检测连接状态 > - 失败后等待5秒自动重连 > - 使用独立线程保持长连接 #### 3. **完整实现要点** 1. **权限声明**(AndroidManifest.xml): ```xml <uses-permission android:name="android.permission.INTERNET"/> ``` 2. **连接管理**: - 使用`Handler`更新UI状态[^5] - 实现`ConnectionListener`接口回调连接状态 3. **异常处理**: ```java catch (UnknownHostException e) { showToast("服务器地址错误"); } catch (SocketTimeoutException e) { showToast("连接超时"); } ``` 4. **安全增强**: - 添加连接超时设置:`socket.connect(address, 5000)` - 使用心跳包维持长连接 #### 4. **优化建议** 1. **指数退避重连**: ```java int retryDelay = 1000 * (int)Math.pow(2, retryCount); Thread.sleep(retryDelay); ``` 2. **网络状态监听**: ```java ConnectivityManager cm = (ConnectivityManager)getSystemService(CONNECTIVITY_SERVICE); cm.registerNetworkCallback(request, networkCallback); ``` 3. **使用连接池管理多个Socket连接** > **注意事项**: > 1. Android 9+需在`network_security_config.xml`中配置明文传输[^2] > 2. 生产环境应使用TLS加密通信[^2] > 3. 应用退到后台时需释放连接资源 完整示例代码可参考:[Android TCP客户端完整实现](https://github.com/android/tcp-client-demo)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值