在看到本文之前,如果读者没看过笔者的前文 Java实现Socket网络编程(四),请先翻阅。
接下来,笔者对几个核心点进行剖析:
1、如果读者是初次进行Socket网络编程开发,起初可能会因为端口的使用不当,而导致Socket无法连接。所以读者可以在编程前进行测试,这里笔者提供了一个方法:
/**
* 用于检测能连接到的端口号
*/
public static void scan(String host) {
Socket socket = null;
for (int port = 1024; port < 10055; port++) {
try {
socket = new Socket(host, port);
System.out.println(socket);
} catch (IOException e) {
continue;
} finally {
try {
if (socket != null) {
socket.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
2、当发现程序只能在本机运行,而在其他机器上不能运行时,是因为读者所使用的ip地址是本机ip地址,如果采用 loop back 地址 "127.0.0.1",则可以在任意机器上运行。(本案例实际上是本机服务器与本机客户端的Socket通信)
3、笔者在测试过程中,发现了这样一个现象:服务器检测客户端断开,零延时;而客户端检测服务器断开,有明显延时(延时长短和运行机器的速度有关)。
这是什么原因导致的呢?经测试发现,服务器检测客户端断开,心跳包异常的捕获速度快于读写错误的捕获,从而零延时。而客户端检测服务器断开,读写错误的捕获速度快于心跳包异常的捕获速度,导致有延时。
这种现象的产生,是由于Java底层对”服务器监听客户端断开“及”客户端监听服务器断开“的实现机制不一样,导致了两者之间的速度差异。
既然是实现机制的原因,那我们是否就没有解决的方法呢?显然不屈不饶的程序员不会就此善罢甘休。
服务器检测客户端断开,零延时,我们不需要再过多干预;我们要干预客户端检测服务器断开,以使得比捕获读写异常的速度更快地发现服务器断开。
笔者经过多次尝试:
①首先是想到自定义一个结束符,例如"bye",希望通过服务器传递”bye“给客户端,客户端就”意识到“服务器关闭。这可以做到,但是存在一个漏动,如果”bye“是由使用者在服务器对话框发送过去的呢?那样客户端就会“以为”服务器断开。
既然这样,笔者就想着改进,把结束符“bye”改成转义字符,如"\\u0025",让用户不容易输入,经过数次测试发现,用户毫无一例会成功输入"\\u0025"结束符,看起来毫无Bug,但这显然还是一个漏动,哪怕只有万分之一的可能?
②为了进一步改进,笔者想着自定义一种数据协议格式:
【服务器是否启动标志位】【要接收的数据】
这看起来不错,但前面已经提到,客服端读取数据要在for循环里,如果用String类的startsWith("启动标志位")方法判断,则客户端显示数据由于无法判断服务器一次性发送内容的结束,会导致如下输出结果:
s
sa
say
say h
say he
say hel
say hell
say hello
say hello!
③既然如此,也就是要为数据提供结束符,以判断数据长度,笔者再一次修改数据协议格式:【要接收的数据】【服务器是否启动标志位】
通过String类的endsWith("启动标志位")进行检测
然而,这又产生了一个新问题,当用户直接输入”服务器断开标志位“,客户端又再一次”认为“服务器已断开。
④笔者进一步考虑,如果加上长度判断,当输入内容s.length()>"标志为长度"且endsWith("启动标志位")进行检测。
心想理应成功,没料到又出现这样的一种情况:当用户输入任意长度字符+”服务器断开标志位“时,客户端又再一次”认为“服务器已断开。
⑤历经多次挫折,笔者最后对比使用C#的Socket网络编程实现,研究了其 Send()方法和 Receive()方法(Send方法用于发送数据,Receive方法用于接收数据,C#封装了底层输入输出流的实现,而Java没有,所以需要进行输入输出流的操作)
总结出一种方法:模拟C#的Receive函数(该函数具有检测服务器是否断开,且能返回接收数据长度)
笔者定义了以下的数据协议格式,彻底解决了”客户端延时发现服务器断开“、”发送数据无边界“的问题:【服务器是否启动标志位】【接收数据的长度】【要接收的数据】
在发送数据前进行包装
String message = Common.OK; // 代表服务器正常连接
String t = "server " + Common.IP + ":" + Common.PORT + " "
+ jtaSendMessage.getText();
/**
* 封装发送数据的长度
*/
String c = "" + t.length();
if (c.length() < 2) {
c = "000" + c;
} else if (c.length() < 3) {
c = "00" + c;
} else if (c.length() < 4) {
c = "0" + c;
}
message += c + t;
在收数据时进行解封
// 使用"GBK"编码读取中文
brIn = new BufferedReader(new InputStreamReader(
mSocket.getInputStream(), "GBK"));
String s = "";// 记录每次读取的内容
int count = -10;// 记录每次读取内容的长度
// 接收内容并把内容添加到信息接收区
for (int c = brIn.read(); c != -1; c = brIn.read()) {
s += (char) c + "";
count++;// 读取的长度
// 如果服务器连接且一次数据接收完成
if (s.startsWith(Common.OK) && s.length() > 10
&& count == Integer.parseInt((s.substring(6, 10)))) {
ClientMain.jtaReceivedMessage.append(s.substring(10)
+ "\n");
count = -10;
s = "";
}// 服务器断开且一次数据接收完成
else if (s.startsWith(Common.ERROR) && s.length() > 10
&& count == Integer.parseInt((s.substring(6, 10)))) {
ClientMain.jtaReceivedMessage.append(s.substring(10)
+ "\n");
count = -10;
s = "";
ClientMain.jlConnect.setText("Out Of Connect.");
}
// 滚动到底端
ClientMain.jtaReceivedMessage
.setCaretPosition(ClientMain.jtaReceivedMessage
.getText().length());
}
以上为本次案例的全部内容,最后,笔者在github上给出了这个案例的完整源码和可运行文件Socket网络编程,供读者学习思考。
谢谢支持!
网友评论