五. TCP网络编程
5.1 网络通信
Java语言的基于套接字TCP编程分为服务端编程和客户端编程,其通信模型如图所示:
基于TCP的Socket通图:
5.2 网络编程案例
5.2.1 基于TCP协议的网络编程案例
案例一:服务器端发送消息给客户端,客户端接收消息
🔔服务器端需求:
- 开启一个TCP协议的服务,在8888端口号监听客户端的连接。
- 接收一个客户端的连接
- 给这个客户端发一句话: 欢迎登录
- 关闭服务器
🔔客户端需求:
- 与服务器建立连接,或者说和服务器发起连接请求
- 直接接收服务器发过来的“欢迎登录”
- 关闭客户端连接
代码演示如下:
public class Server1 { public static void main(String[] args) throws IOException { //开启一个TCP协议的服务,在8888端口号监听客户端的连接 ServerSocket server=new ServerSocket(8888); //正式接收客户端的连接 Socket socket=server.accept(); /* 这是一个阻塞式方法, 如果此时没有客户端来请求,则这句代码一直等待。这句代码执行一次,就表示有一个客户端请求连接了,并且 连接成功; 连接成功后,会给它分配一个Socket对象,这个socket对象用来和这个客户端进行数据的传输通信。 */ //如果连接成功,则会打印如下这句话 System.out.println(socket.getInetAddress()+"连接成功"); //给该客户端发送一句话,欢迎登录 OutputStream out=socket.getOutputStream();//套接字的输出流,发送消息 String info="欢迎登录"; out.write(info.getBytes());//将字符串转换为字节数据 //如果后面不和这个客户端通信,服务器就要关闭 socket.close(); //如果服务器后面不接受其他客户端连接,那么服务关闭,一般不关闭。 server.close(); } }
import java.io.IOException; import java.io.InputStream; import java.net.InetAddress; import java.net.Socket; import java.net.UnknownHostException; public class Client { public static void main(String[] args) throws IOException { Socket socket=new Socket(InetAddress.getLocalHost().getHostAddress(),8888); InputStream input = socket.getInputStream(); //流套接字的输入流,用来接收并读取消息 byte[] data=new byte[1024];//用来装接收来自的服务器的字节数据 int len; while ((len=input.read(data))!=-1){//从输入流中读取一定数量的字节,并将其存储在缓冲区数组 data 中,返回的是实际读取的字节数 System.out.println(new String(data,0,len));// 通过使用平台的默认字符集解码指定的 byte 数组data,构造一个新的 String。 //即将每次读取的实际字节数解码为string输出到控制台 } //后续不不需要通信,就关闭连接 socket.close(); } }
案例二:客户端与服务器端多次通信
🔔服务器端需求:
(1)开启一个TCP协议的服务,在8888端口号监听客户端的连接(2)接收一个客户端的连接
(3)接收客户端发过来的单词或词语,然后我把它“反转”例如: 客户端发过来“heLLo",反转"oLLeh"
(4)把反转后的单词或词语,返回给客户端
(5)直到客户端发过来"bye"为止
🔔客户端需求:
(1) 与服务器建立连接,或者说和服务器发起连接请求(2)从键盘输入单词,并且按行给服务器发送每一个单词
(3) 直到输入bye结束
(4)结束后要断开连接
代码演示如下:
import java.io.*; import java.net.ServerSocket; import java.net.Socket; public class Server { public static void main(String[] args) throws IOException { ServerSocket server=new ServerSocket(8888); Socket socket=server.accept(); System.out.println(socket.getInetAddress()+"连接成功"); InputStream input = socket.getInputStream();//服务器端接收客户端发来的信息【字节输入流】 InputStreamReader isr=new InputStreamReader(input); //将字节输入流通过转换流转换为字符输入流 BufferedReader br=new BufferedReader(isr);//缓冲流包裹字符输入流,我时希望可以使用缓冲流独有的方法readLine() OutputStream output = socket.getOutputStream();//服务器发送给客户端的消息 /* OutputStreamWriter osw=new OutputStreamWriter(output);//将字节输出流转换为字符输出流 BufferedWriter bw=new BufferedWriter(osw);//缓冲流包裹字符输出流,可以使用它独有的方法;*/ PrintStream pr=new PrintStream(output); String s ; while (!(("bye".equals(s=br.readLine())))){ System.out.println("客户端发来的单词或词语:"+s); if (s.equals(null)){ throw new NullPointerException("客户端发来的字符串为空"); } StringBuilder sb=new StringBuilder(s); sb.reverse(); System.out.println("服务器已反转单词:"+sb.toString()); pr.println(sb.toString());//将反转后的字符串换发给客户端 } socket.close(); server.close(); } }
import java.io.*; import java.net.InetAddress; import java.net.Socket; import java.net.UnknownHostException; import java.util.Scanner; public class Client { public static void main(String[] args) throws IOException { Socket socket=new Socket(InetAddress.getLocalHost().getHostAddress(),8888); OutputStream output = socket.getOutputStream();//字节输出流;output输出,从内存写出到文件,可以发送消息 /* OutputStreamWriter osw=new OutputStreamWriter(output);//字节流转换为字符输出流 BufferedWriter bw=new BufferedWriter(osw);//缓冲流包裹字符流*/ PrintStream pr=new PrintStream(output); Scanner input=new Scanner(System.in);//System.in:默认键盘输入 InputStream in = socket.getInputStream();//字节输入流;接收消息 InputStreamReader isr=new InputStreamReader(in);//字节输入流转换为字符输入流 BufferedReader br=new BufferedReader(isr);//缓冲流包裹字符输入流 while (true){ System.out.print("请输入单词或词语:"); String word=input.next(); pr.println(word); if (word.equals("bye")){ break; } System.out.println("服务器返回的结果:"+br.readLine()); } socket.close(); } }
案例三:上述需求不变,服务器端要与多个客户端多次通信
🔔服务器端需求:
(1)开启一个TCP协议的服务,在8888端口号监听客户端的连接(2)接收一个客户端的连接
(3)接收客户端发过来的单词或词语,然后我把它“反转”例如: 客户端发过来“heLLo",反转"oLLeh"
(4)把反转后的单词或词语,返回给客户端
(5)直到客户端发过来"bye"为止
🔔客户端需求:
(1) 与服务器建立连接,或者说和服务器发起连接请求(2)从键盘输入单词,并且按行给服务器发送每一个单词
(3) 直到输入bye结束
(4)结束后要断开连接
🤮服务器端要同时与多个客户端通信
核心实现思路:运用多线程实现同时与多个客户端通信
代码演示如下:
import java.io.*; import java.net.ServerSocket; import java.net.Socket; public class Server { public static void main(String[] args) throws IOException { ServerSocket server = new ServerSocket(8888); //接收多个客户端连接 while (true) { Socket socket = server.accept(); System.out.println(socket.getInetAddress()+"连接成功"); new ClientHandle(socket).start(); //服务器端一直运行 // server.close(); } } } class ClientHandle extends Thread{ private Socket socket; public ClientHandle(Socket socket){ this.socket=socket; } @Override public void run() { //这里编译器报红了,此异常为受检异常,要么抛出去;要么try...catch。但是这里不能跑出去,run()方法是重写方法,对于 //受检异常来说,如果被重写方法没有抛出此受检异常,那么重写的方法不能跑出去,只能自己try..catch try ( InputStream input = socket.getInputStream();//服务器端接收客户端发来的信息【字节输入流】 InputStreamReader isr = new InputStreamReader(input); //将字节输入流通过转换流转换为字符输入流 BufferedReader br = new BufferedReader(isr);//缓冲流包裹字符输入流,我时希望可以使用缓冲流独有的方法readLine() OutputStream output = socket.getOutputStream();//服务器发送给客户端的消息 PrintStream pr = new PrintStream(output); ) { String s; //这里编译器报红了,此异常为受检异常,要么抛出去;要么try...catch while (!(("bye".equals(s = br.readLine())))) { System.out.println("客户端发来的单词或词语:" + s); if (s.equals(null)) { throw new NullPointerException("客户端发来的字符串为空"); } StringBuilder sb = new StringBuilder(s); sb.reverse(); System.out.println("服务器已反转单词:" + sb.toString()); pr.println(sb.toString());//将反转后的字符串换发给客户端 } }catch (Exception e){ e.printStackTrace(); }finally { //这里编译器报红了,此异常为受检异常,要么抛出去;要么try...catch try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } }
import java.io.*; import java.net.InetAddress; import java.net.Socket; import java.util.Scanner; public class Client { public static void main(String[] args) throws IOException { Socket socket=new Socket(InetAddress.getLocalHost().getHostAddress(),8888); OutputStream output = socket.getOutputStream();//字节输出流;output输出,从内存写出到文件,可以发送消息 PrintStream pr=new PrintStream(output); Scanner input=new Scanner(System.in);//System.in:默认键盘输入 InputStream in = socket.getInputStream();//字节输入流;接收消息 InputStreamReader isr=new InputStreamReader(in);//字节输入流转换为字符输入流 BufferedReader br=new BufferedReader(isr);//缓冲流包裹字符输入流 while (true){ System.out.print("请输入单词或词语:"); String word=input.next(); pr.println(word); if (word.equals("bye")){ break; } System.out.println("服务器返回的结果:"+br.readLine()); } socket.close(); } }
案例四:多个客户端上传文件
🔔需求:
每一个客户端启动后都可以给服务器上传一个文件,服务器接收到文件后保存到一个upLoad目录中,可以同时接收多个客户端的文件
代码演示如下:
第一版代码:
import java.io.*; import java.net.ServerSocket; import java.net.Socket; //需求: //每一个客户端启动后都可以给服务器上传一个文件,服务器接收到文件后保存到一个upLoad目录中,可以同时接收多个客户端的文件 public class Server2 { public static void main(String[] args) throws Exception{ ServerSocket server=new ServerSocket(8888); while (true){ Socket socket = server.accept();//建立与客户端的连接 System.out.println(socket.getLocalAddress()+"连接成功"); new ClientsHandle(socket).start();//实现服务器端同时与多个客户端通信 } } } class ClientsHandle extends Thread { private Socket socket; public ClientsHandle(Socket socket) { this.socket = socket; } @Override public void run() { try ( InputStream in = socket.getInputStream();//字节输入流,接收文件 BufferedInputStream bis = new BufferedInputStream(in);//缓冲流 ObjectInputStream ois = new ObjectInputStream(bis);//接收的数据中包含文件名和文件 OutputStream out = socket.getOutputStream();//字节输出流,发送消息 PrintStream pr=new PrintStream(out); ) { String fileName = ois.readUTF(); // String ext= fiLeName.substring(fiLeName.lastIndexOf("."));//拿到文件后缀名 String newFileName=System.currentTimeMillis()+"&"+socket.getInetAddress().getHostAddress()+"&"+fileName; FileOutputStream fos=new FileOutputStream(new File("E:\\JavaDemo\\upLoad",newFileName)); BufferedOutputStream bos=new BufferedOutputStream(fos); byte[] data=new byte[1024];//1kb int len; while ((len=ois.read(data))!=-1){ bos.write(data,0,len); } bos.flush(); pr.println(newFileName+"已上传完毕");//告诉客户端文件已上传完毕 bos.close(); fos.close(); } catch (Exception e) { e.printStackTrace(); }finally { try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } }
import java.io.*; import java.net.InetAddress; import java.net.Socket; import java.util.Scanner; public class Client2 { public static void main(String[] args) throws Exception { Socket socket=new Socket(InetAddress.getLocalHost().getHostAddress(),8888); try( OutputStream out = socket.getOutputStream();//向服务器发送文件 ObjectOutputStream oos=new ObjectOutputStream(out); InputStream in = socket.getInputStream();//字节输入流,接收服务器返回的消息 InputStreamReader isr=new InputStreamReader(in);//将字节输入流转换为字符输入流 BufferedReader br=new BufferedReader(isr); ){ // File file=new File("D:\\BaiduNetdiskDownload\\尚硅谷2022Java\\01-Java基础【完结】\\尚硅谷_JavaEE_01_JavaSE课程资料\\09_api","JDK_API_1.6_zh_中文.CHM"); Scanner input=new Scanner(System.in); System.out.print("请输入要上传的文件路径:"); String filePath = input.nextLine(); File file=new File(filePath); FileInputStream fis=new FileInputStream(file);//文件输入流,从本地文件读取文件内容 BufferedInputStream bis=new BufferedInputStream(fis);//给文件输入流套上字节缓冲流,加快读取数据速度 oos.writeUTF(file.getName());//发送文件名 byte[] data=new byte[1024];//1kb int len; while ((len=bis.read(data))!=-1){ oos.write(data,0,len); } System.out.println("客户端已经发送完毕"); oos.flush(); //D:\Test\Image\dog.png //不加下面的代码,出现了这样的问题:客户端输入本地文件路径后,一直卡住不动,服务器端也是卡住不动, //目的文件夹中的图片虽然有边框,但是无法显示,数据没有完全写入进去 /* 视频中给出的原因:客户端发完文件以后,没有关闭输出通道。服务器端在读取客户端送过来的文件数据中一直没有读到“-1”这个标记 然后就没有返回“newFileName+"已上传完毕" ” 这句话给客户端 我个人的理解:文件的字节数据就像水一样,IO流就像水管,服务器端和客户端就像两个抽水机,客户端把字节数据抓取,放在流中,服务器端一直 在往流中拿数据,若客户端把字节数据都放完了,它那一端的流入口没有关闭,服务器端就一直在流里抓数据,若一直到达流的末尾,它会一直阻塞 若客户端关闭了它那边的流通道,服务器端便读到流的末尾,因为此时的流就像开盖的啤酒瓶,服务器端在瓶口,客户端在瓶底,见底了 api文档这样解释:public abstract int read() throws IOException从输入流中读取数据的下一个字节。返回 0 到 255 范围内的 int 字节值。 如果因为已经到达流末尾而没有可用的字节,则返回值 -1。 在输入数据可用、检测到流末尾或者抛出异常前,此方法一直阻塞。 */ socket.shutdownOutput();//关闭输出通道,对方才能读取到-1标识 System.out.println(br.readLine()); bis.close(); fis.close(); /* * 我在客户端与服务器端中对流的构建均采用了try()...catch,此try()...catch会自动关闭消耗资源的流对象 * 之前测试了几次代码,出现了socket closed的异常:Java.net.SocketEception * 原因:tcp协议在进行三次握手,四次挥手的过程中,程序的底层中socket与所有的IO流都关闭了,因此出现了异常,而tcp挥手的时间点与 * 程序底层try()...catch自动关闭各种流的系统资源的时间点不好把控。 * 解决方案:使用普通的try...catch * * */ }catch (Exception e){ e.printStackTrace(); }finally { socket.close(); } } }![1678981728071](C:\Users\king\AppData\Roaming\Typora\typora-user-images\1678981728071.png)