C#网络编程系列文章(三)之TcpListener实现异步TCP服务器

本文主要是介绍C#网络编程系列文章(三)之TcpListener实现异步TCP服务器,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

原创性声明

本文作者:小竹zz 本文地址http://blog.csdn.net/zhujunxxxxx/article/details/44258719 转载请注明出处

文章系列目录

C#网络编程系列文章(一)之Socket实现异步TCP服务器 

C#网络编程系列文章(二)之Socket实现同步TCP服务器

C#网络编程系列文章(三)之TcpListener实现异步TCP服务器

C#网络编程系列文章(四)之TcpListener实现同步TCP服务器

C#网络编程系列文章(五)之Socket实现异步UDP服务器

C#网络编程系列文章(六)之Socket实现同步UDP服务器

C#网络编程系列文章(七)之UdpClient实现异步UDP服务器

C#网络编程系列文章(八)之UdpClient实现同步UDP服务器


本文介绍

TcpListener 类提供一些简单方法,用于在阻止同步模式下侦听和接受传入连接请求。 可使用 TcpClient 或 Socket 来连接 TcpListener。 可使用 IPEndPoint、本地 IP 地址及端口号或者仅使用端口号,来创建 TcpListener。 可以将本地 IP 地址指定为 Any,将本地端口号指定为 0(如果希望基础服务提供程序为您分配这些值)。 如果您选择这样做,可在连接套接字后使用 LocalEndpoint 属性来标识已指定的信息。使用 Start 方法,可开始侦听传入的连接请求。 Start 将对传入连接进行排队,直至您调用 Stop 方法或它已经完成 MaxConnections 排队为止。 可使用 AcceptSocket 或 AcceptTcpClient 从传入连接请求队列提取连接。 这两种方法将阻止。 如果要避免阻止,可首先使用 Pending 方法来确定队列中是否有可用的连接请求。

虽然TcpListener已经封装的比较不错了,我们于是就使用它在构造一个比较不错的异步TCP服务器,这里依然和前两章一样,给出服务器中的代码,代码中注释很详细,我也会给出相关的封装类。

TcpListener异步TCP服务器

服务器代码
[csharp]  view plain  copy
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Linq;  
  4. using System.Text;  
  5. using System.Net.Sockets;  
  6. using System.Net;  
  7. namespace NetFrame.Net.TCP.Listener.Asynchronous  
  8. {  
  9.     /// <summary>  
  10.     /// TcpListener实现异步TCP服务器  
  11.     /// </summary>  
  12.     public class AsyncTCPServer : IDisposable  
  13.     {  
  14.         #region Fields  
  15.         /// <summary>  
  16.         /// 服务器程序允许的最大客户端连接数  
  17.         /// </summary>  
  18.         private int _maxClient;  
  19.   
  20.         /// <summary>  
  21.         /// 当前的连接的客户端数  
  22.         /// </summary>  
  23.         private int _clientCount;  
  24.   
  25.         /// <summary>  
  26.         /// 服务器使用的异步TcpListener  
  27.         /// </summary>  
  28.         private TcpListener _listener;  
  29.   
  30.         /// <summary>  
  31.         /// 客户端会话列表  
  32.         /// </summary>  
  33.         private List<Object> _clients;  
  34.   
  35.         private bool disposed = false;  
  36.  
  37.         #endregion  
  38.  
  39.         #region Properties  
  40.   
  41.         /// <summary>  
  42.         /// 服务器是否正在运行  
  43.         /// </summary>  
  44.         public bool IsRunning { getprivate set; }  
  45.         /// <summary>  
  46.         /// 监听的IP地址  
  47.         /// </summary>  
  48.         public IPAddress Address { getprivate set; }  
  49.         /// <summary>  
  50.         /// 监听的端口  
  51.         /// </summary>  
  52.         public int Port { getprivate set; }  
  53.         /// <summary>  
  54.         /// 通信使用的编码  
  55.         /// </summary>  
  56.         public Encoding Encoding { getset; }  
  57.  
  58.  
  59.         #endregion  
  60.  
  61.         #region 构造函数  
  62.   
  63.         /// <summary>  
  64.         /// 异步TCP服务器  
  65.         /// </summary>  
  66.         /// <param name="listenPort">监听的端口</param>  
  67.         public AsyncTCPServer(int listenPort)  
  68.             : this(IPAddress.Any, listenPort)  
  69.         {  
  70.         }  
  71.   
  72.         /// <summary>  
  73.         /// 异步TCP服务器  
  74.         /// </summary>  
  75.         /// <param name="localEP">监听的终结点</param>  
  76.         public AsyncTCPServer(IPEndPoint localEP)  
  77.             : this(localEP.Address, localEP.Port)  
  78.         {  
  79.         }  
  80.   
  81.         /// <summary>  
  82.         /// 异步TCP服务器  
  83.         /// </summary>  
  84.         /// <param name="localIPAddress">监听的IP地址</param>  
  85.         /// <param name="listenPort">监听的端口</param>  
  86.         public AsyncTCPServer(IPAddress localIPAddress, int listenPort)  
  87.         {  
  88.             Address = localIPAddress;  
  89.             Port = listenPort;  
  90.             this.Encoding = Encoding.Default;  
  91.   
  92.             _clients = new List<Object>();  
  93.   
  94.             _listener = new TcpListener(Address, Port);  
  95.             _listener.AllowNatTraversal(true);  
  96.         }  
  97.  
  98.         #endregion  
  99.  
  100.         #region Method  
  101.   
  102.         /// <summary>  
  103.         /// 启动服务器  
  104.         /// </summary>  
  105.         public void Start()  
  106.         {  
  107.             if (!IsRunning)  
  108.             {  
  109.                 IsRunning = true;  
  110.                 _listener.Start();  
  111.                 _listener.BeginAcceptTcpClient(  
  112.                   new AsyncCallback(HandleTcpClientAccepted), _listener);  
  113.             }  
  114.         }  
  115.   
  116.   
  117.         /// <summary>  
  118.         /// 启动服务器  
  119.         /// </summary>  
  120.         /// <param name="backlog">  
  121.         /// 服务器所允许的挂起连接序列的最大长度  
  122.         /// </param>  
  123.         public void Start(int backlog)  
  124.         {  
  125.             if (!IsRunning)  
  126.             {  
  127.                 IsRunning = true;  
  128.                 _listener.Start(backlog);  
  129.                 _listener.BeginAcceptTcpClient(  
  130.                   new AsyncCallback(HandleTcpClientAccepted), _listener);  
  131.             }  
  132.         }  
  133.   
  134.         /// <summary>  
  135.         /// 停止服务器  
  136.         /// </summary>  
  137.         public void Stop()  
  138.         {  
  139.             if (IsRunning)  
  140.             {  
  141.                 IsRunning = false;  
  142.                 _listener.Stop();  
  143.                 lock (_clients)  
  144.                 {  
  145.                     //关闭所有客户端连接  
  146.                     CloseAllClient();  
  147.                 }  
  148.             }  
  149.         }  
  150.   
  151.         /// <summary>  
  152.         /// 处理客户端连接的函数  
  153.         /// </summary>  
  154.         /// <param name="ar"></param>  
  155.         private void HandleTcpClientAccepted(IAsyncResult ar)  
  156.         {  
  157.             if (IsRunning)  
  158.             {  
  159.                 //TcpListener tcpListener = (TcpListener)ar.AsyncState;  
  160.   
  161.                 TcpClient client = _listener.EndAcceptTcpClient(ar);  
  162.                 byte[] buffer = new byte[client.ReceiveBufferSize];  
  163.   
  164.                 TCPClientState state  
  165.                   = new TCPClientState(client, buffer);  
  166.                 lock (_clients)  
  167.                 {  
  168.                     _clients.Add(state);  
  169.                     RaiseClientConnected(state);  
  170.                 }  
  171.   
  172.                 NetworkStream stream = state.NetworkStream;  
  173.                 //开始异步读取数据  
  174.                 stream.BeginRead(state.Buffer, 0, state.Buffer.Length, HandleDataReceived, state);  
  175.   
  176.                 _listener.BeginAcceptTcpClient(  
  177.                   new AsyncCallback(HandleTcpClientAccepted), ar.AsyncState);  
  178.             }  
  179.         }  
  180.         /// <summary>  
  181.         /// 数据接受回调函数  
  182.         /// </summary>  
  183.         /// <param name="ar"></param>  
  184.         private void HandleDataReceived(IAsyncResult ar)  
  185.         {  
  186.             if (IsRunning)  
  187.             {  
  188.                 TCPClientState state = (TCPClientState)ar.AsyncState;  
  189.                 NetworkStream stream = state.NetworkStream;  
  190.   
  191.                 int recv = 0;  
  192.                 try  
  193.                 {  
  194.                     recv = stream.EndRead(ar);  
  195.                 }  
  196.                 catch  
  197.                 {  
  198.                     recv = 0;  
  199.                 }  
  200.   
  201.                 if (recv == 0)  
  202.                 {  
  203.                     // connection has been closed  
  204.                     lock (_clients)  
  205.                     {  
  206.                         _clients.Remove(state);  
  207.                         //触发客户端连接断开事件  
  208.                         RaiseClientDisconnected(state);  
  209.                         return;  
  210.                     }  
  211.                 }  
  212.   
  213.                 // received byte and trigger event notification  
  214.                 byte[] buff = new byte[recv];  
  215.                 Buffer.BlockCopy(state.Buffer, 0, buff, 0, recv);  
  216.                 //触发数据收到事件  
  217.                 RaiseDataReceived(state);  
  218.   
  219.                 // continue listening for tcp datagram packets  
  220.                 stream.BeginRead(state.Buffer, 0, state.Buffer.Length, HandleDataReceived, state);  
  221.             }  
  222.         }  
  223.   
  224.         /// <summary>  
  225.         /// 发送数据  
  226.         /// </summary>  
  227.         /// <param name="state">接收数据的客户端会话</param>  
  228.         /// <param name="data">数据报文</param>  
  229.         public void Send(TCPClientState state, byte[] data)  
  230.         {  
  231.             RaisePrepareSend(state);  
  232.             Send(state.TcpClient, data);  
  233.         }  
  234.   
  235.         /// <summary>  
  236.         /// 异步发送数据至指定的客户端  
  237.         /// </summary>  
  238.         /// <param name="client">客户端</param>  
  239.         /// <param name="data">报文</param>  
  240.         public void Send(TcpClient client, byte[] data)  
  241.         {  
  242.             if (!IsRunning)  
  243.                 throw new InvalidProgramException("This TCP Scoket server has not been started.");  
  244.   
  245.             if (client == null)  
  246.                 throw new ArgumentNullException("client");  
  247.   
  248.             if (data == null)  
  249.                 throw new ArgumentNullException("data");  
  250.             client.GetStream().BeginWrite(data, 0, data.Length, SendDataEnd, client);  
  251.         }  
  252.   
  253.         /// <summary>  
  254.         /// 发送数据完成处理函数  
  255.         /// </summary>  
  256.         /// <param name="ar">目标客户端Socket</param>  
  257.         private void SendDataEnd(IAsyncResult ar)  
  258.         {  
  259.             ((TcpClient)ar.AsyncState).GetStream().EndWrite(ar);  
  260.             RaiseCompletedSend(null);  
  261.         }  
  262.         #endregion  
  263.  
  264.         #region 事件  
  265.   
  266.         /// <summary>  
  267.         /// 与客户端的连接已建立事件  
  268.         /// </summary>  
  269.         public event EventHandler<AsyncEventArgs> ClientConnected;  
  270.         /// <summary>  
  271.         /// 与客户端的连接已断开事件  
  272.         /// </summary>  
  273.         public event EventHandler<AsyncEventArgs> ClientDisconnected;  
  274.   
  275.   
  276.         /// <summary>  
  277.         /// 触发客户端连接事件  
  278.         /// </summary>  
  279.         /// <param name="state"></param>  
  280.         private void RaiseClientConnected(TCPClientState state)  
  281.         {  
  282.             if (ClientConnected != null)  
  283.             {  
  284.                 ClientConnected(thisnew AsyncEventArgs(state));  
  285.             }  
  286.         }  
  287.         /// <summary>  
  288.         /// 触发客户端连接断开事件  
  289.         /// </summary>  
  290.         /// <param name="client"></param>  
  291.         private void RaiseClientDisconnected(TCPClientState state)  
  292.         {  
  293.             if (ClientDisconnected != null)  
  294.             {  
  295.                 ClientDisconnected(thisnew AsyncEventArgs("连接断开"));  
  296.             }  
  297.         }  
  298.   
  299.         /// <summary>  
  300.         /// 接收到数据事件  
  301.         /// </summary>  
  302.         public event EventHandler<AsyncEventArgs> DataReceived;  
  303.   
  304.         private void RaiseDataReceived(TCPClientState state)  
  305.         {  
  306.             if (DataReceived != null)  
  307.             {  
  308.                 DataReceived(thisnew AsyncEventArgs(state));  
  309.             }  
  310.         }  
  311.   
  312.         /// <summary>  
  313.         /// 发送数据前的事件  
  314.         /// </summary>  
  315.         public event EventHandler<AsyncEventArgs> PrepareSend;  
  316.   
  317.         /// <summary>  
  318.         /// 触发发送数据前的事件  
  319.         /// </summary>  
  320.         /// <param name="state"></param>  
  321.         private void RaisePrepareSend(TCPClientState state)  
  322.         {  
  323.             if (PrepareSend != null)  
  324.             {  
  325.                 PrepareSend(thisnew AsyncEventArgs(state));  
  326.             }  
  327.         }  
  328.   
  329.         /// <summary>  
  330.         /// 数据发送完毕事件  
  331.         /// </summary>  
  332.         public event EventHandler<AsyncEventArgs> CompletedSend;  
  333.   
  334.         /// <summary>  
  335.         /// 触发数据发送完毕的事件  
  336.         /// </summary>  
  337.         /// <param name="state"></param>  
  338.         private void RaiseCompletedSend(TCPClientState state)  
  339.         {  
  340.             if (CompletedSend != null)  
  341.             {  
  342.                 CompletedSend(thisnew AsyncEventArgs(state));  
  343.             }  
  344.         }  
  345.   
  346.         /// <summary>  
  347.         /// 网络错误事件  
  348.         /// </summary>  
  349.         public event EventHandler<AsyncEventArgs> NetError;  
  350.         /// <summary>  
  351.         /// 触发网络错误事件  
  352.         /// </summary>  
  353.         /// <param name="state"></param>  
  354.         private void RaiseNetError(TCPClientState state)  
  355.         {  
  356.             if (NetError != null)  
  357.             {  
  358.                 NetError(thisnew AsyncEventArgs(state));  
  359.             }  
  360.         }  
  361.   
  362.         /// <summary>  
  363.         /// 异常事件  
  364.         /// </summary>  
  365.         public event EventHandler<AsyncEventArgs> OtherException;  
  366.         /// <summary>  
  367.         /// 触发异常事件  
  368.         /// </summary>  
  369.         /// <param name="state"></param>  
  370.         private void RaiseOtherException(TCPClientState state, string descrip)  
  371.         {  
  372.             if (OtherException != null)  
  373.             {  
  374.                 OtherException(thisnew AsyncEventArgs(descrip, state));  
  375.             }  
  376.         }  
  377.         private void RaiseOtherException(TCPClientState state)  
  378.         {  
  379.             RaiseOtherException(state, "");  
  380.         }  
  381.  
  382.         #endregion  
  383.  
  384.         #region Close  
  385.         /// <summary>  
  386.         /// 关闭一个与客户端之间的会话  
  387.         /// </summary>  
  388.         /// <param name="state">需要关闭的客户端会话对象</param>  
  389.         public void Close(TCPClientState state)  
  390.         {  
  391.             if (state != null)  
  392.             {  
  393.                 state.Close();  
  394.                 _clients.Remove(state);  
  395.                 _clientCount--;  
  396.                 //TODO 触发关闭事件  
  397.             }  
  398.         }  
  399.         /// <summary>  
  400.         /// 关闭所有的客户端会话,与所有的客户端连接会断开  
  401.         /// </summary>  
  402.         public void CloseAllClient()  
  403.         {  
  404.             foreach (TCPClientState client in _clients)  
  405.             {  
  406.                 Close(client);  
  407.             }  
  408.             _clientCount = 0;  
  409.             _clients.Clear();  
  410.         }  
  411.         #endregion  
  412.  
  413.         #region 释放  
  414.         /// <summary>  
  415.         /// Performs application-defined tasks associated with freeing,   
  416.         /// releasing, or resetting unmanaged resources.  
  417.         /// </summary>  
  418.         public void Dispose()  
  419.         {  
  420.             Dispose(true);  
  421.             GC.SuppressFinalize(this);  
  422.         }  
  423.   
  424.         /// <summary>  
  425.         /// Releases unmanaged and - optionally - managed resources  
  426.         /// </summary>  
  427.         /// <param name="disposing"><c>true</c> to release   
  428.         /// both managed and unmanaged resources; <c>false</c>   
  429.         /// to release only unmanaged resources.</param>  
  430.         protected virtual void Dispose(bool disposing)  
  431.         {  
  432.             if (!this.disposed)  
  433.             {  
  434.                 if (disposing)  
  435.                 {  
  436.                     try  
  437.                     {  
  438.                         Stop();  
  439.                         if (_listener != null)  
  440.                         {  
  441.                             _listener = null;  
  442.                         }  
  443.                     }  
  444.                     catch (SocketException)  
  445.                     {  
  446.                         //TODO  
  447.                         RaiseOtherException(null);  
  448.                     }  
  449.                 }  
  450.                 disposed = true;  
  451.             }  
  452.         }  
  453.         #endregion  
  454.     }  
  455. }  
客户端处理封装类
[csharp]  view plain  copy
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Linq;  
  4. using System.Text;  
  5. using System.Net.Sockets;  
  6.   
  7. namespace NetFrame.Net.TCP.Listener.Asynchronous  
  8. {  
  9.     public class TCPClientState  
  10.     {  
  11.         /// <summary>  
  12.         /// 与客户端相关的TcpClient  
  13.         /// </summary>  
  14.         public TcpClient TcpClient { getprivate set; }  
  15.   
  16.         /// <summary>  
  17.         /// 获取缓冲区  
  18.         /// </summary>  
  19.         public byte[] Buffer { getprivate set; }  
  20.   
  21.         /// <summary>  
  22.         /// 获取网络流  
  23.         /// </summary>  
  24.         public NetworkStream NetworkStream  
  25.         {  
  26.             get { return TcpClient.GetStream(); }  
  27.         }  
  28.   
  29.         public TCPClientState(TcpClient tcpClient, byte[] buffer)  
  30.         {  
  31.             if (tcpClient == null)  
  32.                 throw new ArgumentNullException("tcpClient");  
  33.             if (buffer == null)  
  34.                 throw new ArgumentNullException("buffer");  
  35.   
  36.             this.TcpClient = tcpClient;  
  37.             this.Buffer = buffer;  
  38.         }  
  39.         /// <summary>  
  40.         /// 关闭  
  41.         /// </summary>  
  42.         public void Close()  
  43.         {  
  44.             //关闭数据的接受和发送  
  45.             TcpClient.Close();  
  46.             Buffer = null;  
  47.         }  
  48.     }  
  49. }  
服务器事件参数类
[csharp]  view plain  copy
  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Linq;  
  4. using System.Text;  
  5.   
  6. namespace NetFrame.Net.TCP.Listener.Asynchronous  
  7. {  
  8.     /// <summary>  
  9.     /// 异步TcpListener TCP服务器事件参数类   
  10.     /// </summary>  
  11.     public class AsyncEventArgs:EventArgs  
  12.     {  
  13.          /// <summary>  
  14.         /// 提示信息  
  15.         /// </summary>  
  16.         public string _msg;  
  17.   
  18.         /// <summary>  
  19.         /// 客户端状态封装类  
  20.         /// </summary>  
  21.         public TCPClientState _state;  
  22.   
  23.         /// <summary>  
  24.         /// 是否已经处理过了  
  25.         /// </summary>  
  26.         public bool IsHandled { getset; }  
  27.   
  28.         public AsyncEventArgs(string msg)  
  29.         {  
  30.             this._msg = msg;  
  31.             IsHandled = false;  
  32.         }  
  33.         public AsyncEventArgs(TCPClientState state)  
  34.         {  
  35.             this._state = state;  
  36.             IsHandled = false;  
  37.         }  
  38.         public AsyncEventArgs(string msg, TCPClientState state)  
  39.         {  
  40.             this._msg = msg;  
  41.             this._state = state;  
  42.             IsHandled = false;  
  43.         }  
  44.     }  
  45. }  

本文作者:小竹zz 本文地址http://blog.csdn.net/zhujunxxxxx/article/details/44258719 转载请注明出处

这篇关于C#网络编程系列文章(三)之TcpListener实现异步TCP服务器的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/319924

相关文章

MySQL更新某个字段拼接固定字符串的实现

《MySQL更新某个字段拼接固定字符串的实现》在MySQL中,我们经常需要对数据库中的某个字段进行更新操作,本文就来介绍一下MySQL更新某个字段拼接固定字符串的实现,感兴趣的可以了解一下... 目录1. 查看字段当前值2. 更新字段拼接固定字符串3. 验证更新结果mysql更新某个字段拼接固定字符串 -

java实现延迟/超时/定时问题

《java实现延迟/超时/定时问题》:本文主要介绍java实现延迟/超时/定时问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Java实现延迟/超时/定时java 每间隔5秒执行一次,一共执行5次然后结束scheduleAtFixedRate 和 schedu

Java Optional避免空指针异常的实现

《JavaOptional避免空指针异常的实现》空指针异常一直是困扰开发者的常见问题之一,本文主要介绍了JavaOptional避免空指针异常的实现,帮助开发者编写更健壮、可读性更高的代码,减少因... 目录一、Optional 概述二、Optional 的创建三、Optional 的常用方法四、Optio

在Android平台上实现消息推送功能

《在Android平台上实现消息推送功能》随着移动互联网应用的飞速发展,消息推送已成为移动应用中不可或缺的功能,在Android平台上,实现消息推送涉及到服务端的消息发送、客户端的消息接收、通知渠道(... 目录一、项目概述二、相关知识介绍2.1 消息推送的基本原理2.2 Firebase Cloud Me

Spring Boot项目中结合MyBatis实现MySQL的自动主从切换功能

《SpringBoot项目中结合MyBatis实现MySQL的自动主从切换功能》:本文主要介绍SpringBoot项目中结合MyBatis实现MySQL的自动主从切换功能,本文分步骤给大家介绍的... 目录原理解析1. mysql主从复制(Master-Slave Replication)2. 读写分离3.

Redis实现延迟任务的三种方法详解

《Redis实现延迟任务的三种方法详解》延迟任务(DelayedTask)是指在未来的某个时间点,执行相应的任务,本文为大家整理了三种常见的实现方法,感兴趣的小伙伴可以参考一下... 目录1.前言2.Redis如何实现延迟任务3.代码实现3.1. 过期键通知事件实现3.2. 使用ZSet实现延迟任务3.3

基于Python和MoviePy实现照片管理和视频合成工具

《基于Python和MoviePy实现照片管理和视频合成工具》在这篇博客中,我们将详细剖析一个基于Python的图形界面应用程序,该程序使用wxPython构建用户界面,并结合MoviePy、Pill... 目录引言项目概述代码结构分析1. 导入和依赖2. 主类:PhotoManager初始化方法:__in

springboot filter实现请求响应全链路拦截

《springbootfilter实现请求响应全链路拦截》这篇文章主要为大家详细介绍了SpringBoot如何结合Filter同时拦截请求和响应,从而实现​​日志采集自动化,感兴趣的小伙伴可以跟随小... 目录一、为什么你需要这个过滤器?​​​二、核心实现:一个Filter搞定双向数据流​​​​三、完整代码

SpringBoot利用@Validated注解优雅实现参数校验

《SpringBoot利用@Validated注解优雅实现参数校验》在开发Web应用时,用户输入的合法性校验是保障系统稳定性的基础,​SpringBoot的@Validated注解提供了一种更优雅的解... 目录​一、为什么需要参数校验二、Validated 的核心用法​1. 基础校验2. php分组校验3

Python实现AVIF图片与其他图片格式间的批量转换

《Python实现AVIF图片与其他图片格式间的批量转换》这篇文章主要为大家详细介绍了如何使用Pillow库实现AVIF与其他格式的相互转换,即将AVIF转换为常见的格式,比如JPG或PNG,需要的小... 目录环境配置1.将单个 AVIF 图片转换为 JPG 和 PNG2.批量转换目录下所有 AVIF 图