1.《网络编程基础之socket API》
2.socket编程之常用api介绍与socket、select、poll、epoll高并发服务器模型代码实现
3.api是基于socket实现的吗
4.Socket 面对的挑战?
5.Asynchronous socket error 10061
《网络编程基础之socket API》
学习网络编程,基础的socket API是入门的关键,任何学科的合成养成源码学习离不开基础知识体系的建设,"万丈高楼平地起,切勿浮沙筑高台"。本文不会枯燥地介绍socket API,文中会掺杂一些网络编程的其它知识点和思想。C++网络请求相关的接口大多是系统级别的调用,也即从应用层转调到内核层。自然就离不开阻塞、非阻塞、同步、异步的一些场景。那何谓阻塞、非阻塞、同步、异步。
阻塞:调用某个接口,会阻塞当前线程的执行流,直到接口返回,程序才继续往下执行;无论是网络IO还是文件IO相关的接口,都是阻塞的。
非阻塞:调用某个接口,接口立即返回,并不会阻塞当前程序的执行流。
同步、异步只是西安森城源码个思想,并不能用来形容某个接口,任何程序模块或者接口,我们可以设计成同步,也可设计成异步。同步模型下我们会一直等待执行结果的返回,异步会通过我们设置的回调函数,或者监听的事件、信号去获取程序流的执行结果。同步模型中可以使用阻塞的接口,也可以使用非阻塞的接口,异步模块也是如此。同步不等价于阻塞,异步也不代表是非阻塞。介绍完这些概念,下面正式介绍SOCKET API,本文介绍的接口将涉及windows和linux两个平台。
1、Linux平台创建socket接口,接口的入参形式和值有很多种,具体细节还是需要自己去Linux平台查看man手册。
Windows平台创建socket的接口是SOCKET,接口入参和Linux平台一致,只是Windows平台返回的是HANDLE类型值,接口报错了,使用WSAGetLastError()去获取错误码。
创建socket的接口,会发起一次系统调用转调到内核层,因此该接口是阻塞的。但我们可以调用接口将socket设置成非阻塞的印刷网源码模式,成熟的网络通信框架都会把socket设置成非阻塞的,因为socket阻塞与否直接影响connect、send、recv接口是否阻塞。那问题来了,如何将socket设置成非阻塞的?且看如下示例代码,对于初学者可能看不懂,但不要紧,随着学习的逐步深入,这些知识点都能轻松地串起来。
2、连接接口connect(linux和Windows平台下,connect接口签名一致):
客户端调用connect接口连接远程服务端,如果服务端和客户端之间间隔的路由数较多,那么connect会耗费较长的时间,阻塞当前线程。联系上文提到的非阻塞socket,如果我们把一个非阻塞的socket传递给connect接口,那么connect接口是不是也可以实现非阻塞的效果?显然是可以的,但此时connect接口的用法变得不同起来,且看如下分析。
传递非阻塞socket给connect接口,connect一般立即返回-1,但此时并不表示连接出错了,那如何判断连接是成功的?就需要进一步判断系统的错误码了,windows平台下,如果使用WSAGetLastError()获取的错误码为WSAEWOULDBLOCK,表示连接正在进行中,接着需要使用网络IO模型,知识店铺阅读源码比如:select、poll、epoll、WSASelect、WSAAsyncSelect、IOCP等去判断当前socket是否可写,如果可写,表示连接成功。Linux平台,则看errno的值了,如果errno为EINPROGRESS,也即表示连接正在进行中,接着也需要使用Linux平台的IO模型去检测socket是否可写,如果可写,表明连接成功。如果errno为EINTER,表示当前connect连接请求被信号中断,那么就需要进行重试了。具体的实现流程可看如下示例代码:
3、bind接口(顾名思义就是绑定接口,一个TCP连接包含了四元组:客户端IP、端口,服务端IP、端口。bind便是绑定一个IP和端口,无论是客户端还是服务端都可以使用,不要刻板认为bind只能用于服务端):
bind接口使用示例代码:
顺便带一句,假如servAddr的成员变量sin_port被设置成0,表明监听端口号是团队网站介绍源码由操作系统帮我们选定一个未占用的端口。假设某台服务器主机上端口资源比较紧缺,此时监听端口不能写死,很有可能这个端口被其它进程占用,那么将sin_port设置成0,也是不错的选择。
以上示例代码也可以适用于客户端,客户端绑定某个端口向服务器发起请求也是可以的。比如:面试官会问,我们客户端如何使用指定范围的端口向服务端发起请求,我们的做法便是循环调用bind接口去遍历指定范围内的端口,如果某个端口可用,bind接口返回成功,那么使用该端口发起连接请求。
4、listen接口:
5、accept接口(该接口用于服务端接收客户端的连接请求):
如果没有客户端发起连接,accept将会一直阻塞当前线程。不能存在所谓的异步accept或者非阻塞accept。没有客户端连接请求时,至少负责网络连接的线程都是阻塞在该接口处。当然成熟的网络通信框架肯定是借助IO网络模型去等待客户端连接事件的到来,比如: epoll模型中的epoll_wait。
好,介绍完和网络连接相关的接口,那么当网络连接完成后,数据的发送和接收又是哪些接口呢?
6、网络数据接收接口recv:
7、网络数据发送接口send:
针对网络数据发送和接收接口返回值,这里也需要根据socket的类型做下区别对待。假设socket是阻塞的,返回值大于0,表示当前实际发送或者接收到的数据长度realLen(不是说返回值大于0,就表明接口成功了),如果realLen小于接口入参length的值,表明数据没有发送或者接收完,需要循环调用接口发送或者接收数据,直到接口返回值累计的和等于length为止。如果返回值小于0,表明接口出错了。最后当接口的返回值等于0,表明对端关闭了连接。
对于非阻塞的socket,针对接口返回值小于0的情况,recv和send的用法又完全不一样了。返回值小于0并不代表错误,可能是TCP窗口太小,暂时无法把数据发送出去或者无法从内核缓冲区中取到数据,需要进行重试。那如何判断TCP窗口太小,需要结合系统错误码来判断了。具体做法可以参考如下示例代码:
上述代码仅简单地阐述下网络数据收发的主体流程,当TCP窗口太小,一般不会立马重试,而是借助网络IO模型去监听当前socket是否可写或者可读,当网络模型通知我们socket可写或者可读了,那么我们再调用send或者recv接口进行数据的收发。
socket编程之常用api介绍与socket、select、poll、epoll高并发服务器模型代码实现
本文旨在探讨socket网络编程,特别着重于epoll在高并发服务器模型中的应用。epoll作为关键部分,将被后续文章基于的reactor模型所构建。学习此内容的最佳途径为参与零声教育的在线课程,该课程内容丰富且详实,适合对C/C++和Linux课程感兴趣的读者深入了解。
在socket编程中,构建socket pair用于连接两个缓冲区,实现进程间通信。创建socket、绑定IP和PORT、监听请求和连接、以及连接服务器,是使用socket API函数库进行服务端和客户端编程的步骤。
网络字节序包括大端和小端的概念,它们在IP和端口传输中尤为重要。转换为大端字节序是网络通信的需要,反之则适用于本地处理。提供大小端转换函数,确保数据正确传输。IP地址转换函数将点分十进制IP转换为网络模式的整型值,反之亦然。
`struct sockaddr` 结构体是socket编程中的重要组成部分,可查阅man 7 ip获取更多细节。接下来,介绍socket API函数,包括创建socket、绑定IP和PORT、监听、连接、读取和发送数据等功能。
高并发服务器模型中,多路IO技术如select、poll和epoll被广泛使用。select通过委托内核监控多个文件描述符的状态,但受到FD_SETSIZE的限制。poll将三个集合合并为一个,不支持跨平台。epoll则采用事件驱动模型,底层基于红黑树,支持ET(边缘触发)和LT(水平触发)模式,提供高性能的高并发处理能力。
在实现部分,select、poll和epoll各有特点和局限性,选择适合场景的关键在于理解其工作原理和优化策略。epoll因其跨平台限制,仅适用于Linux环境,但其高效性和灵活性使其成为处理高并发请求的首选方案。通过封装数据和操作,epoll创建反应堆模型,实现事件驱动的高效处理机制。
本文详细介绍了socket编程的基础知识、高并发服务器模型中的多路IO技术以及如何使用select、poll和epoll实现高性能的网络通信。通过理解这些技术,读者能够构建具有高扩展性和高吞吐量的网络服务器。
api是基于socket实现的吗
api是基于socket实现的,只有通过Socket,才能使用TCP协议。API是一些预先定义的函数,目的是提供应用程序与开发人员基于某软件或硬件的能力,而又无需访问源码,或理解内部工作机制的细节。API除了有应用应用程序接口的意思外,还特指 API的说明文档,也称为帮助文档。应用程序接口是一组定义、程序及协议的集合,通过API接口实现计算机软件之间的相互通信。API的一个主要功能是提供通用功能集。程序员通过调用API函数对应用程序进行开发,可以减轻编程任务。
Socket 面对的挑战?
Socket API,作为软件界最持久且生命力旺盛的接口之一,自年首次在BSD 4.1c操作系统中发布以来,历经近年仍保持基本稳定,尽管有其他长期使用的API,但它的持久使用证明了其价值。然而,随着互联网和网络世界的变化,Socket API也面临着挑战。
挑战首先体现在网络环境的演进上。自Socket API诞生以来,网络速度飞速提升,但拓扑结构的变化并不明显。早期的网络带宽和数据传输时间与今日相比有了天壤之别,这要求API在性能优化上做出改进,尤其是在处理高带宽和低延迟的应用需求上。
其次,客户机/服务器模型虽然方便,但在处理多媒体或实时应用时,频繁的数据请求和响应机制导致资源浪费。Socket API缺乏直接通知应用程序新数据到达的机制,这在需要连续处理数据的服务中显得效率低下。
此外,随着多网络接口设备的普及,单点连接的限制在多宿主环境中显得明显。标准Socket API缺乏对多接口的支持,使得编写支持多地址的应用程序变得复杂,且在设备故障时可能导致连接中断。
尽管有SCTP等协议试图扩展Socket API,但现有的API结构和广泛的应用使得改变不易。为了应对这些挑战,Socket API需要在性能、低延迟通信和多宿主支持上进行革新,以适应现代网络环境的需求。然而,这些改变尚未广泛实现,反映出在“足够好”原则和不断发展的技术需求之间,平衡的挑战。
Asynchronous socket error
æå¡å¨é误ï¼
端å£æ«æçé®é¢
å¨å端å£æ«ææ¶ ,å¦æä¸æ主æºç¹å®ç«¯å£æ æ³éä¿¡ ,
å°±æ¤ä¸»æºèè¨ ,ææ³åºè¯¥æ以ä¸ä¸¤ç§æ åµ :
1 ãæ¤å°åä¸æ ä»»ä½ä¸»æºåå¨
2 ãæ主æºä½è¢«æ«æçç¹å®ç«¯å£ä¸åå¨ ( ä¹å¯è½æ¯è¢« firewall è¿æ»¤äº )
å¦ä½å¾ç¥æ端å£ä¸æå¼
ç»ä½ æ¥ä¸ªç®åçå§ï¼
procedure TForm1.Timer1Timer(Sender: TObject);
var
I : integer;
begin
Memo1.Clear;
for I := 0 to do begin
ServerSocket1.Close;
ServerSocket1.Port := I;
try
ServerSocket1.Open;
except
Memo1.Lines.Add(IntToStr(I) + ' 端å£è¢«æå¼ !');
end;
end;
end;
对ä¸èµ· ,ææçæ¯å«äººæºå¨ä¸ç PORT
ä½ æ¯è¯´ PORT åªè½è¢«ä¸ä¸ªç¨åºæå¼ä¹ ?
å¯æ¯ ,æç¨ OICQ æ¶å¨æå¼ æ²¡é®é¢å
ææä¸é¢çç¨åºæ¹äºä¸ä¸ ,ä¹å¯ä»¥ç¨çãä½ å°±å»è¯å¾è¿æ¥å¯¹æ¹ ,å¦æéäº ,说ææ¤ç«¯å£è¢«æ
å¼ã
procedure TForm1.ClientSocket1Connect(Sender: TObject;
Socket: TCustomWinSocket);
begin
Memo1.Lines.Add(' ç«¯å£ '+IntToStr(Socket.RemotePort)+' 被æå¼ï¼ ');
end;
procedure TForm1.Timer1Timer(Sender: TObject);
begin
ClientSocket1.Close;
ClientSocket1.Port := PortID;
try
ClientSocket1.Open;
except
end;
Inc(PortID);
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
PortID := 1;
end;
procedure TForm1.ClientSocket1Error(Sender: TObject;
Socket: TCustomWinSocket; ErrorEvent: TErrorEvent;
var ErrorCode: Integer);
begin
try
ClientSocket1.Close;
except
end;
Memo2.Lines.add(IntToStr(Socket.remotePort));
end;
åéªæ¾ä½ çæ¹æ³æè¯è¿äºå¯æ¯æ±é :asynchronous socket error
--------------------------------------------------------------------------------
æ¥èª :xueminliu æ¶é´ :-3-3 :: ID:
è¦åºå tcp å udp
oicq ç¨ udp åè®® ,connect 没æç¨ ,ä½æ¯ tcp å¯ä»¥è¿æ ·
å¦å¤ ,å¦æä½ åæ«æç¨åºå¯åä¸ä¸è¦è¿æ · ,åºè¯¥ä½¿ç¨å«çé¾æ¥æ¹æ³ ,å¦åä½ ç踪迹ä¼è¢«å«äºº
åç° .ä¾å¦ä½¿ç¨ sys æ«ææè fin æ«æ :
æç»ä½ å¼æ¥ socket ç api 代ç :
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls,WInSock, ExtCtrls;
const WM_SOCKET=WM_USER+1; //socket æ¶æ¯
type
TForm1 = class(TForm)
Button1: TButton;
Edit1: TEdit;
Panel1: TPanel;
Memo1: TMemo;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
procedure Button3Click(Sender: TObject);
private
Sockhd : integer; //socket å¥æ
Serv_Addr : Tsockaddr;// ç®æ å°å
procedure SockEvent(var msg: Tmessage);message WM_SOCKET; // å¤ç cocket æ¶æ¯
procedure DspMsg(msg : string); // æ¾ç¤ºä¿¡æ¯
{ Private declarations }
public
{ Public declarations }
end;
Form1: TForm1;
implementation
{ $R *.DFM}
function lookup_hostname(const hostname:string):longint; // æåå转åæ IP å°å
var
RemoteHost : PHostEnt; (* no, don't free it! *)
ip_address: longint;
begin
ip_address:=-1;
try
if hostname='' then
begin (* no host given! *)
lookup_hostname:=ip_address;
EXIT;
end
else
begin
ip_address:=Winsock.Inet_Addr(PChar(hostname)); (* try a xxx.xxx.xxx.xx first *)
if ip_address=SOCKET_ERROR then begin
RemoteHost:=Winsock.GetHostByName(PChar(hostname));
if (RemoteHost=NIL) or (RemoteHost^.h_length<=0) then
begin
lookup_hostname:=ip_address;
EXIT; (* host not found *)
end
else
ip_address:=longint(pointer(RemoteHost^.h_addr_list^)^);
end;
end;
except
ip_address:=-1;
end;
lookup_hostname:=ip_address;
end;
procedure TFOrm1.DspMsg(msg: string);
begin
memo1.Lines.Add(msg+'...');
if Memo1.Lines.Count> then Memo1.Lines.Delete(0);
end;
procedure TForm1.SockEvent(var msg : tmessage); // å¤ç socket æ¶æ¯
begin
case msg.LParam of
FD_READ: begin // æ è¯å¯ä»¥è¯»æ°æ® ,å½ç¶è¯å®å·²ç»é¾æ¥ä¸äº
dspmsg(' å¯ä»¥è¯»åæ°æ® ');
//do what you want do
end;
FD_WRITE: begin
dspmsg(' å¯ä»¥åéæ°æ® ');
//do what you want do
end;
FD_ERROR: begin
dspmsg(' åçé误 ');
// å¦æä½ æ¯å®¢æ·ç«¯ ,ååºè¯¥æ¯è¿æ¥ä¸ä¸ ,å³ç«¯å£æ²¡æå¼
end;
FD_CLOSE: Begin
dspmsg(' æå¡å¨æå¼è¿æ¥ ');
// 对æ¹å ³éè¿æ¥
end;
FD_CONNECT: begin
dspmsg(' è¿ç»ä¸æå¡å¨ ');
// 表示对æ¹ç«¯å£å¼æ¾
end;
FD_ACCEPT: begin
dspmsg(' æ¥æ¶ä¸ä¸ªè¯·æ± ');
// è¿ä¸ªæ¶æ¯åªææå¡ç«¯å¯è½åºç°
end;
end;
end;
procedure TForm1.FormCreate(Sender: TObject);
var wsaData:TwsaData;
begin // å¯å¨ winsock å¨æé¾æ¥åº
if WSAStartup (makeword(2,2), wsaData)<>0 then begin
messagebox(application.handle,' æ æ³å¯å¨ winsock å¨æè¿æ¥åº !',' è¦å ',MB_OK or MB_APPLMODAL or MB_ICONWARNING);
Application.Terminate;
end;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin // å ³é dll
WSACleanup;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
Sockhd := socket(AF_INET,SOCK_STREAM,0); // å建 socket å¥æ
if Sockhd<0 then begin
messagebox(application.handle,' æ æ³å建å¥æ !',' è¦å ',MB_OK or MB_APPLMODAL or MB_ICONWARNING);
exit;
end;
Serv_addr.sin_addr.s_addr:= lookup_hostname(edit1.Text); // 主æºå
Serv_addr.sin_family := PF_INET;
Serv_addr.sin_port := htons(); //any port you want to connect
if WSAAsyncSelect(Sockhd,Form1.handle,WM_SOCKET,FD_ACCEPT or FD_CONNECT or FD_CLOSE or FD_READ or FD_WRITE)=SOCKET_ERROR
then begin
messagebox(application.handle,' æ æ³å建å¥æ !',' è¦å ',MB_OK or MB_APPLMODAL or MB_ICONWARNING);
exit;
end; // å¼æ¥ socket
connect(sockhd,serv_addr,sizeof(serv_addr)); // è¿æ¥ ,ç»æä¼å¨åé¢çå¤çå½æ°å¤ç
end;
end.
ç¸ä¿¡åºè¯¥å¯ä»¥æ»¡è¶³ä½ çè¦æ±
请é®å¦ä½ç¼ç¨åºåè¿ä¸¤ç§æ åµ
æ好详ç»ä¸ç¹å ( æå¾ç¬¨ç )
å¦æå¨æ¤å°åä¸æ 主æºåå¨ ,åååºçæ°æ®å å¾ä¸å°ååº ,åºç¨ç¨åºä¼çå¾ è¶ æ¶æ
认为è¿æ¥å¤±è´¥ ( 被 firewall è¿æ»¤æ¶æ åµä¸æ · ),è¥æ主æºä½è¢«æ«æçç¹å®ç«¯å£ä¸åå¨æ¶ ,
该主æºä¼ååºç®ç端å£ä¸åå¨çåºç
è³äºå¦ä½ç¼ç¨å®ç° ,åºè¯¥å¯ä»¥ç±é误ç æ¥å¤æ ,å¨ OnError äºæ ä¸å¤å® ErrorCode æ¯å¤
å° ,ååå«å¤ç ,ErrorCode ç详æ åè§ Help
è¿ä¹é«æ·±çé®é¢æ å ,å°äº
端å£æ«æä¸æ¯è¿ä¹ç®å ,å¦å大家é½å
é¦å ä½ æ«æ人家ç端å£ä¼çä¸èªå·±çç迹 ,ç³»ç»ææ¥å¿å¯ä»¥å¯ç
å æ¤æ们å端å£æ«æççæ¶åç»å¯¹ä¸ä¼ç´æ¥è¿æ¥å«äºº ,èæ¯éè¿å°å±çæ¥å£ç¼ç¨
ä¾å¦å¨ TCP ä¸æ¬¡æ¡æç第ä¸æ¬¡æ¾å¼ ,对æ¹å°±ä¸ä¼ææ¥å¿ ,è¿ç§°ä¸º sys æ«æ
ç»å¯¹æ¹ç«¯å£åæå¼è¿æ¥ç请æ±ç§°ä¸º fin æ«æ .
éè¿è¿ä¸¤ç§æ«ææ¹å¼é½å¯ä»¥å¾ç¥å¯¹æ¹ç端å£æ¯å¦å¼ ,èä¸ä¸ä¼çä¸ç迹 .
æ»ä¹ç«¯å£æ«æéé¢æå¾å¤å¦é® ,ä¸æ¯è¿éå¯ä»¥è¯´æ¸ é¤ç
æä¹åå ,å¦ä½ æ说çè¯å¥½è±¡è¦ç´æ¥è°ç¨ socket api?
æç°å¨é¦å å ³å¿çæ¯ææåºçé®é¢ ,å¦ä½ç¼ç¨åºåè¿ä¸¤ç§æ åµ :
1 ãæ¤å°åä¸æ ä»»ä½ä¸»æºåå¨
2 ãæ主æºä½è¢«æ«æçç¹å®ç«¯å£ä¸åå¨ ( ä¹å¯è½æ¯è¢« firewall è¿æ»¤äº )
è¿æ ,为ä»ä¹ææ clientsocket ç onread éç errorcode 设为 0 äº ,
è¿æ¯å¸¸å¸¸ä¼åºç° delphi èªå·±çé误æ¶æ¯æ示 ,象 , ä»ä¹ç ,
è¿å¥½è±¡æ¯å¦å¤ä¸ç§ error code,å¦è½æå®å±è½ææ³å°±ä¸ä¼åºç°æç¤ºäº .
æ¯åï¼å¦ææ¯ ,该æä¹åå¢ã
æå : å¦æè½ç»æä¸ä¸ªå¤çº¿ç¨ç端å£æ«ææºç ,æåç» å ( ççå¾ç©·å )
ææ¾å°äº help éçæå ³è¯´æ ( æ¯å¨ç´¢å¼ä¸ Error TCP Event éæ¾å°ç )
WinSock Error Codes
The following error codes apply to the WinSock ActiveX Controls.
Error Code Error Message
The operation is canceled.
The requested address is a broadcast address, but flag is not set.
Invalid argument.
Socket not bound, invalid address or listen is not invoked prior to accept.
No more file descriptors are available, accept queue is empty.
Socket is non-blocking and the specified operation will block.
A blocking Winsock operation is in progress.
The operation is completed. No blocking operation is in progress.
The descriptor is not a socket.
Destination address is required.
The datagram is too large to fit into the buffer and is truncated.
The specified port is the wrong type for this socket.
Option unknown, or unsupported.
The specified port is not supported.
Socket type not supported in this address family.
Socket is not a type that supports connection oriented service.
Address Family is not supported.
Address in use.
Address is not available from the local machine.
Network subsystem failed.
The network cannot be reached from this host at this time.
Connection has timed out when SO_KEEPALIVE is set.
Connection is aborted due to timeout or other failure.
The connection is reset by remote side.
No buffer space is available.
Socket is already connected.
Socket is not connected.
Socket has been shut down.
The attempt to connect timed out.
Connection is forcefully rejected.
Socket already created for this object.
Socket has not been created for this object.
Authoritative answer: Host not found.
Non-Authoritative answer: Host not found.
Non-recoverable errors.
Valid name, no data record of requested type.
ææ³åªè¦å¯¹å®è¿è¡æå ³æä½å°±è½å®å ¨å±è½ winsocket é误æ¶æ¯ ( è³å°
è½å±è½å¾å¤ onerror éç errorcode åæ°æ æ³å±è½çæ¶æ¯ )
æç»äºæ¾å°åå æå¨äº
å¨æå¼ Socket æ¶ä¹è¦æè·å¼å¸¸
try
ClientSocket.Open;
except
MessageBox(MainForm.Handle,'Error connecting to this address','Connect',MB_ICONEXCLAMATION);
end;
å¨ OnError ä¸æåè¦å° ErrorCode 置为 0
if ErrorEvent=eeConnect then
begin
Socket.Close;
MessageBox(MainForm.Handle,'Error connecting to this address','Connect',MB_ICONEXCLAMATION);
end
else if ErrorEvent=eeSend then
Socket.Close;
ErrorCode:=0;
ä½ å¯è½æ å第ä¸æ¥
èè¿æ ·ä¹å¯ä»¥åºåä½ æ说ç两ç§æ åµ
1 ã第äºæ¥ OnError å°±æ¯æ¤å°åä¸æ ä»»ä½ä¸»æºåå¨ ,å°è¶ æ¶å°±è§¦å OnError äºä»¶
2 ã第ä¸æ¥ææå°å¼å¸¸å°±æ¯æ主æºä½è¢«æ«æçç¹å®ç«¯å£ä¸åå¨