JavaEE——网络编程(TCP流编程)

文章目录

  • 一、解释什么是 TCP 流套接字编程
  • 二、代码实现TCP流套接字创建客户端服务器
    • 1. 实现回显服务器
      • (1)服务器对客户端响应的问题分析解决
      • (2) 回显服务器代码整体罗列
    • 2. 实现回显客户端
      • (1) 回显客户端整体代码罗列
  • (2) 对代码中整体存在的小问题分析
  • 三、总结与运行结果展示

一、解释什么是 TCP 流套接字编程

在上一篇文章中,我向大家介绍了有关UDP套接字方面的相关编程。
详见: JavaEE——网络编程(UDP套接字编程)

这篇文章,同样会通过一个简单的回显服务器的形式来解释什么是 TCP流套接字编程。

首先我们要知道的是 TCP 提供的两个主要 API。
这里的 API 主要是两个类,如下:

  1. ServerSocket 类
    专门给服务器使用的 Socket 对象
    在这里插入图片描述
    其中包含的 SeverSocket 方法:
    JavaEE——网络编程(TCP流编程)_第1张图片
  2. Socket 类
    既会给客户端使用,也会给服务器使用。
    JavaEE——网络编程(TCP流编程)_第2张图片
    相关方法
    JavaEE——网络编程(TCP流编程)_第3张图片
    我们在前面已经知道,TCP 传输是面向字节流的,所以,TCP 不需要一个类来表示 “TCP 数据报”。
    TCP 不是以数据报为单位进行传输的,是以字节流的方式,流式传输
    这里的流式传输与 IO 文件操作 十分相似。

二、代码实现TCP流套接字创建客户端服务器

注:这里只是单纯的解释其中的核心代码,整体代码的逻辑会在后面统一展示

1. 实现回显服务器

  • 首先创建出一个服务器流套接字
    private ServerSocket serverSocket = null;

    //构造方法实现创建新的 socket 对象
    //这里的 TcpEchoSever 是类名
    public TcpEchoSever(int port) throws IOException {
        //将端口号传递进来
        serverSocket = new ServerSocket(port);
    }

这里就使用了,创建一个服务器端流套接字 Socket,并指定到端口。
ServerSocket(int port)

  • 实现服务器的启动方法
    public void start() throws IOException {
        System.out.println("启动服务器");
        while(true){
            //这个 clientSever 是和具体的 客户端进行交流
            Socket clientSocket = serverSocket.accept();
            processConnection(clientSocket);
        }
    }

这里的 accept() 方法是 “接受连接” ,前提是得有一个客户端连接。
当客户端在构造 Socket 对象时,就会指定服务器的 IP 和 端口。如果没有客户端来这里连接,此时就会在这里产生阻塞。

  • 实现 processConnection 方法与客户端进行交流

这里与客户端交流大致分为下面的几步操作

  1. 读取客户端的请求
                //将输入的流元素传入到 scanner 中
                Scanner scanner = new Scanner(inputStream);
                //判断输入流元素是否读取结束
                if(!scanner.hasNext()){
                    //没有下个数据,就说明读完。(即就是说明客户端关闭了连接)
                    System.out.printf("[%s:%d] 客户端关闭!\n", clientSocket.getInetAddress().toString(),clientSocket.getPort());
                    break;
                }
                //如果没有结束,就将元素读取到对应的 String 类型的变量中
                //注!! 这里使用的 next 关键字是一直读取到换行符/空格/其他空白字符结束,但是最终返回的内容中,没有 其中的 空白符等。
                String request = scanner.next();

简单分析代码

  1. 这里将输入的流变换成 inputStream 让客户端的数据直接被读取进来。
  2. 这里通过 hasNext() 方法获取字节元素直至最后
  1. 通过请求计算响应
String response = process(request);

实现响应代码

// 因为是回显服务器,所以直接返回元素即可
    public String process(String request) {
        return request;
    }
  1. 返回计算后的请求结果
 PrintWriter printWriter = new PrintWriter(outputStream);
                // 此处使用 println 进行写入,让结果中带有 /n 换行,方便对端进行接受解析
 printWriter.println(request);
                // 使用 flush 刷新缓冲区,保证当前写入的信息确实发送出去
 printWriter.flush();
 System.out.printf("[%s:%d] req: %s; resp: %s\n",clientSocket.getInetAddress().toString(),clientSocket.getPort(),
                   request,response);
  1. 这里的 outputStream 的作用是返回当前套接字的输出流。
  2. 需要注意的是这里使用 PrintWriter 是将这里的流进行转换。
    主要是因为 OutputStream 自身的方法不能够写入字符串,需要使用上面的方法进行转换
  3. 这里的 printWriter.println(request) 就是将处理响应后的 request 元素写回到网卡中,即就是返回到客户端。

要注意理解此处这两个关键字之间的关系和用法。为了更好的理解,我下面举个例子:

以打电话为例。
假设此时我正在办公室里把电话拿在手上接听电话,此时,突然同事给了我一摞文件需要我签字,此时外放又不方便,于是,我拿出了一个耳机带上来接听,同时进行签字。

这里的手上接听电话,就类似于这里的 outputStream。但是此时又不方便使用。
而这里的戴上耳机接听电话,就类似于此处的 PrintWriter
虽然方式方法不同,但是都达到了目的。这里的两个方法也是如此。

到这里,这个回显服务器就基本完成了。

(1)服务器对客户端响应的问题分析解决

虽然上面的代码已经实现了对客户端信息的接受,但是其中存在着一个重要问题,如图:
JavaEE——网络编程(TCP流编程)_第4张图片
上述画红线的代码是我们实现对客户端响应的核心代码。
但是我们要知道,一个服务器绝对不是给一个客户端服务的。 但是代码写到这里每次只能处理一个客户端的请求,很显然这不符合我们最基本的需求。
对此,处理的方式也很简单,要处理多个客户端,多线程是一个很好的解决办法。

对代码简单修改:

    public void start() throws IOException {
        System.out.println("启动服务器");
        while(true){
            //这个 clientSever 是和具体的 客户端进行交流
            Socket clientSocket = serverSocket.accept();
            Thread t = new Thread(()->{
              processConnection(clientSocket);
           });
        }
   

如上,使用多线程包裹了处理客户端信息的方法。虽然解决了问题,但是任然存在缺点。
这里如果有许多客户端频繁建立连接,此时就需要频繁的创建 / 销毁线程。此时的开销就比较繁重。对此,使用线程池是一个很好的办法。

对代码的最终修改

        //此处使用 CachedThreadPool,和 使用 FixedThreadPool 都不太合适(线程数不应该有固定的。。)
        ExecutorService threadPool = Executors.newCachedThreadPool();
        while(true){
            //这个 clientSever 是和具体的 客户端进行交流
            Socket clientSocket = serverSocket.accept();
            // 使用线程池来解决问题
            threadPool.submit(()->{
                processConnection(clientSocket);
            });
        }
    }

这样就更一步优化了代码。

(2) 回显服务器代码整体罗列

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TcpEchoSever {
    private ServerSocket serverSocket = null;

    //构造方法实现创建新的 socket 对象
    public TcpEchoSever(int port) throws IOException {
        //将端口号传递进来
        serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("启动服务器");
        //创建线程池
        //此处使用 CachedThreadPool,和 使用 FixedThreadPool 都不太合适(线程数不应该有固定的。。)
        ExecutorService threadPool = Executors.newCachedThreadPool();
        while(true){
            //这个 clientSever 是和具体的 客户端进行交流
            Socket clientSocket = serverSocket.accept();
//            //在这里对创建方式进行改变,使用多线程的方式
//            // 此处使用多线程的方式对代码进行优化,会出现多次的创建删除线程的操作,此时开销会比较大
//            Thread t = new Thread(()->{
//                processConnection(clientSocket);
//            });
//            t.start();

            // 使用线程池来解决问题
            threadPool.submit(()->{
                processConnection(clientSocket);
            });
        }
    }

    //使用下面的方法实现和客户端的交流
    //这里一个连接实现一个交互,但是要注意的是,这里可能会有多次的交流
    private void processConnection(Socket clientSocket){
        //先打印一个客户端开启时的响应,打印一下当前的端口号和IP地址
        System.out.printf("[%s:%d] 客户端开启\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
        //基于上述的 Socket 对象实现与客户端进行交流

        //对输入输出方法进行异常抓取
        try(InputStream inputStream = clientSocket.getInputStream();
        OutputStream outputStream = clientSocket.getOutputStream()){
            //由于要处理多个响应,所以这里使用循环执行
            while(true){
                //1. 读取请求
                //将输入的流元素传入到 scanner 中
                Scanner scanner = new Scanner(inputStream);
                //判断输入流元素是否读取结束
                if(!scanner.hasNext()){
                    //没有下个数据,就说明读完。(即就是说明客户端关闭了连接)
                    System.out.printf("[%s:%d] 客户端关闭!\n", clientSocket.getInetAddress().toString(),clientSocket.getPort());
                    break;
                }
                //如果没有结束,就将元素读取到对应的 String 类型的变量中
                //注!! 这里使用的 next 关键字是一直读取到换行符/空格/其他空白字符结束,但是最终返回的内容中,没有 其中的 空白符等。
                String request = scanner.next();
                //2. 通过请求计算响应
                String response = process(request);
                //3,返回计算后的请求结果
                //      OutputStream 中没有 write String 这样的功能,可以将 String 中的字节数组拿出来进行写入
                //      也可以使用字符流进行交换
                PrintWriter printWriter = new PrintWriter(outputStream);
                // 此处使用 println 进行写入,让结果中带有 /n 换行,方便对端进行接受解析
                printWriter.println(request);
                // 使用 flush 刷新缓冲区,保证当前写入的信息确实发送出去
                printWriter.flush();

                System.out.printf("[%s:%d] req: %s; resp: %s\n",clientSocket.getInetAddress().toString(),clientSocket.getPort(),
                        request,response);
            }
        }catch (IOException e){
            e.printStackTrace();
        }
        finally {
            try {
                //将关闭操作放在这里更加合适
                //对服务器进行关闭
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    //根据请求计算响应
    public String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        //创建服务器并给定端口号
        TcpEchoSever tcpEchoSever = new TcpEchoSever(9090);
        tcpEchoSever.start();
    }
}

2. 实现回显客户端

  1. 实现客户端核心代码初步准备

有关 TCP 客户端的配置和 UDP 客户端的配置十分相似。都需要两个关键信息:
服务器 IP 和 服务器 端口

除此之外,我们在前面的第一板块描述过。对于客户端 使用的是Socket 关键字以及其内部的方法
所以,代码如下

    //对于客户端要使用 Socket 来创建客户端
    private Socket socket = null;

    //使用构造方法实现客户端
    public TcpEchoClient(String severIP, int severPart) throws IOException {
        // Socket 构造方法,能够识别点分十进制的 IP 地址,比 DatagramPacket 使用更方便
        // new 这个对象的同时,就会进行 TCP 连接操作
        socket = new Socket(severIP,severPart);
    }
  1. 实现客户端核心代码

对于客户端代码,需要分为下面三部分:

  • 先从键盘上获取用户的输入请求
    这里的代码比较简单,如下:
       System.out.println("> ");
       String request = scanner.next();
              if(request.equals("exit")){
                    System.out.println("good bye");
                    break;
                }
  • 将读取到的元素发送给服务器
    这里需要传输的元素仍然是一个字符串,同样,这里的传输也需要使用到 OutputStream 来将信息传输。
    呢么这里的问题就和前面服务器将信息返回给客户端的问题相同。 对此,这里也需要使用 PrintWrite 方法修饰。

代码如下:

       // 2. 把读到的内容构造成请求,发送回服务器。
       PrintWriter printWriter = new PrintWriter(outputStream);
       printWriter.println(request);
       //此处加上一个 flush 确保数据已经发送
       printWriter.flush();
  • 将从服务器返回的数据流接受并分析到客户端
       Scanner respScanner = new Scanner(inputStream);
       String response = respScanner.next();
  • 打印返回结果
  System.out.println(response);

(1) 回显客户端整体代码罗列

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;

public class TcpEchoClient {
    //对于客户端要使用 Socket 来创建客户端
    private Socket socket = null;

    //使用构造方法实现客户端
    public TcpEchoClient(String severIP, int severPart) throws IOException {
        // Socket 构造方法,能够识别点分十进制的 IP 地址,比 DatagramPacket 使用更方便
        // new 这个对象的同时,就会进行 TCP 连接操作
        socket = new Socket(severIP,severPart);
    }

    public void start(){
        System.out.println("客户端启动");
        Scanner scanner = new Scanner(System.in);
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()){
            while(true){
                // 1. 先从键盘上读取用户输入的请求
                System.out.println("> ");
                String request = scanner.next();
                if(request.equals("exit")){
                    System.out.println("good bye");
                    break;
                }
                // 实现发送数据
                // 2. 把读到的内容构造成请求,发送回服务器。
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(request);
                //此处加上一个 flush 确保数据已经发送
                printWriter.flush();
                // 3. 读取服务器的响应
                // 使用这个 scanner 进行读取数据
                Scanner respScanner = new Scanner(inputStream);
                String response = respScanner.next();
                // 4. 将响应内容显示到页面上
                System.out.println(response);
            }
        }catch(IOException e){
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException {
        TcpEchoClient client = new TcpEchoClient("127.0.0.1",9090);
        client.start();
    }
}

(2) 对代码中整体存在的小问题分析

到此,对应的客户端和服务器之间的代码都已经实现完毕,在最后,这里还需要在说明一个问题,如下图:
JavaEE——网络编程(TCP流编程)_第5张图片
在上图的代码中,客户端和服务器都是用的是 println 对数据进行发送。
我们知道,println 会在发送的数据后面加上 \n 换行。

问题:
呢么,这里不使用 println 而是使用 print(不带换行) 呢么这个代码是否可以正常运行?

其实答案很明确,就是不可以
TCP 协议是面向字节流的协议,对于读的一方,一次读多少字节都可以。但是,对于接收方,这次需要读多少字节是不明确的

所以,针对上面的问题,就需要在数据传输中进行明确地约定。在此处的代码中,隐性约定就是使用 \n 来作为当前代码请求和响应的分割约定。

JavaEE——网络编程(TCP流编程)_第6张图片
如上图所示,将双方的情况调转过来也是相同的。

三、总结与运行结果展示

  1. 总结
  • 简单分析 TCP 流套接字的客户端服务器之间的响应过程
    JavaEE——网络编程(TCP流编程)_第7张图片
    JavaEE——网络编程(TCP流编程)_第8张图片
    JavaEE——网络编程(TCP流编程)_第9张图片

  • 横向对比 TCP 和 UDP 两个版本之间的代码

JavaEE——网络编程(TCP流编程)_第10张图片

  • TCP 中客户端和服务器交流图解
    JavaEE——网络编程(TCP流编程)_第11张图片
  1. 运行结果展示

首先启动客户端和服务器
JavaEE——网络编程(TCP流编程)_第12张图片
创建两个客户端并行运行
JavaEE——网络编程(TCP流编程)_第13张图片
服务器端的反馈响应
JavaEE——网络编程(TCP流编程)_第14张图片

你可能感兴趣的:(JavaEE,网络,java-ee,tcp/ip)