C#編寫簡單聊天工具_第1頁
C#編寫簡單聊天工具_第2頁
C#編寫簡單聊天工具_第3頁
C#編寫簡單聊天工具_第4頁
C#編寫簡單聊天工具_第5頁
已閱讀5頁,還剩14頁未讀 繼續(xù)免費閱讀

下載本文檔

版權說明:本文檔由用戶提供并上傳,收益歸屬內容提供方,若內容存在侵權,請進行舉報或認領

文檔簡介

1、#編寫簡單的聊天程序引言這是一篇基于Socket進行網絡編程的入門文章,我對于網絡編程的學習并不夠深入,這篇文章是對于自己知識的一個鞏固,同時希望能為初學的朋友提供一點參考。文章大體分為四個部分:程序的分析與設計、C#網絡編程基礎(篇外篇)、聊天程序的實現(xiàn)模式、程序實現(xiàn)。程序的分析與設計1.明確程序功能如果大家現(xiàn)在已經參加了工作,你的經理或者老板告訴你,“小王,我需要你開發(fā)一個聊天程序”。那么接下來該怎么做呢?你是不是在腦子里有個雛形,然后就直接打開VS2005開始設計窗體,編寫代碼了呢?在開始之前,我們首先需要進行軟件的分析與設計。就拿本例來說,如果只有這么一句話“一個聊天程序”,恐怕現(xiàn)在大

2、家對這個“聊天程序”的概念就很模糊,它可以是像QQ那樣的非常復雜的一個程序,也可以是很簡單的聊天程序;它可能只有在對方在線的時候才可以進行聊天,也可能進行留言;它可能每次將消息只能發(fā)往一個人,也可能允許發(fā)往多個人。它還可能有一些高級功能,比如向對方傳送文件等。所以我們首先需要進行分析,而不是一上手就開始做,而分析的第一步,就是搞清楚程序的功能是什么,它能夠做些什么。在這一步,我們的任務是了解程序需要做什么,而不是如何去做。了解程序需要做什么,我們可以從兩方面入手,接下來我們分別討論。1.1請求客戶提供更詳細信息我們可以做的第一件事就是請求客戶提供更加詳細的信息。盡管你的經理或老板是你的上司,但

3、在這個例子中,他就是你的客戶(當然通常情況下,客戶是公司外部委托公司開發(fā)軟件的人或單位)。當遇到上面這種情況,我們只有少得可憐的一條信息“一個聊天程序”,首先可以做的,就是請求客戶提供更加確切的信息。比如,你問經理“對這個程序的功能能不能提供一些更具體的信息?”。他可能會像這樣回答:“哦,很簡單,可以登錄聊天程序,登錄的時候能夠通知其他在線用戶,然后與在線的用戶進行對話,如果不想對話了,就注銷或者直接關閉,就這些吧。”有了上面這段話,我們就又可以得出下面幾個需求:1. 程序可以進行登錄。2. 登錄后可以通知其他在線用戶。3. 可以與其他用戶進行對話。4. 可以注銷或者關閉。1.2對于用戶需求進

4、行提問,并進行總結經常會有這樣的情況:可能客戶給出的需求仍然不夠細致,或者客戶自己本身對于需求就很模糊,此時我們需要做的就是針對用戶上面給出的信息進行提問。接下來我就看看如何對上面的需求進行提問,我們至少可以向經理提出以下問題:NOTE:這里我穿插一個我在見到的一個印象比較深刻的例子:客戶往往向你表達了強烈的意愿他多么多么想擁有一個屬于自己的網站,但是,他卻沒有告訴你網站都有哪些內容、欄目,可以做什么。而作為開發(fā)者,我們顯然關心的是后者。1. 登錄時需要提供哪些內容?需不需要提供密碼?2. 允許多少人同時在線聊天?3. 與在線用戶聊天時,可以將一條消息發(fā)給一個用戶,還是可以一次將消息發(fā)給多個用

5、戶?4. 聊天時發(fā)送的消息包括哪些內容?5. 注銷和關閉有什么區(qū)別?6. 注銷和關閉對對方需不需要給對方提示?由于這是一個范例程序,而我在為大家講述,所以我只能再充當一下客戶的角色,來回答上面的問題:1. 登錄時只需要提供用戶名稱就可以了,不需要輸入密碼。2. 允許兩個人在線聊天。(這里我們只講述這種簡單情況,允許多人聊天需要使用多線程)3. 因為只有兩個人,那么自然是只能發(fā)給一個用戶了。4. 聊天發(fā)送的消息包括:用戶名稱、發(fā)送時間還有正文。5. 注銷并不關閉程序,只是離開了對話,可以再次進行連接。關閉則是退出整個應用程序。6. 注銷和關閉均需要給對方提示。好了,有了上面這些信息我們基本上就掌

6、握了程序需要完成的功能,那么接下來做什么?開始編碼了么?上面的這些屬于業(yè)務流程,除非你對它已經非常熟悉,或者程序非常的小,那么可以對它進行編碼,但是實際中,我們最好再編寫一些用例,這樣會使程序的流程更加的清楚。1.3編寫用例通常一個用例對應一個功能或者叫需求,它是程序的一個執(zhí)行路徑或者執(zhí)行流程。編寫用例的思路是:假設你已經有了這樣一個聊天程序,那么你應該如何使用它?我們的使用步驟,就是一個用例。用例的特點就每次只針對程序的一個功能編寫,最后根據(jù)用例編寫代碼,最終完成程序的開發(fā)。我們這里的需求只有簡單的幾個:登錄,發(fā)送消息,接收消息,注銷或關閉,上面的分析是對這幾點功能的一個明確。接下來我們首先

7、編寫第一個用例:登錄。在開始之前,我們先明確一個概念:客戶端,服務端。因為這個程序只是在兩個人(機器)之間聊天,那么我們大致可以繪出這樣一個圖來:我們期望用戶A和用戶B進行對話,那么我們就需要在它們之間建立起連接。盡管“用戶A”和“用戶B”的地位是對等的,但按照約定俗稱的說法:我們將發(fā)起連接請求的一方稱為客戶端(或叫本地),另一端稱為服務端(或叫遠程)。所以我們的登錄過程,就是“用戶A”連接到“用戶B”的過程,或者說客戶端(本地)連接到服務端(遠程)的過程。在分析這個程序的過程中,我們總是將其分為兩部分,一部分為發(fā)起連接、發(fā)送消息的一方(本地),一方為接受連接、接收消息的一方(遠程)。登錄和連

8、接(本地)主路徑可選路徑1.打開應用程序,顯示登錄窗口2.輸入用戶名3.點擊“登錄”按鈕,登錄成功3.“登錄”失敗如果用戶名為空,重新進入第2步。4.顯示主窗口,顯示登錄的用戶名稱5.點擊“連接”,連接至遠程6.連接成功6.1提示用戶,連接已經成功。6.連接失敗6.1 提示用戶,連接不成功5.在用戶界面變更控件狀態(tài)5.2連接為灰色,表示已經連接5.3注銷為亮色,表示可以注銷5.4發(fā)送為亮色,表示可以發(fā)消息這里我們的用例名稱為登錄和連接,但是后面我們又打了一個括號,寫著“本地”,它的意思是說,登錄和連接是客戶端,也就是發(fā)起連接的一方采取的動作。同樣,我們需要寫下當客戶端連接至服務端時,服務端采取

9、的動作。登錄和連接(遠程)主路徑可選路徑1-4 同客戶端5.等待連接6.如果有連接,自動在用戶界面顯示“遠程主機連接成功”接下來我們來看發(fā)送消息。在發(fā)送消息時,已經是登錄了的,也就是“用戶A”、“用戶B”已經做好了連接,所以我們現(xiàn)在就可以只關注發(fā)送這一過程:發(fā)送消息(本地)主路徑可選路徑1.輸入消息2.點擊發(fā)送按鈕2.沒有輸入消息,重新回到第1步3.在用戶界面上顯示發(fā)出的消息3.服務端已經斷開連接或者關閉3.1在客戶端用戶界面上顯示錯誤消息然后我們看一下接收消息,此時我們只關心接收消息這一部分。接收消息(遠程)主路徑可選路徑1.偵聽到客戶端發(fā)來的消息,自動顯示在用戶界面上。注意到這樣一點:當遠

10、程主機向本地返回消息時,它的用例又變?yōu)榱松厦娴挠美鞍l(fā)送消息(本地)”。因為它們的角色已經互換了。最后看一下注銷,我們這里研究的是當我們在本地機器點擊“注銷”后,雙方采取的動作:注銷(本地主動)主路徑可選路徑1.點擊注銷按鈕,斷開與遠程的連接2.在用戶界面顯示已經注銷3.更改控件狀態(tài)3.1注銷為灰色,表示已經注銷3.2連接為亮色,表示可以連接3.3發(fā)送為灰色,表示無法發(fā)送與此對應,服務端應該作出反應:注銷(遠程被動)主路徑可選路徑1.自動顯示遠程用戶已經斷開連接。注意到一點:當遠程主動注銷時,它采取的動作為上面的“本地主動”,本地采取的動作則為這里的“遠程被動”。至此,應用程序的功能分析和用例

11、編寫就告一段落了,通過上面這些表格,之后再繼續(xù)編寫程序變得容易了許多。另外還需要記得,用例只能為你提供一個操作步驟的指導,在實現(xiàn)的過程中,因為技術等方面的原因,可能還會有少量的修改。如果修改量很大,可以重新修改用例;如果修改量不大,那么就可以直接編碼。這是一個迭代的過程,也沒有一定的標準,總之是以高效和合適為標準。2.分析與設計我們已經很清楚地知道了程序需要做些什么,盡管現(xiàn)在還不知道該如何去做。我們甚至可以編寫出這個程序所需要的接口,以后編寫代碼的時候,我們只要去實現(xiàn)這些接口就可以了。這也符合面向接口編程的原則。另外我們注意到,盡管這是一個聊天程序,但是卻可以明確地劃分為兩部分,一部分發(fā)送消息

12、,一部分接收消息。另外注意上面標識為自動的語句,它們暗示這個操作需要通過事件的通知機制來完成。關于委托和事件,可以參考這兩篇文章: C#中的委托和事件- 委托和事件的入門文章,同時捎帶講述了Observer設計模式和.NET的事件模型 C#中的委托和事件(續(xù))- 委托和事件更深入的一些問題,包括異常、超時的處理,以及使用委托來異步調用方法。2.1消息Message首先我們可以定義消息,前面我們已經明確了消息包含三個部分:用戶名、時間、內容,所以我們可以定義一個結構來表示這個消息:publicstructMessageprivatereadonlystringuserName;privatere

13、adonlystringcontent;privatereadonlyDateTimepostDate;publicMessage(stringuserName,stringcontent) this.userName = userName;this.content = content;this.postDate = DateTime.Now; publicMessage(stringcontent) :this(System, content) publicstringUserName get returnuserName; publicstringContent get returncon

14、tent; publicDateTime PostDate get returnpostDate; publicoverridestringToString() returnString.Format(01:rn2rn, userName, postDate, content); 2.2消息發(fā)送方IMessageSender從上面我們可以看出,消息發(fā)送方主要包含這樣幾個功能:登錄、連接、發(fā)送消息、注銷。另外在連接成功或失敗時還要通知用戶界面,發(fā)送消息成功或失敗時也需要通知用戶界面,因此,我們可以讓連接和發(fā)送消息返回一個布爾類型的值,當它為真時表示連接或發(fā)送成功,反之則為失敗。因為登錄沒有任何的

15、業(yè)務邏輯,僅僅是記錄控件的值并進行顯示,所以我不打算將它寫到接口中。因此我們可以得出它的接口大致如下:publicinterfaceIMessageSenderboolConnect(IPAddress ip,intport);/ 連接到服務端boolSendMessage(Message msg);/ 發(fā)送用戶voidSignOut();/ 注銷系統(tǒng)2.3消息接收方IMessageReceiver而對于消息接收方,從上面我們可以看出,它的操作全是被動的:客戶端連接時自動提示,客戶端連接丟失時顯示自動提示,偵聽到消息時自動提示。注意到上面三個詞都用了“自動”來修飾,在C#中,可以定義委托和事件

16、,用于當程序中某種情況發(fā)生時,通知另外一個對象。在這里,程序即是我們的IMessageReceiver,某種情況就是上面的三種情況,而另外一個對象則為我們的用戶界面。因此,我們現(xiàn)在首先需要定義三個委托:publicdelegatevoidMessageReceivedEventHandler(stringmsg);publicdelegatevoidClientConnectedEventHandler(IPEndPoint endPoint);publicdelegatevoidConnectionLostEventHandler(stringinfo);接下來,我們注意到接收方需要偵聽消息

17、,因此我們需要在接口中定義的方法是StartListen()和StopListen()方法,這兩個方法是典型的技術相關,而不是業(yè)務相關,所以從用例中是看不出來的,可能大家現(xiàn)在對這兩個方法是做什么的還不清楚,沒有關系,我們現(xiàn)在并不寫實現(xiàn),而定義接口并不需要什么成本,我們寫下IMessageReceiver的接口定義:publicinterfaceIMessageReceivereventMessageReceivedEventHandlerMessageReceived;/ 接收到發(fā)來的消息eventConnectionLostEventHandlerClientLost;/ 遠程主動斷開連接e

18、ventClientConnectedEventHandlerClientConnected;/ 遠程連接到了本地voidStartListen();/ 開始偵聽端口voidStopListen();/ 停止偵聽端口我記得曾經看過有篇文章說過,最好不要在接口中定義事件,但是我忘了他的理由了,所以本文還是將事件定義在了接口中。2.4主程序Talker而我們的主程序是既可以發(fā)送,又可以接收,一般來說,如果一個類像獲得其他類的能力,以采用兩種方法:繼承和復合。因為C#中沒有多重繼承,所以我們無法同時繼承實現(xiàn)了IMessageReceiver和IMessageSender的類。那么我們可以采用復合,將

19、它們作為類成員包含在Talker內部:publicclassTalkerprivateIMessageReceiverreceiver;privateIMessageSendersender;publicTalker(IMessageReceiver receiver, IMessageSender sender) this.receiver = receiver;this.sender = sender; 現(xiàn)在,我們的程序大體框架已經完成,接下來要關注的就是如何實現(xiàn)它,現(xiàn)在讓我們由設計走入實現(xiàn),看看實現(xiàn)一個網絡聊天程序,我們需要掌握的技術吧。C#網絡編程基礎(篇外篇)這部分的內容請參考C#網

20、絡編程系列文章,共5個部分較為詳細的講述了基于Socket的網絡編程的初步內容。編寫程序代碼如果你已經看完了上面一節(jié)C#網絡編程,那么本章完全沒有講解的必要了,所以我只列出代碼,對個別值得注意的地方稍微地講述一下。首先需要了解的就是,我們采用的是三個模式中開發(fā)起來難度較大的一種,無服務器參與的模式。還有就是我們沒有使用廣播消息,所以需要提前知道連接到的遠程主機的地址和端口號。1.實現(xiàn)IMessageSender接口publicclassMessageSender:IMessageSenderTcpClientclient;StreamstreamToServer;/ 連接至遠程publicbo

21、olConnect(IPAddress ip,intport) tryclient =newTcpClient(); client.Connect(ip, port); streamToServer = client.GetStream();/ 獲取連接至遠程的流returntrue; catchreturnfalse; / 發(fā)送消息publicboolSendMessage(Message msg) trylock(streamToServer) byte buffer =Encoding.Unicode.GetBytes(msg.ToString(); streamToServer.Wri

22、te(buffer, 0, buffer.Length);returntrue; catchreturnfalse; / 注銷publicvoidSignOut() if(streamToServer !=null) streamToServer.Dispose();if(client !=null) client.Close(); 這段代碼可以用樸實無華來形容,所以我們直接看下一段。2.實現(xiàn)IMessageReceiver接口publicdelegatevoidPortNumberReadyEventHandler(intportNumber);publicclassMessageRecei

23、ver:IMessageReceiverpubliceventMessageReceivedEventHandlerMessageReceived;publiceventConnectionLostEventHandlerClientLost;publiceventClientConnectedEventHandlerClientConnected;/ 當端口號Ok的時候調用 - 需要告訴用戶界面使用了哪個端口號在偵聽/ 這里是業(yè)務上體現(xiàn)不出來,在實現(xiàn)中才能體現(xiàn)出來的publiceventPortNumberReadyEventHandlerPortNumberReady;privateThr

24、eadworkerThread;privateTcpListenerlistener;publicMessageReceiver() (IMessageReceiver)this).StartListen(); / 開始偵聽:顯示實現(xiàn)接口voidIMessageReceiver.StartListen() ThreadStartstart =newThreadStart(ListenThreadMethod);workerThread =newThread(start); workerThread.IsBackground =true; workerThread.Start(); / 線程入口

25、方法privatevoidListenThreadMethod() IPAddresslocalIp = IPAddress.Parse();listener =newTcpListener(localIp, 0); listener.Start();/ 獲取端口號IPEndPointendPoint = listener.LocalEndpointasIPEndPoint;intportNumber = endPoint.Port;if(PortNumberReady !=null) PortNumberReady(portNumber);/ 端口號已經OK,通知用戶界面

26、while(true) TcpClientremoteClient;try remoteClient = listener.AcceptTcpClient(); catchbreak; if(ClientConnected !=null) / 連接至本機的遠程端口 endPoint = remoteClient.Client.RemoteEndPointasIPEndPoint; ClientConnected(endPoint);/ 通知用戶界面遠程客戶連接 StreamstreamToClient = remoteClient.GetStream();bytebuffer =newbyte

27、8192;while(true) tryintbytesRead = streamToClient.Read(buffer, 0, 8192);if(bytesRead = 0) thrownewException(客戶端已斷開連接); stringmsg =Encoding.Unicode.GetString(buffer, 0, bytesRead);if(MessageReceived !=null) MessageReceived(msg);/ 已經收到消息 catch(Exceptionex) if(ClientLost !=null) ClientLost(ex.Message);

28、/ 客戶連接丟失break;/ 退出循環(huán) / 停止偵聽端口publicvoidStopListen() try listener.Stop(); listener =null; workerThread.Abort(); catch 這里需要注意的有這樣幾點:我們StartListen()為顯式實現(xiàn)接口,因為只能通過接口才能調用此方法,接口的實現(xiàn)類看不到此方法;這通常是對于一個接口采用兩種實現(xiàn)方式時使用的,但這里我只是不希望MessageReceiver類型的客戶調用它,因為在MessageReceiver的構造函數(shù)中它已經調用了StartListen。意思是說,我們希望這個類型一旦創(chuàng)建,就立

29、即開始工作。我們使用了兩個嵌套的while循環(huán),這個它可以為多個客戶端的多次請求服務,但是因為是同步操作,只要有一個客戶端連接著,我們的后臺線程就會陷入第二個循環(huán)中無法自拔。所以結果是:如果有一個客戶端已經連接上了,其它客戶端即使連接了也無法對它應答。最后需要注意的就是四個事件的使用,為了向用戶提供偵聽的端口號以進行連接,我又定義了一個PortNumberReadyEventHandler委托。3.實現(xiàn)Talker類Talker類是最平庸的一個類,它的全部功能就是將操作委托給實際的IMessageReceiver和IMessageSender。定義這兩個接口的好處也從這里可以看出來:如果日后想

30、重新實現(xiàn)這個程序,所有Windows窗體的代碼和Talker的代碼都不需要修改,只需要針對這兩個接口編程就可以了。publicclassTalkerprivateIMessageReceiverreceiver;privateIMessageSendersender;publicTalker(IMessageReceiver receiver, IMessageSender sender) this.receiver = receiver;this.sender = sender; publicTalker() this.receiver =newMessageReceiver();this.

31、sender =newMessageSender(); publiceventMessageReceivedEventHandlerMessageReceived add receiver.MessageReceived += value; remove receiver.MessageReceived -= value; publiceventClientConnectedEventHandlerClientConnected add receiver.ClientConnected += value; remove receiver.ClientConnected -= value; pu

32、bliceventConnectionLostEventHandlerClientLost add receiver.ClientLost += value; remove receiver.ClientLost -= value; / 注意這個事件publiceventPortNumberReadyEventHandlerPortNumberReady add (MessageReceiver)receiver).PortNumberReady += value; remove (MessageReceiver)receiver).PortNumberReady -= value; / 連接

33、遠程 - 使用主機名publicboolConnectByHost(stringhostName,intport) IPAddress ips = Dns.GetHostAddresses(hostName);returnsender.Connect(ips0, port); / 連接遠程 - 使用IPpublicboolConnectByIp(stringip,intport) IPAddressipAddress;try ipAddress = IPAddress.Parse(ip); catchreturnfalse; returnsender.Connect(ipAddress, po

34、rt); / 發(fā)送消息publicboolSendMessage(Message msg) returnsender.SendMessage(msg); / 釋放資源,停止偵聽publicvoidDispose() try sender.SignOut(); receiver.StopListen(); catch / 注銷publicvoidSignOut() try sender.SignOut(); catch 4.設計窗體,編寫窗體事件代碼現(xiàn)在我們開始設計窗體,我已經設計好了,現(xiàn)在可以先進行一下預覽:這里需要注意的就是上面的偵聽端口,是程序接收消息時的偵聽端口,也就是IMessageR

35、eceiver所使用的。其他的沒有什么好說的,下來我們直接看一下代碼,控件的命名是自解釋的,我就不多說什么了。唯一要稍微說明下的是txtMessage指的是下面發(fā)送消息的文本框,txtContent指上面的消息記錄文本框:publicpartialclassPrimaryForm:FormprivateTalkertalker;privatestringuserName;publicPrimaryForm(stringname) InitializeComponent(); userName = lbName.Text = name;this.talker =newTalker();this.

36、Text = userName + Talking .; talker.ClientLost +=newConnectionLostEventHandler(talker_ClientLost); talker.ClientConnected +=newClientConnectedEventHandler(talker_ClientConnected); talker.MessageReceived +=newMessageReceivedEventHandler(talker_MessageReceived); talker.PortNumberReady +=newPortNumberR

37、eadyEventHandler(PrimaryForm_PortNumberReady); voidConnectStatus() voidDisconnectStatus() / 端口號OKvoidPrimaryForm_PortNumberReady(intportNumber) PortNumberReadyEventHandlerdel =delegate(intport) lbPort.Text = port.ToString(); ; lbPort.Invoke(del, portNumber); / 接收到消息voidtalker_MessageReceived(stringm

38、sg) MessageReceivedEventHandlerdel =delegate(stringm) txtContent.Text += m; ; txtContent.Invoke(del, msg); / 有客戶端連接到本機voidtalker_ClientConnected(IPEndPoint endPoint) ClientConnectedEventHandlerdel =delegate(IPEndPoint end) IPHostEntryhost = Dns.GetHostEntry(end.Address); txtContent.Text += String.Fo

39、rmat(System0:rn遠程主機1連接至本地。rn, DateTime.Now, end); ; txtContent.Invoke(del, endPoint); / 客戶端連接斷開voidtalker_ClientLost(stringinfo) ConnectionLostEventHandlerdel =delegate(stringinformation) txtContent.Text +=String.Format(System0:rn1rn, DateTime.Now, information); ; txtContent.Invoke(del, info); / 發(fā)送消

40、息privatevoidbtnSend_Click(objectsender, EventArgs e) if(String.IsNullOrEmpty(txtMessage.Text) MessageBox.Show(請輸入內容!); txtMessage.Clear(); txtMessage.Focus();return; Messagemsg =newMessage(userName, txtMessage.Text);if(talker.SendMessage(msg) txtContent.Text += msg.ToString(); txtMessage.Clear(); else txtContent.Text +=String.Format(System0:rn遠程主機已斷開連接rn, DateTime.Now); DisconnectStatus(); / 點擊連接priva

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網頁內容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
  • 4. 未經權益所有人同意不得將文件中的內容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網僅提供信息存儲空間,僅對用戶上傳內容的表現(xiàn)方式做保護處理,對用戶上傳分享的文檔內容本身不做任何修改或編輯,并不能對任何下載內容負責。
  • 6. 下載文件中如有侵權或不適當內容,請與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

最新文檔

評論

0/150

提交評論