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

文章目录

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

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

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

首先我们要知道的是 TCP 提供的两个主要 API。

这里的 API 主要是两个类,如下:

  1. ServerSocket 类
    专门给服务器使用的 Socket 对象

    其中包含的 SeverSocket 方法:
  2. Socket 类
    既会给客户端使用,也会给服务器使用。

    相关方法

    我们在前面已经知道,TCP 传输是面向字节流 的,所以,TCP 不需要一个类来表示 "TCP 数据报"。
    TCP 不是以数据报为单位进行传输的,是以字节流的方式,流式传输
    这里的流式传输与 IO 文件操作 十分相似。

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

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

1. 实现回显服务器

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

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

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

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

这里的 accept() 方法是 "接受连接" ,前提是得有一个客户端连接。

当客户端在构造 Socket 对象时,就会指定服务器的 IP 和 端口。如果没有客户端来这里连接,此时就会在这里产生阻塞。

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

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

  1. 读取客户端的请求
java 复制代码
                //将输入的流元素传入到 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. 通过请求计算响应
java 复制代码
String response = process(request);

实现响应代码

java 复制代码
// 因为是回显服务器,所以直接返回元素即可
    public String process(String request) {
        return request;
    }
  1. 返回计算后的请求结果
java 复制代码
 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)服务器对客户端响应的问题分析解决

虽然上面的代码已经实现了对客户端信息的接受,但是其中存在着一个重要问题,如图:

上述画红线的代码是我们实现对客户端响应的核心代码。

但是我们要知道,一个服务器绝对不是给一个客户端服务的。 但是代码写到这里每次只能处理一个客户端的请求,很显然这不符合我们最基本的需求。

对此,处理的方式也很简单,要处理多个客户端,多线程是一个很好的解决办法。

对代码简单修改:

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

如上,使用多线程包裹了处理客户端信息的方法。虽然解决了问题,但是任然存在缺点。

这里如果有许多客户端频繁建立连接 ,此时就需要频繁的创建 / 销毁线程。此时的开销就比较繁重。对此,使用线程池是一个很好的办法。

对代码的最终修改

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

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

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

java 复制代码
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 关键字以及其内部的方法

所以,代码如下

java 复制代码
    //对于客户端要使用 Socket 来创建客户端
    private Socket socket = null;

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

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

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

代码如下:

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

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

java 复制代码
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) 对代码中整体存在的小问题分析

到此,对应的客户端和服务器之间的代码都已经实现完毕,在最后,这里还需要在说明一个问题,如下图:

在上图的代码中,客户端和服务器都是用的是 println 对数据进行发送。
我们知道,println 会在发送的数据后面加上 \n 换行。

问题:

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

其实答案很明确,就是不可以

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

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

如上图所示,将双方的情况调转过来也是相同的。

三、总结与运行结果展示

  1. 总结
  • 简单分析 TCP 流套接字的客户端服务器之间的响应过程


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

  • TCP 中客户端和服务器交流图解
  1. 运行结果展示

首先启动客户端和服务器

创建两个客户端并行运行

服务器端的反馈响应

相关推荐
长弓三石1 小时前
鸿蒙网络编程系列44-仓颉版HttpRequest上传文件示例
前端·网络·华为·harmonyos·鸿蒙
xianwu5431 小时前
反向代理模块
linux·开发语言·网络·git
follycat1 小时前
[极客大挑战 2019]HTTP 1
网络·网络协议·http·网络安全
xiaoxiongip6662 小时前
HTTP 和 HTTPS
网络·爬虫·网络协议·tcp/ip·http·https·ip
JaneJiazhao2 小时前
HTTPSOK:智能SSL证书管理的新选择
网络·网络协议·ssl
CXDNW2 小时前
【网络面试篇】HTTP(2)(笔记)——http、https、http1.1、http2.0
网络·笔记·http·面试·https·http2.0
无所谓จุ๊บ3 小时前
树莓派开发相关知识十 -小试服务器
服务器·网络·树莓派
道法自然04023 小时前
Ethernet 系列(8)-- 基础学习::ARP
网络·学习·智能路由器
EasyCVR4 小时前
萤石设备视频接入平台EasyCVR多品牌摄像机视频平台海康ehome平台(ISUP)接入EasyCVR不在线如何排查?
运维·服务器·网络·人工智能·ffmpeg·音视频
明月看潮生5 小时前
青少年编程与数学 02-003 Go语言网络编程 15课题、Go语言URL编程
开发语言·网络·青少年编程·golang·编程与数学