【网络编程】网络编程中的基本概念及Java实现UDP、TCP客户端服务器程序(万字博文)

 系列文章目录

【网络通信基础】网络中的常见基本概念

【网络编程】网络编程中的基本概念及Java实现UDP、TCP客户端服务器程序(万字博文)

【网络原理】UDP协议的报文结构 及 校验和字段的错误检测机制(CRC算法、MD5算法)


文章目录

一、什么是网络编程?

二、网络编程中的基本概念

1. 客户端和服务器

2. 请求和响应

三、Socket套接字

UDP数据报套接字编程

1. DatagramSocket

2. DatagramPacket

3. UDP回显客户端服务器程序

4. UDP字典客户端服务器程序

TCP流套接字编程

1. ServerSocket

2. Socket

3. TCP回显客户端服务器程序

4. 服务器引入多线程

5. 服务器引入线程池

6. TCP字典客户端服务器程序 


一、什么是网络编程?

网络编程,指网络上的主机,通过不同的进程,以编程的方式实现网络通信(或称网络数据传输)

即便是同一个主机,只要是不同进程,基于网络来传输数据,也属于网络编程。但是,我们的目的是提供网络上的不同主机,基于网络来传输数据资源。

网络编程,本质上就是学习“传输层”给“应用层”提供的API,通过代码的形式,把数据交给传输层,进一步的通过层层封装,就可以把数据通过网卡发送出去了。

二、网络编程中的基本概念 

1. 客户端和服务器

客户端:主动发起通信的一方,称为客户端.

服务器:被动接受的一方,称为服务器,可以提供对外服务.

同一个程序在不同场景中,可能是客户端也可能是服务器(服务器可能还需要主动向别的服务器发起通信,此时的服务器相对于被发起通信的服务器来说,就是客户端).

2. 请求和响应

请求(request):客户端给服务器发送数据。

响应(response):服务器给客户端返回数据。

一般来说,获取一个网络资源,涉及到两次网络数据传输:

  • 第一次:请求数据的发送.
  • 第二次:响应数据的发送.

就比如在快餐店点一份炒饭:

先要发起请求:点一份炒饭;再有快餐店提供的对于响应:提供一份炒饭。

三、Socket套接字

Socket套接字,是由系统提供的用于网络编程的技术,是基于TCP/IP协议的网络通信的基本操作单元。基于Socket套接字的网络程序开发就是网络编程。

前面说过,要想进行网络编程,需要使用的系统API,本质上是由传输层提供的。

传输层涉及到的主要协议有两个:

  1. 流套接字:TCP(传输控制协议)
  2. 数据报套接字:UDP(用户数据报协议)

TCP的特点:                                                                 

  • 有连接(类似于打电话,需要接通才能通信)             
  • 可靠传输(尽可能完成数据传输,起码可以知道当前这个数据对方是否接收到了)                       
  • 面向字节流(此处字节流和文件中的字节流完全一致,网络中传输数据的基本单位就是字节)
  • 全双工(一个信道,可以双向通信)                            

UDP的特点:

  • 无连接(类似于发微信/短信,直接发送过去)            
  • 不可靠传输(没有确认、重传机制;如果因为网络故障等原因,数据无法发到对方,UDP协议层也不会给应用层返回任何错误信息)
  • 面向数据报(每次传输的基本单位是一个数据报(由一系列的字节构成的)特定的结构)
  • 全双工(一个信道,可以双向通信)     

UDP数据报套接字编程

UDP socket API的使用

1. DatagramSocket

DatagramSocket 是 UDP Socket(套接字),用于发送和接收UDP数据报

构造方法:

946ad3b6a5d143ed8c34945a42104d91.png

重要方法:415f951ea05048a8a52ab6a091195a9c.png

2. DatagramPacket

DatagramPacket 是 UDP Socket(套接字)发送和接收的数据报(每次发送接收数据的基本单位)

构造方法:

2527d88cdd46485da843d63d5d301f5f.png

3. UDP回显客户端服务器程序

通过这个程序,了解 socket api 的使用,和典型的客户端服务器基本工作流程。

对于服务器,需要指定端口号来创建 socket (类似于饭店,需要指定具体位置),主要流程如下:

  1. 接收客户端请求,并解析
  2. 根据请求,计算出响应(回显服务器,则是直接将请求的数据返回)
  3. 将响应写回给客户端

注意事项详见代码注释: 

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;//Udp回显服务器
public class UdpEchoServer {//Udp套接字private DatagramSocket socket;public UdpEchoServer(int port) throws SocketException {socket = new DatagramSocket(port); //服务器:指定端口号创建}public void start() throws IOException {System.out.println("服务器启动");while (true) {//1.接收客户端的请求,并解析DatagramPacket requestServer = new DatagramPacket(new byte[4096], 4096);socket.receive(requestServer);//2.根据请求,计算出响应String request = new String(requestServer.getData(), 0, requestServer.getLength());String response = process(request);//3.将响应写回给客户端(需要指定发送到的IP地址及端口号)DatagramPacket responseServer = new DatagramPacket(response.getBytes(), response.getBytes().length, requestServer.getSocketAddress());socket.send(responseServer);//打印日志System.out.printf("[%s:%d] request:%s response:%s\n",responseServer.getAddress(), responseServer.getPort(), request, response);}}//根据请求计算响应(由于是回显程序,直接返回请求的内容)public String process(String request) {return request;}public static void main(String[] args) throws IOException {UdpEchoServer udpEchoServer = new UdpEchoServer(9090);udpEchoServer.start();}
}

对于客户端,服务器的端口号可以由系统随机分配,但需要知道服务器的IP地址及端口号(去饭店吃饭,需要知道饭店的地址及具体是哪个门店),主要流程如下:

  1. 客户端读取用户请求
  2. 构造请求的数据报,并发送到服务器(此时就需要指定服务器的IP地址及端口号)
  3. 读取服务器的响应,并解析出响应的内容
  4. 输出服务器的响应
import java.io.IOException;
import java.net.*;
import java.util.Scanner;//Udp回显客户端
public class UdpEchoClient {private DatagramSocket socket;private String address;private int port;//客户端需要知道服务器的IP地址及端口号public UdpEchoClient(String address, int port) throws SocketException {this.address = address;this.port = port;socket = new DatagramSocket(); //服务器:随机端口号创建}public void start() throws IOException {System.out.println("客户端启动");Scanner in = new Scanner(System.in);while (true) {System.out.print("-> ");if (!in.hasNext()) {break;}//1.控制台读取请求内容String request = in.next();//2.构造请求的数据报,并发送到服务器(需要指定目的IP地址和目的端口号发送请求)DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length, InetAddress.getByName(address), port);socket.send(requestPacket);//3.读取服务器的响应,并解析出响应的内容DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);socket.receive(responsePacket);String response = new String(responsePacket.getData(), 0, responsePacket.getLength());//4.将响应内容输出到控制台System.out.println(response);}}public static void main(String[] args) throws IOException {UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1", 9090);udpEchoClient.start();}
}

先运行服务器,再运行客户端,看程序的执行效果:

0d8532796a264e6aad79343584899933.png

10354716e58e4bbfac7063e6e9394f8b.png

  • 可以看到,服务器端能够正确接收到请求并作出响应,并打印出日志(客户端IP,客户端端口号,请求内容,响应内容)
  • 客户端也能够正确发送请求,并正确解析服务器端返回的响应

这个程序并不能直接做到“跨主机通信”,因为这台主机可能不能直接访问到另一台主机(NAT机制)。但是可以通过以下手段实现“跨主机通信”:

  1. 将服务器端程序打成 jar 包
  2. 把 jar 包传到云服务器上,并运行

经过这样的操作,其他主机通过运行上述的客户端程序,就能够发起通信了。

4. UDP字典客户端服务器程序

基于上述回显服务器,还可以实现出一些其他带有一点业务逻辑的服务器。

改进成一个“字典服务器”,英译汉的效果。请求是一个英文单词,响应返回对应的中文翻译。

主要逻辑其实和回显服务器基本一致,唯一的区别就在于,服务器端将客户端请求的数据,计算成响应的方式不一致。回显服务器是直接返回客户端请求的数据,这里的字典服务器则是英译汉效果。

而上述代码中,这个根据请求数据计算响应数据的操作,是通过process方法实现的。因此只需要让这个字典服务器继承回显服务器,并重写process方法即可。这里英译汉的业务逻辑通过打表的方式实现。

import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;//Udp字典服务器
public class UdpDictServer extends UdpEchoServer {Map<String, String> map;public UdpDictServer(int port) throws SocketException {super(port);map = new HashMap<>();map.put("cat", "小猫");map.put("dog", "小狗");map.put("animal", "动物");}//通过重写 计算响应的process方法,达成 英->汉 的效果@Overridepublic String process(String request) {return map.getOrDefault(request, "找不到该单词");}public static void main(String[] args) throws IOException {UdpDictServer udpDictServer = new UdpDictServer(9090);udpDictServer.start();}
}

先运行字典服务器,再运行回显客户端(这里客户端是通用的,因为回显客户端只进行发送请求和接收响应并解析的操作),看程序的执行效果:

be0582b961e94458a92de7b2c0ae562a.png

c9a2c7a68fcc4e5eac4f2c517306ddc7.png

  • 同样,服务器端能够正确接收到请求、解析请求,并计算出响应、写回给客户端,并打印出日志(客户端IP,客户端端口号,请求内容,响应内容)
  • 客户端也能够正确发送请求,并正确解析服务器端返回的响应

TCP流套接字编程

1. ServerSocket

ServerSocket 类是创建TCP服务器端Socket的API. (只能给服务器端使用)

构造方法:

51f59e0815844130ba0546cf0153be6a.png

重要方法:

1d2012f5553d408f9cb2a9f99f842cb5.png

678599abcb294d76af46efa995a4583d.png

2. Socket

Socket 类用于创建客户端 Socket,或服务器端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket. (服务器端和客户端都能使用)

构造方法:

525a8199537d4eaa9b81c1b4fe951bc3.png

重要方法:

d6b7bef0b825494291b1ea8d824ec284.png

3. TCP回显客户端服务器程序

使用TCP协议实现回显客户端服务器程序。与UDP协议实现的最大区别是,TCP是有连接的,和打电话一样,需要一方(客户端)拨号,一方(服务器)接通,因此TCP协议首要操作就是等待客户端连接。

和UDP回显服务器一样,对于这里的服务器,同样需要指定端口号创建TCP服务器端Socket,即ServerSocket。

  1. 服务器启动后,就需要监听当前绑定端口(accept方法),等待客户端连接。
  2. 当成功建立连接后,会返回一个Socket对象。这个对象保存了对端信息,即客户端信息,可以用来接收和发送数据(TCP是面向字节流的,通过这个Socket对象获取对应输入流和输出流,通过输入输出流进行对 socket 的读写,达成接收和发送数据的功能)。

后续流程和UCP回显服务器一致。此处由于每有一个客户端连接,就会有一个clientSocket,这里消耗的Socket会越来越多,因此每当一个客户端连接结束,就需要释放这个clientSocket。

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;//TCP回显服务器
public class TcpEchoServer {private ServerSocket serverSocket;public TcpEchoServer(int port) throws IOException {//指定服务器端口号,创建一个serverSocketserverSocket = new ServerSocket(port);}public void start() throws IOException {System.out.println("服务器启动!");while (true) {//监听当前绑定的端口,等待客户端连接 连接后,返回一个socket,里面保存客户端(对端)信息Socket clientSocket = serverSocket.accept();processConnection(clientSocket);}}private void processConnection(Socket clientSocket) throws IOException {System.out.printf("[%s:%d] 客户端上线\n", clientSocket.getInetAddress(), clientSocket.getPort());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(), clientSocket.getPort());break;}//这里的next()需要遇到\n才停止,因此需要对端写入的时候,要同时写入\n换行符String request = scanner.next();//2.解析请求的数据,并计算出响应String response = process(request);//3.将响应写回到客户端//outputStream.write(response.getBytes(), 0, response.getBytes().length);PrintWriter writer = new PrintWriter(outputStream);writer.println(response);writer.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 {clientSocket.close();}}//回显服务器,直接返回原数据public String process(String request) {return request;}public static void main(String[] args) throws IOException {TcpEchoServer tcpEchoServer = new TcpEchoServer(9090);tcpEchoServer.start();}
}

对于客户端,需要指定服务器的IP和端口号建立连接。使用 Socket(String host, int port) 创建Socket的时候,就开始发起与对应服务器建立连接的请求了。

主要流程和UDP回显客户端程序的流程也基本一致,只需要注意请求和响应数据的方式是不同的,是通过操作输入输出流完成的即可。

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket;
import java.util.Scanner;//TCP回显客户端
public class TcpEchoClient {private Socket clientSocket;//需要指定服务器的IP和端口号public TcpEchoClient(String serverAddress, int serverPort) throws IOException {//与对应客户端建立连接clientSocket = new Socket(InetAddress.getByName(serverAddress), serverPort);}public void start() {System.out.println("客户端启动!");try (Scanner scannerConsole = new Scanner(System.in);InputStream inputStream = clientSocket.getInputStream();OutputStream outputStream = clientSocket.getOutputStream()) {while (true) {//1.用户从控制台输入数据System.out.print("-> ");String request = scannerConsole.next();//2.将该数据作为请求,发送给服务器//outputStream.write(request.getBytes(), 0, request.getBytes().length);//outputStream.write('\n');PrintWriter writer = new PrintWriter(outputStream);writer.println(request);writer.flush(); //刷新缓冲区,确保数据发送出去//3.读取服务器的响应,并解析响应的内容Scanner scannerNetwork = new Scanner(inputStream);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();}
}

先运行服务器,再运行客户端,看执行效果:

7bf0bb63b325493d9637dd510d2ed02f.pngd0b3e4f4313f4c9bad1c6796753b6b4a.png

可以看到,服务器和客户端都能满足我们的需求,但这里其实还存在一个问题。

当我们开启多个客户端想要进行连接通信时,只有第一个连接到的客户端才能正确通信,其他的客户端是没有反应的。要想某个客户端能正常通信,只有当其他客户端都下线(结束程序),这个客户端才能接收到响应数据。

cd371bbc05404528bb4310c3c9bdef70.png

fd72a4e6b15c4c31b542bb63d1bbc909.png

可以看到,此处的这个客户端并没有正确通信,当另一个客户端下线之后,该客户端此前发送的数据又正常请求并响应了。

分析过程:

  • 第一个客户端连上服务器之后,服务器就会从accept这里返回(解除阻塞),然后进入到processConnection方法中.
  • 接下来服务器就会在processConnection循环处理客户端的请求,只有当客户端退出之后,连接结束,才会退出循环.
  • 而服务器在循环处理客户端请求的时候,第二个客户端发起连接请求,而服务器这里并不能执行到accept。因此并不能成功连接,只有当客户端退出,才会执行回到accept进行连接.

第二个客户端之前发的请求为什么能被立即处理?

  • 当前TCP在内核中,每个 socket 都是有缓冲区的。客户端发送的数据通过客户端代码,已经写入到服务器的缓冲区了,这里数据确实发送了,只不过数据在服务器的接收缓冲区中。
  • 一旦第一个客户端退出,回到第一层循环,执行accept连接操作,后续processConnection方法里的 next 就能把之前缓冲区的内容给读出来。

解决上述问题的核心思路就是使用多线程:

  • 单个线程,无法既能给客户端提供服务,又能去快速执行到第二次 accept 方法进行连接。
  • 通过引入多线程,让主线程只负责执行 accept。每次有一个客户端连接上来,就分配一个新的线程,由新的线程负责给客户端提供服务。

由于这里不涉及多个线程修改共享变量,因此没有线程安全问题,我们只需要改动 start 方法即可。

4. 服务器引入多线程

    //多线程public void start() throws IOException {System.out.println("服务器启动!");while (true) {//监听当前绑定的端口,等待客户端连接 连接后,返回一个socket,里面保存客户端(对端)信息Socket clientSocket = serverSocket.accept();Thread t = new Thread(() -> {try {processConnection(clientSocket);} catch (IOException e) {throw new RuntimeException(e);}});t.start();}}

ca089af3c6e24b239820ac8395aeca8a.png

通过引入多线程,这里的服务器就能支持多个客户端同时与其通信了。

上述问题,不是TCP引起的,而是代码两次循环嵌套引起的,UDP服务器,就是只有一层循环,因此不会有这个问题。

而这个多线程版本同样还有一些问题: 

  • 每有一个客户端连接,就会创建一个新的线程,每当这个客户端结束,就要销毁这个线程。
  • 如果客户端比较多,并且频繁连接、关闭,就会使服务器频繁创建和销毁线程。

前面讲过,线程池解决的就是线程频繁创建和销毁的问题,因此,这里的优化方案就是使用线程池。

5. 服务器引入线程池

    public void start() throws IOException {System.out.println("服务器启动!");ExecutorService threadPool = Executors.newCachedThreadPool();while (true) {//监听当前绑定的端口,等待客户端连接 连接后,返回一个socket,里面保存客户端(对端)信息Socket clientSocket = serverSocket.accept();threadPool.submit(new Runnable() {@Overridepublic void run() {try {processConnection(clientSocket);} catch (IOException e) {throw new RuntimeException(e);}}});}}

线程开销问题解决了。但是,如果当前的场景使线程频繁创建,但是不销毁呢?

  • 这种情况下,如果继续使用多线程/线程池,就会导致当前服务器积累大量的线程,此时,对于服务器的负担是非常重的。

要解决这个新问题,还可以引入其他的方案:

  1. 协程:轻量级线程,本质上还是一个线程,用户态可以通过手动调度的方式让这一个线程“并发”执行多个任务。
  2. IO 多路复用:系统内核级别的机制,本质上是让一个线程同时去负责处理多个socket。

6. TCP字典客户端服务器程序 

同UDP字典客户端服务器程序:

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;//TCP字典服务器
public class TcpDictServer extends TcpEchoServer {Map<String, String> map;public TcpDictServer(int port) throws IOException {super(port);map = new HashMap<>();map.put("cat", "小猫");map.put("dog", "小狗");map.put("animal", "动物");}@Overridepublic String process(String request) {return map.getOrDefault(request, "未找到该单词");}public static void main(String[] args) throws IOException {TcpDictServer tcpDictServer = new TcpDictServer(9090);tcpDictServer.start();}
}

4ab42f44482d4a8cb7bb4e6342681ffe.png

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

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

相关文章

密码学 | 承诺:绑定性 + 隐藏性

&#x1f951;原文&#xff1a;承诺方案&#xff08;Commitment&#xff09;学习笔记 &#x1f951;写在前面&#xff1a; 本文属搬运博客&#xff0c;自己留存学习。本文只会讲承诺的两个安全属性&#xff0c;不会再讲解承诺的定义。 正文 承诺方案需要满足两个安全属性&…

C++笔记:C++中的重载

重载的概念 一.函数重载 代码演示例子&#xff1a; #include<iostream> using namespace std;//函数名相同&#xff0c;在是每个函数的参数不相同 void output(int x) {printf("output int : %d\n", x);return ; }void output(long long x) {printf("outp…

手撕netty源码(一)- NioEventLoopGroup

文章目录 前言一、NIO 与 netty二、NioEventLoopGroup 对象的创建过程2.1 创建流程图2.2 EventExecutorChooser 的创建 前言 processOn文档跳转 本文是手撕netty源码系列的开篇文章&#xff0c;会先介绍一下netty对NIO关键代码的封装位置&#xff0c;主要介绍 NioEventLoopGro…

浅谈叉车车载电脑的市场现状

叉车的起源 叉车源于美国&#xff0c;兴于日本&#xff0c;虽然中国起步较晚&#xff0c;但是近些年来发展迅速。叉车又称叉式装载车&#xff0c;是对于成件托盘类货物进行装卸、堆垛和短距离运输&#xff0c;实现重物搬运作业的轮式工业车辆。 叉车的分类 叉车分为以上六大类…

OpenWrt上的docker容器无法访问外网解决

容器里能ping通OpenWrt的管理地址和wan口地址&#xff0c;但ping外网别的ip或域名就无法访问 简单修改设置就可以&#xff1a; Luci>网络>防火墙>转发&#xff1a;接受 ->保存应用

【Web】DASCTF X GFCTF 2024|四月开启第一局 题解(全)

目录 EasySignin cool_index SuiteCRM web1234 法一、条件竞争(没成功) 法二、session反序列化 EasySignin 先随便注册个账号登录&#xff0c;然后拿bp抓包改密码(username改成admin) 然后admin / 1234567登录 康好康的图片功能可以打SSRF&#xff0c;不能直接读本地文…

Hive安装部署

Apache Hive是一个基于Hadoop分布式文件系统、使用MapReduce算法执行大规模离线数据分析的数据仓库&#xff0c;本文主要描述Hive的安装部署。 如上所示&#xff0c;Hive总体应用架构图&#xff0c;其中&#xff0c;Hive基于HBase或者使用Hadoop分布式文件系统执行MapReduce的分…

Zephyr sensor子系统学习

一、背景 2023年7月份nRF Connect SDK 2.4.0最新版本&#xff0c;使用的Zephyr V3.3版本。从Zephyr 3.5版本在子系统中加入了sensing子系统。 现在最新的nRF Connect SDK 2.6.0 release支持v3.5.99-ncs1&#xff0c;已经支持sensing子系统 nRF52840现在官方支持两个传感器de…

yudao-cloud微服务系统系统模块+后台管理系统成功运行

&#x1f339;作者主页&#xff1a;青花锁 &#x1f339;简介&#xff1a;Java领域优质创作者&#x1f3c6;、Java微服务架构公号作者&#x1f604; &#x1f339;简历模板、学习资料、面试题库、技术互助 &#x1f339;文末获取联系方式 &#x1f4dd; 系列文章目录 第一章 芋…

python基础知识—while和for循环(三)

&#x1f3ac; 秋野酱&#xff1a;《个人主页》 &#x1f525; 个人专栏:《Java专栏》《Python专栏》 ⛺️心若有所向往,何惧道阻且长 文章目录 一&#xff1a;while循环1.1程序的三种执行流程1.2while循环1.3循环变量和死循环 二&#xff1a;for循环2.1for循环2.2range 一&…

OSI七层模型、TCP/IP五层模型理解(个人解读,如何理解网络模型)

OSI七层模型 七层模型&#xff0c;亦称OSI&#xff08;Open System Interconnection&#xff09;。参考模型是国际标准化组织&#xff08;ISO&#xff09;制定的一个用于计算机或通信系统间互联的标准体系&#xff0c;一般称为OSI参考模型或七层模型。它是一个七层的、抽象的模…

UVa12313 A Tiny Raytracer

UVa12313 A Tiny Raytracer 题目链接题意分析AC 代码 题目链接 UVA - 12313 A Tiny Raytracer 题意 给出 《训练指南》题意翻译 本题的任务是实现一个小型光线追踪渲染器。场景由若干三角形网格&#xff08;triangle mesh&#xff09;组成&#xff0c;有且仅有一个点光源&…

ESP32开发

目录 1、简介 1.1 种类 1.2 特点 1.3 管脚功能 1.4 接线方式 1.5 工作模式 2、基础AT指令介绍 2.1 AT指令类型 2.2 基础指令及其描述 2.3 使用AT指令需要注意的事 3、AT指令分类和提示信息 3.1 选择是否保存到Flash的区别 3.2 提示信息 3.3 其他会保存到Flash的A…

界面组件DevExpress Blazor UI v23.2 - 支持.NET 8、全新的项目模版

DevExpress Blazor UI组件使用了C#为Blazor Server和Blazor WebAssembly创建高影响力的用户体验&#xff0c;这个UI自建库提供了一套全面的原生Blazor UI组件&#xff08;包括Pivot Grid、调度程序、图表、数据编辑器和报表等&#xff09;。 DevExpress Blazor控件目前已经升级…

RISC-V CVA6 在 Linux 下相关环境下载与安装

RISC-V CVA6 在 Linux 下相关环境下载与安装 所需环境与源码下载 CVA6 源码下载 首先&#xff0c;我们可以直接从 GitHub 一次性拉取所有源码&#xff1a; git clone --recursive https://github.com/openhwgroup/cva6.git如果这里遇到网络问题&#xff0c;拉取失败&#x…

阿里云企业邮箱API的使用方法?调用限制?

阿里云企业邮箱API性能如何优化&#xff1f;配置邮箱API的优势&#xff1f; 阿里云企业邮箱以其稳定、高效和安全的特点&#xff0c;受到了众多企业的青睐。而阿里云企业邮箱API的开放&#xff0c;更是为企业提供了更加灵活、便捷的管理和操作方式。下面&#xff0c;我AokSend…

Linux的学习之路:22、线程(2)

摘要 本章继续讲一下线程的东西 目录 摘要 一、抢票 二、加锁保护 三、死锁 1、死锁四个必要条件 2、避免死锁 四、同步 1、常见的线程安全的情况 2、常见不可重入的情况 3、常见可重入的情况 4、可重入与线程安全联系 5、可重入与线程安全区别 一、抢票 这里回…

启动 UE4编辑器报 加载 Plugin 失败

启动 UE4编辑器报 加载 Plugin 失败&#xff0c;报如下错误&#xff1a; Plugin ‘SteamVR’ failer to load because module ‘SteamVR’ could not be found. Please ensure the plugin is properly installed, otherwise consider disabling the plugin for this project. …

新时代凌迟:考研

我不喜欢上班&#xff0c;但我很欣赏老板的品味&#xff0c;因为咱们公司竟然还在订阅报纸&#xff0c;而且只有一份&#xff0c;《中国青年报》。 这份报纸我最喜欢看的是“冰点周刊”专栏&#xff0c;因为这个栏目能让读者相信&#xff1a;报纸远远可以超越一天的生命。 昨天…

前端框架编译器之模板编译

编译原理概述 编译原理&#xff1a;是计算机科学的一个分支&#xff0c;研究如何将 高级程序语言 转换为 计算机可执行的目标代码 的技术和理论。 高级程序语言&#xff1a;Python、Java、JavaScript、TypeScript、C、C、Go 等。计算机可执行的目标代码&#xff1a;机器码、汇…