网络编程__【TCP传输】(重点)【Socket & ServerSocket】
概述:
TCP是面向连接的,在建立socket服务时就要有服务端存在,连接成功形成通路后,在该通道内进行数据的传输。
与UDP不同,TCP加入了网络流的概念,作为客户端InputStream的源
TCP传输步骤:
Socket和ServerSocket
建立客户端和服务器端
建立连接后,通过Socket中的IO流进行数据的传输
关闭socket
同样,客户端与服务器端是两个独立的应用程序。
一、TCP传输示例
客户端Socket
Socket对象在建立时就可以去连接指定主机
步骤:
1,创建Socket服务,明确要连接的主机和端口
2,通过Socket流获取iol流对象,进行数据的传输
3,关闭资源
import java.net.*; import java.io.*; class TcpClient { public static void main(String[] args) throws Exception { Socket s = new Socket("127.0.0.1",10005); OutputStream out = s.getOutputStream(); out.write("TCP is coming".getBytes()); s.close(); } }
服务端:ServerSocket
1,建立服务端的socket服务,并监听一个端口
2,获取链接过来的客户端对象;通过ServerSocket的accept()方法
该方法是阻塞式的,需要等待连接
3,客户端如果发送数据,服务端要通过对应的客户端对象获取该客户端对象的读取流来读取发送过来的数据
打印在控制台
4, 关闭客户端和服务端(可选)
class TcpServer { public static void main(String[] args) throws Exception { ServerSocket ss = new ServerSocket(10005); Socket s = ss.accept(); //该阻塞式方法获取到客户端对象 String ip = s.getInetAddress().getHostAddress(); System.out.println(ip+"...connected"); InputStream in = s.getInputStream(); byte[] buf = new byte[1024]; int len = in.read(buf); System.out.println(new String(buf,0,len)); s.close(); //关闭客户端,节省资源 ss.close(); //关闭服务端(可选) } }
二;演示TCP传输客户端和服务端互访
需求:客户端给服务端发送数据,服务端收到后给客户端反馈信息客户端:
1,建立Socket服务,指定要连接的主机和端口
2,获取socket流中的输出流,将数据写到该流中
3,获取Socket流中的输入流,获取服务端反馈
4,关闭客户端资源
import java.io.*; import java.net.*; class TcpClient2 { public static void main(String[] args) throws Exception { Socket s = new Socket("127.0.0.1",10008); OutputStream out = s.getOutputStream(); out.write("服务端你好,我是客户端".getBytes());//发送信息 InputStream in = s.getInputStream(); //读取反馈 byte[] buf = new byte[1024]; int len = in.read(buf); System.out.println(new String(buf,0,len)); s.close(); } }
服务端,接收数据进行处理并给客户端做出反馈
class TcpServer2 { public static void main(String[] args) throws Exception { ServerSocket ss = new ServerSocket(10008); Socket s = ss.accept(); String ip = s.getInetAddress().getHostAddress(); InputStream in = s.getInputStream();//读取信息 byte[] buf = new byte[1024]; int len = in.read(buf); System.out.println(ip+":"+(new String(buf,0,len))); OutputStream out = s.getOutputStream();//发送反馈 out.write("服务端收到!".getBytes()); s.close(); ss.close(); } }
三、需求:建立一个文本转换服务器
客户端给服务端发送文本,服务端将文本转成大写返回给客户端客户端可以持续进行文本转换,当客户端输入over时,转换结束。
分析:
客户端:
操作设备上的数据,可以使用IO技术,并按照io的操作规律来思考
源:键盘录入
目的:网络设备,网络输出流。
操作的是文本数据,可以使用字符流,加入缓冲区提高效率
客户端步骤:
1,建立服务
2,获取键盘录入
3,将数据发给服务端
4,获取服务端返回的大写数据
5,关闭资源
import java.io.*; import java.net.*; class TransClient { public static void main(String[] args) throws Exception { Socket s = new Socket("127.0.0.1",10086); //定义源,键盘录入数据 BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in)); //定义目的,将数据写入到Socket输出流,发送给服务器 // BufferedWriter bufOut = new BufferedWriter(new OutputStreamWriter(s.getOutputStream())); PrintWriter out = new PrintWriter(s.getOutputStream(),true);//替换BufferedWriter //定义源:取Socket输入流,接收服务器返回的信息 BufferedReader bufIn = new BufferedReader(new InputStreamReader(s.getInputStream())); String line = null; while ((line=bufr.readLine()) !=null) { if ("over".equals(line)) break; out.println(line);//目的:打印流,自动换行刷新 String str = bufIn.readLine(); System.out.println("server"+str); } bufr.close(); s.close(); } }
服务端:
class TransServer { public static void main(String[] args) throws Exception { ServerSocket ss = new ServerSocket(10086); Socket s = ss.accept(); String ip = s.getInetAddress().getHostAddress(); System.out.println(ip+"Connect"); //源,读取Socket输入流中的数据 BufferedReader bufIn = new BufferedReader(new InputStreamReader(s.getInputStream())); //目的,Socket输出流,将大写数据写入到socket输出流,反馈给客户端 // BufferedWriter bufOut = new BufferedWriter(new OutputStreamWriter(s.getOutputStream())); PrintWriter out = new PrintWriter(s.getOutputStream(),true);//打印流,替代输出流 String line = null; while ((line=bufIn.readLine()) !=null) { System.out.println(line); out.println(line.toUpperCase());//自动换行刷新,一步顶三步替换write/newLine/flush } s.close(); ss.close(); } }
运行结果:
可能会出现的问题:
1,客户端和服务端都在等待,无法进行操作
原因:
客户端和服务端都有阻塞式的方法,没读到结束标记就一直阻塞,导致两端等待
如readLine()读到换行符才会停止,需要加上手动换行标记,使其在流中传输时仍有换行标记;
2,可以使用PrintWriter来替代BufferedWriter。可以简化操作步骤;如println()方法可以替代字符流缓冲区的write/newLine/flush三个方法
PrintWriter();的构造方法不要忘记加"true"
四、Tcp上传文本文件
客户端上传文本文件到服务端,服务端保存后给客户端做出反馈
用到io流的字符流,使用缓冲区提高效率
客户端:
import java.io.*; import java.net.*; class TextClient { public static void main(String[] args) throws Exception { Socket s = new Socket("127.0.0.1",10086); BufferedReader bufr = new BufferedReader(new FileReader("IPDemo.java"));//源:读取文件 PrintWriter out = new PrintWriter(s.getOutputStream(),true);//目的:将数据写入到Socket写入流 String line = null; while ((line=bufr.readLine()) !=null) { out.println(line); } s.shutdownOutput();//关闭客户端的输出流,相当于给流中加一个结束标记-1 BufferedReader bufIn = new BufferedReader(new InputStreamReader(s.getInputStream()));//源:获取客户端反馈 String str = bufIn.readLine(); //阻塞式方法,等待服务端Socket流的反馈,会导致客户端阻塞 System.out.println(str); //目的:打印到控制台 bufr.close(); s.close(); } }
服务端:
class TextServer { public static void main(String[] args) throws Exception { ServerSocket ss = new ServerSocket(10086); Socket s = ss.accept(); String ip = s.getInetAddress().getHostAddress(); System.out.println(ip+"connected"); //源:获取Socket输入流的数据 BufferedReader bufIn = new BufferedReader(new InputStreamReader(s.getInputStream())); PrintWriter out = new PrintWriter(new FileWriter("server.txt"),true);//目的:将文件写入到目的地 String line = null; while ((line=bufIn.readLine()) !=null)//阻塞式方法:无结束标记就会一直等待 { out.println(line); // } PrintWriter pw = new PrintWriter(s.getOutputStream());//服务端阻塞,Socket无法到达客户端 pw.println("文件上传成功"); //循环不结束,下面就执行不到, out.close(); s.close(); ss.close(); } }
运行结果:
【小结】
因为用到阻塞式方法,会造成两端阻塞等待,所以每次TCP会话都要定义结束标记
定义结束标记方法:
1,自定义文字标记,例如输入字符"over"结束程序
优点:简单易用。缺点:如果文本中出现该字符会导致操作提前结束
2,使用时间戳:long time = System.currentTimeMillis();
使用DataOutputStream获取Socket中流;操作readLong
优点:时间唯一,不会重复。缺点:操作太繁琐
3,Socket自带方法;shutdownOutput()
简单易用,不会重复
五、TCP客户端并发上传图片
需求:实现多个客户端同时向服务端发送图片,服务端保存图片并返回给客户端上传成功的信息
分析:图片传输要用到io流中的字节流传输; 服务端要对输入的路径进行健壮性判断:文件是否存在、图片格式、大小等; 服务端要同时对多个客户端请求进行处理,需要用到多线程技术,将要处理的任务封装到线程对象中,客户端每发起一个成功的请求服务端就开辟一条线程;如果客户端重复上传了同一个文件就给文件加标识。
步奏:客户端:
1,创建服务
2,读取客户端已有的图片数据
3,通过Socket输出流将数据发送给服务端
4,读取服务端的反馈信息
5,关闭资源
import java.io.*; import java.net.*; class PicClient { public static void main(String[] args) throws Exception { //从命令行输入一个路径,路径必须是一个连续的字符串 if (args.length==0){//如果没有传参数 System.out.println("请选择一张图片"); return; } File file = new File(args[0]); if(!(file.exists() && file.isFile())){//如果路径不存在或者不是文件 System.out.println("未找到文件,请重新输入"); return; } if (file.getName().endsWith(".jpg")){ System.out.println("格式错误:jpg"); return; } if (file.length()>10240*1024*3){//图片大小限制在3M以内 System.out.println("文件过大,请压缩到3M以内上传"); return; } //以上为上传图片前客户端的建壮性判断;下面开始图片的上传 Socket s = new Socket("127.0.0.1",10086); FileInputStream fis = new FileInputStream(file); OutputStream out = s.getOutputStream(); byte[] buf = new byte[1024]; int len = 0; while ((len=fis.read(buf)) !=-1) { out.write(buf,0,len); } s.shutdownOutput();//数据写完时通知服务端,不再阻塞 InputStream in = s.getInputStream();//获取服务端反馈 byte[] bufIn = new byte[1024]; int num = in.read(bufIn); System.out.println(new String(bufIn,0,num)); fis.close(); s.close(); } }
服务端:
局限性:
服务端一次只能处理一个客户端请求,该请求没有执行完就无法循环回来执行accept();其它客户端请求只能等待
为了能够让多个客户端并发访问服务端,在服务端引入多线程技术;将每个客户端请求封装到一个单独的线程中进行处理
定义线程:
只要明确客户端在服务端执行的代码即可,将该任务存入run()方法中
class PicServer //服务端 { public static void main(String[] args) throws Exception { ServerSocket ss = new ServerSocket(10086); while (true) { Socket s = ss.accept();//获取socket对象 new Thread(new PicThread(s)).start();//将socket对象封装进线程对象,通过Thread开启线程 } } } class PicThread implements Runnable//线程任务 { private Socket s; PicThread(Socket s){ this.s = s; } public void run()//实现run 方法 { int count = 0;//要定义成局部,循环判断;如果count定义为成员会共享该数据,而且会造成线程安全问题 String ip = s.getInetAddress().getHostAddress(); try { System.out.println(ip+"...Conected"); InputStream in = s.getInputStream();//源:获取客户端文件 File file = new File(ip+".jpg"); while(file.exists())//循环判断在服务端该文件是否存在 file = new File(ip+"("+(++count)+")"+".jpg"); FileOutputStream fos = new FileOutputStream(file);//目的: byte[] buf = new byte[1024]; int len = 0; while ((len=in.read(buf)) !=-1) { fos.write(buf,0,len); } OutputStream out = s.getOutputStream();//通知客户端上传成功 out.write("图片上传成功".getBytes()); fos.close(); s.close(); } catch (Exception e){ throw new RuntimeException(ip+":图片上传失败!"); } } }
运行结果:
【总结:】
在实际的使用过程中,服务端不可能一次只处理一个客户端的请求,通常是大量客户端的并发访问;该示例是服务器多线程处理的雏形,要做重点掌握
六、客户端并发登陆
客户端并发登陆
需求:实现多个客户端同时登陆服务端
分析:
客户端将登陆信息发送给服务端,服务端通过验证后确认是否够登陆,并将登陆结果反馈给客户端;为防止服务器压力过大,每个用户名只能登陆三次。
要点:服务端需要用到多线程处理多条请求,使用io流验证用户信息和传输数据
代码实现:
import java.net.*; import java.io.*; class LoginCilent//客户端 { public static void main(String[] args) throws Exception { Socket s = new Socket("127.0.0.1",10086); BufferedReader buf = new BufferedReader(new InputStreamReader(System.in));//用户输入 PrintWriter out = new PrintWriter(s.getOutputStream(),true); //发送到服务器 BufferedReader bufIn = new BufferedReader(new InputStreamReader(s.getInputStream()));//获取反馈 for (int x=0; x<3 ;x++ ) { String line = buf.readLine(); if (line == null) break; out.println(line); String info = bufIn.readLine();//读取反馈 System.out.println("info:"+info); if(info.contains("欢迎"))//登陆成功的信息 break; } buf.close(); s.close(); } } class LoginServer//服务端 { public static void main(String[] args) throws Exception { ServerSocket ss = new ServerSocket(10086); while (true)//服务端应长时间开启等待客户端访问 { Socket s = ss.accept(); new Thread(new UserThread(s)).start(); } } } class UserThread implements Runnable//线程任务 { private Socket s; UserThread(Socket s){ this.s = s; } public void run() { String ip = s.getInetAddress().getHostAddress(); System.out.println(ip+"...connected..."); try { for (int x=0; x<3 ;x++ ) { BufferedReader bufIn = new BufferedReader(new InputStreamReader(s.getInputStream())); String name = bufIn.readLine(); if(name==null) break; BufferedReader bufr = new BufferedReader(new FileReader("User.txt")); PrintWriter out = new PrintWriter(s.getOutputStream(),true); String line = null; boolean flag = false; while ((line=bufr.readLine()) !=null) { if(line.equals(name)) { flag = true; break;//找到用户就跳出当前while循环,将标记改为true } } if (flag)//由标记来决定是否执行 { System.out.println(name+"已登录");//服务端监控 out.println(name+": 登陆成功,欢迎光临");//反馈 break; //跳出for循环 } else { System.out.println(name+"尝试登陆"); //服务端监控 out.println(name+": 用户名不存在!");//反馈 } } s.close(); } catch (Exception e) { throw new RuntimeException("验证失败"+ip); } } }运行结果: