简单得不能再简单的需求:
简单模拟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
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>
*/