这是我参与8月更文挑战的第16天,活动详情查看:8月更文挑战
搞懂Socket通信(三)
前两篇都是理论知识,是实实在在的干货,这篇文章会用代码诠释和证明理论知识,包括完整的自己封装长连接通信。由于笔者从事Android开发,所以代码分析会更偏向于客户端。
一、场景
简单点说
场景:上位机(客户端) 想给 下位机(服务端) 发条消息
我们再延伸出一些需求
- 消息内容可能是字符串。
- 消息内容可能很大。
- 消息内容非常重要,要确保能收到。
- 他们不聊天了要及时让对方知道
二、代码设计
2.1 启动Socket TCP服务器
下位机对应服务端,在发送消息之前,要先让服务器运行起来,才能让上位机(客户端)进行连接。
启动Socket TCP 服务
ServerSocket serverSocket = new ServerSocket(Config.PORT);
serverSocket.setReuseAddress(true);
Socket socket = serverSocket.accept();
socket.setSendBufferSize(3000 * 1024);
实际上仅仅new ServerSocket(端口)
已经可以开启服务了,看看它内部是如何实现的。
public ServerSocket(int port) throws IOException {
this(port, 50, null);
}
public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException {
setImpl();
if (port < 0 || port > 0xFFFF)
throw new IllegalArgumentException(
"Port value out of range: " + port);
if (backlog < 1)
backlog = 50;
try {
bind(new InetSocketAddress(bindAddr, port), backlog);
} catch(SecurityException e) {
close();
throw e;
} catch(IOException e) {
close();
throw e;
}
}
重点在 Bind
方法。
setReuseAddress()
方法是复用端口地址,避免端口被占用的问题。
setSendBufferSize()
方法是设置缓冲区的大小,并不是说设置了缓冲区大小就能发送这么大的数据,传输数据的MTU首先于最小的瓶颈,比如说也局限于客户端的缓冲区大小,带宽的大小等等。
2.1 TCP服务器的设计
由于客户端可能会有多个,所以需要有个容器存放这么多的客户端。
我们需要 new
很多 SocketServerClient
放在容器中。
SocketServerClient
是什么,它是一个可以管理客户端对象的一个类,可以监听到客户端消息,可以给客户端发消息,可以监听到客户端是否在线。
SocketServerClient
方法中监听客户端的消息,由于是阻塞方法监听消息,我们需要开一个线程,等着消息过来。
新线程
while (true) {
try {
InputStream inputStream = socket.getInputStream();
out = socket.getOutputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String str = reader.readLine();
// string 为收到的消息
} catch (IOException e) {
e.printStackTrace();
}
}
SocketServerClient
方法发送消息,我们只需要在输出流OutputStream
放入数据即可
String data = msg + "\n";
byte[] bytes = data.getBytes();
out.write(bytes);
out.flush();
- 管理Socket客户端
List serverClients = new ArrayList<>();
serverClients.add(client);
2.2 客户端-连接
客户端连接到服务端有两种方式
- 方式一
Socket socket = new Socket(ip, port);
- 方式二
Socket socket = new Socket();
socket.connect(new InetSocketAddress(ip, port), 10 * 1000);
这里推荐方式二,因为可以便捷的设置连接超时时间,有人说方式一也可以设置setSoTimeout()
,但我自己测下来,它并不是连接超时时间。
完整代码:
/**
* 尝试建立tcp连接
*
* @param ip
* @param port
*/
private boolean startTcpConnection(final String ip, final int port) {
try {
if (mSocket == null) {
mSocket = new Socket();
mSocket.connect(new InetSocketAddress(ip, port), 10 * 1000);
mSocket.setKeepAlive(true);
mSocket.setTcpNoDelay(true);
mSocket.setReuseAddress(true);
mSocket.setReceiveBufferSize(3000 * 1024);
}
is = mSocket.getInputStream();
br = new BufferedReader(new InputStreamReader(is));
OutputStream os = mSocket.getOutputStream();
pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(os)), true);
LogWrapper.d(TAG, "tcp 创建成功...");
return true;
} catch (Exception e) {
e.printStackTrace();
LogWrapper.e(TAG, "startTcpConnection error:" + e.toString());
}
return false;
}
2.2 客户端-监听消息
创建接收线程 接收的消息必须附带换行,否则readLine无法读取到。测试过readLine方法不存在粘包的问题,重点在于长数据服务端发送时是否加锁。 TCP 虽然是有序的,但不保证服务端长数据分包发送时放入缓冲区的顺序是对的。
private void startReceiveTcpThread() {
ThreadPool.getInstance().execute(new Runnable() {
@Override
public void run() {
// 获取数据
String line = "";
try {
while ((line = br.readLine()) != null) {
handleReceiveTcpMessage(line);
}
if (line == null) {
LogWrapper.e(TAG, "line == null(上位机断开),开始断开...");
disConnect(); // 上位机断开
}
} catch (IOException e) {
e.printStackTrace();
// 没有正常关闭时会出现
LogWrapper.e(TAG, "接收消息异常, 开始断开...:" + e.toString());
disConnect();
}
}
});
}
2.3 客户端-发送消息
数据直接丢在PrintWriter
就可以了
public void sendTcpMessage(final String msg, final SendCallback sendCallback) {
ThreadPool.getInstance().execute(new Runnable() {
@Override
public void run() {
try {
pw.println(msg);
if (sendCallback != null)
sendCallback.success();
} catch (Exception e) {
if (sendCallback != null)
sendCallback.failed();
}
}
});
}
2.4 客户端-心跳机制
如果是强制断网的情况,两边无法即时收到消息。这个时候就需要心跳来保证两边都在线的状态。
心跳机制的原理是比如10秒一次心跳,当发送心跳三次对方没有回应时,则认定为对方不在线,自己就应该执行disconnect
方法去断开TCP连接,及时更新状态。
流程图如下:
graph TD
A[开始] --> B(发送心跳)
B --> C{心跳失败是否大于三次}
C -->|是| D[断开]
C -->|否| E[重置心跳]
心跳机制关键代码如下:
private class HeartRunnable implements Runnable {
@Override
public void run() {
LogWrapper.d(TAG, "心跳机制-发送心跳任务 run");
mHeartCurIndex++;
if (mHeartCurIndex > HEART_SEND_COUNT) {
// 三次失败了。
LogWrapper.d(TAG, "心跳机制-三次失败-自动断开");
if (mTcpSocket != null) {
LogWrapper.e(TAG, "心跳机制-三次失败-自动断开");
mTcpSocket.disConnect();
}
return;
}
String heartJson = "{ping}"; // 举例而已
if (mTcpSocket != null)
mTcpSocket.sendTcpMessage(heartJson, null);
if (mHeartRunnable != null)
mHeartHandler.postDelayed(mHeartRunnable, HEART_SEND_INTERVAL);
}
}
2.5 客户端-断线重连机制
在正常使用过程中,难免会遇到网络不好的情况,这个时候就需要进行断线重连机制来更快的让双方都在线。
graph TD
A[开始] --> B(断线了)
B --> C{断线重连次数>20}
C -->|是| D[20秒一次重连]
C -->|否| E[5秒一次重连]
断线重连机制关键代码
/**
* 异常重连机制 逻辑
*/
public void errorReConnect() {
//重置
resetReconnectRunnable();
//判断是否有网络
if (!NetWorkUtils.isNetworkAvailable(AppCache.getContext())) {
mReconnectCount = 0;
return;
}
mReconnectCount++;
long reconnectTime = minInterval;
if (mReconnectCount > 20) {
reconnectTime = maxInterval;
}
if (mReconnectRunnable == null) {
mReconnectRunnable = new ReconnectRunnable();
}
LogWrapper.d(TAG, "执行重连的间隔时间" + reconnectTime);
mReConnectHandler.postDelayed(mReconnectRunnable, reconnectTime);
}