网络编程 - 4 ( TCP )
目录
TCP 流套接字编程
API 介绍
SeverSocket
Socket
用 TCP 实现一个回显服务器
服务端
客户端
运行调试
第一个问题:PrintWriter 内置的缓冲区 - flush 刷新解决
第二个问题:上述代码中,需要进行 close 操作吗?
第三个问题:多个客户端来访问服务器
梳理代码:
最终版本代码~
流程图
文字解释:
图文解释~
完!
TCP 流套接字编程
API 介绍
SeverSocket
SeverSocket 是创建 TCP 服务端 Socket 的 API
ServerSocket 构造方法:
ServerSocket 方法:
Socket
Socket 是客户端 Socket,或服务器端中接收到客户端建立连接(accept 方法)的请求之后,返回的服务端 Socket。
不管是客户端还是服务端 Socket,都是双方建立连接以后,保存的对端信息,以及用来与对方收发数据的。
Socket 构造方法:
Socket 方法:
用 TCP 实现一个回显服务器
服务端
TCP 是面向字节流的,传输的基本单位是字节~
先是创建一个 ServerSocket 然后是构造方法:
TCP 是有连接的,和打电话一样,需要客户端拨号,服务器来接听,所以在服务端有一个 accept 方法去“接听”。
客户端的内核发起建立连接的流程之后,服务器的内核就会配合客户端那边的工作,来完成连接的建立。这个连接建立的过程,就相当于是,电话的这边在拨号,另一边就在响铃。但是需要等到用户点击了接听之后,才能进行后续通信。(内核建立的连接不是决定性的,还需要用户程序,把这个连接进行“接听”(accept)操作,然后才能进行后续的通信~)
accept 也是一个可能产生阻塞的操作,如果当前没有客户端连接过来,此时的 accept 就会阻塞。(有一个客户端连接过来,accept 一次就能返回一次。如果有多个客户端连接过来,accept 就需要执行多次~)
accept 有一个 Soceket 类型的返回值,我们 Socket 一个 clientSocket 来接收。
那问题就来了,在服务器这里,为什么有两个 Socket 呀,一个类型是 ServerSocket,一个类型是 Socket,但我们可能会晕晕傻傻搞不明白~
举个栗子来解释一下 serverSocket 和 clientSocket 的作用~~~
不知道大家是否有买房的经历,就假设,我们现在要买一套房子~~~
我们刚一出出租屋,就看到一位西装革履的帅哥在马路牙子上站着,看我们走出来,就凑上来问我们,“要买房子吗??? 他们有个 xx 楼盘,这几天正好开盘,如果我们感兴趣,可以带我们过去看一看~~”。我们正好对这个 xx 楼盘有意向,也就说 OK!帅哥拿起电话,五分钟就来了一辆车给我们拉到那个售楼部了。
去了之后,那个帅哥一招手,就过来了一个 OL 的小姐姐,帅哥介绍说“这个小姐姐就是我们的专业顾问小姐姐,由她来给我们介绍楼盘的详细情况~~~”。
趁着这个小姐姐给我们介绍的过程中,帅哥人就不见了,又回到马路牙子上去继续揽客了~~
我们上述代码的 severSocket 变量,就是我们上述例子中的西装小哥,负责在外面揽客~~
我们上述代码的 clientSocket 变量,就是我们上述例子中的专业顾问小姐姐,负责给我们进行后续的通信交互。
每一个客户端,都会分配一个 clientSocket(专业顾问)
销售帅哥,就负责拉客,拉到的客人就会交给对应的专业顾问小姐姐~
当把客人拉来以后,专业顾问小姐姐就可以在 processConnnection 方法中给我们介绍啦~
我们前面介绍了 TCP 是有连接的,所以 TCP 中的 socket 中就会保存对端的信息!!!
所以就可以直接在 clientSocket 变量中调用 getInetAddress 方法和 getPort 方法
然后我们就可以进行循环的读取客户端的请求并返回响应了,因为 TCP 的面向字节流的,我们仍按需要使用到 IO 操作中的一些做法,即使用 try - with - resources 方法
其中 inputStream 变量,用于从网卡上读数据,outputStream 变量用于从网卡上写数据。
TCP 是面向字节流的,这里的字节流 和 文件操作中的字节流是完全一致的~~~使用和文件操作中一样的类和方法完成对 TCP socket 的读写~~~
接下来进行读操作,这里的读操作,可以通过 read 方法来完成,read 是把收到的数据放到 byte 数组中了,但我们后续根据请求处理响应,还需要把这个 byte 数组再转换成 String 类型
我们在这里可以使用更简单的方法来完成 --> Scanner
Scanner scanner = new Scanner(inputStream); 这段代码中,new 的 Scanner 对象,传入的参数是 inputStream ==》 Scanner 后面参数传的那个对象,就从那里读数据~ (这个 Scanner 对象可以创建在 while 外面,可以创建在 while 里面)
仍然是三步:
1. 读取请求并解析
2. 根据请求计算响应
3. 把响应返回给客户端
String request 就完成了我们的第一步读取请求并解析。在 scanner.next() 中,我们要注意隐藏的约定,next 读的时候,要读到空白字符才会结束!因此就要求客户端发来的请求必须要带有空白符的结尾,比如 \n 或者 空格。
我们也可以用 if 判断,如果读取完毕,客户端断开连接,就会产生读取完毕的提示信息~
第二步:根据请求计算响应
因为我们还是回显服务器,所以在业务方法这里,还是简单的 return request 即可~
第三步:把响应返回给客户端
在这里,我们可以直接 outputStream.write(response.getBytes(), 0, response.getBytes().length); 通过这段代码进行写回,但我们要考虑到的是,在客户端也是用 Scanner 来接收服务端的响应,这种形式是以二进制进行返回了,没办法直接带一个 \n
我们可以像 Scanner 包装输入流一样,对输出流进行一个包装~
还可以在最后加一个打印日志:
再跟上 main 方法
到此,上述代码把 TCP 回显服务器基本就写完了,说是“基本”,即代码还存在三个比较严重的问题,我们待会解决~
代码:
package network;import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;public class TcpEchoServer {private ServerSocket serverSocket = null;public TcpEchoServer(int port) throws IOException {serverSocket = new ServerSocket(port);}public void start() throws IOException {System.out.println("服务器启动!");while (true) {Socket clientSocket = serverSocket.accept();processConnection(clientSocket);}}private void processConnection(Socket clientSocket) {System.out.printf("[%s:%d] 客户端上线!\n",clientSocket.getInetAddress(),clientSocket.getPort());try (InputStream inputStream = clientSocket.getInputStream();OutputStream outputStream = clientSocket.getOutputStream()) {Scanner scanner = new Scanner(inputStream);while (true) {// 读取完毕,客户端断开连接,就会产生读取完毕的提示信息if (!scanner.hasNext()) {System.out.printf("[%s:%d] 客户下线!\n",clientSocket.getInetAddress(),clientSocket.getPort());break;}// 1. 读取请求并解析。这里注意隐藏的约定,next 读的时候要读到空白字符才会结束// 因此就要求客户端发来的请求必须要带有空白符的结尾,比如 \n 或者 空格String request = scanner.next();// 2. 根据请求计算响应String response = process(request);// 3. 把响应返回给客户端// 用 PrintWriter 对 outputStream 进行一层包装~PrintWriter printWriter = new PrintWriter(outputStream);printWriter.println(request);System.out.printf("[%s:%d] request:%s, response:%s \n",clientSocket.getInetAddress(),clientSocket.getPort(),request,response);}} catch (IOException e) {throw new RuntimeException(e);}}private String process(String request) {return request;}public static void main(String[] args) throws IOException {TcpEchoServer tcpEchoServer = new TcpEchoServer(9090);tcpEchoServer.start();}
}
客户端
首先创建 Socekt 类型的对象用来与对方收发数据。
因为 TCP 的特点是有连接的,因此 socket 里面就会保存好我们传入的服务器的 ip 和端口,因此,此处就不用像 UDP 一样在类中保存了。
注意,客户端的 socket 对象,和,服务器端的 clientSocket 不是一个对象。
在客户端中执行代码:socket = new Socket(serverIp, serverPort); 就会和对应的服务器进行 TCP 的连接建立流程(系统内核完成)。当客户端中把内核中连接的流程走完之后,服务器这边的 clientSocket 就能从 accept 中的阻塞状态返回。(他们并不是同一个对象,他们是打电话的时候一方的“听筒”和另一方的“话筒”的关系~)
接下来就是 strat 方法:
还是打印提示信息,同样的,TCP 是面向字节流的,所以仍然是 try - with - resources。有了刚刚服务器的前车之鉴,我们可以直接在这里想到封装相关的方法 Scanner 和 PrintWirter 方法
在 strat 中有四步:
1. 从控制台读取输入的字符串
2. 把请求发送给服务器
3. 从服务器读取响应,这里也是和服务器返回响应的逻辑进行对应
4. 把响应显示出来
第一步:从控制台读取输入的字符串
这里的流程和 UDP 的客户端的类似
这里也可以假如 if 判断,来判断是否有信息
第二步:把请求发送给服务器
这里我们发送的时候,需要使用 println 来发送,为了让发送的请求末尾带有 \n,这里和服务器的 scanner.next 响应
第三步:从服务器读取响应
第四步:把响应显示出来
最后再加一个 main 方法
客户端代码:
package network;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 {private Socket socket = null;public TcpEchoClient(String serverIp, int serverPort) throws IOException {// 此处可以把这里的 ip 和 port 直接传给 socket 对象// 由于 TCP 是有连接的,因此 socket 里面就会保存好这俩信息socket = new Socket(serverIp, serverPort);}public void start() {System.out.println("客户端启动!");try (InputStream inputStream = socket.getInputStream();OutputStream outputStream = socket.getOutputStream()) {Scanner scannerConsole = new Scanner(System.in);Scanner scannerNetwork = new Scanner(inputStream);PrintWriter writer = new PrintWriter(outputStream);while (true) {// 这里的流程和 UDP 的客户端类似// 1. 从控制台读取输入的字符串System.out.println("->");if (!scannerConsole.hasNext()) {break;}String request = scannerConsole.next();// 2. 把请求发给服务器,这里需要使用 println 来进行发送,是为了让发送的请求末尾带有 \n// 和服务器中的 scanner.next 对应~writer.println(request);// 3. 从服务器读取响应String response = scannerNetwork.next();// 4. 把响应显示出来System.out.println(response);}} catch (IOException e) {throw new RuntimeException(e);}}public static void main(String[] args) throws IOException {TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1",9090);tcpEchoClient.start();}
}
运行调试
当我们信心满满的像 UDP 一样运行,在客户端输入 hello 的时候,发现,唉,TCP 并没有像我们预期一样实现功能:
重复启动几次客户端也无济于事,服务端倒是很给力的打印出了一些提示信息:
这里的提示信息,是客户端退出的时候,就会触发 TCP 的“断开连接”流程(服务器这边的代码能够感知到,对应的 Scanner 就能够在 hasNext 这里返回 false)
我们再仔细阅读上面的两份代码,就能大概猜到问题所在,出现上述没有反应的情况,那么就是客户端的请求没有正确的发送出去,要么是服务器的响应没有正确的返回回来(服务器收到请求最起码会打印日志)
第一个问题:PrintWriter 内置的缓冲区 - flush 刷新解决
之所以会出现上述的情况,本质上原因是在于 PrintWriter 内置的缓冲区在作祟~~
IO 操作都是比较低效的操作,我们作为一个程序员,就希望能够让低效操作,进行的尽量少一些~~所以在 PrintWriter 的方法中引入了缓冲区(内存),先把要写入的网卡的数据放入到内存缓冲区,等攒一波之后,再统一进行发送~~~(这个操作,把多次 IO 操作合并成一次了~)
但这也引出了一个问题,就像我们刚刚,仅仅输入了一个 hello,如果发送的数据很少,此时由于缓冲区还没满,数据就会只呆在缓冲区中,没有真正的被发送出去~~~
如何解决呢? 我们可以手动刷新以下缓冲区 --> flush 方法 实现刷新缓冲区的效果 。
在服务器和客户端的 PrinterWriter 对象调用完 println 方法之后,分别加上 flush 方法就好啦!!!
第二个问题:上述代码中,需要进行 close 操作吗?
其实是需要的~~
在服务器端的 try - with - resources 中,需要在最后再添加一个 finally 来针对 clientSocket 进行 close 操作~
serverSocket 整个程序只有唯一一个对象,并且这个对象的生命周期很长,是要跟随整个程序的,这个对象是无法提前进行关闭的,只要程序退出,随着进程的销毁一起被释放即可(不需要我们手动进行释放)
调用 socket.close 本质上也是关闭文件,释放文件操作符表。这里进程销毁,自然文件操作符表就没有了。因此我们在写 UDP 的程序的时候,并没有进行 close 操作~
但是 TCP 中的 clientSocket 是每个客户端都有一个。随着客户端越来越多,这里消耗的 socket 也会越来越多。(如果我们加入一个资源释放,就很可能把文件描述符表给占满)
这次连接处理完毕(processConnection 结束),就可以 close clientSocket了~~
注意:我们的 try - with - sources 中只是关闭了流对象,并没有释放文件本体~ 这俩流对象,都是 socket 对象给我们创建出来的~
释放了 socket 对象之后,即使上述流对象不进行释放,问题也不大,这俩流对象内部不持有文件描述符表,只是持有一些内存结构,而在 Java 中,内存文件可以被 gc 释放掉~~~
但如果只释放流对象,不释放 socket 就不行了,socket 持有了文件描述符,本质上还是要释放文件标识符的~~~
第三个问题:多个客户端来访问服务器
服务器需要能支持多个客户端同时访问,这是天经地义应该的~~~
IDEA 默认情况下,貌似一个进程运行两次,需要小小设置一下:
找到我们要运行的进程,选中即可
然后将两个客户端启动,发现,咦,第二个启动的客户端,又出 bug 了~~~
但是第一个启动的客户端,还是可以正常运行的~
我们的服务器端,也只检测到了第一个启动的客户端。
让我们去代码层面分析分析~
当第一个客户端连接上服务器之后,服务器就会从 accept 这里返回(解除阻塞),进入到 processConnection 中了。
接下来就会在 scanner.hasNext 这里阻塞,等待客户端的请求。
当客户端的请求到达之后,从 scanner.hasNext 处返回(解除阻塞),继续执行,读取请求,根据请求计算响应,返回相应给客户端~
执行完上述一轮操作之后,服务器循环回来再在 hashNext 处阻塞,等待下一个客户端的请求。
直到客户端退出之后,连接结束,此时 while(true) 循环才会退出。
服务器在里层(第二个 while(true) )中循环转圈的时候,是无法执行到外层 while(true) 中的 accept 方法的。
对然第二个客户端和服务在内核层面上,建立了 TCP 连接了,但是应用程序这里,是无法把连接拿到应用层面里面(processConnection 方法)处理。(人家一直给你打电话,你的手机一直在响,但你就是一直没有电话)
那,如果第一个客户端退出了,第二个客户端之前发送的请求为什么就马上就被处理了,而不是被丢掉呢???
当前 TCP 在内核中,每个 socket 都是由缓冲区的。客户端发送的数据确实是发了,服务器也收到了,只不过数据是在服务器的接收缓冲区中。
一旦第一个客户端退出了,回到第一层循环,继续执行第二次 accept,继续执行 next 就能把之前缓冲区的内容给读出来~(就相当于,快递小哥把包裹拿到了小区门口,我们没时间取,于是他就把包裹放到了菜鸟驿站~)
如何解决呢??? ==》 多线程~
单个线程,是无法既能给客户端循环提供服务,又能去快速调用第二次 accept。
简单的办法就是引入多线程。
主线程就负责执行 accept,每次有一个客户端连上来,就分配一个新的线程,由新的线程负责给客户端提供服务。
此时,就会把 processConnection 提交给新的线程来负责了。主循环就会快速的执行完一次之后,回到 accept 的位置阻塞等待新的客户端到来~~
这样就可以实现多个客户端连接服务器啦!!!
上述问题,并不是由 TCP 引起的,而是我们程序员自己的代码没有写好,出现了两层循环嵌套引起的。
但 UDP 服务器就可以实现多个客户端直接连接服务器,是因为 UDP 服务器中的代码,只有一层循环,就不会涉及到上面这样的问题,UDP 服务器天然就可以处理多个客户端的请求~
我们上面的引入线程,的确是可以解决我们此时的问题。但,再想想,每次来一个客户端,就会创建一个线程,每次这个客户端结束,就要销毁这个线程。如果客户端比较多,就会使得服务器频繁创建和销毁线程,造成资源浪费~~ ==》 线程池~~
这样也解决了频繁创建和销毁线程所造成的开销了~~~
线程池,解决的是频繁创建和销毁线程的问题。如果,当前的场景是线程频繁创建,但是不销毁呢???
我们可以想出两种情况的客户端:
每个客户端处理过程非常短(例如搜索网站的搜索页~)
每个客户端处理过程非常长(游戏服务器) --> 打一局王者荣耀 / 吃鸡 / LOL....
如果继续使用线程池 / 多线程,此时就会导致当前的服务器上一下积累了大量的线程~~ ==》此时对于服务器的负担就会非常重!!!
我们前面在哲学家吃面问题中也探讨多,如果哲学家太多,桌子是没办法让那么多哲学家同时上桌吃饭的,此时就会引起调度成本的增加。
为了解决上述的问题,还可以引入其他的方案:
1. 协程
即轻量级线程,本质上还是一个线程,用户态可以通过手动调度(省去系统调度的开销了~~)的方式让着一个线程“并发”的做多个任务~~
2. IO 多路复用
系统内核级别的机制,本质上是让一个线程同时去负责多个 socket。(本质上是因为这些 socket 数据并不是需要同一时刻需要同时进行处理)
举个栗子~
晚上吃什么??? 爸爸想吃米,妈妈想吃面,我想吃饺子~~~楼下有一个小吃街,就会有很多小摊~~~
方案一:爸爸一个人去买
1)先去买米,等米好了之后,再去第二个小摊
2)再买面,等面好了之后,再去第三个小摊
3)再去买饺子,等好了之后,就齐活回家~~
方案二:三个人一起出动
1)爸爸买米
2)妈妈买面
3)我买饺子
方案三:还是爸爸一个人去买
1)先买米,点完单,付完钱,不等了,直接去下一个小摊
2)再去买面,也是不等了,直接去下一个小摊
3)再去买饺子
把上面的三个单都点完之后,站在这三个小摊的中间位置(花一份时间,同时等待三个任务~~)上述哪个任务号了,对应的小摊老板可以喊爸爸,他就直接过去把对应的餐拿走就行~~
上面的栗子中的三个方案,就对应类我们解决编程问题中的三个方案
方案一,对应的就是我们的单线程,这个方案的效率是比较低的。
方案二,对应的就是我们的多线程,这个方案,大大提升了效率,但是系统的开销也变大了~~
方案三:对应的就是我们的 IO 多路复用,基本点在于,虽然有多个 socket 但是同一时刻,活跃的 socket 只是少数的(需要读写数据的 socket 是少数的)大部分 socket 都是在等,所以就可以使用一个线程来等多个 socket。 ==》 这样效率也不会很低,系统开销也不会很高。
IO 多路复用操作属于操作系统提供的机制,也有对应的 API~ Java 中也对操作系统提供的 API 进行了封装,但这些 API 使用起来比较麻烦,我们这里暂时不介绍了。想要了解的老铁可以搜索 NIO 去仔细研究一下~~~
梳理代码:
最终版本代码~
先在这里贴一下最终的两个类的代码:
服务器:
package network;import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;public class TcpEchoServer {private ServerSocket serverSocket = null;public TcpEchoServer(int port) throws IOException {serverSocket = new ServerSocket(port);}public void start() throws IOException {System.out.println("服务器启动!");ExecutorService pool = Executors.newCachedThreadPool();while (true) {Socket clientSocket = serverSocket.accept();// Thread thread = new Thread(() -> {
// processConnection(clientSocket);
// });
// thread.start();pool.submit(new Runnable() {@Overridepublic void run() {processConnection(clientSocket);}});}}private void processConnection(Socket clientSocket) {System.out.printf("[%s:%d] 客户端上线!\n",clientSocket.getInetAddress(),clientSocket.getPort());try (InputStream inputStream = clientSocket.getInputStream();OutputStream outputStream = clientSocket.getOutputStream()) {Scanner scanner = new Scanner(inputStream);while (true) {// 读取完毕,客户端断开连接,就会产生读取完毕的提示信息if (!scanner.hasNext()) {System.out.printf("[%s:%d] 客户下线!\n",clientSocket.getInetAddress(),clientSocket.getPort());break;}// 1. 读取请求并解析。这里注意隐藏的约定,next 读的时候要读到空白字符才会结束// 因此就要求客户端发来的请求必须要带有空白符的结尾,比如 \n 或者 空格String request = scanner.next();// 2. 根据请求计算响应String response = process(request);// 3. 把响应返回给客户端// 用 PrintWriter 对 outputStream 进行一层包装~PrintWriter printWriter = new PrintWriter(outputStream);printWriter.println(request);printWriter.flush();System.out.printf("[%s:%d] request:%s, response:%s \n",clientSocket.getInetAddress(),clientSocket.getPort(),request,response);}} catch (IOException e) {throw new RuntimeException(e);} finally {try {clientSocket.close();} catch (IOException e) {throw new RuntimeException(e);}}}private String process(String request) {return request;}public static void main(String[] args) throws IOException {TcpEchoServer tcpEchoServer = new TcpEchoServer(9090);tcpEchoServer.start();}
}
客户端:
package network;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 {private Socket socket = null;public TcpEchoClient(String serverIp, int serverPort) throws IOException {// 此处可以把这里的 ip 和 port 直接传给 socket 对象// 由于 TCP 是有连接的,因此 socket 里面就会保存好这俩信息socket = new Socket(serverIp, serverPort);}public void start() {System.out.println("客户端启动!");try (InputStream inputStream = socket.getInputStream();OutputStream outputStream = socket.getOutputStream()) {Scanner scannerConsole = new Scanner(System.in);Scanner scannerNetwork = new Scanner(inputStream);PrintWriter writer = new PrintWriter(outputStream);while (true) {// 这里的流程和 UDP 的客户端类似// 1. 从控制台读取输入的字符串System.out.print("->");if (!scannerConsole.hasNext()) {break;}String request = scannerConsole.next();// 2. 把请求发给服务器,这里需要使用 println 来进行发送,是为了让发送的请求末尾带有 \n// 和服务器中的 scanner.next 对应~writer.println(request);writer.flush();// 3. 从服务器读取响应String response = scannerNetwork.next();// 4. 把响应显示出来System.out.println(response);}} catch (IOException e) {throw new RuntimeException(e);}}public static void main(String[] args) throws IOException {TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1",9090);tcpEchoClient.start();}
}
流程图
文字解释:
1. 服务器端启动:
服务器程序运行 TcpEchoServer 类的 main 方法,调用构造函数,TcpEchoServer(int port),传入一个端口号,这里传入的是 9090,创建一个 ServerSocket 对象,并绑定到该端口,开始监听客户端的连接请求。
接着调用 strat() 方法,输出“服务器启动”的提示信息,创建一个可缓存线程池 ExecutorService 用于管理处理客户端连接的线程。
进入第一层 while(true) 无限循环,调用 serverSocket.accept() 阻塞等待客户端的连接请求
2. 客户端启动:
客户端程序运行 TcpEchoClient 类的 main 方法,调用构造函数,TcpEchoClient(String serverIp, int serverPort) 向客户端传入服务器的 IP 地址是 127.0.0.1(即本机)和端口号 9090,创建一个 Socket 对象,尝试与服务器建立 TCP 连接(系统内核完成)。
调用 start() 方法,输出“客户端启动”的提示信息,获取与服务器连接的输入流 InputStream 和 输出流 OutputStream。
创建两个 Scanner 对象,一个 scannerConnsole 用于读取控制台输入,另一个 scannerNetwork 用于读取服务器的响应。
创建 PrintWirter 对象 writer ,用与向服务器发送请求
3. 服务器接收到客户端的连接
当客户端发起连接请求的时候,服务器的 serverSocket.accept() 方法返回一个 Socket 对象 clientSocket,代表与该客户端的连接。
服务器使用线程池 pool 提交一个新的任务,任务内容是调用 processConnection(Socket clientSocket) 方法来处理客户端的连接。
4. 客户端与服务器交互
客户端进入循环,持续与服务器进行交互:
输出提示信息 -> ,使用 scannerConsole.hasNext() 检查控制台是否有输入,如果没有输入就跳出循环。
使用 scannerConsole.next() 从控制台读取用户输入的请求
使用 writer.println(request) 将请求发送给服务器,这里是用 println 也是确保了请求末尾带有换行符 \n,以便服务器的 scanner.next() 方法能正确读取。
使用 writer.flush() 确保请求数据立即发送。
5. 服务器处理客户端连接
在 processConnection 方法中,服务器输出客户端上线的提示信息,格式为:[客户端IP:客户端端口号] 客户端上线!
获取与客户端连接的输入流 InputStream 和输出流 OutputStream,并使用 Scanner 从输入流读取客户端的请求
进入第二层 while(true) 循环,持续读取客户端的请求。
使用 scanner.hasNext() 检查是否还有数据可读,如果没有数据,则说明客户端断开连接,输出客户端下线的提示信息,格式为[客户端 IP 地址:客户端端口号]客户端下线!,然后跳出循环~
使用 scanner.next() 读取客户端的请求,这里要求客户端发来的请求必须以空白符(\n 或者 空格)结尾
调用 process(String request) 方法,根据请求计算响应,这里是简单的返回请求本身。
使用 PrintWriter 将相应返回给客户端,调用 printWriter.println(request) 发送响应(注意这里是 println 发送响应自然结尾就带了 \n),并调用 printWriter.flush() 确保数据立即发送。
输出请求和响应的详细信息,格式为:[客户端IP地址:客户端端口号]request:请求内容,response:相应内容.
6. 客户端得到响应
使用 scannerNetwork.next()从服务器读取响应
将响应显示在控制台上
7. 连接关闭
当客户端或服务器出现异常(网络中断等等),或者客户端主动关闭连接,会触发响应的异常处理逻辑。
服务器在 processConnection 方法中的 finally 块中关闭于客户端的连接,调用 clientSocket.colse() 方法
客户端在 start() 方法的 try 块结束时候,由于使用了 try - with - resources 语句,会自动关闭InputStream 和 OutputStream
8. 持续交互或者结束
如果客户端没有关闭连接,就会继续循环等待用户输入新的请求,重复步骤 4 中的交互过程。
如果客户端关闭连接,服务器会继续在 strat() 方法中的循环中等待新的客户端连接请求。
图文解释~
1. 服务器启动,阻塞在 accept,等待客户端连接
2. 客户端启动
这里构造方法中的 new 操作,触发了和服务器之间的建立连接的操作(内核中操作~),此时服务器就会从 accept 中返回。
3. 服务器从 accept 返回,进入到 processConnection 方法中
执行到 hashNext 处,产生阻塞,此时虽然连接接建立了,但是客户端还没有发来任何的请求,hashNext 阻塞等待请求到达(此处相当于是,电话接通了,但是没有人说话~)
4. 客户端继续执行到 hasNext,等待用户向控制台写入内容
5. 用户真正输入内容之后,此时 hasNext 就返回了,继续执行这里的发送请求的逻辑
这楼里就会把请求发出去,同时客户端等待服务的相应返回,next 也会产生阻塞
6. 服务器从 hasNext 返回,读取到请求内容并进行处理
读取到请求,构造出响应,把响应写回客户端了。
服务器就结束这次循环了,开始了下一轮循环,继续阻塞在 hasNext 处等待下一个请求
7. 客户端读取到响应,并且打印出来
结束这次循环,进行下一次循环,继续阻塞在 hasNext 等待用户继续输入数据~
回合制~~~
完!
相关文章:
网络编程 - 4 ( TCP )
目录 TCP 流套接字编程 API 介绍 SeverSocket Socket 用 TCP 实现一个回显服务器 服务端 客户端 运行调试 第一个问题:PrintWriter 内置的缓冲区 - flush 刷新解决 第二个问题:上述代码中,需要进行 close 操作吗? 第三…...
在STM32的定时器外设中,选择使用哪个外部时钟配置函数
在STM32的定时器外设中,选择使用哪个外部时钟配置函数主要取决于以下几个因素: 时钟源类型: TIM_ITRxExternalClockConfig:使用内部触发输入(ITRx),即来自其他定时器的时钟信号 TIM_TIxExternalClockConfig࿱…...
【Tauri2】026——Tauri+Webassembly
前言 不多废话 直言的说,笔者看到这篇文章大佬的文章 【04】Tauri 入门篇 - 集成 WebAssembly - 知乎https://zhuanlan.zhihu.com/p/533025312尝试集成一下WebAssembly,直接开始 正文 准备工作 新建一个项目 安装 vite的rsw插件和rsw pnpm instal…...
jenkins尾随命令
在访问jenkins的网址后面可以追加命令,比如访问地址是 http://10.20.0.124:8080/,常用的有以下几种方式: 1.关闭Jenkins 只要浏览器输入http://10.20.0.124:8080/exit即可退出,或者http://localhost:8080/exit 2.重启Jenkins …...
基于机器学习 LSTM 算法的豆瓣评论情感分析系统
基于机器学习 LSTM 算法的豆瓣评论情感分析系统 博主介绍:✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ 🍅文末获取源码联系🍅 ὄ…...
腾讯云对象存储m3u8文件使用腾讯播放器播放
参考腾讯云官方文档: 播放器 SDK Demo 体验_腾讯云 重要的一步来了: 登录腾讯云控制台,找到对象存储的存储桶。 此时,再去刷新刚才创建的播放器html文件,即可看到播放画面了。...
基于chatgpt和deepseek解答显卡的回答
当然可以!了解显卡特别是英伟达(NVIDIA)的系列,对于选购、升级或者了解游戏和创作性能都很重要。下面我帮你系统整理一下 NVIDIA 显卡的各个系列,并加点选购建议,方便你快速上手。 chatgpt 🧠 …...
2025年渗透测试面试题总结-拷打题库06(题目+回答)
网络安全领域各种资源,学习文档,以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具,欢迎关注。 目录 1. Sleep被禁用后的SQL注入 2. XSS属性控制利用 3. CSRF防护 4. 危险请求头 5. XXE高发场景 6. Ja…...
【一起学Rust】使用Thunk工具链实现Rust应用对Windows XP/7的兼容性适配实战
前言 在Rust语言快速发展的今天,开发者经常面临将现代语言特性与遗留系统兼容的挑战。特别是在工业控制、嵌入式设备等场景中,Windows XP/7等经典操作系统仍占据重要地位。本文深入解析如何通过Thunk工具链突破Rust编译器对旧版Windows系统的兼容性限制…...
leetcode 674. Longest Continuous Increasing Subsequence
目录 题目描述 第一步,明确并理解dp数组及下标的含义 第二步,分析明确并理解递推公式 第三步,理解dp数组如何初始化 第四步,理解遍历顺序 代码 题目描述 这是动态规划解决子序列问题的例子。与第300题的唯一区别就是&#…...
在VMware Workstation 17 Pro上实现Windows与UOS虚拟机之间复制粘贴文本及文件
在VMware Workstation 17 Pro上实现Windows与UOS虚拟机之间复制粘贴文本及文件 在本教程中,我们将介绍如何在VMware Workstation 17 Pro中安装UOS虚拟机,并通过安装open-vm-tools-desktop软件来实现Windows和UOS系统之间的文本和文件复制粘贴功能。 1.…...
十一、数据库day03--SQL语句02
文章目录 一、查询语句1. 基本查询2. 条件查询2.1 ⽐较运算符&逻辑运算符2.2 模糊查询2.3 范围查询2.4 判断空 3. 其他复杂查询3.1 排序3.2 聚合函数3.3 分组3.4 分页查询 二、回顾1. 使⽤ Navicat ⼯具中的命令列2.命令⾏基本操作步骤 提示:以下是本篇文章正文…...
第6章 类文件结构《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》
第6章 类文件结构 代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。 6.1 概述 老师说过,计算机只认识0和1,所以我们写的程序需要被编译器翻译成由0和1构成的二进制格式才能被计算机…...
【Vue】模板语法与指令
个人主页:Guiat 归属专栏:Vue 文章目录 1. Vue 模板语法基础1.1 文本插值1.2 原始 HTML1.3 属性绑定 2. Vue 指令系统2.1 条件渲染2.2 列表渲染2.3 事件处理2.4 表单输入绑定 3. 计算属性与侦听器3.1 计算属性3.2 侦听器 4. 类与样式绑定4.1 绑定 HTML 类…...
Python语法系列博客 · 第5期[特殊字符] 模块与包的导入:构建更大的程序结构
上一期小练习解答(第4期回顾) ✅ 练习1:判断偶数函数 def is_even(num):return num % 2 0print(is_even(4)) # True print(is_even(5)) # False✅ 练习2:求平均值 def avg(*scores):return sum(scores) / len(scores)print(…...
HashMap 初步理解 put 操作流程 HashMap 的线程安全问题
一、HashMap 核心原理 HashMap 是 Java 中最常用的哈希表实现,基于 数组 链表/红黑树 的复合结构,核心特性如下: 哈希函数 键的哈希值通过 hashCode() 计算,并通过扰动函数优化分布:static final int hash(Object ke…...
服务治理-服务发现和负载均衡
第一步:引入依赖 第二步:配置地址 改写购物车服务的代码 负载均衡成功实现。 假如有一个服务挂了,比如说8081,cart-service能不能正常访问,感知到。 再重新启动8081端口。 不管服务宕机也好,还是服务刚启动…...
GNU,GDB,GCC,G++是什么?与其他编译器又有什么关系?
文章目录 前言1. GNU和他的工具1.1 gcc与g1.2 gdb 2.Windows的Mingw/MSVC3.LLVM的clang/clang4.Make/CMake 前言 在开始之前我们先放一段Hello World:hello.c #include <stdio.h>int main() {printf("Hello World");return 0; }然后就是一段老生常…...
定制一款国密浏览器(9):SM4 对称加密算法
上一章介绍了 SM3 算法的移植要点,本章介绍对称加密算法 SM4 的移植要点。 SM4 算法相对 SM3 算法来说复杂一些,但还是比较简单的算法,详细算法说明参考《GMT 0002-2012 SM4分组密码算法》这份文档。铜锁开源项目的实现代码在 sm4.c 文件中,直接拿过来编译就可以。 但需要…...
kafka集群认证
1、安装Kerberos(10.10.10.168) yum install krb5-server krb5-workstation krb5-libs -y 查看版本 klist -V Kerberos 5 version 1.20.1 编辑/etc/hosts 10.10.10.168 ms1 10.10.10.150 ms2 10.10.10.110 ms3 vim /etc/krb5.conf # Configuration snippets ma…...
Mermaid 是什么,为什么适合AI模型和markdown
什么是 Mermaid? Mermaid 是一个基于 JavaScript 的开源绘图和图表工具,允许用户通过简单的文本语法创建图表。它支持生成流程图、时序图、类图、甘特图等多种类型的可视化内容,并直接从类似 Markdown 的代码中渲染。Mermaid 因其与 Markdow…...
为什么信号完整性对于高速连接器设计至关重要?
外部连接器通过在各种电子元件和系统之间可靠地传输数据而不损失保真度来保持信号完整性。在本文中,我们将讨论信号完整性的重要性,回顾高速部署挑战,并重点介绍各种连接器设计策略,以防止失真和降级。 了解连接器信号完整性挑战…...
【FFmpeg从入门到精通】第三章-FFmpeg转封装
1 音视频文件转MP4格式 在互联网常见的格式中,跨平台最好的应该是MP4文件,因为MP4文件既可以在PC平台的 Flashplayer 中播放,又可以在移动平台的 Android、ios 等平台中进行播放,而且使用系统默认的播放器即可播放,因…...
PG数据库推进医疗AI向量搜索优化路径研究(2025年3月修订版)
PG数据库推进医疗AI向量搜索优化路径研究 一、医疗 AI 向量搜索的发展现状与挑战 1.1 医疗数据特征与检索需求 医疗数据作为推动医疗领域进步与创新的关键要素,具有鲜明且复杂的特征。从多模态角度看,医疗数据涵盖了结构化数据,如患者基本信息、检验检查报告中的数值结果;…...
Android 下拉栏中的禁用摄像头和麦克风隐藏
Android 下拉栏中的禁用摄像头和麦克风隐藏 文章目录 Android 下拉栏中的禁用摄像头和麦克风隐藏一、前言二、下拉框中的禁用摄像头和麦克风隐藏实现1、设置支持属性为false2、修改代码 三、其他1、下拉栏中的禁用摄像头和麦克风隐藏小结2、 Android SensorPrivacyService ps&a…...
阿里云Clickhouse 冷热数据分层存储 实战记录
一、 背景 某业务Clickhouse库月数据增长超过2.5T,云上Clickhouse容量并不是无限的,单节点有32T上限,而业务已使用一半以上,依此速度,半年内就将达到上限。 与业务讨论,大致有以下几种解决思路:…...
ARINC818-实现
1.编码和解码;分隔符插入和删除;空闲idle字符插入 2.视频TX和RX接口:可以设计为流传输和帧同步传输 3.传输媒介:光纤或者铜缆 4.链路支持fc 1x,2x,3x,5x,6x,8x 上图中N有限制,N不能允许ADVB帧负载和填充长度不超过2112…...
OpenStack Yoga版安装笔记(23)Swift安装
一、官方文档 Object Storage Install Guide — Swift 2.29.3.dev5 documentation 二、环境准备 之前的实验,已经有controller, compute1, block1节点,并已经完成Keystone、Glance、Nova、Neutron、Cinder等主要OpenStack Service的安装。 此处新增…...
MySql 三大日志(redolog、undolog、binlog)详解
:费解的开关
审题: 本题需要我们将多组测试用例中拉灯数小于等于6的最小拉灯数输出,若拉灯数最小值仍大于6,则输出-1 思路: 方法一:二进制枚举 首先我们先分析一下基本特性: 1.所有的灯不可能重复拉:若拉的数…...
2025.04.19-阿里淘天春招算法岗笔试-第三题
📌 点击直达笔试专栏 👉《大厂笔试突围》 💻 春秋招笔试突围在线OJ 👉 笔试突围OJ 03. 数值平衡之道 问题描述 LYA 是一位精通树形数据结构的数学家,她有一棵包含 n n n...
单片机毕业设计选题物联网计算机电气电子类
题目一:基于单片机的PM2.5空气质量检测仪器 选 1.用到ADC0832模数转换芯片,数据更加精准。 2.使用夏普传感器的GP2Y1010AUOF粉尘传感器实时检测空气中的PM2.5值并通过1602显示出来,检测准确。 3.LCD1602液晶第一行 前面显示测到的PM2.5…...
TDOA解算——牛顿迭代法|以4个基站的三维空间下TDOA定位为背景,使用牛顿迭代法解算。附完整代码,订阅专栏后可复制粘贴
本问所介绍的代码通过TDOA(到达时间差)方法,结合牛顿迭代算法,实现了三维空间内目标位置的定位。系统包含1个主锚点和3个副锚点,通过测量信号到达各基站的时间差计算距离差,最终迭代求解目标坐标。订阅专栏后可以获得完整的MATLAB代码,粘贴到空脚本中即可运行 文章目录 …...
海量聊天数据处理:基于Spring Boot与SharingJDBC的分库分表策略及ClickHouse冷热数据分离
引言 随着互联网应用的快速发展,每天产生的聊天记录数量级已经达到了惊人的程度。以2000万条/天为例,一年下来就是大约7.3亿条记录。如此庞大的数据量给数据库的设计和管理带来了前所未有的挑战。本文将探讨如何使用SharingJDBC整合Spring Boot技术来实…...
网络开发基础(游戏)之 Socket API
Socket简介 Socket (套接字)是网络编程的基础,在 C# 中通过 System.Net.Sockets 命名空间提供了一套完整的 API 来实现网络通信。 网络上的两个程序通过一个双向的通信连接实现数据交换, 这个连接的一端称为一个Socket。 一个Socket包含了进行网络通信必…...
在 Amazon Graviton 上运行大语言模型:CPU 推理性能实测与调优指南
引言 在生成式 AI 浪潮中,GPU 常被视为大模型推理的唯一选择。然而,随着 ARM 架构的崛起和量化技术的成熟,CPU 推理的性价比逐渐凸显。本文基于 Amazon Graviton 系列实例与 llama.cpp 工具链,实测了 Llama 3、DeepSeek 等模型的…...
基于尚硅谷FreeRTOS视频笔记——15—系统配制文件说明与数据规范
目录 配置函数 INCLUDE函数 config函数 数据类型 命名规范 函数与宏 配置函数 官网上可以查找 最核心的就是 config和INCLUDE INCLUDE函数 这些就是裁剪的函数 它们使用一个ifndef。如果定义了,就如果定义了这个宏定义,那么代码就生效。 通过ifn…...
Nacos 使用了什么日志框架?如何配置和查看日志?
Nacos 使用的日志框架 Nacos 主要使用 SLF4j (Simple Logging Facade for Java) 作为日志门面(API),并选择 Logback 作为其底层的日志实现。 SLF4j: 这是一个日志抽象层,允许开发者在代码中使用统一的接口进行日志记录ÿ…...
【基于Fluent+Python耦合的热管理数字孪生系统开发:新能源产品开发的硬核技术实践】
引言:热管理数字孪生的技术革命 在新能源领域(如动力电池、储能系统、光伏逆变器等),热管理是决定产品性能与安全的核心问题。传统热设计依赖实验与仿真割裂的流程,而数字孪生技术通过实时数据驱动与动态建模…...
【工具变量】A股上市公司信息披露质量KV指数测算数据集(含do代码 1991-2024年)
KV指数(Key Value Index)作为评估信息披露质量的关键指标,在证券市场,尤其是A股市场上市公司信息披露监管与评估中占据重要地位。该指数通过系统化、定量化的方法,对企业发布的信息进行全面剖析与打分,精准…...
【ELF2学习板】利用OpenMP采用多核并行技术提升FFTW的性能
目录 引言 OpenMP简介 编译OpenMP支持的FFTW库 部署与测试 测试程序 程序部署 测试结果 结语 引言 在前面已经介绍了在ELF2开发板上运行FFTW计算FFT。今天尝试利用RK3588的多核运算能力来加速FFT运算。FFTW利用多核能力可以考虑使用多线程或者OpenMP。今天介绍一下Ope…...
打靶日记 zico2: 1
一、探测靶机IP(进行信息收集) 主机发现 arp-scan -lnmap -sS -sV -T5 -p- 192.168.10.20 -A二、进行目录枚举 发现dbadmin目录下有个test_db.php 进入后发现是一个登录界面,尝试弱口令,结果是admin,一试就出 得到加…...
【技术派后端篇】 Redis 实现用户活跃度排行榜
在各类互联网应用中,排行榜是一个常见的功能需求,它能够直观地展示用户的表现或贡献情况,提升用户的参与感和竞争意识。在技术派项目中,也引入了用户活跃度排行榜,该排行榜主要基于 Redis 的 ZSET 数据结构来实现。接下…...
MySql Innodb详细解读
参考文档:https://www.cnblogs.com/acatsmiling/p/18424729 一、数据存储:从磁盘到内存的"黑帮走私" 1. 物理结构:表空间与页的江湖规矩 表空间(Tablespace): 所有InnoDB数据存在表空间里&…...
每日两道leetcode
399. 除法求值 - 力扣(LeetCode) 题目 给你一个变量对数组 equations 和一个实数值数组 values 作为已知条件,其中 equations[i] [Ai, Bi] 和 values[i] 共同表示等式 Ai / Bi values[i] 。每个 Ai 或 Bi 是一个表示单个变量的字符串。 …...
在RK3588上使用哪个流媒体服务器合适
在RK3588平台上选择合适的流媒体服务器时,需考虑其ARM Cortex-A76/A55架构、硬件编解码能力(如支持H.264/H.265/AV1解码)以及Linux/Android系统支持。以下是推荐的方案: 1. 轻量级方案:GStreamer RTSP 适用场景&…...
分享一个DeepSeek+自建知识库实现人工智能,智能回答高级用法。
这个是我自己搞的DeepSeek大模型自建知识库相结合到一起实现了更强大的回答问题能力还有智能资源推荐等功能。如果感兴趣的小伙伴可以联系进行聊聊,这个成品已经有了实现了,所以可以融入到你的项目,或者毕设什么的还可以去参加比赛等等。 1.项…...
PyTorch 深度学习实战(38):注意力机制全面解析(从Seq2Seq到Transformer)
在上一篇文章中,我们探讨了分布式训练实战。本文将深入解析注意力机制的完整发展历程,从最初的Seq2Seq模型到革命性的Transformer架构。我们将使用PyTorch实现2个关键阶段的注意力机制变体,并在机器翻译任务上进行对比实验。 一、注意力机制演…...
Android Studio 获取配置资源与第三方包信息详解
文章目录 Android Studio 获取配置资源与第三方包信息详解一、获取资源文件中的配置1. 获取颜色值Java 中获取:Kotlin 中获取: 2. 获取字符串Java 中获取:Kotlin 中获取: 3. 获取尺寸值Java 中获取:Kotlin 中获取&…...
【网络初识】从零开始彻底了解网络编程(一)
本篇博客给大家带来的是网络的知识点. 🐎文章专栏: JavaEE初阶 🚀若有问题 评论区见 ❤ 欢迎大家点赞 评论 收藏 分享 如果你不知道分享给谁,那就分享给薯条. 你们的支持是我不断创作的动力 . 王子,公主请阅🚀 要开心要快乐顺便进步 一. 网络…...