[Java]命令行模拟TCP客户端与服务端的简单小程序遇到的有关问题(基础不牢!扎实和亲手实践比什么都重要!)
简单得不能再简单的需求:
简单模拟TCP客户端与服务端的一次连接和通信,客户端发出一个消息,服务端回馈一个消息
自己第一次编写的代码:
Client:
class TcpClient1 { public static void main(String[] args) throws Exception { Socket s=new Socket("127.0.0.1",10010); OutputStream out=s.getOutputStream(); out.write("Tcp ge men lai la".getBytes()); //Receive InputStream in=s.getInputStream(); byte[] buf=new byte[1024]; int len=0; //break off here while((len=in.read(buf))>0){//读,阻塞式方法,这里Ctrl+C才会结束 System.out.println(new String(buf,0,len));//do on your own,find on your own! } s.close(); } }
Server:
class TcpServer1 { public static void main(String[] args) throws Exception { ServerSocket ss=new ServerSocket(10010); Socket s=ss.accept(); String ip=s.getInetAddress().getHostAddress(); System.out.println(ip+"........connected."); InputStream in=s.getInputStream(); int len=0; byte[] buf=new byte[1024]; while((len=in.read(buf))>0){//读,阻塞式方法,这边也一直等! System.out.println(new String(buf,0,len)); } //break off here //Client waiting,so you can write to him right now OutputStream out=s.getOutputStream(); out.write("Copy that.".getBytes()); s.close(); ss.close(); } }
命令行编译,两个命令行窗口,先启动服务端,后启动客户端,结果:
Server:
D:\java\practice3>javac TCP1.java
D:\java\practice3>java TcpServer1
127.0.0.1........connected.
Tcp ge men lai la
Client:
D:\java\practice3>java TcpClient1
两端都阻塞,没有结束。
@
在客户端按Ctrl+C,结果:
Server:
D:\java\practice3>java TcpServer1
127.0.0.1........connected.
Tcp ge men lai la
Exception in thread "main" java.net.SocketException: Connection reset
at java.net.SocketInputStream.read(SocketInputStream.java:168)
at java.net.SocketInputStream.read(SocketInputStream.java:90)
at TcpServer1.main(TCP1.java:51)
D:\java\practice3>
~@
分析:根据服务端输出结果和客户端未收到反馈,以及read方法特点,判断都阻塞在了各自的read方法上。两端都用了循环,那么在未收到文件结束标记前(这里只能用Ctrl+C)都会一直阻塞等待。还是基础不牢的问题。
修改调试和验证:让服务端先只读一次,而故意在服务端这边不关客户端和服务端,让程序自然结束,看客户端的反应和程序终止结果:
修改程序:
Client:
class TcpClient1 { public static void main(String[] args) throws Exception { Socket s=new Socket("127.0.0.1",10010); OutputStream out=s.getOutputStream(); out.write("Tcp ge men lai la".getBytes()); //Receive InputStream in=s.getInputStream(); byte[] buf=new byte[1024]; int len=0; //break off here //len=in.read(buf); while((len=in.read(buf))>0){//读,阻塞式方法,这里Ctrl+C才会结束 System.out.println(new String(buf,0,len));//do on your own,find on your own! } s.close(); } }
Server:
class TcpServer1 { public static void main(String[] args) throws Exception { ServerSocket ss=new ServerSocket(10010); Socket s=ss.accept(); String ip=s.getInetAddress().getHostAddress(); System.out.println(ip+"........connected."); InputStream in=s.getInputStream(); int len=0; byte[] buf=new byte[1024]; len=in.read(buf); //while((len=in.read(buf))>0){//读,阻塞式方法,这边也一直等! System.out.println(new String(buf,0,len)); //} //break off here //Client waiting,so you can write to him right now OutputStream out=s.getOutputStream(); out.write("Copy that.".getBytes()); //s.close(); //ss.close(); } }
运行结果:
Server:
D:\java\practice3>javac TCP1.java
D:\java\practice3>java TcpServer1
127.0.0.1........connected.
Tcp ge men lai la
D:\java\practice3>
Client:
D:\java\practice3>java TcpClient1
Copy that.
Exception in thread "main" java.net.SocketException: Connection reset
at java.net.SocketInputStream.read(SocketInputStream.java:168)
at java.net.SocketInputStream.read(SocketInputStream.java:90)
at TcpClient1.main(TCP1.java:23)
D:\java\practice3>
发现客户端收到了回馈,但由于服务端自然终止,而客户端这边的read方法还在循环等待,所以抛出连接异常(因为服务端已经关闭服务);反过来如果服务端这边循环,客户端那边提前关闭,服务端这边想写入回馈信息发现客户端已关闭(注意不是因为客户端关闭的原因而是因为想写入发现客户端已关闭,服务端这边没有客户端连接是不会发生连接异常的,只有客户端主动连接服务端发现无法连接时才会有连接异常!),也会抛出连接异常,如上文@处所示。
需求2:客户端输入文本数据,服务端转成大写返给客户端,客户端不断输入,直到输入over时结束转换
源程序:
Client:
class TcpClient2 { public static void main(String[] args) throws Exception { Socket s=new Socket("127.0.0.1",10011); OutputStream out=s.getOutputStream(); InputStream in=s.getInputStream(); BufferedWriter bufw=new BufferedWriter(new OutputStreamWriter(out)); BufferedReader bufr=new BufferedReader(new InputStreamReader(System.in)); BufferedReader bufr1=new BufferedReader(new InputStreamReader(in)); String line; //标准输入敲回车是有回车换行符的,这里可以循环读 while(!"over".equals(line=bufr.readLine())){//循环读写,阻塞式--->读标准输入,写给服务端 bufw.write(line); bufw.newLine();//回车换行,为了那边readLine遇见,成功读取! bufw.flush(); String line1=bufr1.readLine(); System.out.println(line1); } s.close();//关闭,自然关闭流,自然给一个文件结束标记,那边readLine结果为null(这不同于回车换行标记!!!回车换行用于成功读取一行,而文件结束标记用于让readLine结果为null!) } }
Server:
class TcpServer2 { public static void main(String[] args) throws Exception { ServerSocket ss=new ServerSocket(10011); Socket s=ss.accept(); String ip=s.getInetAddress().getHostAddress(); System.out.println(ip+".........connected."); InputStream in=s.getInputStream(); OutputStream out=s.getOutputStream(); BufferedReader bufr=new BufferedReader(new InputStreamReader(in)); BufferedWriter bufw=new BufferedWriter(new OutputStreamWriter(out)); String line; //问题:客户端那边的readLine不包括回车换行符,如果读入的一行没有换行标记,这里一直阻塞,所以那边要用newLine方法 while((line=bufr.readLine())!=null){//阻塞式,一直等待------>读客户端发送,写给客户端 bufw.write(line.toUpperCase()); bufw.newLine(); bufw.flush(); } s.close(); ss.close(); } }
测试结果:
Server:
D:\java\practice3>javac TCP2.java
D:\java\practice3>java TcpServer2
127.0.0.1.........connected.
D:\java\practice3>
Client:
D:\java\practice3>java TcpClient2
fwfwfwe
FWFWFWE
gfesaf
GFESAF
gfwgvwergve
GFWGVWERGVE
fwfwfwef
FWFWFWEF
dsfsfsdfsdvgdsv
DSFSFSDFSDVGDSV
sfsfcsdf
SFSFCSDF
sdfcsd
SDFCSD
fvs
FVS
fvds
FVDS
fvs
FVS
df
DF
sd
SD
iver
IVER
over
D:\java\practice3>
一个多线程玩传歌的小例子--->客户端多线程:(本人独创,正确性待多次验证,勿喷勿盗,Thanks~^-^)
(最新更新:简单修改并输出线程测试,实现了此多线程任务,详见下文)
(更新:发现问题--->这种同步方式,根本就是一个线程在上传文件,因为进入循环读写后别的线程都进不来!并且Socket关闭处也应判断文件是否传完和其是否已经关闭再执行!改进的想法是在while循环里面同步,只同步读写部分,但这里read还需要在while循环头中判断,所以一时没有想出好办法。这个程序运行无误,但没有达成想要的目的,没有实现多线程。先留在这,日后想办法解决)
源程序:
Runnable:
class Upload implements Runnable { public Socket s; public FileInputStream fi; public OutputStream out; public byte[] buf; public int length; public Upload(Socket s,FileInputStream fi,OutputStream out,byte[] buf,int length) { this.s=s; this.fi=fi; this.out=out; this.buf=buf; this.length=length; } public void run(){ try { //操作同一个资源,你必须用同步! //这里的this是同一个对象,就用它来锁! synchronized(this){ while((!s.isClosed()) && (length=fi.read(buf))>0){//循环判断,每次判断s是否关闭!!!如果不判断,那么如果s已经关闭而另一个线程仍然企图写入,就会出现异常!!! out.write(buf,0,length); } } } catch (Exception e) { throw new RuntimeException(e); } finally{ try { //凡是操作同一个资源的地方都要加上同步! synchronized(this){ s.close(); } } catch (Exception ex) { throw new RuntimeException(ex); } } } }
Client:
class TcpClient3 { public static void main(String[] args) throws Exception { Socket s=new Socket("127.0.0.1",10012); FileInputStream fi=new FileInputStream("c:\\23. George Michael - Careless Whisper.mp3"); OutputStream out=s.getOutputStream(); byte[] buf=new byte[1024*1024]; int length=0; Upload up=new Upload(s,fi,out,buf,length); new Thread(up).start(); new Thread(up).start(); new Thread(up).start(); } }
Server:
class TcpServer3 { public static void main(String[] args) throws Exception { ServerSocket ss=new ServerSocket(10012); Socket s=ss.accept(); InputStream in=s.getInputStream(); FileOutputStream fo=new FileOutputStream("d:\\3.mp3"); byte[] buf=new byte[1024*1024]; int length; while((length=in.read(buf))>0){ fo.write(buf,0,length); } s.close(); ss.close(); } }
结果:文件品质一致,复制上传成功。
(注:
1.一开始遇见实现Runnable的对象中Socket,FileOutputStream等对象无法处理异常的问题,于是把它们挪回Client主程序,仅在类中保留其引用;
2.注意多线程操作的是同一个实现Runnable的对象;
3.后来又在运行产生的异常中发现了同步问题--->发生异常的原因是一个线程关闭了Socket后另一个线程无法再访问Socket流,并且上传结果也出现与源文件不一致的问题,继而发现写文件处也需要同步;
4.因为始终是同一个流操作同一个源文件,所有多线程调用write应该是顺序续写的,我没有尝试多次,仍需多次验证结果决断这个程序的正确性和健壮性)
改进版源程序及测试结果:
Runnable:
class Upload implements Runnable { public Socket s; public FileInputStream fi; public OutputStream out; public byte[] buf; public int length; public Upload(Socket s,FileInputStream fi,OutputStream out,byte[] buf,int length) { this.s=s; this.fi=fi; this.out=out; this.buf=buf; this.length=length; } public void run(){ try { //操作同一个资源,你必须用同步! //这里的this是同一个对象,就用它来锁! while(true){ synchronized(this){ System.out.println(Thread.currentThread()); if(s.isClosed()) break; if((length=fi.read(buf))<0) break; out.write(buf,0,length); } } /* synchronized(this){ while((!s.isClosed()) && (length=fi.read(buf))>0){//循环判断,每次判断s是否关闭!!!如果不判断,那么如果s已经关闭而另一个线程仍然企图写入,就会出现异常!!! out.write(buf,0,length); } } */ } catch (Exception e) { throw new RuntimeException(e); } finally{ try { //凡是操作同一个资源的地方都要加上同步! synchronized(this){ if(!s.isClosed()) s.close(); } } catch (Exception ex) { throw new RuntimeException(ex); } } } }
Client:
class TcpClient3 { public static void main(String[] args) throws Exception { Socket s=new Socket("127.0.0.1",10012); FileInputStream fi=new FileInputStream("c:\\23. George Michael - Careless Whisper.mp3"); OutputStream out=s.getOutputStream(); byte[] buf=new byte[1024*1024]; int length=0; Upload up=new Upload(s,fi,out,buf,length); new Thread(up).start(); new Thread(up).start(); new Thread(up).start(); } }
Server:
class TcpServer3 { public static void main(String[] args) throws Exception { ServerSocket ss=new ServerSocket(10012); Socket s=ss.accept(); InputStream in=s.getInputStream(); FileOutputStream fo=new FileOutputStream("e:\\3.mp3"); byte[] buf=new byte[1024*1024]; int length; while((length=in.read(buf))>0){ fo.write(buf,0,length); } s.close(); ss.close(); } }
运行结果(客户端):上传正确(注意除主线程外已经有三个线程在跑)
D:\java\practice3>java TcpClient3
Thread[Thread-0,5,main]
Thread[Thread-0,5,main]
Thread[Thread-0,5,main]
Thread[Thread-0,5,main]
Thread[Thread-2,5,main]
Thread[Thread-1,5,main]
Thread[Thread-1,5,main]
Thread[Thread-1,5,main]
Thread[Thread-1,5,main]
Thread[Thread-1,5,main]
Thread[Thread-1,5,main]
Thread[Thread-1,5,main]
Thread[Thread-1,5,main]
Thread[Thread-2,5,main]
Thread[Thread-0,5,main]
D:\java\practice3>
服务端多线程:多用户并发上传