Java网络编程,多线程,IO流综合项目一一ChatBoxes

Java网络编程,多线程,IO流综合小项目一一ChatBoxes

作者:blue

时间:2025.3.7

文章目录

  • Java网络编程,多线程,IO流综合小项目一一ChatBoxes
    • 1.项目介绍
    • 2.项目源码剖析
      • 2.1客户端源码
      • 2.2客户端Sender线程Runnable源码
      • 2.3客户端Receiver线程Runnable源码
      • 2.4服务端源码
      • 2.5服务端Runnable源码
    • 3.项目心得

1.项目介绍

项目目标:实现一个C/S架构,基于TCP协议的控制台版的聊天室,带有注册,登录功能,能实现在局域网内,多个客户端,在一个聊天室中聊天

项目需求

客户端:拥有登录,注册,聊天功能,用户名要唯一,密码第一位必须是字母,后面是纯数字,登录成功后可以直接开始聊天

服务端:对用户,登录和注册的信息进行验证,当登录成功之后,能接收客户端发来的消息,并能向所有已经登录的用户进行转发

2.项目源码剖析

2.1客户端源码

package com.bluening.Client;import java.io.*;
import java.net.Socket;
import java.util.Scanner;/** 客户端程序* 功能:1.登录*      2.注册*      3.聊天* */
public class Client {public static void main(String[] args) throws IOException, InterruptedException {//创建Socket对象,与指定服务端连接Socket socket = new Socket("127.0.0.1", 10086);//没有连接上的话,程序会报错,所以以下代码只有当连接成功才会执行System.out.println("与服务端连接成功");//主界面Scanner sc = new Scanner(System.in);//Scanner对象//字符缓冲输入流,用于接收服务端发回来的数据,利用转换流将socket的InputStream包装BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));//字符缓冲输出流,用于向服务端发送数据,利用转换流将socket的OutputStream包装BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));while (true) {System.out.println("=======控制台版聊天室=======");System.out.println("1.登录");System.out.println("2.注册");System.out.println("请输入你所需要的功能:");String choice = sc.nextLine();//输入所选择的功能if ("1".equals(choice)) {//登录模块while (true) {System.out.println("请输入用户名:");String username = sc.nextLine();System.out.println("请输入密码:");String password = sc.nextLine();bw.write("username=" + username + "&" + "password=" + password + "&" + "login");//向服务器发送信息,login表示这是一个登录请求bw.newLine();bw.flush(); // 刷新缓冲区确保数据发送//接收服务端发来的信息String line = br.readLine();if (line.equals("1")) {System.out.println("登录成功");break;} else if (line.equals("2")) {System.out.println("账户或密码错误,请重新登录");}}} else if ("2".equals(choice)) {//注册模块System.out.println("请输入用户名:");String username = sc.nextLine();String password = null;while (true) {System.out.println("请输入密码(密码只需要以字母开头,后面为纯数字):");password = sc.nextLine();//可以直接在客户端检查密码是否符合条件if (checkPassword(password)) break;else System.out.println("密码不符合条件");}bw.write("username=" + username + "&" + "password=" + password + "&" + "register");//向服务器发送信息,register表示这是一个注册请求bw.newLine();bw.flush(); // 刷新缓冲区确保数据发送//接收服务端发来的信息String line = br.readLine();if (line.equals("1")) {System.out.println("注册成功");} else if (line.equals("2")) {System.out.println("用户名重复");}continue; //注册完应该执行continue逻辑} else continue;//此处表示登录成功后开始聊天//为了使用户收发信息的操作能够同时进行,采用多线程编程来解决问题//创建一条发送信息的线程Thread sender = new Thread(new ClientSendMessageRunnable(bw));sender.start();//创建一条接收信息的线程Thread Receiver = new Thread(new ClientReceiveMessageRunnable(br));Receiver.start();//此时只让线程运行就好,可以跳出主循环break;}//释放资源//socket.close();}private static boolean checkPassword(String password) {for (int i = 0; i < password.length(); i++) {char x = password.charAt(i);if (i == 0) {if (!((x >= 'a' && x <= 'z') || (x >= 'A' && x <= 'Z'))) return false;//不是以字母开头} else {if (x < '0' || x > '9') return false;//不是以纯数字作为后续}}return true;}
}

此处我对客户端源码进行大概剖析,在源代码中我的注释比较完备了,故而在此处我只挑选花费我思考时间较多的部分进行解释。

1.针对客户端其登录和注册的逻辑并不难,但这里有第一个坑点,登录与注册同样是给服务端发送username和password的信息,如何让服务端区分你是在登录还是在注册呢?

答:这里我所采用的方法是,在发送的信息字段上附带上状态信息,如果是登录那在发送的信息后面就加上"&" + “login"字段,同理如果是注册,则加上”&" + “register”,这样就方便服务端识别用户当前的行为了。

2.另外,为了保证Client与Server进行实时交互,我们在每次使用BufferedWriter bw的write方法发送信息后,我们都使用了一个flush方法,这是什么意思呢?

答:因为缓冲区是内存中的一块区域,用于临时存储数据。当进行数据写入操作时,数据不会立即被写入到目标设备(如文件、网络套接字等),而是先被存储在缓冲区中。当缓冲区满了或者满足某些条件时,才会将缓冲区中的数据一次性写入到目标设备。

flush() 方法的作用是强制将缓冲区中暂存的数据立即写入到目标设备中,无论缓冲区是否已满。具体步骤如下

检查缓冲区状态一一>写入数据到目标设备一一>清空缓冲区

3.客户端的难点,在登录成功后如何实现同时可以发送信息给服务器并可以接收服务器传来的消息?

答:我采用了多线程编程的方式来解决这个问题。针对每一个Client在其登录成功后,均有两个线程,对于Sender线程,我利用了构造方法传给了他bw,对于Receiver方法,我传给了他br。因为每个客户端是独立运行的,所以每个客户端的IO流是独立的,不会出现混乱。

对于Sender和Receiver,他们分别拥有当前Socket对象的bw和br,并不相互纠缠。

另外值得注意的是IO流的生命周期和线程的生命周期都是相互独立的。

2.2客户端Sender线程Runnable源码

package com.bluening.Client;import java.io.BufferedWriter;
import java.util.Scanner;//为了使用户收发信息的操作能够同时进行,采用多线程编程来解决问题
public class ClientSendMessageRunnable implements Runnable{//利用构造方法来传递,针对Socket的输出流对象//输入对象BufferedWriter bw;public ClientSendMessageRunnable(BufferedWriter bw) {this.bw = bw;}@Overridepublic void run() {Scanner sc = new Scanner(System.in);try {while(true){System.out.println("请输入你要发送的信息:");if (sc.hasNextLine()) {String message = sc.nextLine();// 检查输入是否为空,如果为空则继续等待有效输入if (!message.isEmpty()) {bw.write(message);bw.newLine();bw.flush();}}}} catch (Exception e) {throw new RuntimeException(e);}}
}

2.3客户端Receiver线程Runnable源码

package com.bluening.Client;import java.io.BufferedReader;
import java.io.IOException;public class ClientReceiveMessageRunnable implements Runnable{BufferedReader br;public ClientReceiveMessageRunnable(BufferedReader br) {this.br = br;}@Overridepublic void run() {try {while(true){String line = br.readLine();if(line!=null) System.out.println(line);}} catch (IOException e) {throw new RuntimeException(e);}}
}

2.4服务端源码

package com.bluening.Server;import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;/*
*服务端程序
* */
public class Server {public static void main(String[] args) throws IOException {//1.读取本地文件中所有正确用户的信息ArrayList<String> userInfo = getUserInfo("Username_Password.txt");//2.创建ServerSocket对象,注意端口与客户端指定的端口保持一致ServerSocket serverSocket = new ServerSocket(10086);//3.服务端会被多个客户端连接,当用户数量过大时,单纯的循环效率低下//我们利用多线程来进行优化,但频繁地创建,销毁线程,对系统的开销比较大,我们可以利用线程池来进行优化//定义线程池ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5,//核心线程的数量16,//最大线程数,不能小于0,最大数量>=核心线程数量60,//空闲线程最大存活时间TimeUnit.SECONDS,//时间单位new ArrayBlockingQueue<>(5),//任务队列Executors.defaultThreadFactory(),//创建线程工厂new ThreadPoolExecutor.AbortPolicy()//任务拒绝策略);//由于涉及到服务端,向多个客户端转发信息,故而可以每个监听到的socket对象,利用集合来存储//由于用户分为登录和未登录两个状态,只有登录状态的用户才能收到服务器发送的聊天信息//所以我们应该用一个双列集合来存储信息//键为Socket,值为其登录状态码 1为登录,0为未登录HashMap<Socket,Integer> AllUserSocketMap = new HashMap<>();while(true){//监听客户端Socket socket = serverSocket.accept();//将socket对象加入到集合中//由于AllUserSocketMap对象有可能在线程中被修改,所以其为一个共享数据//在此利用同步代码块保证线程安全synchronized (Server.class){AllUserSocketMap.put(socket,0);}//监听到一个客户端则开启一条线程poolExecutor.submit(new ServerRunnable(socket,userInfo,AllUserSocketMap));}}//1.读取本地文件中所有正确用户的信息private static ArrayList<String> getUserInfo(String file) throws IOException {ArrayList<String> userInfo = new ArrayList<>();//字符缓冲输入流,可以用其中的readLine方法读取整行数据BufferedReader bw = new BufferedReader(new FileReader(file));//循环获取String line;while((line=bw.readLine())!=null){userInfo.add(line);}//释放资源bw.close();return userInfo;}
}

2.5服务端Runnable源码

package com.bluening.Server;import com.sun.source.tree.WhileLoopTree;import java.io.*;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;/*
* 执行线程的runnable类
* 功能:1.对登录用户进行校验
*      2.完成用户注册
*      3.用户聊天时将聊天信息转发给所有用户
* */
public class ServerRunnable implements Runnable{//显然要完成登录与注册功能需要,该条线程对应的Socket对象,用户信息的集合//完成聊天功能则需要所有客户端对应的socket对象集合//我们可以通过构造方法的方式,来从Server类中获取这些内容Socket socket;//本线程对应的socketString User; //若用户登录成功要记录其usernamestatic String Message; //群发的消息应该是共享的/** 值得注意的是,在此处userInfo,AllUserSocketMap是会发生变化的的数据* 每条线程都有可能对其进行增加,这说明userInfo,AllUserSocketMap是一个共享数据* 我们应该利用同步代码块,来保证线程安全* *///锁对象static final Object LOCK = new Object();//存放每个socket对象,对应的BufferedWriterstatic HashMap<Socket,BufferedWriter> AllSocketBufferedWriter = new HashMap<>();ArrayList<String> userInfo;//所有用户信息HashMap<Socket,Integer> AllUserSocketMap;//所有客户端对应的socket,与登录状态码//构造方法public ServerRunnable(Socket socket, ArrayList<String> userInfo, HashMap<Socket,Integer> AllUserSocketMaps) {this.socket = socket;this.userInfo = userInfo;this.AllUserSocketMap = AllUserSocketMaps;}@Overridepublic void run() {//服务端应该先判断当前的Client用户是否登录,如果登录已经登录,那么就只要群发信息即可//如果没有登录,则需要判断用户当前的行为是注册还是登录Integer status = AllUserSocketMap.get(socket);//获取状态码//字符输入流,准备读取客户端所发送来的信息BufferedReader br = null;try {br = new BufferedReader(new InputStreamReader(socket.getInputStream()));} catch (IOException e) {throw new RuntimeException(e);}//字符输出流,准备向客户端反馈信息BufferedWriter bw = null;try {bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));} catch (IOException e) {throw new RuntimeException(e);}//维护AllSocketBufferedWritersynchronized (LOCK) {AllSocketBufferedWriter.put(socket,bw);}//用于将数据写回文件BufferedWriter file_writer = null;try {//应该打开续写开关,不能清空文件file_writer  = new BufferedWriter(new FileWriter("Username_Password.txt",true));} catch (IOException e) {throw new RuntimeException(e);}while(status==0){//表明用户未登录try {String line = br.readLine();String[] arr = line.split("&");if("login".equals(arr[2])){ //用户正在登录,应当对其账户密码进行验证String username = arr[0].split("=")[1];String password = arr[1].split("=")[1];String loginInfo = username+"="+password;//直接拼接成文件中信息的形式if(checkLogin(loginInfo)){//登录成功//修改当前socket状态码synchronized (LOCK) { //修改操作,应当保证共享数据安全AllUserSocketMap.put(socket,1);}status = AllUserSocketMap.get(socket);//向用户发送登录成功的信息bw.write("1"); //1表示登录成功bw.newLine();bw.flush(); // 刷新缓冲区确保数据发送//登录成功了,要记录当前socket对象的用户名User = username;}else { //登录失败bw.write("2"); //2表示登录失败bw.newLine();bw.flush();}}else if("register".equals(arr[2])){  //用户正在注册,应当对其账户密码进行验证String username = arr[0].split("=")[1];String password = arr[1].split("=")[1];if(usernameContains(username)){//用户名重复bw.write("2"); //2表示注册失败bw.newLine();bw.flush();}else {//注册成功,应该把数据写回文件,并且在集合中添加,这都是对共享数据的操作synchronized (LOCK) { //修改操作,应当保证共享数据安全file_writer.write(username+"="+password);// 使用系统默认的换行符file_writer.write(System.lineSeparator());file_writer.flush();userInfo.add(username+"="+password);}bw.write("1"); //1表示注册成功bw.newLine();bw.flush();}}} catch (IOException e) {throw new RuntimeException(e);}}//用户登录成功了,下面部分是群发消息的逻辑while (true) {try {Message = br.readLine();//发送逻辑Set<Map.Entry<Socket,Integer>> AllUserSocketentrySet = AllUserSocketMap.entrySet();for (Map.Entry<Socket, Integer> socketIntegerEntry : AllUserSocketentrySet) {if(socketIntegerEntry.getValue()==1){//如果已经登录//获取到对应socket对应的流BufferedWriter SocketBw = AllSocketBufferedWriter.get(socketIntegerEntry.getKey());synchronized (LOCK) {try {SocketBw.write(User+":"+Message);SocketBw.newLine();SocketBw.flush();} catch (IOException e) {throw new RuntimeException(e);}}}}} catch (IOException e) {throw new RuntimeException(e);}}}//给予注册模块使用,查看用户名是否已经存在private boolean usernameContains(String username) {for (String string : userInfo) {String name = string.split("=")[0];if(name.equals(username)) return true;}return false;}//给予登录模块使用,检查账户密码是否完全匹配private boolean checkLogin(String loginInfo) {for (String string : userInfo) {if(string.equals(loginInfo)) return true;}return false;}
}

服务端的基本逻辑是这样的

①先读取本地文件中所有正确用户信息

②当有客户端来连接时,就开启一条线程

③线程中判断当前用户是登录还是注册操作

④登录,校验用户名和密码是否正确

⑤注册,校验用户名是否唯一,校验用户名和密码格式

⑥如果登录成功,开始聊天

⑦如果注册成功,将用户信息写入到本地,开始聊天

1.首先如何判断用户是否登录了?

答:我的做法是这样的,我利用一个双列集合HashMap<Socket,Integer> AllUserSocketMaps来记录每个Socket对象的状态信息,0是未登录,1是登录

2.在编写服务端代码时,我遇到最严重的问题就是共享数据的混乱,但是我通过梳理思路,一步步解决了bug,在此我想记录我梳理的过程:

答:共享数据UserInfo和AllUserSocketMap是通过构造方法传过来的。

User变量是为了记录每个线程对应的用户名,方便群发消息的时候,知道是谁发的,所以User针对每个线程应该独立

Message则是需要被群发的消息,所以每个线程都需要,而且需要同一个,所以这显然是一个共享数据,我设置其为static,并且在对其修改值做了锁操作,保证线程安全。

此外我应该格外关注ServerRunnable中的IO流,这很关键,最简单的file_writer它是面向username_password.txt的输出流,于各个线程中独立存在,它应该没有什么问题。

其次就是两个针对Socket的流,br和bw,这两个流针对每个线程也是独立的,他们都是针对于自己线程的Socket对象

3.如何做到消息群发?

答:想做到消息群发,就要获取每个线程Socket对象对应的BufferedWriter对象,我是这样设计的,我创建了一个static的(各线程之间共享,所以这是一个共享数据) HashMap,键为Socket,值为对应的BufferedWriter,在群发消息时,我通过遍历AllSocketMap,如果状态码为1,则获取其BufferedWriter对消息进行发送。值得注意的是,这样每个线程的BufferedWriter就不仅是只会在本线程中用到了,故我要对BufferedWriter操作时,应当做同步处理。

3.项目心得

项目编码耗时大约10h左右,主要在线程中的排错比较耗费时间,常有思绪打结的情况,不过我通过梳理思路,也是逐一解决了。另外我在编程时写了必要的注释,进行了分模块的编程,使我修改起来更加方便,我还使用了git来进行版本管理,方便代码混乱时做版本穿梭,回溯版本。

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

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

相关文章

网络版汉译英服务(muduo)

文章目录 网络版汉译英服务&#xff08;muduo&#xff09;muduo库muduo 库是什么muduo 库常见接口介绍muduo::net::EventLoopmuduo::net::TcpConnectionmuduo::net::TcpServermuduo::net::TcpClientmuduo::net::Buffer 汉译英服务服务端客户端 网络版汉译英服务&#xff08;mud…

“此电脑”中删除WPS云盘方法(百度网盘通用)

&#x1f4e3;此方法适用于卸载WPS云盘后&#xff0c;WPS云盘图标依然在此电脑中显示的问题。 原理&#xff1a;通过注册来进行删除 步骤&#xff1a; WIN键R,打开运行窗口&#xff0c;输入regedit命令&#xff0c;来打开【注册表编辑器】&#xff1b; 从左侧&#xff0c;依…

在ArcMap中通过Python编写自定义工具(Python Toolbox)实现点转线工具

文章目录 一、需求二、实现过程2.1、创建Python工具箱&#xff08;.pyt&#xff09;2.2、使用catalog测试代码2.3、在ArcMap中使用工具 三、测试 一、需求 通过插件的形式将点转线功能嵌入ArcMap界面&#xff0c;如何从零开始创建一个插件&#xff0c;包括按钮的添加、工具的实…

Cursor 使用经验,一个需求开发全流程

软件开发中 Cursor 的使用经验成为关注焦点&#xff0c;尤其是处理大型数据集的需求。用户提到“Cursor 使用经验&#xff0c;一个需求开发全流程”&#xff0c;但“Cursor”可能指数据库游标&#xff0c;涉及逐行处理数据。本文将详细探讨开发一个需求的完整流程&#xff0c;包…

selenium库

一、什么是selenium库&#xff1f; selenim是一个用于Web应用程序自动化测试工具&#xff0c;selenium测试直接运行在浏览器中 像真正的用户在操作一样&#xff0c;驱动浏览器执行特定的动作&#xff0c;如点击&#xff0c;下拉等操作 二、selenium在爬虫中的应用 获取动态…

[密码学实战]Java实现国密TLSv1.3单向认证

一、代码运行结果 1.1 运行环境 1.2 运行结果 1.3 项目架构 二、TLS 协议基础与国密背景 2.1 TLS 协议的核心作用 TLS(Transport Layer Security) 是保障网络通信安全的加密协议,位于 TCP/IP 协议栈的应用层和传输层之间,提供: • 数据机密性:通过对称加密算法(如 AE…

## DeepSeek写水果记忆配对手机小游戏

DeepSeek写水果记忆配对手机小游戏 提问 根据提的要求&#xff0c;让DeepSeek整理的需求&#xff0c;进行提问&#xff0c;内容如下&#xff1a; 请生成一个包含以下功能的可运行移动端水果记忆配对小游戏H5文件&#xff1a; 要求 可以重新开始游戏 可以暂停游戏 卡片里的水果…

【愚公系列】《Python网络爬虫从入门到精通》045-Charles的SSL证书的安装

标题详情作者简介愚公搬代码头衔华为云特约编辑&#xff0c;华为云云享专家&#xff0c;华为开发者专家&#xff0c;华为产品云测专家&#xff0c;CSDN博客专家&#xff0c;CSDN商业化专家&#xff0c;阿里云专家博主&#xff0c;阿里云签约作者&#xff0c;腾讯云优秀博主&…

夸父工具箱(安卓版) 手机超强工具箱

如今&#xff0c;人们的互联网活动日益频繁&#xff0c;导致手机内存即便频繁清理&#xff0c;也会莫名其妙地迅速填满&#xff0c;许多无用的垃圾信息悄然占据空间。那么&#xff0c;如何有效应对这一难题呢&#xff1f;答案就是今天新推出的这款工具软件&#xff0c;它能从根…

探秘Transformer系列之(11)--- 掩码

探秘Transformer系列之&#xff08;11&#xff09;— 掩码 文章目录 探秘Transformer系列之&#xff08;11&#xff09;--- 掩码0x00 概述0x01 需求1.1 避免偏差实际情况问题所在解决方案 1.2 防止偷看实际情况问题所在解决方案 0x02 Padding Mask2.1 逻辑掩码矩阵计算注意力步…

使用MPU6050产生中断,唤醒休眠中的STM32

本篇文章源码&#xff1a;STM32L431_RT_Thread_PM_mpu6050_wakeup: 使用MPU6050产生中断&#xff0c;唤醒休眠中的STM32L4 书接上回【笔记】STM32L4系列使用RT-Thread Studio电源管理组件&#xff08;PM框架&#xff09;实现低功耗-CSDN博客 上一篇文章使用PA0外接一个按键实…

国产编辑器EverEdit - 宏功能介绍

1 宏 1.1 应用场景 宏是一种重复执行简单工作的利器&#xff0c;可以让用户愉快的从繁琐的工作中解放出来&#xff0c;其本质是对键盘和菜单的操作序列的录制&#xff0c;并不会识别文件的内容&#xff0c;属于无差别无脑执行。 特别是对一些有规律的重复按键动作&#xff0c;…

ubuntu22.04安装P104-100一些经验(非教程)

一、版本&#xff1a; 系统&#xff1a;ubuntu-22.04.5-desktop-amd64.iso Nvidia 驱动&#xff1a;NVIDIA-Linux-x86_64-570.124.04.run。官网下载即可 二、经验 1、通用教程⭐ 直接关键词搜“ubuntu p104”会有一些教程&#xff0c;比如禁用nouveau等 安装参考&#xff1a…

后智能体时代的LLM和Agent

文章目录 1. 关于AI重塑的哲学体系2. 关于AI大模型体系的认知3. 关于AI大模型体系的畅想4. 关于人和AI大模型体系的共处5. 写在最后 随着OpenAI、Deepseek、Manus等等智能体的爆火&#xff0c;人们茶前饭后、插科打诨的话题都离不开这些智能体&#xff0c;现状也正如《人民日报…

Denoising Diffusion Probabilistic Models

这篇文章就是所谓的DDPM 前向扩散过程之和前一步有关&#xff0c;是一阶马尔可夫链&#xff0c;是图像和标准高斯噪声I的加权&#xff0c;认为方差全部来自I&#xff0c;并且多步可以通过连乘合并为一步&#xff1a; 反向的过程也是类似的形式&#xff1a; 并且由贝叶斯公式&am…

【DeepSeek】5分钟快速实现本地化部署教程

一、快捷部署 &#xff08;1&#xff09;下载ds大模型安装助手&#xff0c;下载后直接点击快速安装即可。 https://file-cdn-deepseek.fanqiesoft.cn/deepseek/deepseek_28348_st.exe &#xff08;2&#xff09;打开软件&#xff0c;点击立即激活 &#xff08;3&#xff09;选…

mac本地安装运行Redis-单机

记录一下我以前用的连接服务器的跨平台SSH客户端。 因为还要准备毕设...... 服务器又过期了&#xff0c;只能把redis安装下载到本地了。 目录 1.github下载Redis 2.安装homebrew 3.更新GCC 4.自行安装Redis 5.通过 Homebrew 安装 Redis 安装地址&#xff1a;https://git…

GCC RISCV 后端 -- GCC Passes 注释

在前面文章提到&#xff0c;当GCC 前端完成对C源代码解析完成后&#xff0c;就会使用 处理过程&#xff08;Passes&#xff09;机制&#xff0c;通过一系列的处理过程&#xff0c;将 GENERIC IR 表示的C程序 转步转换成 目标机器的汇编语言。过程描述如下图所示&#xff1a; 此…

OSPF的各种LSA类型,多区域及特殊区域

一、OSPF的LSA类型 OSPF&#xff08;开放最短路径优先&#xff09;协议使用多种LSA&#xff08;链路状态通告&#xff09;类型来交换网络拓扑信息。以下是主要LSA类型的详细分类及其作用&#xff1a; 1. Type 1 LSA&#xff08;路由器LSA&#xff09; 生成者&#xff1a;每个…

UV,纹理,材质,对象

先上代码&#xff1a; Shader "Unlit/MyFirstShder" {Properties{_MainTex ("Texture", 2D) "white" {}}SubShader{Pass{CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"struct appdata{float4 vertex …