• Java Socket实现简易多人聊天室传输聊天内容或文件


    Java Socket实现简易多人聊天室传输聊天内容或文件

    Java小练手项目:用Java Socket实现多人聊天室,聊天室功能包括传输聊天内容或者文件。相比于其它的聊天室,增加了传输文件的功能供参考。

    模块拆解

    分成服务端和客户端两部分来写

    服务端包括监听线程和处理收发信线程:

    1. 创建监听线程,监听客户端的连接。将每个连接的客户端加入维护的列表,并为每个连接的客户端开启一个处理收发信的线程。
    2. 在每个客户端的收发信线程中,接收每个客户端发回的消息,并对其进行转发到相应接收的客户端上,以此实现多人聊天室。
    3. 添加处理传输文件的判断,通过在传输的字节数组中添加标志位来区分传输的是文本消息,还是文件。

    客户端包括发送消息线程和接收消息线程:

    1. 发送消息线程,用来处理用户的输入信息,判断输入的是文本信息还是文件,并修改传输的字节数组标志位进行区分。最后将信息传输给服务器。
    2. 接收消息线程,用来处理服务器发回的信息,根据标志位判断输入的是文本信息还是文件,并做相应处理。如果是文本信息,则显示在控制台,如果是文件,则保存在指定目录下。

    项目的目录结构如下所示

    接下来,给出实际的代码进行分析

    项目代码

    服务器端

    监听线程

    1. package Server;
    2. import java.io.IOException;
    3. import java.net.InetAddress;
    4. import java.net.ServerSocket;
    5. import java.net.Socket;
    6. import java.util.ArrayList;
    7. import java.util.List;
    8. public class MultiServer {
    9. public static void main(String[] args) {
    10. ServerSocket ss = null;
    11. Socket s = null;
    12. // 定义一个List列表来保存每个客户端,每新建一个客户端连接,就添加到List列表里。
    13. List listSocket = new ArrayList<>();
    14. try {
    15. // 1. 创建ServerSocket类型的对象并提供端口号
    16. ss = new ServerSocket(9999);
    17. // 2. 等待客户端的连接请求,调用accept方法
    18. // 采用多线程的方式,允许多个用户请求连接。
    19. int i = 0;
    20. while (true) {
    21. System.out.println("等待客户端的连接请求...");
    22. s = ss.accept();
    23. listSocket.add(s);
    24. //sArr[i] = s;
    25. i++;
    26. System.out.printf("欢迎用户%d加入群聊!\n", i);
    27. System.out.printf("目前群聊中共有%d人\n", listSocket.size());
    28. InetAddress inetAddress = s.getInetAddress();
    29. System.out.println("客户端" + inetAddress + "连接成功!");
    30. // 调用多线程方法,每一个连上的客户端,服务器都有一个线程为之服务
    31. new MultiServerThread(s, inetAddress, listSocket).start();
    32. }
    33. } catch (IOException e) {
    34. e.printStackTrace();
    35. } finally {
    36. // 关闭流
    37. try {
    38. ss.close();
    39. } catch (IOException e) {
    40. e.printStackTrace();
    41. }
    42. try {
    43. s.close();
    44. } catch (IOException e) {
    45. e.printStackTrace();
    46. }
    47. }
    48. }
    49. }
    50. 复制代码

    上述代码实现服务器监听客户端连接,利用accept方法,每加入一个客户端,服务器都创建一个线程为之服务,同时将其加入一个List集合中,用来保存已加入聊天室的所有客户端。

    处理收发信的线程

    1. package Server;
    2. import java.io.BufferedInputStream;
    3. import java.io.BufferedOutputStream;
    4. import java.io.IOException;
    5. import java.net.InetAddress;
    6. import java.net.Socket;
    7. import java.util.Arrays;
    8. import java.util.List;
    9. public class MultiServerThread extends Thread {
    10. private Socket s;
    11. private InetAddress inetAddress;
    12. private List listSockets;
    13. public MultiServerThread(Socket s, InetAddress inetAddress, List listSockets) {
    14. this.s = s;
    15. this.inetAddress = inetAddress;
    16. this.listSockets = listSockets;
    17. }
    18. public void BroadCast(Socket s, byte[] by, int res)
    19. {
    20. // 将服务器接收到的消息发送给除了发送方以外的其他客户端
    21. int i = 0;
    22. for (Socket socket: listSockets)
    23. {
    24. if (s!=socket) // 判断不是当前发送的客户端
    25. {
    26. System.out.println("发送给用户: " + listSockets.indexOf(socket));
    27. BufferedOutputStream ps = null;
    28. try {
    29. ps = new BufferedOutputStream(socket.getOutputStream());
    30. ps.write(by, 0, res); // 写入输出流,将内容发送给客户端的输入流
    31. ps.flush();
    32. } catch (IOException e) {
    33. e.printStackTrace();
    34. }
    35. }
    36. }
    37. }
    38. // 服务器与客户端的交互线程
    39. @Override
    40. public void run() {
    41. BufferedInputStream ois = null;
    42. BufferedOutputStream oos = null;
    43. try {
    44. ois = new BufferedInputStream(s.getInputStream());
    45. oos = new BufferedOutputStream(s.getOutputStream());
    46. int i = 0;
    47. while (true) {
    48. //System.out.println("进入MultiChatServerThread");
    49. byte[] by = new byte[1024+2];
    50. //System.out.println("by.length: " + by.length);
    51. int res = 0;
    52. res = ois.read(by);
    53. // 对读取到的字节数组第一位位置进行修改,标识该数据流是由哪个用户发送来的
    54. by[0] = (byte)listSockets.indexOf(s);
    55. if (by[1] == 2){
    56. // 因为前两个位置是标志位,所以length的大小为读取的字节数-2,同时offset也从第三个位置(下标是2)开始读
    57. String receive = new String(by, 2, res-2);
    58. if (receive.equalsIgnoreCase("bye"))
    59. {
    60. // 如果客户端发送的是bye, 说明其下线,则从listSockets里删除对应的socket.
    61. oos.write(receive.getBytes()); // 把bye给客户端的读取线程,从而可以关闭掉读取线程
    62. oos.flush();
    63. System.out.printf("用户%d下线, ", listSockets.indexOf(s));
    64. listSockets.remove(s);
    65. System.out.printf("目前聊天室仍有%d人\n", listSockets.size());
    66. }
    67. }
    68. System.out.println("i" + i + "res = " + res);
    69. System.out.println("by.length: " + by.length);
    70. System.out.println("Socket[]: " + Arrays.toString(listSockets.toArray()));
    71. // 调用函数,将接受到的消息发送给所有客户端
    72. BroadCast(s, by, res);
    73. }
    74. } catch (IOException e) {
    75. e.printStackTrace();
    76. } finally {
    77. try {
    78. if (oos != null) {
    79. oos.close();
    80. }
    81. } catch (IOException e) {
    82. e.printStackTrace();
    83. }
    84. try {
    85. if (ois != null) {
    86. ois.close();
    87. }
    88. } catch (IOException e) {
    89. e.printStackTrace();
    90. }
    91. }
    92. }
    93. }
    94. 复制代码

    在处理收发信的线程中,利用BroadCast()方法将服务器接收到的消息发送给除了发送方以外的其他客户端。

    run()方法中,创建字节数组来接收客户端发送的数据流。定义字节数组时,byte[] by = new byte[1024+2];这里+2的原因是,为了区分发送的用户以及传输的数据类型是消息文本还是文件。其中,第一位标志位用来表示用户的id,第二位标志位用1,2来表示发送的是消息还是文件,1表示发送的是消息,2表示发送的是文件。 下面客户端代码时可以更好理解。

    判断用户下线的标志是用户发送bye,说明其下线,则服务端从listSockets里删除对应的socket。同时,将把bye发送给客户端的读取线程,提示其可以关闭掉读取线程。

    以上就是服务端的实现逻辑。整体思路就是:

    1. 首先创建监听线程,接收每个客户端的连接请求,并创建一个List集合保存。
    2. 创建一个处理收发信的线程,即每个客户端发送的聊天内容,都先统一发回给服务器端,再由服务器端进行集中转发给每个客户端。

    客户端

    客户端的两个线程包括发送消息给服务器和读取服务器发送的消息,用主线程和子线程来分别实现。

    客户端1

    1. package Client;
    2. import java.io.*;
    3. import java.net.Socket;
    4. import java.text.SimpleDateFormat;
    5. import java.util.Date;
    6. import java.util.Scanner;
    7. public class MultiClient extends Thread {
    8. private Socket ss;
    9. public MultiClient() {
    10. }
    11. public MultiClient(Socket ss) {
    12. this.ss = ss;
    13. }
    14. public byte[] reviseArr(byte[] by, int res) {
    15. byte[] newByteArr = new byte[by.length + 2];
    16. // 将by字节数组的内容都往后移动两位,即头部的两个位置空出来作为标志位
    17. for (int i = 0; i < by.length; i++)
    18. {
    19. newByteArr[i+2] = by[i];
    20. }
    21. return newByteArr;
    22. }
    23. // 子线程执行读操作,读取服务端发回的数据
    24. @Override
    25. public void run() {
    26. BufferedInputStream bis = null;
    27. BufferedOutputStream bosFile = null; // 与输出文件流相关联
    28. try {
    29. bis = new BufferedInputStream(ss.getInputStream());
    30. //bosFile = new BufferedOutputStream(new FileOutputStream("./directoryTest/src/用户1 IO流的框架图.png"));
    31. // 等待接收服务器发送回来的消息
    32. while(true) {
    33. byte[] by = new byte[1024+2];
    34. int res = bis.read(by);
    35. int sendUser = by[0];
    36. Date date = new Date();
    37. SimpleDateFormat sdf = new SimpleDateFormat("yyyy-mm-dd HH:mm:ss");
    38. String format = sdf.format(date);
    39. if (by[1] == 1) // 说明传的是文件
    40. {
    41. //String filePath = String.format("./directoryTest/src/用户%d传送来的IO流的框架图.png", sendUser);
    42. bosFile = new BufferedOutputStream(new FileOutputStream("./directoryTest/用户" + sendUser + "-传输的文件.png", true));
    43. bosFile.write(by, 2, res-2);
    44. bosFile.flush();
    45. if (res<1026) // 说明是最后一次在传送文件,所以传送的字节数才会小于字节数组by的大小
    46. {
    47. System.out.println("用户" + sendUser + "\t" + format + ":");
    48. System.out.printf("用户%d发送的文件传输完成\n", sendUser);
    49. }
    50. }
    51. else // 说明传输的是聊天内容,则按字符串的形式进行解析
    52. {
    53. // 利用String构造方法的形式,将字节数组转化成字符串打印出来
    54. String receive = new String(by, 2, res);
    55. System.out.println("用户" + sendUser + "\t" + format + ":");
    56. System.out.println(receive);
    57. }
    58. }
    59. } catch (IOException e) {
    60. e.printStackTrace();
    61. }finally{
    62. try {
    63. bis.close();
    64. } catch (IOException e) {
    65. e.printStackTrace();
    66. }
    67. }
    68. }
    69. public static void main(String[] args) {
    70. // 主线程执行写操作,发送消息到服务器
    71. Socket ss = null;
    72. BufferedOutputStream bos = null;
    73. BufferedInputStream bis = null; // 与文件关联的流
    74. MultiClient mcc = null;
    75. try {
    76. ss = new Socket("127.0.0.1", 9999);
    77. System.out.println("服务器连接成功");
    78. System.out.println("-----------聊天室-----------");
    79. bos = new BufferedOutputStream(ss.getOutputStream());
    80. Scanner sc = new Scanner(System.in);
    81. mcc = new MultiClient(ss);
    82. mcc.start();
    83. byte[] by = new byte[1024];
    84. int res = 0;
    85. int i = 0;
    86. while(true) {
    87. // 由用户输入选择执行不同的传输任务
    88. // 若用户输入传输文件,则传输指定文件,否则,则正常聊天任务
    89. String str = sc.nextLine();
    90. if (str.equals("传输文件")) {
    91. bis = new BufferedInputStream(new FileInputStream("./directoryTest/壁纸1.png"));
    92. while ((res = bis.read(by)) != -1) {
    93. //System.out.println("i" + i + " res: " + res);
    94. byte[] newByteArr = mcc.reviseArr(by, res);;
    95. newByteArr[1] = 1; // 表示第二个位置上的值为1时表示传输的是文件
    96. bos.write(newByteArr, 0, res+2);
    97. bos.flush();
    98. }
    99. }
    100. else{
    101. byte[] sb = str.getBytes(); // 转化为字节数组
    102. byte[] newByteArr = mcc.reviseArr(sb, sb.length);
    103. newByteArr[1] = 2; // 表示第二个位置上的值为2时表示传输的是聊天内容
    104. bos.write(newByteArr); // 把内容发给服务器
    105. bos.flush();
    106. if (str.equalsIgnoreCase("bye"))
    107. {
    108. System.out.println("用户下线!");
    109. break;
    110. }
    111. }
    112. }
    113. }catch (IOException e) {
    114. e.printStackTrace();
    115. }finally {
    116. try {
    117. bos.close();
    118. } catch (IOException e) {
    119. e.printStackTrace();
    120. }
    121. try {
    122. mcc.stop();
    123. ss.close();
    124. } catch (IOException e) {
    125. e.printStackTrace();
    126. }
    127. }
    128. }
    129. }
    130. 复制代码

    在客户端实现中,主线程执行写操作,发送消息到服务器,接收键盘的标准输入。在主线程发送消息时,判断用户的输入,如果输入的文本内容是传输文件,则会去读取指定路径下的文件,并利用BufferedOutputStream方法将文件转化为字节缓冲输出流发送。

    这里reviseArr方法,是将读入文件输入流的102大小的字节数组往后移动两位,实现前两位作为标志位,区分用户id和传输数据类型的目的。如果传输的是文件,会在第二个标志位赋1,如果传输的是消息文本,则第二个标志位赋2。

    若用户发送的消息是"bye",则表示用户下线,利用break跳出主线程循环,并在finally中调用mcc.stop()关闭子线程,从而关闭该客户端。

    在子线程中,若用户发送的是文件,则利用字节缓冲输入流BufferedInputStream将文件写入到指定路径中,并在文件名中简易标识发送用户。若接收的字节数组长度小于设定的1026,说明是最后一次在传送文件,在将最后一次文件的输入流写入后,在控制台打印文件传输完成的提示信息。

    以上是客户端1的实现代码,其它客户端的实现代码类似

    客户端2

    1. package Client;
    2. import java.io.*;
    3. import java.net.Socket;
    4. import java.text.SimpleDateFormat;
    5. import java.util.Date;
    6. import java.util.Scanner;
    7. public class MultiClient2 extends Thread {
    8. private Socket ss;
    9. public MultiClient2() {
    10. }
    11. public MultiClient2(Socket ss) {
    12. this.ss = ss;
    13. }
    14. public byte[] reviseArr(byte[] by, int res) {
    15. byte[] newByteArr = new byte[by.length + 2];
    16. // 将by字节数组的内容都往后移动两位,即头部的两个位置空出来作为标志位
    17. for (int i = 0; i < by.length; i++)
    18. {
    19. newByteArr[i+2] = by[i];
    20. }
    21. return newByteArr;
    22. }
    23. @Override
    24. public void run() {
    25. BufferedInputStream bis = null;
    26. BufferedOutputStream bosFile = null; // 与输出文件流相关联
    27. try {
    28. bis = new BufferedInputStream(ss.getInputStream());
    29. // 等待接收服务器发送回来的消息
    30. while(true) {
    31. byte[] by = new byte[1024+2];
    32. int res = bis.read(by);
    33. int sendUser = by[0];
    34. Date date = new Date();
    35. SimpleDateFormat sdf = new SimpleDateFormat("yyyy-mm-dd HH:mm:ss");
    36. String format = sdf.format(date);
    37. if (by[1] == 1) // 说明传的是文件
    38. {
    39. //String filePath = String.format("./directoryTest/src/用户%d传送来的IO流的框架图.png", sendUser);
    40. bosFile = new BufferedOutputStream(new FileOutputStream("./directoryTest/用户" + sendUser + "-传输的文件.png", true));
    41. bosFile.write(by, 2, res-2);
    42. bosFile.flush();
    43. if (res<1026) // 说明是最后一次在传送文件,所以传送的字节数才会小于字节数组by的大小
    44. {
    45. System.out.println("用户" + sendUser + "\t" + format + ":");
    46. System.out.printf("用户%d发送的文件传输完成\n", sendUser);
    47. }
    48. }
    49. else // 说明传输的是聊天内容,则按字符串的形式进行解析
    50. {
    51. // 利用String构造方法的形式,将字节数组转化成字符串打印出来
    52. String receive = new String(by,2, res);
    53. System.out.println("用户" + sendUser + "\t" + format + ":");
    54. System.out.println(receive);
    55. }
    56. }
    57. } catch (IOException e) {
    58. e.printStackTrace();
    59. }finally{
    60. try {
    61. bis.close();
    62. } catch (IOException e) {
    63. e.printStackTrace();
    64. }
    65. }
    66. }
    67. public static void main(String[] args) {
    68. // 主线程写操作
    69. Socket ss = null;
    70. BufferedOutputStream bos = null;
    71. BufferedInputStream bis = null; // 与文件关联的流
    72. MultiClient2 mcc = null;
    73. try {
    74. ss = new Socket("127.0.0.1", 9999);
    75. System.out.println("服务器连接成功");
    76. System.out.println("-----------聊天室-----------");
    77. bos = new BufferedOutputStream(ss.getOutputStream());
    78. Scanner sc = new Scanner(System.in);
    79. mcc = new MultiClient2(ss);
    80. mcc.start();
    81. byte[] by = new byte[1024];
    82. int res = 0;
    83. int i = 0;
    84. while(true) {
    85. String str = sc.nextLine();
    86. if (str.equals("传输文件")) {
    87. bis = new BufferedInputStream(new FileInputStream("./directoryTest/壁纸1.png"));
    88. while ((res = bis.read(by)) != -1) {
    89. i += 1;
    90. //System.out.println("i" + i + " res: " + res);
    91. byte[] newByteArr = mcc.reviseArr(by, res);;
    92. newByteArr[1] = 1; // 表示第二个位置上的值为1时表示传输的是文件
    93. bos.write(newByteArr, 0, res+2);
    94. bos.flush();
    95. }
    96. }
    97. else{
    98. byte[] sb = str.getBytes(); // 转化为字节数组
    99. byte[] newByteArr = mcc.reviseArr(sb, sb.length);
    100. //System.out.println("newByteArr: " + Arrays.toString(newByteArr));
    101. newByteArr[1] = 2; // 表示第二个位置上的值为2时表示传输的是聊天内容
    102. bos.write(newByteArr); // 把内容发给服务器
    103. bos.flush();
    104. if (str.equalsIgnoreCase("bye"))
    105. {
    106. System.out.println("用户下线!");
    107. break;
    108. }
    109. }
    110. }
    111. }catch (IOException e) {
    112. e.printStackTrace();
    113. }finally {
    114. try {
    115. bos.close();
    116. } catch (IOException e) {
    117. e.printStackTrace();
    118. }
    119. try {
    120. mcc.stop();
    121. ss.close();
    122. } catch (IOException e) {
    123. e.printStackTrace();
    124. }
    125. }
    126. }
    127. }
    128. 复制代码

    客户端3

    1. package Client;
    2. import java.io.*;
    3. import java.net.Socket;
    4. import java.text.SimpleDateFormat;
    5. import java.util.Date;
    6. import java.util.Scanner;
    7. public class MultiClient3 extends Thread {
    8. private Socket ss;
    9. public MultiClient3() {
    10. }
    11. public MultiClient3(Socket ss) {
    12. this.ss = ss;
    13. }
    14. public byte[] reviseArr(byte[] by, int res) {
    15. byte[] newByteArr = new byte[by.length + 2];
    16. // 将by字节数组的内容都往后移动两位,即头部的两个位置空出来作为标志位
    17. for (int i = 0; i < by.length; i++)
    18. {
    19. newByteArr[i+2] = by[i];
    20. }
    21. return newByteArr;
    22. }
    23. @Override
    24. public void run() {
    25. BufferedInputStream bis = null;
    26. BufferedOutputStream bosFile = null; // 与输出文件流相关联
    27. try {
    28. bis = new BufferedInputStream(ss.getInputStream());
    29. // 等待接收服务器发送回来的消息
    30. while(true) {
    31. byte[] by = new byte[1024+2];
    32. int res = bis.read(by);
    33. int sendUser = by[0];
    34. Date date = new Date();
    35. SimpleDateFormat sdf = new SimpleDateFormat("yyyy-mm-dd HH:mm:ss");
    36. String format = sdf.format(date);
    37. if (by[1] == 1) // 说明传的是文件
    38. {
    39. bosFile = new BufferedOutputStream(new FileOutputStream("./directoryTest/用户" + sendUser + "-传输的文件.png", true));
    40. bosFile.write(by, 2, res-2);
    41. bosFile.flush();
    42. if (res<1026) // 说明是最后一次在传送文件,所以传送的字节数才会小于字节数组by的大小
    43. {
    44. //System.out.println("客户端接收到的信息" + receive);
    45. System.out.println("用户" + sendUser + "\t" + format + ":");
    46. System.out.printf("用户%d发送的文件传输完成\n", sendUser);
    47. }
    48. }
    49. else // 说明传输的是聊天内容,则按字符串的形式进行解析
    50. {
    51. // 利用String构造方法的形式,将字节数组转化成字符串打印出来
    52. String receive = new String(by, 2, res);
    53. System.out.println("用户" + sendUser + "\t" + format + ":");
    54. System.out.println(receive);
    55. }
    56. }
    57. } catch (IOException e) {
    58. e.printStackTrace();
    59. }finally{
    60. try {
    61. bis.close();
    62. } catch (IOException e) {
    63. e.printStackTrace();
    64. }
    65. }
    66. }
    67. public static void main(String[] args) {
    68. // 主线程写操作
    69. //MultiClient mc = new MultiClient();
    70. Socket ss = null;
    71. BufferedOutputStream bos = null;
    72. BufferedInputStream bis = null; // 与文件关联的流
    73. MultiClient3 mcc = null;
    74. try {
    75. ss = new Socket("127.0.0.1", 9999);
    76. System.out.println("服务器连接成功");
    77. System.out.println("-----------聊天室-----------");
    78. bos = new BufferedOutputStream(ss.getOutputStream());
    79. Scanner sc = new Scanner(System.in);
    80. mcc = new MultiClient3(ss);
    81. mcc.start();
    82. byte[] by = new byte[1024];
    83. int res = 0;
    84. int i = 0;
    85. while(true) {
    86. String str = sc.nextLine();
    87. if (str.equals("传输文件")) {
    88. bis = new BufferedInputStream(new FileInputStream("./directoryTest/壁纸1.png"));
    89. while ((res = bis.read(by)) != -1) {
    90. byte[] newByteArr = mcc.reviseArr(by, res);;
    91. newByteArr[1] = 1; // 表示第二个位置上的值为1时表示传输的是文件
    92. bos.write(newByteArr, 0, res+2);
    93. bos.flush();
    94. }
    95. }
    96. else{
    97. byte[] sb = str.getBytes(); // 转化为字节数组
    98. byte[] newByteArr = mcc.reviseArr(sb, sb.length);
    99. newByteArr[1] = 2; // 表示第二个位置上的值为2时表示传输的是聊天内容
    100. bos.write(newByteArr); // 把内容发给服务器
    101. bos.flush();
    102. // 如果用户输入bye则表示用户下线
    103. if (str.equalsIgnoreCase("bye"))
    104. {
    105. System.out.println("用户下线!");
    106. break;
    107. }
    108. }
    109. }
    110. }catch (IOException e) {
    111. e.printStackTrace();
    112. }finally {
    113. try {
    114. bos.close();
    115. } catch (IOException e) {
    116. e.printStackTrace();
    117. }
    118. try {
    119. mcc.stop();
    120. ss.close();
    121. } catch (IOException e) {
    122. e.printStackTrace();
    123. }
    124. }
    125. }
    126. }
    127. 复制代码

    功能展示

    开启服务器和3个客户端,初始状态

    聊天室

    每有一个客户端连接,会打印相应的提示信息

    三个客户端连接成功,打印提示信息

    接下来,进行聊天功能的展示

    可以看到,聊天室里标识出每个用户以及发送的时间和消息,可以实现基本的聊天功能。

    传输文件

    在客户端1输入传输文件

    进入到写入的目录下,存在相应的文件:

    即实现了聊天室和传输文件的功能,最后客户端1发送bye,该客户端断开连接


  • 相关阅读:
    物联网iot全称
    使用zdppy_api+onlyoffice word文档在线共同编辑,附完整的vue3前端代码和python后端代码
    Spring5 框架学习笔记
    ts的接口和对象类型
    Redis_01_Redis安装与使用
    海康设备、LiveNVR等通过GB35114国密协议对接到LiveGBS GB28181/GB35114平台的详细操作说明
    redis的原理和源码-客户端结构体的介绍和源码解析
    解决gif导出后显示异常的现象
    代码工程化问题
    【蜂鸟E203的FPGA验证】Chap.6 基于Iverilog的指令功能与流水线仿真
  • 原文地址:https://blog.csdn.net/m0_71777195/article/details/126157763