Java 网络编程:(六)TCP网络编程

一、TCP协议概述

  TCP(Transmission Control Protocol,传输控制协议)被称作一种端对端协议。是一种面向连接的、可靠的、基于字节流的传输层的通信协议,可以连续传输大量的数据。

  这是因为它为当一台计算机需要与另一台远程计算机连接时,TCP协议会采用“三次握手”方式让它们建立一个连接,用于发送和接收数据的虚拟链路。数据传输完毕TCP协议会采用“四次挥手”方式断开连接。

  TCP协议负责收集这些信息包,并将其按适当的次序放好传送,在接收端收到后再将其正确的还原。TCP协议保证了数据包在传送中准确无误。TCP协议使用重发机制,当一个通信实体发送一个消息给另一个通信实体后,需要收到另一个通信实体确认信息,如果没有收到另一个通信实体确认信息,则会再次重复刚才发送的消息。

  TCP 通信能实现两台计算机之间的数据交互,通信的两端,要严格区分为客户端(Client)与服务端(Server)。

  

  两端通信时步骤:

    1、服务端程序,需要事先启动,等待客户端的连接;

    2、客户端主动连接服务器端,连接成功才能通信。服务端不可以主动连接客户端。

  在 Java 中,提供了两个类用于实现 TCP 通信程序:

    1、客户端:java.net.Socket 类表示。创建 Socket 对象,向服务端发出连接请求,服务器响应请求,两者建立连接开始通信。

    2、服务端:java.net.ServerSocket 类表示。创建 ServerSocket 对象,相当于开启一个服务,并等待客户端的连接。

二、Socket 类

  1、Socket 概述

    Socket 类:该类实现客户端套接字,套接字指的是两台设备之间通讯的端点。

  2、常用构造方法

public Socket(InetAddress address,int port)创建一个流套接字并将其连接到指定 IP 地址的指定端口号。

public Socket(String host, int port) :创建套接字对象并将其连接到指定主机上的指定端口号。如果指定的host是null ,则相当于指定地址为回送地址。

    Tips:回送地址(127.x.x.x)是本机回送地址(Loopback Address),主要用于网络软件测试以及本地进程间通信,无论什么程序,一旦使用回送地址发送数据,立即返回,不进行任何网络传输。

  3、常用方法

public InputStream getInputStream() : 返回此套接字的输入流。

       ① 如果此 Socket 具有相关联的通信,则生成的 InputStream 的所有操作也关联该通道。

       ② 关闭生成的 InputStream 也将关闭相关的 Socket。

public OutputStream getOutputStream() : 返回此套接字的输出流

     ① 如果此 Socket 具有相关联的通道,则生成的 OutputStream 的所有操作也关联该通道。

     ② 关闭生成的 OutputStream 也将关闭相关的 Socket。

public void close() :关闭此套接字

    ① 一旦一个 Socket 被关闭,它不可再使用。

    ② 关闭此 Socket 也将关闭相关的 InputStream 和 OutputStream。

public void shutdownOutput() : 禁用此套接字的输出流

    任何先前写出的数据将被发送,随后终止输出流。

三、ServerSocket 类

  1、Socket 概述

    ServerSocket 类:这个类实现了服务器套接字,该对象等待通过网络的请求。

  2、常用构造方法

public ServerSocket(int port) :使用该构造方法在创建ServerSocket对象时,就可以将其绑定到一个指定的端口号上,参数port就是端口号。

    port 为服务器端监听的端口号。

  3、常用方法

public Socket accept() :侦听并接受连接,返回一个新的Socket对象,用于和客户端实现通信。该方法会一直阻塞直到建立连接。

  

四、基于 TCP 协议的网络通信

  1、通信模型

    Java 语言的基于套接字 TCP 编程分为服务端编程和客户端编程,其通信模型如图所示:

    

  2、客户端

    (1)客户端Socket的工作过程包含以下四个基本的步骤:

      ① 创建 Socket: 根据指定服务端的 IP 地址或端口号构造 Socket 类对象。若服务器端响应,则建立客户端到服务器的通信线路。若连接失败,会出现异常。

      ② 打开连接到 Socket 的输入/出流: 使用 getInputStream()方法获得输入流,使用getOutputStream()方法获得输出流,进行数据传输

      ③ 按照一定的协议对 Socket 进行读/写操作: 通过输入流读取服务器放入线路的信息(但不能读取自己放入线路的信息),通过输出流将信息写入线程。

      ④ 关闭 Socket: 断开客户端到服务器的连接,释放线路;

    (2)客户端创建 Socket 对象

      客户端程序可以使用Socket类创建对象, 创建的同时会自动向服务器方发起连接Socket的构造器是:

Socket(String host,int port)throws UnknownHostException,IOException: 向服务器(域名是host。端口号为port)发起TCP连接,若成功,则创建Socket对象,否则抛出异常。

Socket(InetAddress address,int port)throws IOException: 根据InetAddress对象所表示的IP地址以及端口号port发起连接。

  

      客户端建立socketAtClient对象的过程就是向服务器发出套接字连接请求

      

  3、服务器端

    (1)服务器程序的工作过程包含以下五个基本的步骤

      ① 使用 ServerSocket(int port) :创建一个服务器端套接字,并绑定到指定端口上。用于监听客户端的请求;

      ② 调用 accept()方法:监听连接请求,如果客户端请求连接,则接受连接,创建与该客户端的通信套接字对象。否则该方法将一直处于等待;

      ③ 调用 该Socket对象的 getOutputStream() 和 getInputStream ():获取输出流和输入流,开始网络数据的发送和接收;

      ④ 关闭Socket对象:某客户端访问结束,关闭与之通信的套接字;

      ⑤ 关闭ServerSocket:如果不再接收任何客户端的连接的话,调用close()进行关闭;

    (2)服务器建立 ServerSocket 对象

      ServerSocket 对象负责等待客户端请求建立套接字连接,类似邮局某个窗口中的业务员。也就是说, 服务器必须事先建立一个等待客户请求建立套接字连接的ServerSocket对象。

      所谓“接收”客户的套接字请求,就是accept()方法会返回一个 Socket 对象:

      

  4、注意

    客户端和服务器端在获取输入流和输出流时要对应,否则容易死锁。例如:客户端先获取字节输出流(即先写),那么服务器端就先获取字节输入流(即先读);反过来客户端先获取字节输入流(即先读),那么服务器端就先获取字节输出流(即先写)。

  5、

五、简单的 TCP网络程序

  TCP 通信分析图解

   1、【服务端】启动,创建 ServerSocket 对象,等待连接。

   2、【客户端】启动,创建 Socket 对象,请求连接。

   3、【服务端】接收连接,调用 accept 方法,并返回一个 Socket 对象

   4、【客户端】Socket 对象,获取 OutputStream ,向服务端写出数据

   5、【服务端】Socket对象,获取 InputStream,读取客户端发送的数据。

    到此,客户端向服务端发送数据成功。

  

     自此,服务端向客户端回写数据。

   6、【服务端】Socket对象,获取 OutputStream,向客户端回写数据。

   7、【客户端】Socket对象,获取 InputStream,解析回写数据。

   8、【客户端】释放资源,断开连接。

六、TCP案例一

  客户端发送信息给服务端,服务端将数据显示在控制台上

  客户端:

 1     @Test
 2     public void client()  {
 3         Socket socket = null;
 4         OutputStream os = null;
 5         try {
 6             //1.创建Socket对象,指明服务器端的ip和端口号
 7             InetAddress inet = InetAddress.getByName("192.168.14.100");
 8             socket = new Socket(inet,8899);
 9             //2.获取一个输出流,用于输出数据
10             os = socket.getOutputStream();
11             //3.写出数据的操作
12             os.write("你好,我是客户端mm".getBytes());
13         } catch (IOException e) {
14             e.printStackTrace();
15         } finally {
16             //4.资源的关闭
17             if(os != null){
18                 try {
19                     os.close();
20                 } catch (IOException e) {
21                     e.printStackTrace();
22                 }
23 
24             }
25             if(socket != null){
26                 try {
27                     socket.close();
28                 } catch (IOException e) {
29                     e.printStackTrace();
30                 }
31 
32             }
33         }
34     }

  服务端:

 1     @Test
 2     public void server()  {
 3 
 4         ServerSocket ss = null;
 5         Socket socket = null;
 6         InputStream is = null;
 7         ByteArrayOutputStream baos = null;
 8         try {
 9             //1.创建服务器端的ServerSocket,指明自己的端口号
10             ss = new ServerSocket(8899);
11             //2.调用accept()表示接收来自于客户端的socket
12             socket = ss.accept();
13             //3.获取输入流
14             is = socket.getInputStream();
15 
16             //不建议这样写,可能会有乱码
17 //        byte[] buffer = new byte[1024];
18 //        int len;
19 //        while((len = is.read(buffer)) != -1){
20 //            String str = new String(buffer,0,len);
21 //            System.out.print(str);
22 //        }
23             //4.读取输入流中的数据
24             baos = new ByteArrayOutputStream();
25             byte[] buffer = new byte[5];
26             int len;
27             while((len = is.read(buffer)) != -1){
28                 baos.write(buffer,0,len);
29             }
30 
31             System.out.println(baos.toString());
32 
33             System.out.println("收到了来自于:" + socket.getInetAddress().getHostAddress() + "的数据");
34 
35         } catch (IOException e) {
36             e.printStackTrace();
37         } finally {
38             if(baos != null){
39                 //5.关闭资源
40                 try {
41                     baos.close();
42                 } catch (IOException e) {
43                     e.printStackTrace();
44                 }
45             }
46             if(is != null){
47                 try {
48                     is.close();
49                 } catch (IOException e) {
50                     e.printStackTrace();
51                 }
52             }
53             if(socket != null){
54                 try {
55                     socket.close();
56                 } catch (IOException e) {
57                     e.printStackTrace();
58                 }
59             }
60             if(ss != null){
61                 try {
62                     ss.close();
63                 } catch (IOException e) {
64                     e.printStackTrace();
65                 }
66             }
67 
68         }
69     }

七、TCP案例二

  客户端发送文件给服务端,服务端将文件保存在本地。

  客户端:

 1     @Test
 2     public void client() throws IOException {
 3         //1.创建Socket对象,指明服务器端的ip和端口号
 4         Socket socket = new Socket(InetAddress.getByName("127.0.0.1"),9090);
 5         //2.获取一个输出流,用于输出数据
 6         OutputStream os = socket.getOutputStream();
 7         //3.创建本地文件读取流
 8         FileInputStream fis = new FileInputStream(new File("beauty.jpg"));
 9         //4.读取文件流,并写入到网络中
10         byte[] buffer = new byte[1024];
11         int len;
12         while((len = fis.read(buffer)) != -1){
13             os.write(buffer,0,len);
14         }
15         //5.关闭资源
16         fis.close();
17         os.close();
18         socket.close();
19     }

  服务端:

 1     @Test
 2     public void server() throws IOException {
 3         //1.创建服务器端的ServerSocket,指明自己的端口号
 4         ServerSocket ss = new ServerSocket(9090);
 5         //2.调用accept()表示接收来自于客户端的socket
 6         Socket socket = ss.accept();
 7         //3.获取输入流
 8         InputStream is = socket.getInputStream();
 9         //4.本地文件输出流
10         FileOutputStream fos = new FileOutputStream(new File("beauty1.jpg"));
11         //5.写入到文件中
12         byte[] buffer = new byte[1024];
13         int len;
14         while((len = is.read(buffer)) != -1){
15             fos.write(buffer,0,len);
16         }
17         //6.关闭资源
18         fos.close();
19         is.close();
20         socket.close();
21         ss.close();
22 
23     }

八、TCP案例三

  从客户端发送文件给服务端,服务端保存到本地。并返回“发送成功”给客户端,并关闭相应的连接。

  客户端:

 1     @Test
 2     public void client() throws IOException {
 3         //1.创建Socket对象,指明服务器端的ip和端口号
 4         Socket socket = new Socket(InetAddress.getByName("127.0.0.1"),9090);
 5         //2.获取一个输出流,用于输出数据
 6         OutputStream os = socket.getOutputStream();
 7         //3.创建本地文件读取流
 8         FileInputStream fis = new FileInputStream(new File("beauty.jpg"));
 9         //4.读取文件,并写入到网络中
10         byte[] buffer = new byte[1024];
11         int len;
12         while((len = fis.read(buffer)) != -1){
13             os.write(buffer,0,len);
14         }
15         //关闭数据的输出
16         socket.shutdownOutput();
17 
18         //5.接收来自于服务器端的数据,并显示到控制台上
19         InputStream is = socket.getInputStream();
20         ByteArrayOutputStream baos = new ByteArrayOutputStream();
21         byte[] bufferr = new byte[20];
22         int len1;
23         while((len1 = is.read(buffer)) != -1){
24             baos.write(buffer,0,len1);
25         }
26 
27         System.out.println(baos.toString());
28 
29         //6.关闭资源
30         fis.close();
31         os.close();
32         socket.close();
33         baos.close();
34     }

  服务端:

 1     @Test
 2     public void server() throws IOException {
 3         //1.创建服务器端的ServerSocket,指明自己的端口号
 4         ServerSocket ss = new ServerSocket(9090);
 5         //2.调用accept()表示接收来自于客户端的socket
 6         Socket socket = ss.accept();
 7         //3.获取输入流
 8         InputStream is = socket.getInputStream();
 9         //4.要保存的本地文件
10         FileOutputStream fos = new FileOutputStream(new File("beauty2.jpg"));
11         //5.读取文件,并保存到本地
12         byte[] buffer = new byte[1024];
13         int len;
14         while((len = is.read(buffer)) != -1){
15             fos.write(buffer,0,len);
16         }
17 
18         System.out.println("图片传输完成");
19 
20         //6.服务器端给予客户端反馈
21         OutputStream os = socket.getOutputStream();
22         os.write("你好,照片我已收到,非常漂亮!".getBytes());
23 
24         //7.关闭资源
25         fos.close();
26         is.close();
27         socket.close();
28         ss.close();
29         os.close();
30 
31     }

  注意:这里的客户端的 shutdownOutput() 很重要,这是告诉服务器自己已经关闭输出流,即自己不再发送数据了,否则服务器没有收到这个信息,就会一直等待阻塞在读的过程,不会把返回给客户端。

九、文件上传案例

 文件上传分析图解

  1.【客户端】输入流,从硬盘读取文件数据到程序中。

  2.【客户端】输出流,写出文件数据到服务端。

  3.【服务端】输入流,读取文件数据到服务程序。

  4.【服务端】输出流,写出文件数据到服务器硬盘中。

   基本实现

  服务端实现:

 1 public class FileUpload_Server {
 2   public static void main(String[] args) throws IOException {
 3     System.out.println("服务器 启动..... ");
 4     // 1. 创建服务端ServerSocket
 5     ServerSocket serverSocket = new ServerSocket(6666);
 6     // 2. 建立连接
 7     Socket accept = serverSocket.accept();
 8     // 3. 创建流对象
 9     // 3.1 获取输入流,读取文件数据
10     BufferedInputStream bis = new BufferedInputStream(accept.getInputStream());
11     // 3.2 创建输出流,保存到本地 .
12     BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copy.jpg"));
13     // 4. 读写数据
14     byte[] b = new byte[1024 * 8];
15     int len;
16     while ((len = bis.read(b)) != ‐1) {
17       bos.write(b, 0, len);
18     } 
19     //5. 关闭 资源
20     bos.close();
21     bis.close();
22     accept.close();
23     System.out.println("文件上传已保存");
24   }
25 }

  客户端实现:

 1 public class FileUPload_Client {
 2   public static void main(String[] args) throws IOException {
 3     // 1.创建流对象
 4     // 1.1 创建输入流,读取本地文件
 5     BufferedInputStream bis = new BufferedInputStream(new FileInputStream("test.jpg"));
 6     // 1.2 创建输出流,写到服务端
 7     Socket socket = new Socket("localhost", 6666);
 8     BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
 9     //2.写出数据.
10     byte[] b = new byte[1024 * 8 ];
11     int len ;
12     while (( len = bis.read(b))!=‐1) {
13       bos.write(b, 0, len);
14       bos.flush();
15     } 
16     System.out.println("文件发送完毕");
17     // 3.释放资源
18     bos.close();
19     socket.close();
20     bis.close();
21     System.out.println("文件上传完毕 ");
22   }
23 }

  文件上传优化分析:

  1、文件名称写死的问题

    服务端,保存文件的名称如果写死,那么最终导致服务器硬盘,只会保留一个文件,建议使用系统时间优化,保证文件名唯一性。

    代码实现:

FileOutputStream fis = new FileOutputStream(System.currentTimeMillis()+".jpg") // 文件名称
BufferedOutputStream bos = new BufferedOutputStream(fis);

  2、循环接收的问题

    服务端,指保存一个文件就关闭了,之后的用户无法再上传,这是不符合实际的,使用循环改进,可以不断的接收不同用户的文件。

    代码实现:

// 每次接收新的连接,创建一个Socket
while(true){
Socket accept = serverSocket.accept();
......
}

  3、效率问题

    服务端,在接收大文件时,可能消耗几秒钟的时间,此时不能接收其他用户上传,所以,使用多线程技术优化。

    代码实现:

while(true){
Socket accept = serverSocket.accept();
// accept 交给子线程处理.
new Thread(() ‐> {
......
InputStream bis = accept.getInputStream();
......
}).start();
}

  优化实现:

 1 public class FileUpload_Server {
 2   public static void main(String[] args) throws IOException {
 3     System.out.println("服务器 启动..... ");
 4     // 1. 创建服务端ServerSocket
 5     ServerSocket serverSocket = new ServerSocket(6666);
 6     // 2. 循环接收,建立连接
 7     while (true) {
 8       Socket accept = serverSocket.accept();
 9       /*
10       3. socket对象交给子线程处理,进行读写操作
11       Runnable接口中,只有一个run方法,使用lambda表达式简化格式
12       */
13       new Thread(() ‐> {
14         try (
15           //3.1 获取输入流对象
16           BufferedInputStream bis = new BufferedInputStream(accept.getInputStream());
17           //3.2 创建输出流对象, 保存到本地 .
18           FileOutputStream fis = new FileOutputStream(System.currentTimeMillis() +19 ".jpg");
20           BufferedOutputStream bos = new BufferedOutputStream(fis);) {
21           // 3.3 读写数据
22           byte[] b = new byte[1024 * 8];
23           int len;
24           while ((len = bis.read(b)) != ‐1) {
25             bos.write(b, 0, len);
26           } 
27           //4. 关闭 资源
28           bos.close();
29           bis.close();
30           accept.close();
31           System.out.println("文件上传已保存");
32         } catch (IOException e) {
33           e.printStackTrace();
34         }
35       }).start();
36     }
37   }
38 }

  信息回写分析图解

   前四部与基本文件上传一致。

   5.【服务端】获取输出流,回写数据。

   6.【客户端】获取输入流,解析回写数据。

  

   回写实现:

    服务端实现:

 1 public class FileUpload_Server {
 2   public static void main(String[] args) throws IOException {
 3     System.out.println("服务器 启动..... ");
 4     // 1. 创建服务端ServerSocket
 5     ServerSocket serverSocket = new ServerSocket(6666);
 6     // 2. 循环接收,建立连接
 7     while (true) {
 8       Socket accept = serverSocket.accept();
 9       /*
10       3. socket对象交给子线程处理,进行读写操作
11       Runnable接口中,只有一个run方法,使用lambda表达式简化格式
12       */
13       new Thread(() ‐> {
14         try (
15           //3.1 获取输入流对象
16           BufferedInputStream bis = new BufferedInputStream(accept.getInputStream());
17           //3.2 创建输出流对象, 保存到本地 .
18           FileOutputStream fis = new FileOutputStream(System.currentTimeMillis() +".jpg");
19           BufferedOutputStream bos = new BufferedOutputStream(fis);
20          ) {
21           // 3.3 读写数据
22           byte[] b = new byte[1024 * 8];
23           int len;
24           while ((len = bis.read(b)) != ‐1) {
25             bos.write(b, 0, len);
26           } 
27           // 4.=======信息回写===========================
28           System.out.println("back ........");
29           OutputStream out = accept.getOutputStream();
30           out.write("上传成功".getBytes());
31           out.close();
32           //================================
33           //5. 关闭 资源
34           bos.close();
35           bis.close();
36           accept.close();
37           System.out.println("文件上传已保存");
38          } catch (IOException e) {
39             e.printStackTrace();
40          }
41         }).start();
42      }
43   }
44 }

    客户端实现:

 1 public class FileUpload_Client {
 2   public static void main(String[] args) throws IOException {
 3     // 1.创建流对象
 4     // 1.1 创建输入流,读取本地文件
 5     BufferedInputStream bis = new BufferedInputStream(new FileInputStream("test.jpg"));
 6     // 1.2 创建输出流,写到服务端
 7     Socket socket = new Socket("localhost", 6666);
 8     BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
 9     //2.写出数据.
10     byte[] b = new byte[1024 * 8 ];
11     int len ;
12     while (( len = bis.read(b))!=‐1) {
13       bos.write(b, 0, len);
14     } 
15     // 关闭输出流,通知服务端,写出数据完毕
16     socket.shutdownOutput();
17     System.out.println("文件发送完毕");
18     // 3. =====解析回写============
19     InputStream in = socket.getInputStream();
20     byte[] back = new byte[20];
21     in.read(back);
22     System.out.println(new String(back));
23     in.close();
24     // ============================
25     // 4.释放资源
26     socket.close();
27     bis.close();
28   }
29 }

十、小结

原文地址:https://www.cnblogs.com/niujifei/p/14878919.html