[Java]命令行模拟TCP客户端与服务端的简单小程序遇到的问题(基础不牢!扎实和亲手实践比什么都重要!)

16 篇文章 0 订阅

简单得不能再简单的需求:

简单模拟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>


服务端多线程:多用户并发上传

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>

*/


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值