论坛首页 Java企业应用论坛

做一个通讯项目的一点心得

浏览 16686 次
精华帖 (0) :: 良好帖 (14) :: 新手帖 (12) :: 隐藏帖 (3)
作者 正文
   发表时间:2009-02-20   最后修改:2009-02-20
最近做了一个小型的通讯项目,有一点体会,在这里拿来和大家分享一下:

一:关于Socket的长连接
这个项目中,客户提出了“一次连接,10次交互”的需求,就是说创建一个Socket后,在客户端与服务端完成10次交互前(客户端请求,服务端响应算一次交互),不会被关掉。起初按照这种思路来做Socket的长连接,发现在使用阻塞IO的情况下,如果,在一次交互后不关闭Socket,那么下次客户端虽然可以正常发送请求,但是怎么也读不出服务端的响应信息,因为服务端的输入流被阻塞在上次读取后。如果,每次交互后都关掉Socket,就不符合客户的要求了。于是查资料,说是用异步输入输出流java.nio,于是将其引入项目中,结果发现,异步IO虽然为多个Socket提供了不同的通道,但是对一个Socket而言,依然存在上述问题。于是和客户沟通,客户问一技术牛人,结果得到“Socket长连接指的是服务端将消息往客户端推, 就是在一次连接下,客户端向服务端发多条消息, 服务端一次响应完毕,其间,客户端如果在一定的时间内没有消息发往服务端,服务端会主动断开连接”。

二:关于Socket使用中读取响应消息的方法
使用Socket进行通信会涉及到读取服务端的响应消息。读取的方法可分为2类三种。

类1:一次性全部读取。

代码:
方法一:public String getResultStr(Socket sourceSocket)
{

String resultStr = null;

InputStream in;
try {
in = sourceSocket.getInputStream();
int readIndex = 5 * 1024 * 1024;
BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(in), readIndex);
char[] charArray = new char[readIndex];
int read_rst = bufferedReader.read(charArray);
resultStr = new String(charArray, 0, read_rst);
bufferedReader.close();

} catch (IOException e) {
e.printStackTrace();
}

return resultStr;
}
类2:非一次性读取

方法二:一行行读
public String getResultStr(Socket sourceSocket)
{
String resultStr = null;

InputStream in;

try {
in = sourceSocket.getInputStream();
BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(in));
StringBuffer responseBuffer = new StringBuffer();
String line = bufferedReader.readLine();
while (null != line)
{
responseBuffer.append(line);
line = bufferedReader.readLine();
}

bufferedReader.close();

resultStr = responseBuffer.toString();

} catch (IOException e) {
e.printStackTrace();
}

return resultStr;
}

方法三:一字节一字节读
public String getResultStr(Socket sourceSocket)
{
String resultStr = null;

InputStream in = null;

try {
in = sourceSocket.getInputStream();
BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(in));

int read_rst = bufferedReader.read();

StringBuffer readBuffer = new StringBuffer();
while (-1 != read_rst)
{
char singleChar = (char) read_rst;

readBuffer.append(singleChar);
}

bufferedReader.close();

resultStr = readBuffer.toString();
} catch (IOException e) {
e.printStackTrace();
}

return resultStr;
}

其中方法一的优点是读取速度快,且不用被超时所扰。缺陷是,只能读取一定量的字节,虽然BufferedReader的构造方法提供了设置缓冲区大小的功能,但是不管设多大,都只能读取一定量的字节,据项目中的情况来看,是65536个。如果响应消息有几MB的话,这种方法是肯定不行的。但是如果读取的消息很小,在65536个字节以内,则可以使用。

方法二的优点是便于做读取后的算法操作,速度嘛也挺快。缺陷是不能精确读取。因为readLine的方法读不出’\n’,’\r\n’,所以读出来的内容其长度与实际长度有出入。

方法三是最优解。(以上只是三种方法的原型,具体业务还要具体实现)


三:关于解决Socket读取响应消息超时的分析及解决方法:

分析
在使用Socket的过程中会遇到读取响应消息超时的问题,这是为什么呢?就我现在的理解,一句话:在服务端还没有关闭连接前,客户端读取响应消息就会一直等待,直到超时。

解决方法:
1.Socket提供的setSoTimeout(int timeout) 方法
在获得Socket的实例后,设置下超时时间,然后当read或是readLine完最后一个字节或是字符串后,会抛一个InterruptedIOException,在catch里做你想做的事情,或break,或关掉连接。

2.如果服务端也是你设计的话(就是响应消息也是你拼接的),请看。
可以在服务端里拼接响应消息的方法里为响应消息加上一个‘\r\n‘,注意一定要加在消息末尾。然后用“二:关于Socket使用中读取响应消息的方法”里第三种方法来读取响应消息,注意,通信消息头中肯定会有“Content-Length“一项,先取出其值(Content-Length表示消息主体的字节长度),接着找出主体消息的起始位置(主体消息中的最前方一定要是个固定的内容),开始计算响应消息的实际字节长度,最后将取出的Content-Length的值与实际计算出的长度进行比较,如果相等就break掉,这样,就不会读到会引起超时消息末尾。

其方法1的优点时易于实现,缺陷是不安定因素太大。setSoTimeout里总要设个值,设多少呢?假设在网络正常情况下读一个1MB的响应消息需要1s,那么如果网络阻塞呢?如果你设一个很大的值,就会影响使用。
方法2的优点是不受网络因素的影响。缺陷是,如果服务端的中文字符编码与流中的中文字符编码不一致,会导致乱码,进而会影响计算的准确度。一旦计算的不准,就会超时。
不过,方法2仍是最优解。

以上就是自己对这个项目的一点体会,欢迎大家指正。
   发表时间:2009-02-20   最后修改:2009-02-20
lei_cbd 写道
说创建一个Socket后,在客户端与服务端完成10次交互前(客户端请求,服务端响应算一次交互),不会被关掉。


这个可以做到,首先setKeepAlive(true),然后写入流和读取流在使用完之后都不要 close,写入流可以用 BufferedOutputStream 包装,写入数据后,用 flush 方法确认把数据全部发送出去,但千万别把 BufferedOutputStream close 掉,否则连原始的写入流也会 close 掉。读取流不要用 BufferedReader 包装,否则它可能会自作主张的多读入数据,以至于影响你的后续通讯。读取流的时候直接用返回的原始流就可以,然后按照字节读取,读取的数据可以先读入 byte[] 数组,然后写入 ByteArrayOutputStream 中,最后转成完整的数据 byte[],或者根据需要再将其转换为字符串。也可以一个字节一个字节的读,然后用 StringBuffer 组成完整字符串。后一种方法更适合于读取的都是 ASCII 码的数据(比如 HTTP 的头)。最关键的就是不要多读,一般协议中会包含有消息的长度,然后根据那个长度就可以判断是否读取完成,也有根据标志来标识是否结束的(例如 HTTP 的头是两次连续的 \r\n 表示头结束),当然按字节读,读到 -1 也是流结束的标志。通过这种方式,你就可以在一个 Socket 上反复的写入读取数据,不要说交互10次,就是100次也没问题。另外,你还要保证不要多线程的在一个 Socket 上同时发出多个这样的读写请求,否则通讯就乱掉抛出异常了。

我在写 PHPRPC 客户端时,就是用这种方法来跟服务器通讯的,实践证明是很有效的。
3 请登录后投票
   发表时间:2009-02-20  
多谢,这个方法我接收了。
0 请登录后投票
   发表时间:2009-02-20  
似乎大家都不知道有个东西叫ByteBuffer,有个东西叫DataInputStream。

楼主我记的在这里发帖提过那个所谓长连接的问题,客户端阻塞肯定是因为读不到什么东西或者读不够你设定的缓冲区字节数,阻塞怎么会是关闭socket,读到-1或者抛出EOFException也才是对端关闭了输出流,但是也不代表socket关闭了,所以很大可能是你们客户的服务端的实现问题。


0 请登录后投票
   发表时间:2009-02-20  
为什么不用mina呢?
0 请登录后投票
   发表时间:2009-02-20  
两个问题:
1 为什么网络流不建议用字节读取行 line = bufferreader.readLine()
2 起初我也为如何判断读取完成而恼火,读取文件流可以用(line = br.readLine()) != null(按字节读取用-1判断),但是在网络流里面这个怎么也读不对,后来别人告诉我在服务器发送完信息后用socket.shutdownOutput(),之后!= null可以判断了
0 请登录后投票
   发表时间:2009-02-20  
BufferReader 读文件没问题是因为文件长度是已知的,而 BufferReader 实际上是把文件内容在读取时一次不是读入一个字节,而是一次读入一段(这一段的大小跟Buffer大小设置有关),所以,你在 BufferReader 里用 readLine 读入一行,实际上可能不止一行被读入到内存,只是返回一行的内容给你,当你下一次读取时,如果内存中的数据是你想要的,它就不再读取磁盘上的文件,这就是 Buffer 的作用。

你用它读网络流它仍然是按照它的 Buffer 大小来读入字节数的,所以读多了就会有麻烦了。
0 请登录后投票
   发表时间:2009-02-20  
我一只不理解读多了指的是什么,是不是可以考虑有这么3种情况
1 缓存中有多于一行(\r\n)的纪录,那么readLine()读取一行,buff里还剩下一些
2 缓存中有不到一行的记录,那么readLine()
  (1)是阻塞,直到缓存中出现\r\n为止 ?
  (2)现在缓存中有多少就认为是一行?

我觉得很可能是第2个情况,因为一个网络流很可能没有换行,那么不可能直到超时才继续执行。

我觉得可能是这样:

情况一:你只是希望你文件所有的内容都传到服务器上而已,那么一行读取是否为实际真的一行就不重要了,因为最终拼成的文件是完整的就够了。

情况二:你的客户端基于服务器段传输信息格式,比如说第一行是命令,第二行是版本号,那么你需要明确知道读多少是一行,那可能buff就有你说的问题了,总之仅仅需要传输完整的文件,无所谓读多读少问题。
0 请登录后投票
   发表时间:2009-03-01   最后修改:2009-03-01
为啥要用字符流去操作捏???

BufferReader以及XXXReader 这种东西还是别在Socket中用的为好
0 请登录后投票
   发表时间:2009-03-01  
先定义好自己的数据包,建议不要按行读取.你可以用开头4个字节表示要读取的字节大小.然后用两个交替的缓冲区去读取数据库再设置一个已经读取的字节长度去判断是否读完.

关于长连接十次交互,理论上是不允许这样设计的,其实你可以做出逻辑上的长连接不一定要做成物理上的长连接. 你可以靠令牌去实现这个. 大概可以分为以下几个步骤 第一 请求连接 服务端返回任务令牌  第二次 发送请求 这个和你具体业务有关  要分包的分包 要编码的编码等等 这个步骤是不确定 你可以进行多次的交互 第三 就是断开连接 注明该次任务情况等等   以上连接你可以定义出 你的发送包数据格式和回复包数据格式 包头可以用固定字节  比如 发送长度用4个字节  令牌用12个字节  命令用4个字节 等等
2 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics