这是我参与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连接,及时更新状态。
流程图如下: