一、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 类

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

  1、构造方法

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

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

  2、成员方法

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类

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

  1、构造方法

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

  Demo:

ServerSocket server = new ServerSocket(6666);

  2、成员方法

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

  

四、基于 TCP 协议的网络通信程序结构

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

  Java 之 TCP 通信程序-LMLPHP

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

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

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

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

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

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

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

   (1)创建 Socket:根据指定服务端的 IP 地址或端口号构造 Socket 类对象,创建的同时会自动向服务器方发起连接。若服务器端响应,则建立客户端到服务器的通信线路。若连接失败,会出现异常

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

   (3)进行读/写操作:通过输入流读取服务器发送的信息,通过输出流将信息发送给服务器

   (4)关闭 Socket:断开客户端到服务器的连接

   注意:

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

四、简单的 TCP 网络程序

  TCP 通信分析图解

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

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

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

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

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

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

  Java 之 TCP 通信程序-LMLPHP

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

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

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

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

  客户端向服务器发送数据

    服务端实现:

 public class ServerTCP {
public static void main(String[] args) throws IOException {
  System.out.println("服务端启动 , 等待连接 .... ");
    // 1.创建 ServerSocket对象,绑定端口,开始等待连接
    ServerSocket ss = new ServerSocket(6666);
    // 2.接收连接 accept 方法, 返回 socket 对象.
    Socket server = ss.accept();
    // 3.通过socket 获取输入流
    InputStream is = server.getInputStream();
    // 4.一次性读取数据
    // 4.1 创建字节数组
    byte[] b = new byte[1024];
    // 4.2 据读取到字节数组中.
    int len = is.read(b);
    // 4.3 解析数组,打印字符串信息
    String msg = new String(b, 0, len);
    System.out.println(msg);
    //5.关闭资源.
    is.close();
    server.close();
  }
}

    客户端实现:

 public class ClientTCP {
  public static void main(String[] args) throws Exception {
    System.out.println("客户端 发送数据");
    // 1.创建 Socket ( ip , port ) , 确定连接到哪里.
    Socket client = new Socket("localhost", 6666);
    // 2.获取流对象 . 输出流
    OutputStream os = client.getOutputStream();
    // 3.写出数据.
    os.write("你好么? tcp ,我来了".getBytes());
    // 4. 关闭资源 .
    os.close();
    client.close();
  }
}

  服务器向客户端回写数据

    服务端实现:

 public class ServerTCP {
  public static void main(String[] args) throws IOException {
    System.out.println("服务端启动 , 等待连接 .... ");
    // 1.创建 ServerSocket对象,绑定端口,开始等待连接
    ServerSocket ss = new ServerSocket(6666);
    // 2.接收连接 accept 方法, 返回 socket 对象.
    Socket server = ss.accept();
    // 3.通过socket 获取输入流
    InputStream is = server.getInputStream();
    // 4.一次性读取数据
    // 4.1 创建字节数组
    byte[] b = new byte[1024];
    // 4.2 据读取到字节数组中.
    int len = is.read(b);
    // 4.3 解析数组,打印字符串信息
    String msg = new String(b, 0, len);
    System.out.println(msg);
    // =================回写数据=======================
    // 5. 通过 socket 获取输出流
    OutputStream out = server.getOutputStream();
    // 6. 回写数据
    out.write("我很好,谢谢你".getBytes());
    // 7.关闭资源.
    out.close();
    is.close();
    server.close();
  }
}

    客户端实现:

 public class ClientTCP {
  public static void main(String[] args) throws Exception {
    System.out.println("客户端 发送数据");
    // 1.创建 Socket ( ip , port ) , 确定连接到哪里.
    Socket client = new Socket("localhost", 6666);
    // 2.通过Scoket,获取输出流对象
    OutputStream os = client.getOutputStream();
    // 3.写出数据.
    os.write("你好么? tcp ,我来了".getBytes());
    // ==============解析回写=========================
    // 4. 通过Scoket,获取 输入流对象
    InputStream in = client.getInputStream();
    // 5. 读取数据数据
    byte[] b = new byte[100];
    int len = in.read(b);
    System.out.println(new String(b, 0, len));
    // 6. 关闭资源 .
    in.close();
    os.close();
    client.close();
  }
}

五、文件上传案例

  文件上传分析图解

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

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

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

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

Java 之 TCP 通信程序-LMLPHP

  基本实现

  服务端实现:

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

  客户端实现:

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

  文件上传优化分析:

  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.【客户端】获取输入流,解析回写数据。

  Java 之 TCP 通信程序-LMLPHP

  回写实现:

    服务端实现:

 public class FileUpload_Server {
  public static void main(String[] args) throws IOException {
    System.out.println("服务器 启动..... ");
    // 1. 创建服务端ServerSocket
    ServerSocket serverSocket = new ServerSocket(6666);
    // 2. 循环接收,建立连接
    while (true) {
      Socket accept = serverSocket.accept();
      /*
      3. socket对象交给子线程处理,进行读写操作
      Runnable接口中,只有一个run方法,使用lambda表达式简化格式
      */
      new Thread(() ‐> {
        try (
          //3.1 获取输入流对象
          BufferedInputStream bis = new BufferedInputStream(accept.getInputStream());
          //3.2 创建输出流对象, 保存到本地 .
          FileOutputStream fis = new FileOutputStream(System.currentTimeMillis() +".jpg");
          BufferedOutputStream bos = new BufferedOutputStream(fis);
         ) {
          // 3.3 读写数据
          byte[] b = new byte[1024 * 8];
          int len;
          while ((len = bis.read(b)) != ‐1) {
            bos.write(b, 0, len);
          }
          // 4.=======信息回写===========================
          System.out.println("back ........");
          OutputStream out = accept.getOutputStream();
          out.write("上传成功".getBytes());
          out.close();
          //================================
          //5. 关闭 资源
          bos.close();
          bis.close();
          accept.close();
          System.out.println("文件上传已保存");
         } catch (IOException e) {
            e.printStackTrace();
         }
        }).start();
     }
  }
}

    客户端实现:

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