当前位置: 首页 > news >正文

Java Web 实战 15 - 计算机网络之网络编程套接字

文章目录

  • 一 . 网络编程中的基本概念
    • 1.1 网络编程
    • 1.2 客户端(client) / 服务器(server)
    • 1.3 请求(request) / 响应(response)
    • 1.4 客户端和服务器之间的交互数据
      • 1.4.1 一问一答
      • 1.4.2 多问一答
      • 1.4.3 一问多答
      • 1.4.4 多问多答
  • 二 . socket 套接字
    • 2.1 UDP 的 Socket API
      • 2.1.1 引子
      • 2.1.2 服务器代码
      • 2.1.3 客户端代码
      • 2.1.4 通过代码分析 UDP 的特点
      • 2.1.5 小结
      • 2.1.6 当我们发现某个端口 , 被其他进程占用了 , 单只咱们服务器启动不起来 , 怎么办 ?
    • 2.2 TCP 的 Socket API
      • 2.2.1 引子
      • 2.2.2 服务器的代码
      • 2.2.3 客户端的代码
      • 2.2.4 其他 Bug
      • 2.2.5 解决缺陷 : 频繁创建销毁
      • 2.2.6 TCP 版本的翻译服务器
  • 三 . 小结
    • 3.1 Socket API
    • 3.2 客户端/服务器 工作流程
    • 3.3 TCP 的 Socket API 中 , 服务器使用多线程

大家好 , 这篇文章给大家带来的是网络编程中的套接字 , 我们会着重讲解 socket 的套接字 , 包括了 UDP 的 Socket API 和 TCP 的 Socket API , 干货十足
推荐大家跳转到此链接查看文章
上一篇文章的链接也给大家贴在这里了
文章专栏在此
在这里插入图片描述

一 . 网络编程中的基本概念

1.1 网络编程

写代码 , 实现 两个/多个 进程 , 通过网络 , 来进行相互通信

我们之前介绍过 : 进程具有隔离性 (每个进程都有自己独立的虚拟地址空间)
进程间通信 , 借助每一个进程都能访问到的公共区域 , 完成数据交换
网络编程 , 也是一种进程间通信的方式 , 借助的公共区域就是网卡 , 这是当下最主流的方式 , 既能够让同一个主机的多个进程间通信 , 也可以让不同主机的多个进程间通信

1.2 客户端(client) / 服务器(server)

客户端 : 主动发送网络数据的一方
服务器 : 被动接受网络数据的一方

因为服务器无法知道客户端什么时候发送来资源 , 因此就只能长时间运行 , 甚至 7*24 小时运行

1.3 请求(request) / 响应(response)

请求 : 客户端给服务器发送的数据.
响应 : 服务器给客户端返回的数据.

1.4 客户端和服务器之间的交互数据

1.4.1 一问一答

客户端给服务器发个请求 , 服务器给客户端返回个响应
这是最常见的方式 , 比如 : 浏览网页

1.4.2 多问一答

客户端发多个请求 , 服务器返回一个响应
更少见一些 , 比如上传文件

1.4.3 一问多答

客户端发一个请求 , 服务器返回多个响应
还比较常见 , 比如下载文件

1.4.4 多问多答

客户端发送多个请求 , 服务器返回多个响应
比如 : 远程控制、游戏串流

所谓“串流游戏”,实际上是指通过有线或无线 WiFi 网络,将 PC / macOS / Linux 电脑上的游戏画面 实时“远程投射” 到另外一台设备屏幕上,如 iPhone、iPad、安卓手机、平板、智能电视或笔记本上,并进行远程游

二 . socket 套接字

进行网络编程 , 需要使用操作系统提供的网络编程 API
image.png
在传输层提供了两个非常重要且截然不同的协议 : TCP 和 UDP
这两个协议对应的 socket api 也是完全不同的
先简单的给大家总结一下 TCP 和 UDP
TCP : 有连接 , 可靠传输 , 面向字节流 , 全双工
UDP : 无连接 , 不可靠传输 , 面向数据报 , 全双工

有连接 : 打电话 -> 先建立连接 , 然后再通信
无连接 : 发微信 -> 不必建立连接 , 直接通信即可

网络通信无法保证 100% 到达的 (最糟糕的情况 : 网线被修狗咬断了)
可靠传输 : 数据 对方收没收到 , 发送方能够有感知
不可靠传输 : 数据 对方收没收到 , 发送方啥也不知道
打电话就相当于可靠传输 , 发微信就相当于不可靠传输

面向字节流 : 这里的字节流和文件那里的字节流是一样的 , 像水流一样 , 想传多少就传多少
面向数据报 : 以数据报作为传输的基本单位

全双工 : 双向通信 , 一个管道 , 能够 A -> B , 同时也能 B -> A (同时进行)
半双工 : 单向通信 , 一个管道 , 同一时刻 , 只能够 A -> B , 要么 B -> A (合走各的)
image.png
类似 , 网络通信主要借助网线来进行通信 . 一个标准的以太网线 , 里面其实是 8 根铜线 , 就分成了两条路 : 4 条线用来上传 , 4 条路用来下载

2.1 UDP 的 Socket API

2.1.1 引子

在 UDP 的 Socket API 中 , 有两个核心类 :
DatagramSocket :
叫做 socket 类 , 本质上相当于是一个 “文件” . 在系统中 , 还有一种特殊的 socket 文件 , 对应到网卡设备 .
要想进行网络通信 , 就需要先打开 socket 文件 . 我们构造一个 DatagramSocket 对象 , 就相当于打开了一个内核中的 socket 文件 .
打开之后 , 就可以传输数据了
传输数据通过两个方法 : send(发送数据)、receive(接受数据)
还有一个 close 操作关闭文件

DatagramPacket :
表示一个 UDP 数据报 , UDP 是面向数据报的协议 , 传输数据 , 就是以 DatagramPacket 为基本单位

还有一个非常重要的类 : InetSocketAddress
他描述了网络上的一个地址 , 也就是 IP 地址 + 端口号

接下来 , 我们来写一个 UDP版本的回显服务器-客户端 (echo server)

回显服务器 : 客户端发什么 , 服务器返回什么
不涉及任何业务逻辑 , 单纯演示 API 的用法

我们创建两个类 : UdpEchoServer(回显服务器) 和 UdpEchoClient(回显客户端)
先编写服务器代码

2.1.2 服务器代码

package network;import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;public class UdpEchoServer {// 要想创建 UDP 服务器,就需要先打开一个 socket 文件// 先不进行初始化private DatagramSocket socket = null;// 在构造方法中进行初始化// 创建实例的同时指定端口号(绑定一个端口) -> 把一个进程和一个端口号关联起来public UdpEchoServer(int port) throws SocketException {socket = new DatagramSocket(port);}// 启动服务器public void start() throws IOException {System.out.println("服务器已经启动");// 服务器是 7*24 运行的while (true) {// 1. 读取客户端发来的请求// 尝试读取,并不是调用了就一定能读到// 如果客户端没有发来请求,就会阻塞等待// 直到真的有客户端的请求过来了,receive才会返回// receive 属于输出型参数// 我们需要先构造参数,然后将数据写入到输出型参数中DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);socket.receive(requestPacket);// 2. 对请求进行解析(把 DatagramPacket 转换成 String)// requestPacket 内部就是字节数组,用 getData 将内部的字节数组取出来// 再设置从哪到哪取出String request = new String(requestPacket.getData(), 0, requestPacket.getLength());// 3. 根据请求处理响应// 虽然咱们这里是个回显服务器,但是还是可以单独搞个方法处理响应String response = process(request);// 4. 把响应构造成 DatagramPacket 对象// 第一个参数:把 String 转换成字节数组// 第二个参数:返回的字节数组的长度// 第三个参数:构造响应对象->谁给咱们发来的请求,就把响应发给谁DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());// 5. 把 DatagramPacket 返回socket.send(responsePacket);// 打印日志// IP地址+端口号+请求+响应System.out.printf("[%s:%d] req=%s resp=%s\n",requestPacket.getAddress().toString(),requestPacket.getPort(),request,response);}}// 根据请求处理响应// 回显服务器:不用处理任何逻辑public String process(String request) {return request;}// 启动服务器public static void main(String[] args) throws IOException {// 端口号可以在一定范围内(0~65535)随便写// 一般来说,1024以下的端口是系统留着自己用的,我们就不跟他抢了UdpEchoServer server = new UdpEchoServer(8000);server.start();}
}

其中 , 服务器的代码中有许多问题 , 我们也在这张图片里面给大家讲解了
image.png

再去编写客户端的代码

2.1.3 客户端代码

package network;import java.io.IOException;
import java.net.*;
import java.util.Scanner;public class UdpEchoClient {// 还是一样,先设置成 nullprivate DatagramSocket socket = null;// 客户端的端口号一般都是操作系统自动分配的public UdpEchoClient() throws SocketException {socket = new DatagramSocket();}public void start() throws IOException {// 前置工作:让用户往控制台中输入请求内容Scanner sc = new Scanner(System.in);while (true) {// 1. 让客户端从控制台读取一个请求数据System.out.print("> ");String request = sc.next();// 2. 把这个字符串请求发送给服务器// 就需要构造 DatagramPacket// 构造的 DatagramPacket 既要包含要传输的数据,还要指定要把数据传输到哪里DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,InetAddress.getByName("127.0.0.1"), 8000);// 3. 把数据报发给服务器socket.send(requestPacket);// 4. 从服务器读取响应数据DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);socket.receive(responsePacket);// 5. 把响应的数据获取出来,转成字符串String response = new String(responsePacket.getData(), 0, responsePacket.getLength());// 6. 打印报文System.out.printf("req:%s,resp:%s\n", request, response);}}public static void main(String[] args) throws IOException {UdpEchoClient client = new UdpEchoClient();client.start();}
}

我把客户端的代码中常见问题也展示在下方了
image.png

我们来梳理一下客户端和服务器的工作流程
无标题2.png
这个事情 , 就相当于客户端想干一件事 , 但是不想亲力亲为 , 就要安排给别人做
上述流程 , 不仅仅是回显服务器客户端如此 , 大部分的客户端服务器都是如此 . 这是一套基本套路

我们运行一下
image.png
另外 , 我们的服务器是可以给多个客户端提供服务的
我们可以再运行一个客户端 , 不过需要设置
image.png
image.png
image.png
把这个选项勾选上 , 就可以启动多个客户端了
image.png

一个服务器的灵魂所在 , 就是 process 方法
一个服务器要完成的工作 , 都是通过 “根据请求计算相应” 来体现的 .
不管是啥样的服务器 , 读取请求并解析 , 构造响应并返回 , 这两个步骤 , 大同小异
唯有 “根据请求计算相应” 是千变万化的 , 是非常复杂的 , 可能一次处理请求就要经历 几w 几十w行 的代码来完成
如果我现在不是想写一个回显服务器了 , 而是一个带有业务逻辑的服务器 , 基于上述框架 , 稍作修改即可
image.png
举个栗子 : 我们新建一个 UdpDictServer 类(字典服务器 / 翻译服务器)
我们希望实现一个 英译汉 的效果
请求是一个英文单词 , 响应是他对应的翻译
UdpDictServer 只需要继承我们之前写过的 UdpEchoServer 即可

package network;// 翻译服务器
// 我们希望实现一个 英译汉 的效果
// 请求是一个英文单词 , 响应是他对应的翻译import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;// 我们之前已经实现过最基础的翻译服务器
// 只需要继承这个最简单的服务器,然后在这基础上稍加改动即可
public class UdpDictServer extends UdpEchoServer {private HashMap<String, String> dict = new HashMap<>();public UdpDictServer(int port) throws SocketException {super(port);// 我们在构造方法中构造数据dict.put("cat", "小猫");dict.put("dog", "小狗");dict.put("ox", "小牛");}// 和父类的 UdpEchoServer 相比// 只需要修改 process 方法@Overridepublic String process(String request) {// getOrDefault:哈希表中,有就返回.没有,就返回"我也不会"return dict.getOrDefault(request, "这个词我也不会");}public static void main(String[] args) throws IOException {UdpDictServer udpDictServer = new UdpDictServer(8000);udpDictServer.start();}
}

客户端还用我们刚才写好的即可
运行一下
image.png

2.1.4 通过代码分析 UDP 的特点

UDP 是无连接的
image.png
UDP 是不可靠传输 , 这个在我们的代码中是看不出来的 , 是操作系统就这样实现的
UDP 是面向数据报的
image.png
UDP 是全双工的
image.png

2.1.5 小结

总结一下 : UDP 版本的 Socket API 有两个核心类
DatagramSocket : 表式内核里面的 socket 文件

  • send
  • receive
  • close

DatagramPacket : 发送接收数据的基本单位
构造方法 :

  1. 只包含缓冲区 , 用于接收数据的时候 , 构造了一个空的数据报.
  2. 包含缓冲区和一个 InetAddress , 用于发送数据的时候 , 指定数据包含啥以及发到哪里去.
  3. 包含缓冲区和一个 InetAddress + int , 用于发送数据的时候 , 指定数据包含啥以及发到哪里去.

但是我们刚才写的代码 , 并没有去 close 啊
文件打开之后 , 用完了就要及时关闭啊 , 那上述代码中为啥没 close ?
我们的服务器一般都是 7*24 小时运行的 , 上述代码中的 socket 对象 , 生命周期应该是伴随整个进程的
因此 , 进程结束之前 , 提前关闭 socket 对象是不合适的
而且如果进程已经结束 , 对应的 PCB 也就没了 , PCB 中的文件描述符表也就没了 . 此时也就相当于是关闭了

2.1.6 当我们发现某个端口 , 被其他进程占用了 , 单只咱们服务器启动不起来 , 怎么办 ?

使用 netstat 命令来找到是哪个进程占用的
netstat -ano | findstr "8000"
image.png
通过这条指令 , 就可以找到对应进程的 PID 了
我们就可以去任务管理器中找到对应线程然后关闭它

2.2 TCP 的 Socket API

2.2.1 引子

在 TCP 的 Socket API 中 , 主要涉及两个主要的类 :
ServerSocket API : 服务器端使用的 Socket
构造方法 : ServerSocket(int port) , 创建一个服务端流套接字Socket,并绑定到指定端口
核心方法 :
accept() : accept没有参数 , 返回值是一个 Socket 对象 , 功能是等待有客户端和服务器建立上连接.
accept 则会把这个连接获取到进程中 , 进一步的通过返回值的 Socket 对象来和客户端进行交互

Socket : 服务器和客户端都会使用的 Socket
通过 Socket 对象,就可以进行发送/接收数据了.
核心方法 :
InputStream getInputStream()
OutputStream getOutputStream()
通过这两个方法 , 传输数据不再通过 Socket 对象 , 而是 Socket 内部包含了输入流对象和输出流对象
借助输入流对象来去读数据 , 也就是接收数据
借助输出流对象来去写数据 , 也就是发送数据

举个栗子 : 比如我们去万达吃饭 , 一般每个店铺都会让一个人专门在门口拉客
一位小哥哥把我们叫住 , 问我们想不想吃火锅 , 我们就跟他进去了
进去店铺之后 , 小哥哥就把我们交给一位小姐姐 , 接下来就是这个美女来服务我们
这种店铺是有明确分工的 , 小哥哥是外场拉客的 , 小姐姐是内场提供服务的
外场拉客就相当于 ServerSocket
内场服务就相当于 Socket
当外场小哥哥拉到了客人 (有连接过来了) , 就通过 accept 把连接交给了 Socket , 后续就通过 Socket 对象和客户端进行沟通

2.2.2 服务器的代码

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) {// accept 干的活是拉客// 对于操作系统来说,建立一个 TCP 连接,是内核操作// accept 要干的就是等连接建立好了,把这个连接给拿到应用程序中// 如果当前连接还未建立,accpet就会阻塞等待// accept 相当于有人给你打电话了,你按下接听键.如果没人给你打电话,你就会阻塞等待直到有人给你打电话Socket clientSocket = serverSocket.accept();processConnect(clientSocket);}}// 通过这个方法,给当前连接上的客户端提供服务// 一个连接过来了,服务方式可能有两种// 1. 一个连接只进行一次数据交互(一个请求+一个响应)  短连接// 2. 一个连接进行多次数据交互(N个请求+N个响应)     长连接// 此处是长连接的版本public void processConnect(Socket clientSocket) throws IOException {// 在最开始获取一下 IP地址和端口号System.out.printf("[%s,%d] 建立连接\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());/*// 接收数据InputStream inputStream = clientSocket.getInputStream();// 发送数据OutputStream outputStream = clientSocket.getOutputStream();*/// 使用字节流是不方便的// 我们就可以用 Scanner PrintWriter 来对他们进行封装try (InputStream inputStream = clientSocket.getInputStream(); OutputStream outputStream = clientSocket.getOutputStream()) {// 接收数据Scanner scanner = new Scanner(inputStream);// 发送数据PrintWriter printWriter = new PrintWriter(outputStream);// 长连接的写法 -> 循环while (true) {// 判断还有没有数据// 读完了,后面没有数据,就相当于读到了 EOF// 这个操作就代表客户端断开连接,此时 hasNext 返回 falseif (!scanner.hasNext()) {// 连接断开// 打印一下哪个 IP+端口 连接断开了System.out.printf("[%s,%d] 断开连接\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());break;}// 1. 读取请求并解析String request = scanner.next();// 2. 根据请求计算响应String response = process(request);// 3. 把响应写回客户端printWriter.write(response);// 刷新一下缓冲区,避免数据真的没有发出去printWriter.flush();// 每次发送完毕,再来打印一下相关信息System.out.printf("[%s,%d] req:%s,resp=%s\n", clientSocket.getInetAddress().toString(), clientSocket.getPort(), request, response);}}}// 回显服务器public String process(String request) {return request;}public static void main(String[] args) throws IOException {TcpEchoServer server = new TcpEchoServer(8000);server.start();}
}

上面的代码跟文件操作非常类似
因为 TCP 是面向字节流的 , Java 中就把 TCP 的相关方法设置成跟文件类似的操作了

2.2.3 客户端的代码

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 {// 在 TcpEchoServer 中需要 ServerSocket Socket 对象全部用到// 在 TcpEchoClient 中只需要 Socket 对象即可private Socket socket = null;public TcpEchoClient() throws IOException {// new 对象的时候就需要和服务器建立连接了// 要想建立连接,就需要知道服务器在哪里(IP地址+端口)socket = new Socket("127.0.0.1", 8000);}public void start() throws IOException {// 用户从控制台输入Scanner scanner = new Scanner(System.in);// 由于我们是实现的是长连接,一个连接会处理 N 个请求和响应try (InputStream inputStream = socket.getInputStream();OutputStream outputStream = socket.getOutputStream();) {// 从网络输入Scanner scannerNet = new Scanner(inputStream);PrintWriter printWriter = new PrintWriter(outputStream);while (true) {// 1. 从控制台读取用户输入什么?System.out.print("> ");String request = scanner.next();// 2. 把请求发送给服务器printWriter.write(request);printWriter.flush();// 3. 从服务器读取响应String response = scannerNet.next();// 4. 把结果显示到界面上System.out.printf("req:%s,resp:%s\n", request, response);}}}public static void main(String[] args) throws IOException {TcpEchoClient client = new TcpEchoClient();client.start();}
}

image.png
运行一下吧 (大家注意 : 要先启动服务器 , 再启动客户端)

得先餐馆开业 , 咱们才能去吃饭

Demo.png
那改正好的代码也贴给大家
TcpEchoServer

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) {// accept 干的活是拉客// 对于操作系统来说,建立一个 TCP 连接,是内核操作// accept 要干的就是等连接建立好了,把这个连接给拿到应用程序中// 如果当前连接还未建立,accpet就会阻塞等待// accept 相当于有人给你打电话了,你按下接听键.如果没人给你打电话,你就会阻塞等待直到有人给你打电话Socket clientSocket = serverSocket.accept();processConnect(clientSocket);}}// 通过这个方法,给当前连接上的客户端提供服务// 一个连接过来了,服务方式可能有两种// 1. 一个连接只进行一次数据交互(一个请求+一个响应)  短连接// 2. 一个连接进行多次数据交互(N个请求+N个响应)     长连接// 此处是长连接的版本public void processConnect(Socket clientSocket) throws IOException {// 在最开始获取一下 IP地址和端口号System.out.printf("[%s,%d] 建立连接\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());/*// 接收数据InputStream inputStream = clientSocket.getInputStream();// 发送数据OutputStream outputStream = clientSocket.getOutputStream();*/// 使用字节流是不方便的// 我们就可以用 Scanner PrintWriter 来对他们进行封装try (InputStream inputStream = clientSocket.getInputStream(); OutputStream outputStream = clientSocket.getOutputStream()) {// 接收数据Scanner scanner = new Scanner(inputStream);// 发送数据PrintWriter printWriter = new PrintWriter(outputStream);// 长连接的写法 -> 循环while (true) {// 判断还有没有数据// 读完了,后面没有数据,就相当于读到了 EOF// 这个操作就代表客户端断开连接,此时 hasNext 返回 falseif (!scanner.hasNext()) {// 连接断开// 打印一下哪个 IP+端口 连接断开了System.out.printf("[%s,%d] 断开连接\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());break;}// 1. 读取请求并解析String request = scanner.next();// 2. 根据请求计算响应String response = process(request);// 3. 把响应写回客户端printWriter.println(response);// 刷新一下缓冲区,避免数据真的没有发出去printWriter.flush();// 每次发送完毕,再来打印一下相关信息System.out.printf("[%s,%d] req:%s,resp=%s\n",clientSocket.getInetAddress().toString(),clientSocket.getPort(),request,response);}}}// 回显服务器public String process(String request) {return request;}public static void main(String[] args) throws IOException {TcpEchoServer server = new TcpEchoServer(8000);server.start();}
}

TcpEchoClient

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 {// 在 TcpEchoServer 中需要 ServerSocket Socket 对象全部用到// 在 TcpEchoClient 中只需要 Socket 对象即可private Socket socket = null;public TcpEchoClient() throws IOException {// new 对象的时候就需要和服务器建立连接了// 要想建立连接,就需要知道服务器在哪里(IP地址+端口)socket = new Socket("127.0.0.1", 8000);}public void start() throws IOException {// 用户从控制台输入Scanner scanner = new Scanner(System.in);// 由于我们是实现的是长连接,一个连接会处理 N 个请求和响应try (InputStream inputStream = socket.getInputStream();OutputStream outputStream = socket.getOutputStream();) {// 从网络输入Scanner scannerNet = new Scanner(inputStream);PrintWriter printWriter = new PrintWriter(outputStream);while (true) {// 1. 从控制台读取用户输入什么?System.out.print("> ");String request = scanner.next();// 2. 把请求发送给服务器printWriter.println(request);printWriter.flush();// 3. 从服务器读取响应String response = scannerNet.next();// 4. 把结果显示到界面上System.out.printf("req:%s,resp:%s\n", request, response);}}}public static void main(String[] args) throws IOException {TcpEchoClient client = new TcpEchoClient();client.start();}
}

代码出现 “卡死” 的情况 , 一定先不要慌 . 先冷静 , 然后仔细分析
线程阻塞了 , 就得知道具体是在哪里阻塞的 , 可以使用 jconsole 或者使用其他工具 , 定位到是哪个代码阻塞的
结合代码分析阻塞的原因 , 然后给出解决方案再次进行验证

2.2.4 其他 Bug

  1. 上述代码中,使用 clientSocket , 用完之后 , 是否要关闭呢 ?

当然要关闭
和上面的 ServerSocket 不同 , ServerSocket 的生命周期 , 是跟随整个程序的 .
clientSocket 的生命周期 , 只是当前连接 . 就应该在连接之后 , 把这里的 socket 进行关闭
如果不关的话 , 就会造成内存泄漏
ServerSocket,只有一个 , 但是 clientSocket 会有无数个 . 每个客户端的连接 , 都是一个 clientSocket
image.png

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) {// accept 干的活是拉客// 对于操作系统来说,建立一个 TCP 连接,是内核操作// accept 要干的就是等连接建立好了,把这个连接给拿到应用程序中// 如果当前连接还未建立,accpet就会阻塞等待// accept 相当于有人给你打电话了,你按下接听键.如果没人给你打电话,你就会阻塞等待直到有人给你打电话Socket clientSocket = serverSocket.accept();processConnect(clientSocket);}}// 通过这个方法,给当前连接上的客户端提供服务// 一个连接过来了,服务方式可能有两种// 1. 一个连接只进行一次数据交互(一个请求+一个响应)  短连接// 2. 一个连接进行多次数据交互(N个请求+N个响应)     长连接// 此处是长连接的版本public void processConnect(Socket clientSocket) throws IOException {// 在最开始获取一下 IP地址和端口号System.out.printf("[%s,%d] 建立连接\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());/*// 接收数据InputStream inputStream = clientSocket.getInputStream();// 发送数据OutputStream outputStream = clientSocket.getOutputStream();*/// 使用字节流是不方便的// 我们就可以用 Scanner PrintWriter 来对他们进行封装try (InputStream inputStream = clientSocket.getInputStream(); OutputStream outputStream = clientSocket.getOutputStream()) {// 接收数据Scanner scanner = new Scanner(inputStream);// 发送数据PrintWriter printWriter = new PrintWriter(outputStream);// 长连接的写法 -> 循环while (true) {// 判断还有没有数据// 读完了,后面没有数据,就相当于读到了 EOF// 这个操作就代表客户端断开连接,此时 hasNext 返回 falseif (!scanner.hasNext()) {// 连接断开// 打印一下哪个 IP+端口 连接断开了System.out.printf("[%s,%d] 断开连接\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());break;}// 1. 读取请求并解析String request = scanner.next();// 2. 根据请求计算响应String response = process(request);// 3. 把响应写回客户端printWriter.println(response);// 刷新一下缓冲区,避免数据真的没有发出去printWriter.flush();// 每次发送完毕,再来打印一下相关信息System.out.printf("[%s,%d] req:%s,resp=%s\n",clientSocket.getInetAddress().toString(),clientSocket.getPort(),request,response);}} finally {// 加在这里更稳妥clientSocket.close();}}// 回显服务器public String process(String request) {return request;}public static void main(String[] args) throws IOException {TcpEchoServer server = new TcpEchoServer(8000);server.start();}
}
  1. 上述代码 , 无法处理多个客户端

image.png
当我们断开第一个客户端
image.png
上述行为 , 当然是 Bug . 一个服务器 , 能处理多个客户端 , 这是天经地义的事情
无标题.png
image.png

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) {// accept 干的活是拉客// 对于操作系统来说,建立一个 TCP 连接,是内核操作// accept 要干的就是等连接建立好了,把这个连接给拿到应用程序中// 如果当前连接还未建立,accpet就会阻塞等待// accept 相当于有人给你打电话了,你按下接听键.如果没人给你打电话,你就会阻塞等待直到有人给你打电话Socket clientSocket = serverSocket.accept();// 我们在这里不再直接调用了,二是创建新的线程,让新的线程来调用// processConnect(clientSocket);Thread t = new Thread(() -> {try {processConnect(clientSocket);} catch (IOException e) {e.printStackTrace();}});t.start();}}// 通过这个方法,给当前连接上的客户端提供服务// 一个连接过来了,服务方式可能有两种// 1. 一个连接只进行一次数据交互(一个请求+一个响应)  短连接// 2. 一个连接进行多次数据交互(N个请求+N个响应)     长连接// 此处是长连接的版本public void processConnect(Socket clientSocket) throws IOException {// 在最开始获取一下 IP地址和端口号System.out.printf("[%s,%d] 建立连接\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());/*// 接收数据InputStream inputStream = clientSocket.getInputStream();// 发送数据OutputStream outputStream = clientSocket.getOutputStream();*/// 使用字节流是不方便的// 我们就可以用 Scanner PrintWriter 来对他们进行封装try (InputStream inputStream = clientSocket.getInputStream(); OutputStream outputStream = clientSocket.getOutputStream()) {// 接收数据Scanner scanner = new Scanner(inputStream);// 发送数据PrintWriter printWriter = new PrintWriter(outputStream);// 长连接的写法 -> 循环while (true) {// 判断还有没有数据// 读完了,后面没有数据,就相当于读到了 EOF// 这个操作就代表客户端断开连接,此时 hasNext 返回 falseif (!scanner.hasNext()) {// 连接断开// 打印一下哪个 IP+端口 连接断开了System.out.printf("[%s,%d] 断开连接\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());break;}// 1. 读取请求并解析String request = scanner.next();// 2. 根据请求计算响应String response = process(request);// 3. 把响应写回客户端printWriter.println(response);// 刷新一下缓冲区,避免数据真的没有发出去printWriter.flush();// 每次发送完毕,再来打印一下相关信息System.out.printf("[%s,%d] req:%s,resp=%s\n",clientSocket.getInetAddress().toString(),clientSocket.getPort(),request,response);}} finally {// 加在这里更稳妥clientSocket.close();}}// 回显服务器public String process(String request) {return request;}public static void main(String[] args) throws IOException {TcpEchoServer server = new TcpEchoServer(8000);server.start();}
}

2.2.5 解决缺陷 : 频繁创建销毁

我们目前的代码 : 来一个客户端 , 就要创建线程 . 客户端创建连接 , 就要销毁线程
如果客户端很多 , 就是 “频繁的创建和销毁线程”
我们就需要使用到** 线程池**

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;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 service = Executors.newCachedThreadPool();// 可自动扩容版本while (true) {// accept 干的活是拉客// 对于操作系统来说,建立一个 TCP 连接,是内核操作// accept 要干的就是等连接建立好了,把这个连接给拿到应用程序中// 如果当前连接还未建立,accpet就会阻塞等待// accept 相当于有人给你打电话了,你按下接听键.如果没人给你打电话,你就会阻塞等待直到有人给你打电话Socket clientSocket = serverSocket.accept();// [版本1]单线程版本,存在 Bug,无法处理多个客户端// processConnect(clientSocket);// [版本2]多线程版本,主线程负责拉客,新线程负责通信// 我们在这里不再直接调用了,二是创建新的线程,让新的线程来调用// Thread t = new Thread(() -> {//     try {//         processConnect(clientSocket);//     } catch (IOException e) {//         e.printStackTrace();//     }// });// t.start();// [版本3]使用线程池来解决上面的频繁创建销毁线程的问题service.submit(new Runnable() {@Overridepublic void run() {try {processConnect(clientSocket);} catch (IOException e) {e.printStackTrace();}}});}}// 通过这个方法,给当前连接上的客户端提供服务// 一个连接过来了,服务方式可能有两种// 1. 一个连接只进行一次数据交互(一个请求+一个响应)  短连接// 2. 一个连接进行多次数据交互(N个请求+N个响应)     长连接// 此处是长连接的版本public void processConnect(Socket clientSocket) throws IOException {// 在最开始获取一下 IP地址和端口号System.out.printf("[%s,%d] 建立连接\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());/*// 接收数据InputStream inputStream = clientSocket.getInputStream();// 发送数据OutputStream outputStream = clientSocket.getOutputStream();*/// 使用字节流是不方便的// 我们就可以用 Scanner PrintWriter 来对他们进行封装try (InputStream inputStream = clientSocket.getInputStream(); OutputStream outputStream = clientSocket.getOutputStream()) {// 接收数据Scanner scanner = new Scanner(inputStream);// 发送数据PrintWriter printWriter = new PrintWriter(outputStream);// 长连接的写法 -> 循环while (true) {// 判断还有没有数据// 读完了,后面没有数据,就相当于读到了 EOF// 这个操作就代表客户端断开连接,此时 hasNext 返回 falseif (!scanner.hasNext()) {// 连接断开// 打印一下哪个 IP+端口 连接断开了System.out.printf("[%s,%d] 断开连接\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());break;}// 1. 读取请求并解析String request = scanner.next();// 2. 根据请求计算响应String response = process(request);// 3. 把响应写回客户端printWriter.println(response);// 刷新一下缓冲区,避免数据真的没有发出去printWriter.flush();// 每次发送完毕,再来打印一下相关信息System.out.printf("[%s,%d] req:%s,resp=%s\n",clientSocket.getInetAddress().toString(),clientSocket.getPort(),request,response);}} finally {// 加在这里更稳妥clientSocket.close();}}// 回显服务器public String process(String request) {return request;}public static void main(String[] args) throws IOException {TcpEchoServer server = new TcpEchoServer(8000);server.start();}
}

但是目前还是存在一些缺陷 :
当前是使用了线程池 , 来解决问题
线程池解决的是频繁创建销毁 , 但是如果并发量太高了 , 就会导致池子里的线程特别特别多 .
线程特别多就会导致内存等资源/调度过度开销
再进一步的改进 , 就需要考虑能否减少线程的数目
当前这是一个线程对应一个客户端 , 那能否想办法 , 让一个线程对应多个客户端呢 ?
可以使用 “IO 多路复用” , 本质上就是一个线程处理多个 socket , 这是操作系统提供的机制
在 Java 中封装之后 , 对应的东西叫做 NIO

2.2.6 TCP 版本的翻译服务器

package network;import java.io.IOException;
import java.util.HashMap;public class TcpDictServer extends TcpEchoServer {private HashMap<String, String> dict = new HashMap<>();public TcpDictServer(int port) throws IOException {super(port);dict.put("cat", "小猫");dict.put("dog", "小狗");dict.put("ox", "小牛");}@Overridepublic String process(String request) {return dict.getOrDefault(request,"我也不知道啥意思");}public static void main(String[] args) throws IOException {TcpDictServer server = new TcpDictServer(8000);server.start();}
}

image.png

三 . 小结

3.1 Socket API

UDP
(1) DatagramSocket : 对应一个 socket 文件
(2) DatagramPacket : 对应一个 UDP 数据报

TCP
(1) ServerSocket : 只是在服务器这边使用 , 目的是拉客 . 其中有一个重要的方法 : accept , 他负责把操作系统内核建立好的连接拿到应用程序中 , 相当于别人给你打电话 , 你按下了接听键
(2) Socket : 客户端和服务器都能够使用 , 负责给客人提供服务
Socket 对象中能够获取到 InputStream (接收) 和 OutputStream (发送)

3.2 客户端/服务器 工作流程

image.png

3.3 TCP 的 Socket API 中 , 服务器使用多线程

TCP 服务器这里之所以使用了多线程 , 是因为在代码中处理的"长连接" (一个连接会处理 N 个请求和响应)
客户端连接建立好了之后 , 啥时候断开连接不确定 . 这一个连接里要处理多少个请求 , 也不确定
处理连接的循环 , 导致主循环无法继续执行到 accept 了.

大家不要误会成只要是 TCP 服务器就得使用多线程
之前的代码如果是处理短连接 , 每次客户端一个连接只处理一个请求 . 不使用多线程也能处理多个客户端.


那本篇文章到这里就讲解完毕了~
如果对您有帮助的话 , 请一键三连嗷~
在这里插入图片描述

相关文章:

Java Web 实战 15 - 计算机网络之网络编程套接字

文章目录一 . 网络编程中的基本概念1.1 网络编程1.2 客户端(client) / 服务器(server)1.3 请求(request) / 响应(response)1.4 客户端和服务器之间的交互数据1.4.1 一问一答1.4.2 多问一答1.4.3 一问多答1.4.4 多问多答二 . socket 套接字2.1 UDP 的 Socket API2.1.1 引子2.1.2…...

基于pdf2docx模块Python实现批量将PDF转Word文档(安装+完整代码教程)

PDF文件是一种常见的文档格式&#xff0c;但是在编辑和修改时不太方便&#xff0c;因为PDF本质上是一种静态的文档格式。因此&#xff0c;有时候我们需要将PDF文件转换成Word格式&#xff0c;以便更好地编辑和修改文档。在本篇文章中&#xff0c;我们将介绍如何使用Python实现P…...

3.21~3.22

识编程语言中的&#xff0c;局部变量&#xff0c;全局变量&#xff0c;以及变量生存周期&#xff0c;整形&#xff0c;浮点型数据的内存表示&#xff0c;od的内存窗口的使用 先看一个代码样例 #include<windows.h> #include<stdio.h>#pragma warning(disable:499…...

Chromium 改造实录:增加 MPEG TS 格式支持

在《选择最新 Chromium&#xff0c;支持 H264 / H265》一文中&#xff0c;记录了我通过升级 Chromium 版本解决了 H264 / H265 视频支持难题。然而难题接踵而至&#xff0c;这次的难题是 MPEG TS 流的支持。MPEG2-TS 传输流广泛应用于数字电视广播系统&#xff0c;所以是一个不…...

性能优化之-事件代理

js中的事件委托或是事件代理简单理解 事件委托也叫事件代理&#xff0c;“事件代理”即是把原本需要绑定在子元素的响应事件&#xff08;click、keydown…&#xff09;委托给父元素&#xff0c;让父元素担当事件监听的职务。事件代理的原理是DOM元素的事件冒泡。 概述&#x…...

MSDS 即化学品安全说明书

MSDS 即化学品安全说明书&#xff0c;亦可译为化学品安全技术说明书或化学品安全数据说明书&#xff0c;是化学品生产商和进口商用来阐明化学品的理化特性&#xff08;如PH值&#xff0c;闪点&#xff0c;易燃度&#xff0c;反应活性等&#xff09;以及对使用者的健康&#xff…...

真人手办没法实现网购?我有一个好办法!

记得以前在网上看到过一个冷笑话式的问答&#xff0c;问的是中国最早的手办是什么&#xff0c;有网友回答是秦始皇兵马俑&#xff0c;这个抖机灵式的回答简直妙得让人会心一笑。 你接触过手办吗&#xff1f; 提到手办&#xff0c;大家第一时间想到的&#xff0c;肯定都会是各…...

2019湖南省大学生程序设计竞赛题解(D)

D-Modulo Nine 很妙的类似区间dp&#xff0c; 我自己是想不到&#xff0c;本题解题思路来自学长的博客&#xff1a; 长沙橘子猫 题意 有一个长度为 nnn 的序列&#xff0c;你可以给每个位置填 0∼90\sim90∼9 的一个数&#xff0c;有 mmm 个限制&#xff0c;每个限制 [li,ri…...

【开发】中间件——RocketMQ

分布式消息系统 RocketMQ概念&#xff0c;用途&#xff0c;特性安装RocketMQ掌握RocketMQ的api使用对producer、consumer进行详解了解RocketMQ的存储特点 简介及相关概念JavaAPISpringBoot整合RocketMQ消息的顺序收发消息系统的事务、存储、重试策略消息系统的集群 RocketMQ R…...

36 UnitTest框架 - 参数化

目录 一、参数化环境准备 1、方式一&#xff1a;在终端&#xff08;cmd&#xff09;安装parameterized 2、方式二&#xff1a;在Pycharm中安装parameterized 二、参数化 1、什么事参数化&#xff1f; 2、参数化引入案例 &#xff08;1&#xff09;需求 &#xff08;2&a…...

Qt源码阅读(四) 事件循环

事件系统 文章为本人理解&#xff0c;如有理解不到位之处&#xff0c;烦请各位指正。 文章目录事件系统什么是事件循环&#xff1f;事件是如何产生的&#xff1f;sendEventpostEvent事件是如何处理的&#xff1f;事件循环是怎么遍历的&#xff1f;事件过滤器event夹带私货时间Q…...

银行数字化转型导师坚鹏:银行数字化领导力提升之道

银行数字化领导力提升之道 ——融合中西智慧&#xff0c;践行知行合一思想&#xff0c;实现知行果合一 课程背景&#xff1a; 很多银行存在以下问题&#xff1a;不知道如何领导数字员工&#xff1f;不清楚银行数字化领导力模型的内涵&#xff1f;不知道如何开展银行数字化…...

Vue2 -- 自定义单选内容的单选框组件

自定义单选内容的单选框组件 之前做的一个项目&#xff0c;在项目中有一个关于人员权限分配的功能&#xff0c;给人员指定各个模块的权限信息&#xff0c;分为 write 可写权限read 可读权限none 没有权限 项目要求画面中只显示 W R 两个按钮控制指定权限信息&#xff0c;都不…...

让PyTorch训练速度更快,你需要掌握这17种方法

掌握这 17 种方法&#xff0c;用最省力的方式&#xff0c;加速你的 Pytorch 深度学习训练。近日&#xff0c;Reddit 上一个帖子热度爆表。主题内容是关于怎样加速 PyTorch 训练。原文作者是来自苏黎世联邦理工学院的计算机科学硕士生 LORENZ KUHN&#xff0c;文章向我们介绍了在…...

LeetCode-309. 最佳买卖股票时机含冷冻期

目录题目思路动态规划题目来源 309. 最佳买卖股票时机含冷冻期 题目思路 每天最多只可能有三种状态中的一种 0表示当前处于买入状态(持有股票) 1表示当前处于卖出状态(不持有股票) 2表示当前处于冷冻状态 设dp[i][j]表示i - 1天状态为j时所拥有的最大现金 dp[i][0] Math.ma…...

AUTOSAR知识点Com(七):CANSM初认知

目录 1、概述 2、CanSM主要做什么 2.1、CAN控制器状态管理 2.2、CAN收发器状态管理 2.3、Busoff检测 1、概述 CANSM&#xff08;Controller Area Network State Manager&#xff09;是AUTOSAR&#xff08;Automotive Open System Architecture&#xff09;标准中的一个模块…...

递归:斐波那契数列、递归实现指数型枚举、递归实现排列型枚举

递归&#xff1a;O(2^n) 调用自己 例题及代码模板&#xff1a; 斐波那契数列 输入一个整数 n &#xff0c;求斐波那契数列的第 n 项。 假定从 0 开始&#xff0c;第 0 项为 0。 数据范围 0≤n≤39 样例 输入整数 n5 返回 5 #include <iostream> #include <cstring&g…...

oracle模糊查询时字段内容包含下划线的解决办法

最近项目中遇到一个关于模糊查询问题。表tabA中的字段name的值有下划线的情况&#xff0c;在模糊查询时发现查询的记录不对。 表的结构 表名&#xff1a;tabA id name sex 1 test_601 1 2 test_602 2 3 test16 1 4 t…...

C++:explicit关键字

C中的explicit关键字只能用于修饰只有一个参数的类构造函数&#xff0c;它的作用是表明该构造函数是显示的&#xff0c;而非隐式的&#xff0c;跟它相对应的另一个关键字是implicit&#xff0c;意思是隐藏的&#xff0c;类构造函数默认情况下即声明为implicit(隐式)。那么显示声…...

【C5】bmc wtd,post

文章目录1.bmc_wtd_cpld&#xff1a;syscpld.c中wd_en和wd_kick节点对应寄存器&#xff0c;crontab&#xff0c;FUNCNAME2.AST芯片WDT切换主备&#xff1a;BMC用WDT2作为主备切换的控制器2.1 AC后读取&#xff1a;bmc处于主primary flash&#xff08;设完后&#xff1a;实际主&…...

200.Spark(七):SparkSQL项目实战

一、启动环境 需要启动mysql,hadoop,hive,spark。并且能让spark连接上hive(上一章有讲) #启动mysql,并登录,密码123456 sudo systemctl start mysqld mysql -uroot -p#启动hive cd /opt/module/ myhadoop.sh start#查看启动情况 jpsall#启动hive cd /opt/module/hive/…...

区块链系统:挖矿原理

在比特币的P2P网络中&#xff0c;有一类节点&#xff0c;它们时刻不停地进行计算&#xff0c;试图把新的交易打包成新的区块并附加到区块链上&#xff0c;这类节点就是矿工。因为每打包一个新的区块&#xff0c;打包该区块的矿工就可以获得一笔比特币作为奖励。所以&#xff0c…...

【博弈】【清华冬令营2018模拟】取石子

写完敢说全网没有这么详细的题解了。 注意&#xff1a;题解长是为了方便理解&#xff0c;所以读起来速度应该很快。 题目描述 有 nnn 堆石子&#xff0c;第 iii 堆有 xix_ixi​ 个。 AliceAliceAlice 和 BobBobBob 轮流去石子&#xff08;先后手未定&#xff09;&#xff0c; …...

嵌入式:BSP的理解

BSP概念总结BSP定义BSP的特点BSP的主要工作BSP在嵌入式系统和Windowsx系统中的不同BSP和PC机主板上的BIOS区别BSP与 HAL关系嵌入式计算机系统主要由 硬件层&#xff0c;中间层&#xff0c;系统软件层和应用软件层四层组成。硬件层&#xff1a;包含CPU&#xff0c;存储器(SDRAM&…...

Linux主机Tcpdump使用-centos实例

1、安装前系统信息 ifconfig查看系统网络接口情况。这里可以看到3个interface&#xff0c;ens160是正常使用的网口&#xff0c;lo是主机的loopback地址127.0.0.1。另外&#xff0c;由于centos安装在虚拟主机上&#xff0c;virbr0是KVM默认创建的一个Bridge,其作用是为连接其上的…...

线性DP——AcWing 898. 数字三角形、AcWing 895. 最长上升子序列

AcWing 898. 数字三角形 1.题目 898. 数字三角形 2.思路 DP问题首先考虑状态转移方程&#xff0c;定义一个集合f ( i , j) &#xff0c;表示从第一个数字&#xff08;1,1&#xff09;走到第 i行&#xff0c;第 j列&#xff08;i , j&#xff09;的所有方案的集合&#xff0c…...

SpringMVC

SpringMVC配置 引入Maven依赖 &#xff08;springmvc&#xff09;web.xml配置DispatcherServlet配置 applicationContext 的 MVC 标记开发Controller控制器 几点注意事项&#xff1a; 在web.xml中 配置<load-on-startup> 0 </load-on-startup> 会自动创建Spring…...

C++模板基础(二)

函数模板&#xff08;二&#xff09; ● 模板实参的类型推导 – 如果函数模板在实例化时没有显式指定模板实参&#xff0c;那么系统会尝试进行推导 template<typename T> void fun(T input, T input2) {std::cout << input << \t << input2 << …...

什么是linux内核态、用户态?

目录标题为什么需要区分内核空间与用户空间内核态与用户态如何从用户空间进入内核空间整体结构为什么需要区分内核空间与用户空间 在 CPU 的所有指令中&#xff0c;有些指令是非常危险的&#xff0c;如果错用&#xff0c;将导致系统崩溃&#xff0c;比如清内存、设置时钟等。如…...

day8—选择题

文章目录1.Test.main() 函数执行后的输出是&#xff08;D&#xff09;2. JUnit主要用来完成什么&#xff08;D&#xff09;3.下列选项中关于Java中super关键字的说法正确的是&#xff08;A&#xff09;1.Test.main() 函数执行后的输出是&#xff08;D&#xff09; public clas…...