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

2023-09-22 00:05:53

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

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

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

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

  1. ServerSocket 类
    专门给服务器使用的 Socket 对象
    在这里插入图片描述
    其中包含的 SeverSocket 方法:
    在这里插入图片描述
  2. Socket 类
    既会给客户端使用,也会给服务器使用。
    在这里插入图片描述
    相关方法
    在这里插入图片描述
    我们在前面已经知道,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)服务器对客户端响应的问题分析解决

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

对代码简单修改:

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

到此,对应的客户端和服务器之间的代码都已经实现完毕,在最后,这里还需要在说明一个问题,如下图:
在这里插入图片描述
在上图的代码中,客户端和服务器都是用的是 println 对数据进行发送。
我们知道,println 会在发送的数据后面加上 \n 换行。

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

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

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

在这里插入图片描述
如上图所示,将双方的情况调转过来也是相同的。

三、总结与运行结果展示

  1. 总结
  • 简单分析 TCP 流套接字的客户端服务器之间的响应过程
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

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

在这里插入图片描述

  • TCP 中客户端和服务器交流图解
    在这里插入图片描述
  1. 运行结果展示

首先启动客户端和服务器
在这里插入图片描述
创建两个客户端并行运行
在这里插入图片描述
服务器端的反馈响应
在这里插入图片描述

更多推荐

[C语言]栈与队列——喵喵队,冲冲冲

宝子,你不点个赞吗?不评个论吗?不收个藏吗?最后的最后,关注我,关注我,关注我,你会看到更多有趣的博客哦!!!喵喵喵,你对我真的很重要。目录前言栈栈的实现队列队列的实现总结前言实践,实践,实践,多练几遍力扣,牛客的题。落实到脚下。栈栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作

Ubuntu安装RabbitMQ server - 在外远程访问

文章目录前言1.安装erlang语言2.安装rabbitMQ3.内网穿透3.1安装cpolar内网穿透(支持一键自动安装脚本)3.2创建HTTP隧道4.公网远程连接5.固定公网TCP地址5.1保留一个固定的公网TCP端口地址5.2配置固定公网TCP端口地址前言RabbitMQ是一个在AMQP(高级消息队列协议)基础上完

单例模式的安全写法

要想知道怎么写单例模式,那么必须得知道什么是单例模式。单例模式是一种设计模式,它确保某个类只有一个实例,并且提供一个全局访问该实例的方法。单例模式不会创建实例副本,而是返回对已创建实例的引用。单例模式的创建可以分为两类。第一类是饿汉式单例模式,它在类加载时就创建了唯一的实例对象,并在全局范围内提供访问点。第二类是懒汉式

如何制作一个成功的超市购物小程序

随着互联网的普及和移动支付的便捷性,越来越多的消费者选择在网上购物,这也促使越来越多的商家开始搭建自己的小程序商城。对于超市便利店来说,拥有一个便捷、易用的小程序商城能够吸引更多的消费者,提高销售效率。那么如何快速搭建一个超市便利店小程序呢?下面我们将通过乔拓云平台来介绍这个过程。步骤1:登录乔拓云网后台,进入商城管理

rabbitmq 面试题

1.交换机类型RabbitMQ是一个开源的消息队列系统,它支持多种交换机类型,用于在消息的生产者和消费者之间路由和分发消息DirectExchange(直接交换机):Direct交换机是最简单的交换机类型之一。它将消息按照消息的RoutingKey(路由键)与绑定的队列的RoutingKey进行精确匹配,并将消息发送到

一键自助建站系统源码带安装教程 傻瓜式部署搭建,让您的建站更高效

在这个数字时代,网站已成为企业或个人展示形象、推广业务的重要工具。为了满足这一需求,许多自助建站系统应运而生,大大降低了用户建站的门槛。给大家分享一款傻瓜式部署搭建的一键自助建站系统源码,让您轻松拥有高效建站能力。一、一键自助建站系统源码介绍这款一键自助建站系统源码具有以下特点:简单易用:用户只需通过简单的鼠标点击和输

Golang Gorm 一对多 关联模式 Association + Find 查询关联

查找关联//User拥有并属于多种language,`user_languages`是连接表typeUserstruct{gorm.ModelLanguages[]Language`gorm:"many2many:user_languages;"`}typeLanguagestruct{gorm.ModelNamest

I Pa?sWorD

2023icpc网络赛第一场I题意:题目给出只包含大小写字母,数字以及'?'的字符串,对于每一个小写字母,这一位字符既有可能是该小写字母,也有可能是该小写字母的对应大写字母,也就是该位的字符有两种可能,对于问号,可能是所有大写字母或者所有小写字母,或者所有单数字,则共有62种情况,而对于大写字母和数字位则都是确定的只有

Hive【非交互式使用、三种参数配置方式】

前言今天开始学习Hive,因为毕竟但凡做个项目基本就避不开用Hive,争取这学期结束前做个小点的项目。第一篇博客内容还是比较少的,环境的搭建配置太琐碎没有写。Hive常用使用技巧交互式使用就是我们正常的进入hive命令行下的使用模式。非交互式使用所谓非交互式,也就是不需要进入hive命令行,直接在我们linuxShel

STViT-R 代码阅读记录

目录一、SwinTransformer1、原理2、代码二、STViT-R1、中心思想2、代码与原文本次不做具体的训练。只是看代码。所以只需搭建它的网络,执行一次前向传播即可。一、SwinTransformer1、原理主要思想,将token按区域划分成窗口,只需每个窗口内的token单独进行self-attention。

Mybatis sql参数自动填充

问题描述在日常开发中,经常会遇到Mybatissql语句的操作问题,由于Mybatis实现sql的动态拼接,开发过程中,为了验证sql是否书写正确,通常需要获取的控制台打印的sql语句来检查是否拼接正确。如下图所示:那么为了验证sql的正确性,需要复制控制台sql以及sql参数,手工进行拼接后在数据库连接工具(比如na

热文推荐