【JavaEE初阶 — 网络编程】实现基于TCP协议的Echo服务

      c96f743646e841f8bb30b2d242197f2f.gif

ddb5ae16fc92401ea95b48766cb03d96.jpeg692a78aa0ec843629a817408c97a8b84.gif


    TCP流套接字编程    


    1. TCP & UDP 的区别    


TCP 的核心特点是面向字节流,读写数据的基本单位是字节 byte 


    2 API介绍    


    2.1 ServerSocket    


     定义     


ServerSocket 是创建 TCP 服务端 Socket 的API。


     构造方法     


方法签名
 
方法说明
 
ServerSocket(int port)创建一个服务端流套接字Socket,并绑定到指定端口

所以服务器启动,需要绑定端口号


     方法      


方法签名
 
方法说明
 
 Socket accept()
 
开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待
 
 void close() 
 
关闭此套接字


    2.2  Socket    


  • Socket 是客户端 Socket ,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。

  • 不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。

     构造方法     


方法签名方法说明
Socket(String host, int port)
 
创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接

这个方法的两个参数,都是服务器的IP & 端口,这个版本的构造方法,就是给客户端用的,服务器怎么通过这个类来构造对象呢?后续再来看;


     方法      


方法签名方法说明
InetAddress getlnetAddress()
 
返回套接字所连接的地址
 
InputStream getlnputStream()
 
返回此套接字的输入流
 
OutputStream getOutputStream()
 
返回此套接字的输出流

TCP没有 send(),receive() 这样的操作,但是 TCP 调用 getlnputStream() 会得到个 InputStream 对象;调用 getOutputStream(),得到一个 OutputStream 对象,这两个对象是字节流对象

虽然 Socket 自身没有读写操作,但是 Socket 可以拿到字节流对象,就可以通过字节流对象,来进行读写操作;


     3. 通过TCP实现回显服务器        


     TCP Echo Server    


    创建关联对象    


通过构造方法,绑定关联的端口号 (和 UDP类似,都是在构造对象的时候,绑定端口号)


    实现 start()    


    处理客户端发送的连接    


TCP 和 UDP 服务器 start() 的主循环的第一步有所区别:


  • UDP 进入主循环,就可以直接处理请求,根据请求计算响应,把响应返回客户端;
  • TCP服务器 ,进入主循环后,因为 TCP 是有连接的,所以第一步是先处理客户端发来的连接;
  • 这个连接就类似于打电话,在客户端打电话给服务器时,服务器要先接通电话,才可以进行后续的正常通信;

所以TCP服务器进入主循环的第一步,就是进行接通电话的操作,而拨号操作,是客户端来完成的;

调用 ServerSocket 对象底下的 accept() 方法,起到接听电话的作用;


还需要接收 accept() 方法的返回值:

  • 如果客户端和服务器确实已经建立连接了,那 accept() 是可以拿到这个请求连接的;
  • 如果客户端没有发起连接,那么 accept() 就会产生阻塞 ,和前面的 receive() 类似;

TCP 服务器后续通过对clientSocket 进行读写数据,来和客户端进行通信;


    进一步理解 ServerSocket 和 Socket 的职责划分    


    处理一个客户端的连接        

处理连接的过程比较复杂,因此我们把这个操作封装成一个方法;

可能会涉及到多个客户端的请求和响应,如果服务器接收到多个请求,就要返还给客户端多个响应;


    服务器与客户端成功连接,打印日志: 

这两个方法可以拿到对端(客户端)的 IP & 端口号;


    获取输入流对象&输出流对象     

打印出客户端的IP&端口号后,就需要进一步地处理客户端的请求和响应,需要借助 Socket 类内置的 InputStream & OutputStream 来处理这些请求和响应

这里获取到的是输入流对象,后续提供这个对象,来读取客户端的请求;


接下来获取输出流对象,并且处理异常:


拿到输入流对象&输出流对象后,后续读取请求,就使用输入流对象;返回响应,就把响应的内容写入输出流对象中; 


接下来,在 try 的代码块中,实现读取请求和返回响应的操作,这些操作分成三步

  1. 读取请求并解析 
  2. 根据请求计算响应
  3. 返回响应给客户端

因为在一次连接中,这三个操作可能会解析多次,所以我们提供 while 循环来处理


    读取请求    

下列操作,读取到的请求是一个字节数组,还需要手动把字节数组再转成字符串,才方便后续的处理和打印:


我们可以借助 Scanner 来进行更简单直接的读取操作,既可以读请求,读出来的请求又已经是一个字符串;

 

  • 把刚刚从 Socket 中拿到的 InputStream 填入 Scanner 中,后续通过 Scanner 直接读取请求中的内容;
  • 如果 Scanner 没有再读取到数据,说明连接断开,就可以结束循环了

所以读取请求,可以直接借助 read(),也可以借助 Scanner 来辅助完成

    补充     

  • Scanner 可以控制处理台输入,又可以控制处理文件的输入,还可以控制处理网络的输入;
  • Scanner 的构造方法:

  • Scanner 的构造方法,填入的是一个InputStream 对象

    根据请求计算响应    

  • 当前编写的是一个回显服务器代码,所以可以直接在计算响应的逻辑返回请求即可


     返回响应给客户端    

下列写法,会直接拿到 response 中的字节数组,然后通过 outputStream 提供的 write(),来写入输出流对象即可;

这种计算响应的方法,是提供字节的方式填充输出流对象;


除了上述写法,我们还可以利用字符流的方式:


这里的 writer 和 System.out 起到的的效果类似,所以 println,printf 等等都可以通过调用


    打印返回响应的日志和连接断开的日志    


    服务器一次连接可处理多个请求的原理   

服务器 start() 的代码块中, process() 方法处理请求返回响应的逻辑,相当于嵌套了两层 while 循环:

因此,可以在服务器与客户端的一次连接中,服务器处理多个请求;

如果在一次连接中,客户端发送多次请求,服务器就返还多个响应(打一次电话可以说一句话或者很多句话);


    补充     

  • 一个连接一个请求(短连接),一个连接多个请求(长连接);
  • 因为连接过程的开销非常大,所以在日常开发中,更主流的是长连接,一个连接处理多个请求;
  • 就好比锁消除,针对要加锁的多个逻辑,每个逻辑都进行加锁,开销非常大,所以更科学的做法是把这些逻辑合在一起,只进行一次加锁;(和领导汇报工作成果,应该在一次电话中一次性汇总完毕,而不是打多次电话。每次电话只汇报一个成果)

    TCP Echo Client    


     创建 Socket 对象    

Socket 在客户端和服务器都可以使用,服务器的 Socket 通过调用accept()拿到,但是客户端的 Socket 就需要通过实例来创建对象;

     实现客户端构造方法    

在客户端的构造方法中,传入服务器的IP和端口号;

传到构造方法中的字符串IP地址(类似127.0.0.1这样的字符串),不需要任何转换;


对比UDP 的客户端,TCP客户端在构造方法在实例Socket对象后,就会在底层和对端建立TCP连接,连接好后,服务器会记录对端的信息(实例化Socket对象时传入的IP和端口号);

因此,服务器的IP和端口号,在TCP客户端中就不需要再创建变量来保存了;


    从控制台中读取请求,发送给服务器    


    从控制台中读取用户输入信息作为请求    

为了实现客户端能够和服务器在一次连接的情况下,发送多次请求,我们设置一个循环:

这步操作可以读取刚刚输入控制台的一行信息,读取到的信息作为客户端的请求;


    拿到输入流&输出流    

之后就把这个请求写入 Socket 对象中,写的时候也需要拿到Socket对象的 InputStream 输入流& OutputStream 输出流;


为了使用方便,可以对拿到的输入流和输出流再套一层壳


    完善循环逻辑     

所以在主循环中,第一步操作是从控制台中读取用户输入,把读取到的输入设置为请求:


第二步就是把请求发送给服务器


第三步,就是读取服务器返回的响应,并且把读取到的响应打印到控制台


    客户端与服务器交互过程    


    区分客户端与服务器的 Socket 对象    

下列服务器和客户端的两个 socket 对象,分别在不同进程中,甚至在不同主机中,因此绝对不是同一个对象;

这两个对象存在密切的关联关系,可以把这两个 socket 对象理解为两部电话:

  • 接通这两部电话后,从A听筒说话,B可以听见;从B听筒说话,A可以听见(从一边对Socket对象写数据,另一边的 Socket 对象就可以读到);
  • 但是这两个对象绝对不是同一部电话;

    处理细节问题    

    问题一:冲刷缓冲区     

完善 main 方法


程序运行结果


关掉客户端:


再启动一次客户端,并且发送一个数据,并且一敲回车,发现没有反应:


为什么没有反应呢?因为其实刚刚客户端代码,并没有真的把请求发送出去:

这个操作只是把数据放到 “发送缓冲区” 中,还没有真正写入网卡里;


  •  发送缓冲区其实就是一块内存空间,对网络/硬盘写数据是一个非常低效的操作,如果频繁地调用这些比较低效的操作,程序运行是非常缓慢的;
  • 为了提高效率,就引入一个内存缓冲区,把要写入的数据都放入缓冲区中,再统一进行发送,这样可以减小写硬盘和写网络的次数;
  • 但是提高效率的同时,也会产生副作用,就是调用 writer.println 这样的操作,并没有真正地触发发送数据操作,而只是把数据写入缓冲区;
  • 当然,把数据写入缓冲区,而不是直接发送这样的行为,是 PrintWriter的行为,如果不套壳,是可以直接发送的;
  • 但是在实际开发中,广泛使用了缓冲区这样的概念,调用flush()来刷新缓冲区这个操作是非常关键的;

如何真正地把数据发送出去呢?我们要使用刷新操作(调用 flush() 方法来冲刷缓冲区),把缓冲区的数据强制写入 IO 设备中:


客户端服务器交互结果


    问题二:针对 hasNext 对一个完整请求/响应设置标识符    

 println 的操作,会自动加上一个 \n : 


但是如果在这个代码中不加这个 \n,直接使用 print行不行呢?


我们重新启动一下客户端,并且发送内容,发现客户端又没有反应了,并且服务器也没有读到信息:


造成上述原因,是因为 next() 的问题,修改成 print 后,客户端输入的数据也是发送到服务器上了,并且服务器也收到了,但是服务器并没有真正处理,因为服务器有一个hasNext()判断:


    补充    

  • hasNext() 的行为是,判断当前收到的数据是否包含“空白符”,什么是空白符呢?
  • 换行,回车,空格,制表符,翻页符......都是空白符;
  • 遇到空白符,hasNext()才会认为是一个完整的 next,否则在遇到空白符之前,hasNext() 都会阻塞。

所以刚刚在修改成 print 之后,发送的内容是不包含空白符的内容,hasNext() 就会阻塞而无法进入下面读取请求的逻辑;


    总结    


  • 使用 println,是在约定一个请求/响应,是在使用 \n 作为结束标记,对端在读取数据的时候,也会在读取到 \n 时,判断读取到一个完整的请求/响应;
  • 这是我们在使用TCP时,特别需要注意的事项,并且和UDP不一样;
  • UDP是以 DatagramPacket 作为单位的,但是TCP则是以字节为单位,但是实际上一个请求,往往是以多个字节构成的;
  • 到底多少个字节为一个完整的请求/响应,就需要程序员想办法标记出来,引入分割符是标记一次完整请求/响应的典型方式,不一定是换行,也可以是其他分割符; 

    问题三:根据不同Socket的生命周期判断是否需要手动关闭    

在TCP服务器刚刚编写的代码中,涉及两种Socket:

  • ServerSocket的生命周期,贯彻整个服务区进程,不需要手动关闭


  • clientSocket 的生命周期是一次连接,而不是整个服务器进程;

  • 所以每个客户端连接,都会创建一个新的 clientSocket,每个客户端断开,这个对象就应该 close() 了,但是当前代码并没有对 clientSocket 进行 close() 释放;
  • 没有在每次连接结束后,对 clientSocket 进行关闭,就会造成文件资源泄漏的问题(文件一直在打开,而不进行关闭,在打开到一定程度,会把文件描述表耗尽,就无法继续打开新文件)

  • 在 poccessConnection() 方法的逻辑执行完毕之后,我们就可以对 clientSocket 进行关闭

    总结    

  • 对于是否需要手动关闭 Socket ,需要我们分析请求它的生命周期是跟随整个进程,还是跟随某个环节;
  • 如果是跟随整个进程,那么可以不手动关闭 Socket;如果是每个请求都会创建应该 Socket,或者每一次连接都会创建一个 Socket,或者某一个环节的执行周期,会创建一个Socket,这样的情况,就需要我们手动关闭 Socket;
    问题四:服务器无法同时等待 accept() &等待已连接的客户端发送响应    

一个服务器能同时给多个客户端提供服务,那么刚刚编写的TCP服务器也可以处理多个请求吗?我们关掉客户端,再重新启动多个客户端:


    补充:修改同时启动多个客户端的IDEA设置    



IDEA 会默认只启动一个客户端,再启动别的客户端,会先关闭上一个启动的客户端;我们通过这里的设置,就可以让 IDEA 启动多个客户端;

设置好后,我们再来重新启动两个客户端, 并且先后发送请求:

结果在第二个客户端发送请求时,又卡住了,并且第一个创建的客户端多次发送请求的操作是没问题的,服务器都会有一个正常响应,但是第二个客户端无论怎么发送请求,都不会有响应:

并且我们通过服务器日志,可以发现,在第二个客户端上线时,并没有再次打印日志;

如果我们关掉第一个客户端,第二个客户端发送的请求,会马上被服务器接收,并且返回响应

所以当前代码,TCP服务器在同一时刻,只能处理一个客户端发送的请求;


针对上面出现的问题,我们对服务器的处理请求,返回响应这一块代码的关键逻辑进行分析:

如果同一时刻,有多个客户端对一个服务器进行连接,那么第一个和服务器连接的客户端1:

所以当前这个服务器代码,如果已经在处理一个客户端的请求,就没办法处理另一个客户端的请求,服务器代码会卡在循环中,无法重新调用 accept()连接新的服务器,直到连接的客户端退出,导致循环终止;


    总结    


  • 当前服务器代码,无法同时等待 accept 和 等待用户请求;在等待客户端发送请求的时候,没办法等待 accept() ,这个时候,如果有新的客户端连接,也无法接听电话(一个专业销售没办法在给先来的顾客讲解产品的时候,又去接待低级销售后面揽入店的顾客);
  • 因此,服务器的代码,导致服务器一次只能处理一个客户端发送的请求,这个代码是不合理的; 

    4. 服务器引入多线程    


如果只是单个线程,无法同时响应多个客户端;为了解决这个问题,此处给每个客户端都分配一个线程;


在服务器主线程中,就只是进行 accept(),每次有新的客户端 accept() 连接成功,就创建一个新的线程,又新线程负责完成,后续对客户端 Socket 对象的引用 clientSocket 的读写操作;

引入线程之后,重新启动服务器和两个客户端,可以发现服务器打印了两个带着不同端口号的日志;


因此,在引入线程之后,服务器可以一边等待请求,一边等待 accept() 连接新的客户端;



    5. 服务器引入线程池    


客户端连接,服务器就会创建新线程,客户端断开连接,客户端就会销毁线程,为了避免频繁创建销毁线程,也可以引入线程池;

线程池


对于在服务器引入线程池,一般不会使用 newFixedThreadPool,因为会创建一个固定线程数的线程池,意味着同时处理的客户端连接数目就固定了;

把任务都交到线程池中,线程池已经预先创建好了一些线程,提前创建好的线程就可以立刻投入工作,从而减少再去创建线程的开销;


线程不是越多越好,如果线程数量过多,CPU的利用率无法再被提高,还会导致系统调度速度下降;并且创建线程也是需要系统资源的,系统总体资源是有限的(一个主机差不多只能创建几千个线程);

无论是多线程,还是线程池,一个线程都对应一个客户端,并且一个主机创建的线程数目是有上限的,那如果有成千上万个客户端,同时访问一个服务器(一台主机),该怎么办呢?

IO多路复用/IO多路连接(这里不重点讲解,因为JVM中没有原生的 IO 多路复用 API,而是把API重新封装,已经不仅仅是多路复用了,后续会详细讲解针对 Java 的 IO多路复用所使用的 NIO,Netty 等知名网络框架,当前讲的IO是 BIO/Blocking IO);


    扩展    


基于BIO(同步阻塞IO)的长连接会一直占用系统资源。对于并发要求很高的服务端系统来说,这样的消耗是不能承受的。

  • 由于每个连接都需要不停的阻塞等待接收数据,所以每个连接都会在一个线程中运行。
  • 一次阻塞等待对应着一次请求、响应,不停处理也就是长连接的特性:一直不关闭连接,不停的处理请求。

实际应用时,服务端一般是基于NIO(即同步非阻塞IO)来实现长连接,性能可以极大的提升。 


    6. 长短连接    


TCP发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接;


    长连接和短连接的概念    


  • 短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能一次收发数据。
  • 长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接。也就是说,长连接可以多次收发数据。

    长连接和短连接区别    


  • 建立连接、关闭连接的耗时:短连接每次请求、响应都需要建立连接,关闭连接;而长连接只需要第一次建立连接,之后的请求、响应都可以直接传输。相对来说建立连接,关闭连接也是要耗时的,长连接效率更高。
  • 主动发送请求不同:短连接一般是客户端主动向服务端发送请求;而长连接可以是客户端主动发送请求,也可以是服务端主动发。
  • 两者的使用场景有不同:短连接适用于客户端请求频率不高的场景,如浏览网页等。长连接适用于客户端与服务端通信频繁的场景,如聊天室,实时游戏等。

   

   c96f743646e841f8bb30b2d242197f2f.gif

692a78aa0ec843629a817408c97a8b84.gif

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/483636.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[linux应用]emby媒体服务器软件简单部署和使用

一、介绍 Emby 是一款媒体服务器软件,用于组织、管理和共享个人的音乐、电影、电视节目和其他媒体文件。简单来说,是管理和播放电影的软件。官方网址:Emby - The open media solution 二、部署 安装包下载地址:Download Emby -…

burp2

声明! 学习视频来自B站up主 **泷羽sec** 有兴趣的师傅可以关注一下,如涉及侵权马上删除文章,笔记只是方便各位师傅的学习和探讨,文章所提到的网站以及内容,只做学习交流,其他均与本人以及泷羽sec团队无关&a…

基于hexo框架的博客搭建流程

这篇博文讲一讲hexo博客的搭建及文章管理,也算是我对于暑假的一个交代 !!!注意:下面的操作是基于你已经安装了node.js和git的前提下进行的,并且拥有github账号 创建一个blog目录 在磁盘任意位置创建一个…

106.【C语言】数据结构之二叉树的三种递归遍历方式

目录 1.知识回顾 2.分析二叉树的三种遍历方式 1.总览 2.前序遍历 3.中序遍历 4.后序遍历 5.层序遍历 3.代码实现 1.准备工作 2.前序遍历函数PreOrder 测试结果 3.中序遍历函数InOrder 测试结果 4.后序遍历函数PostOrder 测试结果 4.底层分析 1.知识回顾 在99.…

游戏引擎学习第25天

Git: https://gitee.com/mrxiao_com/2d_game 今天的计划 总结和复述: 这段时间的工作已经接近尾声,虽然每次编程的时间只有一个小时,但每一天的进展都带来不少收获。尽管看起来似乎花费了很多时间,实际上这些日积月累的时间并未…

【C++】深入优化计算题目分析与实现

博客主页: [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: C 文章目录 💯前言💯第一题:圆的计算我的代码实现代码分析改进建议改进代码 老师的代码实现代码分析可以改进的地方改进代码 💯第二题:对齐输出我的代码实现…

Kafka配置SASL/PLAINTEXT安全认证

1、下载安装 Kafka下载地址:Apache Kafka 下载文件 wget https://downloads.apache.org/kafka/3.8.0/kafka_2.12-3.8.0.tgz 文件解压缩 tar -zxvf kafka_2.12-3.8.0.tgz 进入目录 cd kafka_2.12-3.8.0 2、Zookeeper 配置 2.1、修改 Zookeeper 配置文件 con…

go并发设计模式runner模式

go并发设计模式runner模式 真正运行的程序不可能是单线程运行的,go语言中最值得骄傲的就是CSP模型了,可以说go语言是CSP模型的实现。 假设现在有一个程序需要实现,这个程序有以下要求: 程序可以在分配的时间内完成工作&#xff0…

机器学习周志华学习笔记-第13章<半监督学习>

机器学习周志华学习笔记-第13章<半监督学习> 卷王,请看目录 13半监督学习13.1 生成式方法13.2 半监督SVM13.3 基于分歧的方法13.4 半监督聚类 13半监督学习 前面我们一直围绕的都是监督学习与无监督学习,监督学习指的是训练样本包…

DevOps工程技术价值流:GitLab源码管理与提交流水线实践

在当今快速迭代的软件开发环境中,DevOps(开发运维一体化)已经成为提升软件交付效率和质量的关键。而GitLab,作为一个全面的开源DevOps平台,不仅提供了强大的版本控制功能,还集成了持续集成/持续交付(CI/CD)…

Android笔记【12】脚手架Scaffold和导航Navigation

一、前言 学习课程时,对于自己不懂的点的记录。 对于cy老师第二节课总结。 二、内容 1、PPT介绍scaffold 2、开始代码实操 先新建一个screen包,写一个Homescreen函数,包括四个页面。 再新建一个compenent包,写一个displayText…

动态规划-----路径问题

动态规划-----路径问题 下降最小路径和1:状态表示2:状态转移方程3 初始化4 填表顺序5 返回值6 代码实现 总结: 下降最小路径和 1:状态表示 假设:用dp[i][j]表示:到达[i,j]的最小路径 2:状态转…

C_字符串的一些函数

1.字符串输入函数 scanf("%s",数组名)&#xff1b; gets(数组名)&#xff1b; 区别&#xff1a; scanf(“%s”,数组名); 把空格识别为输入结束 #include <stdio.h>int main() {char a[10];printf("输入&#xff1a;");scanf("%s",a)…

【数据事务】.NET开源 ORM 框架 SqlSugar 系列

.NET开源 ORM 框架 SqlSugar 系列 【开篇】.NET开源 ORM 框架 SqlSugar 系列【入门必看】.NET开源 ORM 框架 SqlSugar 系列【实体配置】.NET开源 ORM 框架 SqlSugar 系列【Db First】.NET开源 ORM 框架 SqlSugar 系列【Code First】.NET开源 ORM 框架 SqlSugar 系列【数据事务…

Zookeeper集群数据是如何同步的?

大家好&#xff0c;我是锋哥。今天分享关于【Zookeeper集群数据是如何同步的?】面试题。希望对大家有帮助&#xff1b; Zookeeper集群数据是如何同步的? 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 Zookeeper集群中的数据同步是通过一种称为ZAB&#xff08;Zo…

MySQL需掌握到何种程度?才能胜任工作

大家好&#xff0c;我是袁庭新。星友问&#xff1a;MySQL需要学到什么程度&#xff1f;才能胜任日常的软件开发工作呢&#xff01;以下是一些建议的学习目标和程度&#xff0c;这些目标旨在帮助你在工作中高效地使用MySQL。 数据库的基本概念、MySQL的安装及配置、SQL的概念、S…

HTML 快速上手

目录 一. HTML概念 二. HTML标签 1. 标题标签 2. 段落标签 3. 换行标签 4. 图片标签 5. 超链接标签 6. 表格标签 7. 表单标签 7.1 form 标签 7.2 input 标签 (1) 文本框 (2) 单选框 (3) 密码框 (4) 复选框 (5) 普通按钮 (6) 提交按钮 8. select标签 9. 无语义…

Qt 2D绘图之三:绘制文字、路径、图像、复合模式

参考文章链接: Qt 2D绘图之三:绘制文字、路径、图像、复合模式 绘制文字 除了绘制图形以外,还可以使用QPainter::darwText()函数来绘制文字,也可以使用QPainter::setFont()设置文字所使用的字体,使用QPainter::fontInfo()函数可以获取字体的信息,它返回QFontInfo类对象…

java调用ai模型:使用国产通义千问完成基于知识库的问答

整体介绍&#xff1a; 基于RAG&#xff08;Retrieval-Augmented Generation&#xff09;技术&#xff0c;可以实现一个高效的Java智能问答客服机器人。核心思路是将预先准备的问答QA文档&#xff08;例如Word格式文件&#xff09;导入系统&#xff0c;通过数据清洗、向量化处理…

Java 基于Spring AI RAG组件做AI智能问答_rag检索增强_AI智能问答

基于RAG技术构建高效Java智能问答客服机器人 基于RAG&#xff08;Retrieval-Augmented Generation&#xff09;技术&#xff0c;可以构建高效的Java智能问答客服机器人。首先&#xff0c;通过向量化处理将Word格式的问答QA文档转换为机器可理解的形式&#xff0c;并存储于Vect…