网络编程--8
C/S常见的“双卡”现象和解决 ----------TCP复制文本文件示例II
阻塞式循环的分析过程 ---总结
----------- android培训、java培训、java学习型技术博客、期待与您交流! ------------
1. C/S常见的“双卡”现象和解决-- TCP复制文本文件示例II
1). 需求和常规做法
(1). 需求
客户端向服务器上传文件
(2). 客户端程序分析
[1]. 从硬盘文件读取数据(本地输入流) + 两个网络流 -----> 三个流对象
[2]. 分析各个流对象的写法
{1}. 本地输入流 -----源
文本?Y -->高效?Y --->设备:硬盘文件
BufferedReaderbufr =new BufferedReader(new FileReader("IPDemo.class"));
{2}. 网络输出流-----目的(输出流使用打印流替代缓冲输出流)
文本?Y -->高效?Y --->设备:网络输出流(socket.getOutputStream())
PrintWriterbufOut = new PrintWriter(socket.getOutputStream(),true);
{3}. 网络输入流 ----- 源
文本?Y -->高效?Y --->设备:网络输入流(socket.getInputStream())
BufferedReaderbufIn = new BufferedReader(new InputStreamReader(socket.getInputStream()));
[3]. 文本转换器
客户端示例代码
class TextClientI{
public static void main(String[] args) throws Exception{
Socketsocket =new Socket("192.168.1.102", 10006);
//本地流
BufferedReaderbufr =new BufferedReader(new FileReader("IPDemo.class"));
//网络输入流
BufferedReaderbufIn =new BufferedReader(newInputStreamReader(socket.getInputStream()));
//网络输出流
PrintWriterout =newPrintWriter(socket.getOutputStream(), true);
//从硬盘中读取文件信息
StringfileContentLine =null;
while((fileContentLine =bufr.readLine())!=null){
out.println(fileContentLine);
}
//接收服务器端上传文件的结果信息
StringuploadedMsg = bufIn.readLine();
System.out.println("server response: "+ uploadedMsg);
//关闭流
bufr.close();
socket.close();
}
}
(3). 服务器端程序分析
[1]. 向硬盘文件写入数据 (本地输出流) +两个网络流 -----> 3个流对象
[2]. 分析各个流对象的写法
{1}. 本地输出流 -----目的(缓冲输出流使用PrintWriter替代)
文本?Y -->高效?Y --->设备:硬盘文件
PrintWriter pwFile= new PrintWriter(socket.getOutputStream(),true);
{2}.
网络输入流 -----
源
文本?Y -->高效?Y --->设备:网络输入流(socket.getInputStream())
BufferedReaderbufIn = new BufferedReader(newInputStreamReader(socket.getInputStream()));
{2}.
网络输出流 -----
目的 (缓冲输出流使用PrintWriter替代)
文本?Y -->高效?Y --->设备:网络输出流(socket.getOutputStream())
PrintWriter out= new PrintWriter(socket.getOutputStream(),true);
[3]. 文本转换器
服务器端示例代码
class TextServerI{
public static void main(String[] args) throws Exception{
ServerSocketss =new ServerSocket(10006);
SocketclientSocket = ss.accept();
Stringip =clientSocket.getInetAddress().getHostAddress();
System.out.println(ip+ "...connected!");
BufferedReaderbufIn =new BufferedReader(newInputStreamReader(clientSocket.getInputStream()));
PrintWriterpwFile =new PrintWriter(new FileWriter("server.txt"), true);
PrintWriterout =new PrintWriter(clientSocket.getOutputStream(),true);
//读取客户端中的文件信息(这个文件信息位于网络流中)
Stringline =null;
while((line =bufIn.readLine()) !=null){
pwFile.println(line);
}
//完成之后,回应客户端一个反馈
out.println("上传成功,感谢您的上传...");
pwFile.close();
clientSocket.close();
ss.close();
}
}
(4). 运行上述程序
[1]. 效果
双方都卡在那里,都阻塞不动了。但是此时发现客户端上传的文件已经生成。
【注意1】这次对已经使用了PrintWriter针对文本转换器中出现的没有及时清理缓冲区或者没有为readLine()提供有效的行文本数据结束符才能结束阻塞进行了预防。所以上述问题在这个程序中不存在。
【注意2】当C/S形式程序出现了“双卡”现象的时候,要从循环是否能结束+ 阻塞是否被解除两个方面进行考虑。
2). 出现的问题、原因
分析
[1]. 通过程序分析问题
[2]. 客户端的循环完成情况 + 阻塞情况
{1}. 客户端的while循环是用来对本地文本的读取。由于文本文件每行都有有效的的行数据,所以while循环判断位置的readLine方法不会阻塞,主线程可以正常执行循环体。
{2}. 文本文件的结尾有文件结束标记,因此readLine方法是可以读取到这个文本文件的结束标记。当这个文本文件的结束标记被读取到的时候,readLine方法就会知道已经读到了本地输入流的末尾,这样readLine方法自动返回null。这个时候客户端的while循环就会正确结束。
{3}. 之后客户端的主线程就会向下运行到第二个阻塞点。由于这个时候没有读取到从服务器端发送来有效的行文本数据或者网络流的结束标记,因此此时客户端程序被阻塞在第二个阻塞点。
[3]. 服务器端的循环完成情况 + 阻塞情况
{1}. 由于客户端使用了PrintStream的语句out.println(fileContentLine);向服务器端发送数据。由于这个语句自动添加换行符+ 自动flush,因此这条语句执行完,服务器端处于while循环判断条件的readLine方法会接收到有效的行文本数据。因此此时服务器端保存了客户端上传的数据。
{2}. 但是当客户端发送完毕所有的有效的行文本数据之后,客户端的程序被阻塞在while循环之外的readLine方法处。此时客户端的Socket关闭语句没有办法被执行到,因此客户端的程序没有办法为服务器端的网络输入流加上流结束标记。因此当服务器端的while循环执行完保存文件之后,有没有等到流结束的标记,所以卡在那不动。这样服务器同样没有办法执行对客户端的反馈。这样客户端也这样等着。
因此双卡现象的僵持局面形成
3). 解决服务器端无法获取到流的结尾标记的问题
(1). 解决办法I
[1]. 不理智的抄袭办法:像读取键盘输入的情况一样直接定义一个字符串"xxx"。之后服务器端通过对读取到的内容进行判断,如果读取到"xxx"就使用break强行跳出循环。
【不可行的原因】无法保证客户端上传的文本文件中是否含有自定义的标记字符串。一旦含有自定的字符串,可能上传的文件仅仅保留到这个自定义字符串之前的内容,不会完整正确地保存客户端上传的文本文件的所有内容。
[2]. 使用时间戳作为服务器端中循环结束的标记字符串。
[2]1. 客户端程序修正 ----前后发送标记法
{1}. 在while循环正常发送本地文本数据之前先发送时间戳结束标记告诉服务器什么内容是你要判断的结束标记。
{2}. while正常发送完有效行文本数据之后,再次发送内容是时间戳的结束字符串标记。这个第二次发送告诉服务器我正式的文本已经发送完成。
{3}. 修正后的客户端的代码
String endMark = System.currentTimeMillis()+""; //自定义时间戳结束标记
out.println(endMark);
//从硬盘中读取文件信息
String fileContentLine =null;
while((fileContentLine =bufr.readLine())!=null){
out.println(fileContentLine);
}
//将结束标记添加到网络传输流的末尾
out.println(endMark);
[2]2. 服务器端程序修正 ---- 最先读取标记法
{1}. 在while循环之前,最先进行一次读取操作,将读取到的有效行文本数据记做while循环内要结束的字符串标记。
{2}. 在服务器端程序的while循环中增加自行对收到的时间戳结束标记进行判断。一旦读取到这个客户端发送过来的标记之后,就强行使用break语句结束服务器端的while循环。
{3}. 修正后的服务器端的代码
String line =null;
String clientEndMark=bufIn.readLine();
System.out.println("The end mark from client is:"+clientEndMark);
while((line =bufIn.readLine()) !=null){
if(clientEndMark.equals(line))
break;
pwFile.println(line);
}
[3]. 运行结果
查看文件成功上传到服务器的本地文件。
(2). 解决办法II
[1]. 在客户端使用Socket类的关闭流的方法。关闭流但是不关闭Socket就可以直接在客户端的输出流中加入流结束标记。这时候服务器端就可以从网络中读到流的结束标记是的服务器端的readLine方法返回null,最后不满足条件,循环结束。
[2]. 客户端程序修正
String fileContentLine =null;
while((fileContentLine =bufr.readLine())!=null){
out.println(fileContentLine);
}
//将结束标记添加到网络传输流的末尾
socket.shutDownOutput();
【 注意】服务器端代码不用修改。维持以前的服务器端代码不变
String line =null;
while((line =bufIn.readLine()) !=null){
pwFile.println(line);
}
[3]. 运行结果
同时文件复制成功。
2. 阻塞式循环的分析过程 ---总结
(1). C/S通信的程序分析的要点
[1]. 注意到C和S两端程序中,阻塞式方法被调用的位置
[2]. 一旦程序被卡住, 一定要分析各个阻塞点位置的情况,一定要分析服务器端和客户端之间通信的过程中哪个阻塞式方法卡住了程序的运行
[3]. 分析出每一种阻塞式方法的每一种解除阻塞的条件
[4]. 对待单语句的阻塞式方法的调用(即没有出现在循环中的阻塞式方法),只要分析出来要么有一种情况可以解除这个阻塞式情况,就可以了。
【注意】循环式阻塞的分析是非常关键的。
(2). C/S中出现循环式阻塞方法的调用的分析
举例说明:以bufIn.readLine() != null为例,采用倒推法
倒推法
[1]. 先判断循环条件成立的时候------循环体能够正常执行
bufIn.readLine()!=null成立 ----> 这时候需要readLine方法返回的内容是非null才可以 ----> 那就是返回String数据即可----> readLine()返回String要求读到的是有效的行数据。此时如果读不到有效的行文本数据 ---- 阻塞{阻塞的条件最后带出来}
[2]. 再判断循环条件不成立的时候------循环整体能够正常结束
{1}. 这种情况分析的必要性
如果这种情况不分析,就很可能会出现当循环体都正常执行完需要结束的时候,很有可能因为无法返回不满足循环条件的返回值而卡在出现在循环中判断条件的阻塞式方法。
{2}. 分析如下
bufIn.readLine()!=null不成立 ----> 这时候需要readLine方法返回的内容是null才可以 ----> 此时需要readLine的这个缓冲输入流读取到流的结尾------> 流的结尾需要有结束标记被加入
***分析输入流的源是关键
[2]1. 如果是本地流 --- 源是本地文件。硬盘文件的末尾一定有结束标记,这样readLine读取到这个结束标记并返回null。所以读取本地文件的while循环可以正常结束
[2]2. 如果是本地流 --- 源是用户从键盘输入的数据,用户没有办法从键盘上输入流的结束标记。因此这个循环条件的判断形同虚设,无法正常结束。此时就需要在循环体中自定义结束标记来自行判断结束循环。
[2]3. 如果是网络流 --- 源一定是从客户端接收到的数据,这个数据的内容就不能确定。
因为客户端传来的数据很有可能被直接或者间接处理过,很有可能本来含有结束标记或者行标记的数据被破坏掉。因此这个地方要从客户端的程序出发,仔细分析客户端传送的数据的内容。
[2]3{1}. 如果此时客户端发送的数据没有包含流结束标记,这个时候服务器端就没有办法读取到流的结尾,这时候处于判断位置上的阻塞式方法会因为在有效数据读取完的情况下,无法读取到流的结尾而再一次循环到判断位置的阻塞式方法而阻塞下来,这样就会导致服务器端的程序被卡住。
这个时候,处理办法有两种:
****在客户端发送完不含有流结束标记的数据之后,强行加入“结束标记”
**{1}. 加入流的结束标记
主动在客户端使用socket的shutdownInputStream()在流中加入结束标记,确保服务器端的程序中的循环可以正常结束而不被卡住。此时服务器端程序不需要修正。
**{2}. 加入自定义的结束标记:
****在客户端为服务器端自定义结束标记:时间戳形式或者自定义结束字符串。这样要修正服务器端的代码。在服务器端的循环中对指定的结束标记进行判断,判断到就是用break强制结束循环。
[2]3{2}. 在客户端发送完正常的数据之后,如果能够没有其他阻塞方法而直接运行到socket.close()这句代码,这样这句代码就会自动为服务器端要读取到的输入流对象添加上流结束标记。此时客户端和服务器端的程序不用修正。
----------- android培训、java培训、java学习型技术博客、期待与您交流! ------------