简单得不能再简单的需求:
简单模拟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>
[email protected]
分析:根据服务端输出结果和客户端未收到反馈,以及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>
服务端多线程:多用户并发上传
1.第一次编写的代码,未成功,但思路应该没问题,贴出来日后找原因:
//分析:两种方式 //1.把整个客户端程序封装成Runnable,服务端这边直接new对象封装多线程 //2.你控制不了客户端程序代码的编写,只有在服务端这边编程,获取客户端请求任务,在服务端这边封装成Runnable创建多线程 import java.io.*; import java.net.*; class Server implements Runnable { public ServerSocket ss; public Socket s=null; public InputStream is; public FileOutputStream fo=null; public byte[] buffer=new byte[1024*1024]; public int length=0; public Server(ServerSocket ss){//ss始终是同一个ServerSocket,s是多线程接收的客户端Socket,destDir应该是先传输过来的文件要保存路径 try { this.ss=ss; this.s=ss.accept();//接收几个客户端,初始化几个线程 this.is=s.getInputStream(); //获得文件目的路径 BufferedReader bufr=new BufferedReader(new InputStreamReader(is)); String destDir=bufr.readLine(); this.fo=new FileOutputStream(destDir); } catch (Exception e) { throw new RuntimeException(e); } } public void run(){//多个线程,一个线程一个单独任务,一个线程一个循环,多个线程在服务端抢夺执行 try { //初始化好的同样的所需变量,这里只执行循环 while((length=is.read(buffer))!=-1){ fo.write(buffer,0,length); } } catch (Exception ex) { throw new RuntimeException(ex); } finally{ try { if(fo!=null) fo.close(); if(s!=null) s.close(); } catch (Exception exc) { throw new RuntimeException(exc); } } } } class TcpClient5 { public static void main(String[] args) throws Exception { Socket s=new Socket("127.0.0.1",10015); FileInputStream fi=new FileInputStream("c:\\original_2hFx_6bb6000094b5118c.jpg");//现实中客户端程序这里也是可变的 OutputStream out=s.getOutputStream(); BufferedReader bufr=new BufferedReader(new InputStreamReader(System.in)); System.out.println("Please input your Destination:"); String str=bufr.readLine(); BufferedWriter bufw=new BufferedWriter(new OutputStreamWriter(out)); bufw.write(str); bufw.newLine();//加回车换行,那边readLine可以结束 byte[] buffer=new byte[1024*1024]; int length=0; while((length=fi.read(buffer))!=-1){ out.write(buffer,0,length); } fi.close(); s.close(); } } class TcpServer5 { public static void main(String[] args) throws Exception { ServerSocket ss=new ServerSocket(10015); new Thread(new Server(ss)).start(); new Thread(new Server(ss)).start(); new Thread(new Server(ss)).start(); ss.close(); } }
2.第二次从头编写,达到目的,运行成功,继续改进完善和增强些功能即可(代码结尾附两个服务线程情况下的运行结果):
import java.io.*; import java.net.*; class Server implements Runnable { public Server(ServerSocket ss)//强制初始化,并且只这一项必须要传值! { this.ss=ss;//初始化是初始化,run是run,分开来看,不要乱! } private ServerSocket ss=null; private Socket s=null; private String line=null; public void run(){//不慌不忙地琢磨,做成一个任务!!! try { //System.out.println(Thread.currentThread()); //没关系,单独的线程创建并启动,完成初始化运行了上面一句,但阻塞在这里!等待客户机访问!!! s=ss.accept();//捕获才能操纵------>会对同一个客户端重复获取吗?--->试验 //如果第二个线程没有接收同一个Socket,它应该始终阻塞在那里,前一个线程有控制权执行,而这里应该只打印一次!并且显示是第一个线程! System.out.println(Thread.currentThread()); BufferedWriter bufw=new BufferedWriter(new OutputStreamWriter(s.getOutputStream())); //如果服务端这边是两个线程服务了一个客户端,那么客户端应该打印两句这话,实际上没有,所以那边一个线程接收和服务一个客户端设计正确!想服务无上限只要把这里的accept提到主线程中,while(true)接收即可!!! bufw.write("Please type in your destination:"); bufw.newLine();//给那边readLine一个回车符 bufw.flush(); BufferedReader bufr=new BufferedReader(new InputStreamReader(s.getInputStream())); //以后改进:图形化让用户自己在本地选择要上传文件 //以后改进:获取文件名在这边建立同名文件,用户只需指出这边保存文件夹路径 String destDir=bufr.readLine();//阻塞等待 System.out.println(destDir); //接收那边的文件流,写入这边的文件 //???这里传过来的文件路径非法,竟然不报异常!并且从打印测试看,while循环竟然整个执行!!!java怎么做到的?!!! BufferedWriter bufw1=new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File(destDir)))); //开始接收文件 while(true){ if((line=bufr.readLine())==null)//这里要等对方流关闭,或者对方加入一个流shutDown!!! break; //System.out.println(line); bufw1.write(line); bufw1.newLine(); bufw1.flush(); } System.out.println("Done");//竟然输出,没发生异常??? bufw1.close(); s.close(); } catch (Exception e) { throw new RuntimeException(e); } } } class TcpClient { public static void main(String[] args){ try { Socket s=new Socket("127.0.0.1",10009); //这里要先接收消息啊!!!不然那边没写过来这边就写过去,用的是同一个客户端的网络流!冲突! BufferedReader bufr2=new BufferedReader(new InputStreamReader(s.getInputStream())); BufferedWriter bufw2=new BufferedWriter(new OutputStreamWriter(System.out)); String line2=bufr2.readLine(); bufw2.write(line2); bufw2.newLine(); bufw2.flush();//别忘了!!! //System.out.println("Hello"); //接收信息后传递目的地址 BufferedReader bufr=new BufferedReader(new InputStreamReader(System.in)); BufferedWriter bufw=new BufferedWriter(new OutputStreamWriter(s.getOutputStream())); String line=bufr.readLine();//阻塞在这里,而上边服务端开启的线程也因accept阻塞了! bufw.write(line); bufw.newLine();//给那边readLine一个回车符 bufw.flush(); //那边继续读,这边开始写文件 BufferedReader bufr1=new BufferedReader(new InputStreamReader(new FileInputStream(new File("c:\\1.txt")))); while((line=bufr1.readLine())!=null){//两边while循环,这边相当于一次性读入,那边阻塞式按行接收,再写入 //System.out.println(line); bufw.write(line); bufw.newLine();//给那边readLine一个回车符 bufw.flush(); } //测试后补充一个上传成功的反馈信息,那边接收一下,注意这边要先插入一个流shutDown让 //那边while结束(null)!!!然后再反馈,如果这里不反馈,下边s.close()自然会让那边while结束! bufr.close(); bufr1.close(); s.close(); } catch (Exception e) { throw new RuntimeException(e); } } } //异常处理日后改进(加finally,加嵌套,加判断,加处理,Socket异常单拿出来) class TcpServer//重要的核心的东西写完了,再修修补补做次要的东西!从最简单最重要的开始,不慌不忙,不要一下子想所有问题! { public static void main(String[] args){ try { ServerSocket ss=new ServerSocket(10009); new Thread(new Server(ss)).start();//是多线程中的accept让服务端处于等待状态!!!accept接收Servlet后,才进行文件复制工作! new Thread(new Server(ss)).start();//好玩的事情来了!------>客户端那边程序退出后这边并没有退出,因为第一个线程成功接收Socket完全执行完(第二个线程因为一直阻塞等待所以一直没有执行权)后,第二个线程还在等待接收Socket,这时你再访问,它会接着为您服务(从线程名打印中看出来了)!!!这就是服务端多线程了!服务多个随时来访用户!!!但这里第二个线程执行完后就没有服务线程了,想持续服务要while(true),把accept接收Socket从run方法中提到主线程中来!!! } catch (Exception e) { throw new RuntimeException(e); } } } /* 莫名的地方:输入非法路径值不报异常! 缺憾:用户自己输入本-地-上-传文件路径选择要上传的文件,服务端创建同名文件,同一个文件上传多次服务端创建带标码的同名文件,服务端while(true)无上限接收客户,并为该用户即使创建、开启新线程(执行完即退出!) 附:两个线程的执行结果 Server: D:\java\practice4>javac TCP6_TextCopy.java D:\java\practice4>java TcpServer Thread[Thread-0,5,main]---------------->线程0 ertgrgr Done Thread[Thread-1,5,main]---------------->线程1,继续接收任务,此后不再有线程 e:\\xiaobai.txt Done D:\java\practice4> Client: D:\java\practice4>java TcpClient------------>线程0为您服务 Please type in your destination: ertgrgr D:\java\practice4>java TcpClient-------------->线程1竭诚为您服务! Please type in your destination: e:\\xiaobai.txt D:\java\practice4> */