




版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請(qǐng)進(jìn)行舉報(bào)或認(rèn)領(lǐng)
文檔簡介
C#網(wǎng)絡(luò)編程(基本概念和操作)-Part.l
引言
C#網(wǎng)絡(luò)編程系列文章計(jì)劃簡單地講述網(wǎng)絡(luò)編程方面的基礎(chǔ)知識(shí),由于本人在這方面功
力有限,所以只能提供一些初步的入門知識(shí),希望能對(duì)剛開始學(xué)習(xí)的朋友提供一些幫助。如
果想要更加深入的內(nèi)容,可以參考相關(guān)書籍。
本文是該系列第一?篇,主要講述了基于套接字(Socket)進(jìn)行網(wǎng)絡(luò)編程的基本概念,
其中包括TCP協(xié)議、套接字、聊天程序的三種開發(fā)模式,以及兩個(gè)基本操作:偵聽端口、連
接遠(yuǎn)程服務(wù)端;第二篇講述了一個(gè)簡單的范例:從客戶端傳輸字符串到服務(wù)端,服務(wù)端接收
并打印字符串,將字符串改為大寫,然后再將字符串回發(fā)到客戶端,客戶端最后打印傳回的
字符串;第三篇是第二篇的一個(gè)強(qiáng)化,講述了第二篇中沒有解決的一個(gè)問題,并使用了異步
傳輸?shù)姆绞絹硗瓿珊偷?篇同樣的功能;第四篇?jiǎng)t演示了如何在客戶端與服務(wù)端之間收發(fā)文
件;第五篇實(shí)現(xiàn)了?個(gè)能夠在線聊天并進(jìn)行文件傳輸?shù)牧奶斐绦颍瑢?shí)際上是對(duì)前面知識(shí)的一
個(gè)綜合應(yīng)用。
與本文相關(guān)的還有一篇文章是:C#編寫簡單的聊天程序,但這個(gè)聊天程序不及本系列
中的聊天程序功能強(qiáng)大,實(shí)現(xiàn)方式也不相同。
網(wǎng)絡(luò)編程基本概念
1.面向連接的傳輸協(xié)議:TCP
對(duì)于TCP協(xié)議我不想說太多東西,這屬于大學(xué)課程,又涉及計(jì)算機(jī)科學(xué),而我不是“學(xué)
院派”,對(duì)于這部分內(nèi)容,我覺得作為開發(fā)人員,只需要掌握與程序相關(guān)的概念就可以了,
不需要做太艱深的研究。
我們首先知道TCP是面向連接的,它的意思是說兩個(gè)遠(yuǎn)程主機(jī)(或者叫進(jìn)程,因?yàn)閷?shí)
際上遠(yuǎn)程通信是進(jìn)程之間的通信,而進(jìn)程則是運(yùn)行中的程序),必須首先進(jìn)行一個(gè)握手過程,
確認(rèn)連接成功,之后才能傳輸實(shí)際的數(shù)據(jù)。比如說進(jìn)程A想將字符串“It'safinedaytoday”
發(fā)給進(jìn)程B,它首先要建立連接。在這一過程中,它首先需要知道進(jìn)程B的位置(主機(jī)地址
和端口號(hào))。隨后發(fā)送一個(gè)不包含實(shí)際數(shù)據(jù)的請(qǐng)求報(bào)文,我們可以將這個(gè)報(bào)文稱之為“hello”。
如果進(jìn)程B接收到了這個(gè)“hello”,就向進(jìn)程A回復(fù)一個(gè)"hello",進(jìn)程A隨后才發(fā)送實(shí)
際的數(shù)據(jù)"It'safinedaytoday"。
關(guān)于TCP第二個(gè)需要了解的,就是它是全雙工的。意思是說如果兩個(gè)主機(jī)上的進(jìn)程(比
如進(jìn)程A、進(jìn)程B),一旦建立好連接,那么數(shù)據(jù)就既可以由A流向B,也可以由B流向A。
除此以外,它還是點(diǎn)對(duì)點(diǎn)的,意思是說一個(gè)TCP連接總是兩者之間的,在發(fā)送中,通過一個(gè)
連接將數(shù)據(jù)發(fā)給多個(gè)接收方是不可能的。TCP還有?個(gè)特性,就是稱為可靠的數(shù)據(jù)傳輸,意
思是連接建立后,數(shù)據(jù)的發(fā)送一定能夠到達(dá),并且是有序的,就是說發(fā)的時(shí)候你發(fā)了ABC,
那么收的一方收到的也一定是ABC,而不會(huì)是BCA或者別的什么。
編程中與TCP相關(guān)的最重要的?個(gè)概念就是套接字。我們應(yīng)該知道網(wǎng)絡(luò)七層協(xié)議,如
果我們將上面的應(yīng)用程、表示層、會(huì)話層籠統(tǒng)地算作一層(有的教材便是如此劃分的),那
么我們編寫的網(wǎng)絡(luò)應(yīng)用程序就位于應(yīng)用層,而大家知道TCP是屬于傳輸層的協(xié)議,那么我們
在應(yīng)用層如何使用傳輸層的服務(wù)呢(消息發(fā)送或者文件上傳下載)?大家知道在應(yīng)用程序中
我們用接口來分離實(shí)現(xiàn),在應(yīng)用層和傳輸層之間,則是使用套接字來進(jìn)行分離。它就像是傳
輸層為應(yīng)用層開的?個(gè)小口,應(yīng)用程序通過這個(gè)小口向遠(yuǎn)程發(fā)送數(shù)據(jù),或者接收遠(yuǎn)程發(fā)來的
數(shù)據(jù);而這個(gè)小口以內(nèi),也就是數(shù)據(jù)進(jìn)入這個(gè)口之后,或者數(shù)據(jù)從這個(gè)口出來之前,我們是
不知道也不需要知道的,我們也不會(huì)關(guān)心它如何傳輸,這屬于網(wǎng)絡(luò)其它層次的工作。
舉個(gè)例子,如果你想寫封郵件發(fā)給遠(yuǎn)方的朋友,那么你如何寫信、將信打包,屬于應(yīng)
用層,信怎么寫,怎么打包完全由我們做主;而當(dāng)我們將信投入郵筒時(shí),郵筒的那個(gè)口就是
套接字,在進(jìn)入套接字之后,就是傳輸層、網(wǎng)絡(luò)層等(郵局、公路交管或者航線等)其它層
次的工作了。我們從來不會(huì)去關(guān)心信是如何從西安發(fā)往北京的,我們只知道寫好了投入郵筒
就0K了??梢杂孟旅孢@兩幅圖來表示它:
注意在上面圖中,兩個(gè)主機(jī)是對(duì)等的,但是按照約定,我們將發(fā)起請(qǐng)求的一方稱為客
戶端,將另一端稱為服務(wù)端。可以看出兩個(gè)程序之間的對(duì)話是通過套接字這個(gè)出入口來完成
的,實(shí)際上套接字包含的最重要的也就是兩個(gè)信息:連接至遠(yuǎn)程的本地的端口信息(本機(jī)地
址和端口號(hào)),連接到的遠(yuǎn)程的端口信息(遠(yuǎn)程地址和端口號(hào))。注意上面詞語的微妙變化,
一個(gè)是本地地址,一個(gè)是遠(yuǎn)程地址。
這里又出現(xiàn)了了?個(gè)名詞端口。一般來說我們的計(jì)算機(jī)上運(yùn)行著非常多的應(yīng)用程序,
它們可能都需要同遠(yuǎn)程主機(jī)打交道,所以遠(yuǎn)程主機(jī)就需要有一個(gè)ID來標(biāo)識(shí)它想與本地機(jī)器
上的哪個(gè)應(yīng)用程序打交道,這里的ID就是端口。將端口分配給?個(gè)應(yīng)用程序,那么來自這
個(gè)端口的數(shù)據(jù)則總是針對(duì)這個(gè)應(yīng)用程序的。有這樣一個(gè)很好的例子:可以將主機(jī)地址想象為
電話號(hào)碼,而將端口號(hào)想象為分機(jī)號(hào)。
在.NET中,盡管我們可以直接對(duì)套接字編程,但是.NET提供了兩個(gè)類將對(duì)套接字的編
程進(jìn)行了個(gè)封裝,使我們的使用能夠更加方便,這兩個(gè)類是TcpClient和TcpListener,
它與套接字的關(guān)系如下:
從上面圖中可以看出TcpClient和TcpListener對(duì)套接字進(jìn)行了封裝。從中也可以看
出,TcpListener位于接收流的位置,TcpClient位于輸出流的位置(實(shí)際上TcpListener
在收到一個(gè)請(qǐng)求后,就創(chuàng)建了TcpClient,而它本身則持續(xù)處于偵聽狀態(tài),收發(fā)數(shù)據(jù)都可以
由TcpClient完成。這個(gè)圖有點(diǎn)不夠準(zhǔn)確,而我暫時(shí)沒有想到更好的畫法,后面看到代碼時(shí)
會(huì)更加清楚一些)。
我們考慮這樣一種情況:兩臺(tái)主機(jī),主機(jī)A和主機(jī)B,起初它們誰也不知道誰在哪兒,
當(dāng)它們想要進(jìn)行對(duì)話時(shí),總是需要有一方發(fā)起連接,而另一方則需要對(duì)本機(jī)的某一端口進(jìn)
行偵聽。而在偵聽方收到連接請(qǐng)求、并建立起連接以后,它們之間進(jìn)行收發(fā)數(shù)據(jù)時(shí),發(fā)起
連接的一方并不需要再進(jìn)行偵聽。因?yàn)檫B接是全雙工的,它可以使用現(xiàn)有的連接進(jìn)行收發(fā)
數(shù)據(jù)。而我們前面已經(jīng)做了定義:將發(fā)起連接的一方稱為客戶端,另一段稱為服務(wù)端,則現(xiàn)
在可以得出:總是服務(wù)端在使用TcpListener類,因?yàn)樗枰⑵鹨粋€(gè)初始的連接。
2.網(wǎng)絡(luò)聊天程序的三種模式
實(shí)現(xiàn)一個(gè)網(wǎng)絡(luò)聊天程序本應(yīng)是最后?篇文章的內(nèi)容,也是本系列最后的一個(gè)程序,來
作為一個(gè)終結(jié)。但是我想后面更多的是編碼,講述的內(nèi)容應(yīng)該不會(huì)太多,所以還是把講述的
東西都放到這里吧。
當(dāng)采用這種模式時(shí),即是所謂的完全點(diǎn)對(duì)點(diǎn)模式,此時(shí)每臺(tái)計(jì)算機(jī)本身也是服務(wù)器,
因?yàn)樗枰M(jìn)行端口的偵聽。實(shí)現(xiàn)這個(gè)模式的難點(diǎn)是:各個(gè)主機(jī)(或終端)之間如何知道其
它主機(jī)的存在?此時(shí)通常的做法是當(dāng)某一主機(jī)上線時(shí),使用UDP協(xié)議進(jìn)行一個(gè)廣播
(Broadcast),通過這種方式來“告知”其它主機(jī)自己已經(jīng)在線并說明位置,收到廣播的
主機(jī)發(fā)回個(gè)應(yīng)答,此時(shí)主機(jī)便知道其他主機(jī)的存在。這種方式我個(gè)人并不喜歡,但在C#
編寫簡單的聊天程序這篇文章中,我使用了這種模式,可惜的是我沒有實(shí)現(xiàn)廣播,所以還
很不完善。
第二種方式較好的解決了上面的問題,它引入了服務(wù)器,山這個(gè)服務(wù)器來專門進(jìn)行廣
播。服務(wù)器持續(xù)保持對(duì)端口的偵聽狀態(tài),每當(dāng)有主機(jī)上線時(shí),首先連接至服務(wù)器,服務(wù)器收
到連接后,將該主機(jī)的位置(地址和端口號(hào))發(fā)往其他在線主機(jī)(綠色箭頭標(biāo)識(shí))。這樣其
他主機(jī)便知道該主機(jī)已上線,并知道其所在位置,從而可以進(jìn)行連接和對(duì)話。在服務(wù)器進(jìn)行
了廣播之后,因?yàn)楦鱾€(gè)主機(jī)已經(jīng)知道了其他主機(jī)的位置,因此主機(jī)之間的對(duì)話就不再通過服
務(wù)器(黑色箭頭表示),而是直接進(jìn)行連接。因此,使用這種模式時(shí),各個(gè)主機(jī)依然需要保
持對(duì)端口的偵聽。在某臺(tái)主機(jī)離線時(shí).,與登錄時(shí)的模式類似,服務(wù)器會(huì)收到通知,然后轉(zhuǎn)告
給其他的主機(jī)。
第三種模式是我覺得最簡單也最實(shí)用的一種,主機(jī)的登錄與離線與第二種模式相同。
注意到每臺(tái)主機(jī)在上線時(shí)首先就與服務(wù)器建立了連接,那么從主機(jī)A發(fā)往主機(jī)B發(fā)送消息,
就可以通過這樣一條路徑,主機(jī)A->服務(wù)器一>主機(jī)B,通過這種方式,各個(gè)主機(jī)不需
要在對(duì)端口進(jìn)行偵聽,而只需要服務(wù)器進(jìn)行偵聽就可以了,大大地簡化了開發(fā)。
而對(duì)于一些較大的文件,比如說圖片或者文件,如果想由主機(jī)A發(fā)往主機(jī)B,如果通過
服務(wù)器進(jìn)行傳輸效率會(huì)比較低,此時(shí)可以臨時(shí)搭建?個(gè)主機(jī)A至主機(jī)B之間的連接,用于傳
輸大文件。當(dāng)文件傳輸結(jié)束之后再關(guān)閉連接(桔紅色箭頭標(biāo)識(shí))。
除此以外,由于消息都經(jīng)過服務(wù)器,所以服務(wù)器還可以緩存主機(jī)間的對(duì)話,即是說當(dāng)
主機(jī)A發(fā)往主機(jī)B時(shí),如果主機(jī)B已經(jīng)離線,則服務(wù)器可以對(duì)消息進(jìn)行緩存,當(dāng)主機(jī)B下次
連接到服務(wù)器時(shí),服務(wù)器自動(dòng)將緩存的消息發(fā)給主機(jī)B,
本系列文章最后采用的即是此種模式,不過沒有實(shí)現(xiàn)過多復(fù)雜的功能。接下來我們的
理論知識(shí)告一段落,開始下一階段一一漫長的編碼。
基本操作
1.服務(wù)端對(duì)端口進(jìn)行偵聽
接下來我們開始編寫一些實(shí)際的代碼,第一步就是開啟對(duì)本地機(jī)器上某一端口的偵聽。
首先創(chuàng)建一個(gè)控制臺(tái)應(yīng)用程序,將項(xiàng)目名稱命名為ServerConsole,它代表我們的服務(wù)端。
如果想要與外界進(jìn)行通信,第一件要做的事情就是開啟對(duì)端口的偵聽,這就像為計(jì)算機(jī)打開
了一個(gè)“門”,所有向這個(gè)“門”發(fā)送的請(qǐng)求(“敲門”)都會(huì)被系統(tǒng)接收到。在C#中可
以通過下面幾個(gè)步驟完成,首先使用本機(jī)Ip地址和端口號(hào)創(chuàng)建一個(gè)
System.Net.Sockets.TcpListener類型的實(shí)例,然后在該實(shí)例上調(diào)用Start。方法,從而開
啟對(duì)指定端口的偵聽。
usingSystem.Net;//引入這兩個(gè)命名空間,以下同
usingSystem.Net.Sockets;
using.??〃略
classServer{
staticvoidMain(string[]args){
Console.WriteLine(''Serverisrunning…”);
TPAddressip=newTPAddress(newbyte[]{127,0,0,1});
TcpListenerlistener=newTcpListener(ip,8500);
listener.Start();//開始偵聽
Console.WriteLine(/zStartListening
Console.WriteLineC\n\n輸入\〃Q\〃鍵退出。〃);
ConsoleKeykey;
do{
key=Console.ReadKey(true).Key;
}while(key!=ConsoleKey.Q);
}
)
//獲得IPAddress對(duì)象的另外兒種常用方法:
IPAddressip=IPAddress.Parse(,z127.0.0.l,z);
IPAddressip=Dns.GetHostEntry(,zlocalhostz/).AddressList[0];
上面的代碼中,我們開啟了對(duì)8500端口的偵聽。在運(yùn)行了上面的程序之后,然后打開
“命令提示符",輸入“netstat-a”,可以看到計(jì)算機(jī)器中所有打開的端口的狀態(tài)??梢?/p>
從中找到8500端口,看到它的狀態(tài)是LISTENING,這說明它已經(jīng)開始了偵聽:
TCPjimmy:10300.0.0.0:0LISTENING
TCPjimmy:36030.0.0.0:0LISTENING
TCPjimmy:85000.0.0.0:0LISTENING
TCPjimmy:netbios-ssn0.0.0.0:0LISTENING
在打開了對(duì)端口的偵聽以后,服務(wù)端必須通過某種方式進(jìn)行阻塞(比如
Console.ReadKey()),使得程序不能夠因?yàn)檫\(yùn)行結(jié)束而退出。否則就無法使用“netstat-摩
看到端口的連接狀態(tài),因?yàn)槌绦蛞呀?jīng)退出,連接會(huì)自然中斷,再運(yùn)行“netstat-a”當(dāng)然就
不會(huì)顯示端口了。所以程序最后按“Q”退出那段代碼是必要的,下面的每段程序都會(huì)含有
這個(gè)代碼段,但為了節(jié)省空間,我都省略掉了。
2.客戶端與服務(wù)端連接
2.1單一客戶端與服務(wù)端連接
當(dāng)服務(wù)器開始對(duì)端口偵聽之后,便可以創(chuàng)建客戶端與它建立連接。這一步是通過在客
戶端創(chuàng)建一個(gè)TcpClient的類型實(shí)例完成。每創(chuàng)建一個(gè)新的TcpClient便相當(dāng)于創(chuàng)建了一個(gè)
新的套接字Socket去與服務(wù)端通信,.Net會(huì)自動(dòng)為這個(gè)套接字分配一個(gè)端口號(hào),上面說過,
TcpClient類不過是對(duì)Socket進(jìn)行了一個(gè)包裝。創(chuàng)建TcpClient類型實(shí)例時(shí),可以在構(gòu)造
函數(shù)中指定遠(yuǎn)程服務(wù)器的地址和端口號(hào)。這樣在創(chuàng)建的同時(shí),就會(huì)向遠(yuǎn)程服務(wù)端發(fā)送一個(gè)連
接請(qǐng)求(“握手”),一旦成功,則兩者間的連接就建立起來了。也可以使用重載的無參數(shù)
構(gòu)造函數(shù)創(chuàng)建對(duì)象,然后再調(diào)用Connect。方法,在Connect()方法中傳入遠(yuǎn)程服務(wù)器地址
和端口號(hào),來與服務(wù)器建立連接。
這里需要注意的是,不管是使用有參數(shù)的構(gòu)造函數(shù)與服務(wù)器連接,或者是通過Connect()
方法與服務(wù)器建立連接,都是同步方法(或者說是阻塞的,英文叫block).它的意思是說,
客戶端在與服務(wù)端連接成功、從而方法返回,或者是服務(wù)端不存、從而拋出異常之前,是無
法繼續(xù)進(jìn)行后繼操作的。這里還有一個(gè)名為BeginConnectO的方法,用于實(shí)施異步的連接,
這樣程序不會(huì)被阻塞,可以立即執(zhí)行后面的操作,這是因?yàn)榭赡苡捎诰W(wǎng)絡(luò)擁塞等問題,連接
需要較長時(shí)間才能完成。網(wǎng)絡(luò)編程中有非常多的異步操作,凡事都是由簡入難,關(guān)于異步操
作,我們后面再討論,現(xiàn)在只看同步操作。
創(chuàng)建一個(gè)新的控制臺(tái)應(yīng)用程序項(xiàng)目,命名為Clientconsole,它是我們的客戶端,然后
添加下面的代碼,創(chuàng)建與服務(wù)器的連接:
classClient{
staticvoidMain(string[]args){
Console.WriteLineCClientRunning…”);
TcpClientclient=newTcpClient();
try{
client.Connect("localhost”,8500);//與服務(wù)器連接
}catch(Exceptionex){
Console.WriteLine(ex.Message);
return;
}
//打印連接到的服務(wù)端信息
Console.WriteLine(""ServerConnected!{0}—>⑴”,
client.Client.LocalEndPoint,client.Client.RemoteEndPoint);
〃按Q退出
}
)
上面帶代碼中,我們通過調(diào)用Connect。方法來與服務(wù)端連接。隨后,我們打印了這個(gè)
連接消息:本機(jī)的Ip地址和端口號(hào),以及連接到的遠(yuǎn)程Ip地址和端口號(hào)。TcpClient的
Client屬性返回了一個(gè)Socket對(duì)象,它的LocalEndPoint和RemoteEndPoint屬性分別包
含了本地和遠(yuǎn)程的地址信息。先運(yùn)行服務(wù)端,再運(yùn)行這段代碼??梢钥吹絻蛇叺妮敵銮闆r如
下:
//服務(wù)端:
Serverisrunning
StartListening
//客戶端:
ClientRunning
ServerConnected!127.0.0.1:4761一>127.0.0.1:8500
我們看到客戶端使用的端口號(hào)為4761,上面已經(jīng)說過,這個(gè)端口號(hào)是由.NET隨機(jī)選取
的,并不需要我們來設(shè)置,并且每次運(yùn)行時(shí),這個(gè)端口號(hào)都不同。再次打開“命令提示符”,
輸入“netstat-a",可以看到下面的輸出:
TCPjimmy:85000.0.0.0:0LISTENING
TCPjimmy:8500localhost:4761ESTABLISHED
TCPjimmy:4761localhost:8500ESTABLISHED
從這里我們可以得出幾個(gè)重要信息:1、端口8500和端口4761建立了連接,這個(gè)4761
端口便是客戶端用來與服務(wù)端進(jìn)行通信的端口;2、8500端口在與客戶端建立起?個(gè)連接后,
仍然繼續(xù)保持在監(jiān)聽狀態(tài)。這也就是說一個(gè)端口可以與多個(gè)遠(yuǎn)程端口建立通信,這是顯然的,
大家眾所周之的HTTP使用的默認(rèn)端口為80,但是一個(gè)Web服務(wù)器要通過這個(gè)端口與多少個(gè)
瀏覽器通信啊。
2?2多個(gè)客戶端與服務(wù)端連接
那么既然一個(gè)服務(wù)器端口可以應(yīng)對(duì)多個(gè)客戶端連接,那么接下來我們就看一下,如何
讓多個(gè)客戶端與服務(wù)端連接。如同我們上面所說的,一個(gè)TcpClient就是一個(gè)Socket,所
以我們只要?jiǎng)?chuàng)建多個(gè)TcpClient,然后再調(diào)用Connect()方法就可以了:
classClient(
staticvoidMain(string[]args){
Console.WriteLine(,/C1ientRunning…〃);
TcpCllentclient;
for(inti=0;i<=2;i++){
try(
client=newTcpClient();
client.Connect(,zlocalhost,z,8500);//與服務(wù)器連接
}catch(Exceptionex){
Console.WriteLine(ex.Message);
return;
)
//打印連接到的服務(wù)端信息
Console.WriteLine(''ServerConnected!{0}—>⑴〃,
client.Client.LocalEndPoint,client.Client.RemoteEndPoint);
}
〃按Q退出
}
)
上面代碼最重要的就是client=newTcpClient()這句,如果你將這個(gè)聲明放到循環(huán)
外面,再循環(huán)的第二趟就會(huì)發(fā)生異常,原因很顯然:一個(gè)TcpClient對(duì)象對(duì)應(yīng)一個(gè)Socket,
一個(gè)Socket對(duì)應(yīng)著一個(gè)端口,如果不使用new操作符重新創(chuàng)建對(duì)象,那么就相當(dāng)于使用一
個(gè)已經(jīng)與服務(wù)端建立了連接的端口再次與遠(yuǎn)程建立連接。
此時(shí),如果在“命令提示符"運(yùn)行"netstat-a”,則會(huì)看到類似下面的的輸出:
TCPjimmy:85000.0.0.0:0LISTENING
TCPjimmy:8500localhost:10282ESTABLISHED
TCPjimmy:8500localhost:10283ESTABLISHED
TCPjimmy:8500localhost:10284ESTABLISHED
TCPjimmy:10282localhost:8500ESTABLISHED
TCPjimmy:10283localhost:8500ESTABLISHED
TCPjimmy:10284localhost:8500ESTABLISHED
可以看到創(chuàng)建了三個(gè)連接對(duì),并且8500端口持續(xù)保持偵聽狀態(tài),從這里以及上面我們
可以推斷出TcpListener的Start。方法是一個(gè)異步方法。
3.服務(wù)端獲取客戶端連接
3.1獲取單一客戶端連接
上面服務(wù)端、客戶端的代碼已經(jīng)建立起了連接,這通過使用“netstat-a”命令,從
端口的狀態(tài)可以看出來,但這是操作系統(tǒng)告訴我們的。那么我們現(xiàn)在需要知道的就是:服務(wù)
端的程序如何知道已經(jīng)與一個(gè)客戶端建立起了連接?
服務(wù)器端開始偵聽以后,可以在TcpListener實(shí)例上調(diào)用AcceptTcpClient()來獲取與
一個(gè)客戶端的連接,它返回一個(gè)TcpClient類型實(shí)例。此時(shí)它所包裝的是由服務(wù)端去往客戶
端的Socket,而我們?cè)诳蛻舳藙?chuàng)建的TcpClient則是由客戶端去往服務(wù)端的。這個(gè)方法是
一個(gè)同步方法(或者叫阻斷方法,blockmethod),意思就是說,當(dāng)程序調(diào)用它以后,它會(huì)
一直等待某個(gè)客戶端連接,然后才會(huì)返回,否則就會(huì)一直等下去。這樣的話,在調(diào)用它以后,
除非得到一個(gè)客戶端連接,不然不會(huì)執(zhí)行接下來的代碼。一個(gè)很好的類比就是
Console.ReadLineO方法,它讀取輸入在控制臺(tái)中的一行字符串,如果有輸入,就繼續(xù)執(zhí)行
下面代碼;如果沒有輸入,就會(huì)一直等待下去。
classServer{
staticvoidMain(string[]args){
Console.WriteLineCServerisrunning…”);
IPAddressip=newIPAddress(newbyte[]{127,0,0,1));
TcpListenerlistener=newTcpListener(ip,8500);
listener.Start();//開始偵聽
Console.WriteLine(z,StartListening…”);
//獲取一個(gè)連接,中斷方法
TcpClientremoteClient=listener.AcceptTcpClient();
//打印連接到的客戶端信息
Console.WriteLine(^ClientConnected!{0}<一{1}”,
remoteClient.Client.LocalEndPoint,remoteClient.Client.RemoteEndPoint);
〃按Q退出
)
)
運(yùn)行這段代碼,會(huì)發(fā)現(xiàn)服務(wù)端運(yùn)行到listener.AcceptTcpClient。時(shí)便停止了,并不
會(huì)執(zhí)行下面的Console.WriteLine()方法。為了讓它繼續(xù)執(zhí)行下去,必須有一個(gè)客戶端連接
到它,所以我們現(xiàn)在運(yùn)行客戶端,與它進(jìn)行連接。簡單起見,我們只在客戶端開啟一個(gè)端口
與之連接:
classClient{
staticvoidMain(string[]args){
Console.WriteLine(''ClientRunning…〃);
TcpClientclient=newTcpClient();
try(
client.Connect(,,localhostz,,8500);//與服務(wù)器連接
}catch(Exceptionex){
Console.WriteLine(ex.Message);
return;
)
//打印連接到的服務(wù)端信息
Console.WriteLine(''ServerConnected!{0}-->{1}〃,
client.Client.LocalEndPoint,client.Client.RemoteEndPoint);
//按Q退出
)
)
此時(shí),服務(wù)端、客戶端的輸出分別為:
//服務(wù)端
Serverisrunning
StartListening
ClientConnected!127.0.0.1:8500<一一127.0.0.1:5188
//客戶端
ClientRunning
ServerConnected!127.0.0.1:5188一>127.0.0.1:8500
3.2獲取多個(gè)客戶端連接
現(xiàn)在我們?cè)俳又紤],如果有多個(gè)客戶端發(fā)動(dòng)對(duì)服務(wù)器端的連接會(huì)怎么樣,為了避免
你將瀏覽器向上滾動(dòng),來查看上面的代碼,我將它拷貝了下來,我們先看下客戶端的關(guān)鍵代
碼:
TcpClientclient;
for(inti=0;i<=2;i++){
try(
client=newTcpClient();
client.Connect(z,localhost,z,8500);//與服務(wù)器連接
}catch(Exceptionex){
Console.WriteLine(ex.Message);
return;
}
//打印連接到的服務(wù)端信息
Console.WriteLine(''ServerConnected!{0}—>⑴〃,
client.Client.LocalEndPoint,client.Client.RemoteEndPoint);
如果服務(wù)端代碼不變,我們先運(yùn)行服務(wù)端,再運(yùn)行客戶端,那么接下來會(huì)看到這樣的
輸出:
//服務(wù)端
Serverisrunning
StartListening
ClientConnected!127.0.0.1:8500<-127.0.0.1:5226
//客戶端
ClientRunning
ServerConnected!127.0.0.1:5226-->127.0.0.1:8500
ServerConnected!127.0.0.1:5227―>127.0.0.1:8500
ServerConnected!127.0.0.1:5228一>127.0.0.1:8500
就又回到了本章第2.2小節(jié)“多個(gè)客戶端與服務(wù)端連接”中的處境:盡管有三個(gè)客戶
端連接到了服務(wù)端,但是服務(wù)端程序只接收到了一個(gè)。這是因?yàn)榉?wù)端只調(diào)用了一次
listener.AcceptTcpClient(),而它只對(duì)應(yīng)一個(gè)連往客戶端的Socket。但是操作系統(tǒng)是知
道連接已經(jīng)建立了的,只是我們程序中沒有處理到,所以我們當(dāng)我們輸入“netstal-a”時(shí),
仍然會(huì)看到3對(duì)連接都一經(jīng)建立成功。
為了能夠接收到三個(gè)客戶端的連接,我們只要對(duì)服務(wù)端稍稍進(jìn)行?下修改,將
AcceptTcpClient方法放入一個(gè)do/while循環(huán)中就可以了:
Console.WriteLine(''StartListening
while(true){
//獲取個(gè)連接,同步方法
TcpClientremoteClient=listener.AcceptTcpClient();
//打印連接到的客戶端信息
Console.WriteLine(''ClientConnected!{0}<一{1}〃,
remoteClient.Client.LocalEndPoint,remoteClient.Client.RemoteEndPoint);
這樣看上去是一個(gè)死循環(huán),但是并不會(huì)讓你的機(jī)器系統(tǒng)資源迅速耗盡。因?yàn)榍懊婕航?jīng)
說過了,AcceptTcpClient。再?zèng)]有收到客戶端的連接之前,是不會(huì)繼續(xù)執(zhí)行的,它的大部
分時(shí)間都在等待。另外,服務(wù)端兒乎總是要保持在運(yùn)行狀態(tài),所以這樣做并無不可,還可以
骷“按Q退出”那段代碼。此時(shí)再運(yùn)行代碼,會(huì)看到服務(wù)端可以收到3個(gè)客戶端的連接了。
Serverisrunning
StartListening
ClientConnected!127.0.0.1:8500<--127.0.0.1:5305
ClientConnected!127.0.0.1:8500<-127.0.0.1:5306
ClientConnected!127.0.0.1:8500<-127.0.0.1:5307
c#網(wǎng)絡(luò)編程(同步傳輸字符串)-Part.2
服務(wù)端客戶端通信
在與服務(wù)端的連接建立以后,我們就可以通過此連接來發(fā)送和接收數(shù)據(jù)。端口與端口
之間以流(Stream)的形式傳輸數(shù)據(jù),因?yàn)閹缀跞魏螌?duì)象都可以保存到流中,所以實(shí)際上可
以在客戶端與服務(wù)端之間傳輸任何類型的數(shù)據(jù)。對(duì)客戶端來說,往流中寫入數(shù)據(jù),即為向服
務(wù)器傳送數(shù)據(jù);從流中讀取數(shù)據(jù),即為從服務(wù)端接收數(shù)據(jù)。對(duì)服務(wù)端來說,往流中寫入數(shù)據(jù),
即為向客戶端發(fā)送數(shù)據(jù);從流中讀取數(shù)據(jù),即為從客戶端接收數(shù)據(jù)。
同步傳輸字符串
我們現(xiàn)在考慮這樣一個(gè)任務(wù):客戶端打印一串字符串,然后發(fā)往服務(wù)端,服務(wù)端先輸
出它,然后將它改為大寫,再回發(fā)到客戶端,客戶端接收到以后,最后再次打印一遍它。我
們將它分為兩部分:1、客戶端發(fā)送,服務(wù)端接收并輸出;2、服務(wù)端回發(fā),客戶端接收并輸
出。
1.客戶端發(fā)送,服務(wù)端接收并輸出
1.1服務(wù)端程序
我們可以在TcpClient上調(diào)用GetStreamO方法來獲得連接到遠(yuǎn)程計(jì)算機(jī)的流。注意這
里我用了遠(yuǎn)程這個(gè)詞,當(dāng)在客戶端調(diào)用時(shí),它得到連接服務(wù)端的流;當(dāng)在服務(wù)端調(diào)用時(shí),它
獲得連接客戶端的流。接下來我們來看一下代碼,我們先看服務(wù)端(注意這里沒有使用
do/while循環(huán)):
classServer{
staticvoidMain(string[]args){
constintBufferSize=8192;//緩存大小,8192字節(jié)
Console.WriteLine(z,Serverisrunning…”);
IPAddressip=newIPAddress(newbyte[]{127,0,0,1));
TcpListenerlistener=newTcpListener(ip,8500);
listener.Start();//開始偵聽
Console.WriteLineCStartListening…”);
//獲取一個(gè)連接,中斷方法
TcpClientremoteClient=listener.AcceptTcpClientO;
//打印連接到的客戶端信息
Console.WriteLine(^ClientConnected!{0}<一{1}”,
remoteClient.Client.LocalEndPoint,remoteClient.Client.RemoteEndPoint);
//獲得流,并寫入buffer中
NetworkstreamstreamToClient=remoteClient.GetStreamO;
byte[]buffer=newbyte[BufferSize];
intbytesRead=streamToClient.Read(buffer,0,BufferSize);
Console.WriteLine(^Readingdata,{0}bytes…“,bytesRead);
//獲得請(qǐng)求的字符串
stringmsg=Encoding.Unicode.GetString(buffer,0,bytesRead);
Console.WriteLine(""Received:{0}",msg);
〃按Q退出
)
)
這段程序的上半部分已經(jīng)很熟悉了,我就不再解釋。remoteClient.GetStreamO方法
獲取到了連接至客戶端的流,然后從流中讀出數(shù)據(jù)并保存在了buffer緩存中,隨后使用
Encoding.Unicode.GetStringO方法,從緩存中獲取到了實(shí)際的字符串。最后將字符串打印
在了控制臺(tái)上。這段代碼有個(gè)地方需要注意:在能夠讀取的字符串的總字節(jié)數(shù)大于
BufferSize的時(shí)候會(huì)出現(xiàn)字符串截?cái)喱F(xiàn)象,因?yàn)榫彺嬷械臄?shù)目總是有限的,而對(duì)于大對(duì)象,
比如說圖片或者其它文件來說,則必須采用“分次讀取然后轉(zhuǎn)存”這種方式,比如這樣:
//獲取字符串
byte[]buffer=newbyte[BufferSize];
intbytesRead;//讀取的字節(jié)數(shù)
MemoryStreammsStream=newMemoryStream();
do{
bytesRead=streamToClient.Read(buffer,0,BufferSize);
msStream.Write(buffer,0,bytesRead);
}while(bytesRead>0);
buffer=msStream.GetBuffer();
stringmsg=Encoding.Unicode.GetString(buffer);
這里我沒有使用這種方法,?個(gè)是因?yàn)椴幌腙P(guān)注在太多的細(xì)節(jié)上面,一個(gè)是因?yàn)閷?duì)于
字符串來說,8192字節(jié)已經(jīng)很多了,我們通常不會(huì)傳遞這么多的文本。當(dāng)使用Unicode編
碼時(shí),8192字節(jié)可以保存4096個(gè)漢字和英文字符。使用不同的編碼方式,占用的字節(jié)數(shù)有
很大的差異,在本文最后面,有一段小程序,可以用來測試Unicode、UTF8、ASCH三種常
用編碼方式對(duì)字符串編碼時(shí),占用的字節(jié)數(shù)大小。
現(xiàn)在對(duì)客戶端不做任何修改,然后運(yùn)行先運(yùn)行服務(wù)端,再運(yùn)行客戶端。結(jié)果我們會(huì)發(fā)
現(xiàn)這樣一件事:服務(wù)端再打印完"ClientConnected!127.0.0.1:8500<-127.0.0.1:xxxxx”
之后,再次被阻塞了,而沒有輸出“Readingdata,{0}bytes??梢?,與
AcceptTcpClient()方法類似,這個(gè)Read。方法也是同步的,只有當(dāng)客戶端發(fā)送數(shù)據(jù)的時(shí)候,
服務(wù)端才會(huì)讀取數(shù)據(jù)、運(yùn)行此方法,否則它便會(huì)一直等待。
1.2客戶端程序
接下來我們編寫客戶端向服務(wù)器發(fā)送字符串的代碼,與服務(wù)端類似,它先獲取連接服
務(wù)器端的流,將字符串保存到buffer緩存中,再將緩存寫入流,寫入流這一過程,相當(dāng)于
將消息發(fā)往服務(wù)端。
classCllent{
staticvoidMain(string口args){
Console.WriteLine("ClientRunning…”);
TcpCliontclient;
try{
client=newTcpClient();
client.Connect(/zlocalhost/z,8500);//與服務(wù)器連接
}catch(Exceptionex){
Console.WriteLine(ex.Message);
return;
)
//打印連接到的服務(wù)端信息
Console.WriteLine(Z/ServerConnected!{0}一>{1}”,
client.Client.LocalEndPoint,client.Client.RemoteEndPoint);
stringmsg="\"WelcomeToTraceFact.Net\"";
NetworkstreamstreamToServer=client.GetStreamO;
byte[]buffer=Encoding.Unicode.GetBytes(msg);//獲得緩存
streamToServer.Write(buffer,0,buffer.Length);//發(fā)往服務(wù)器
Console.WriteLine(^Sent:{0}",msg);
〃按Q退出
}
)
現(xiàn)在再次運(yùn)行程序,得到的輸出為:
//服務(wù)端
Serverisrunning
StartListening
ClientConnected!127.0.0.1:8500<—127.0.0.1:7847
Readingdata,52bytes
Received:"WelcomeToTraceFact.Net”
輸入"Q"鍵退出。
//客戶端
ClientRunning
ServerConnected!127.0.0.1:7847—>127.0.0.1:8500
Sent:"WelcomeToTraceFact.Net,z
輸入"Q"鍵退出。
再繼續(xù)進(jìn)行之前,我們假設(shè)客戶端可以發(fā)送多條消息,而服務(wù)端要不斷的接收來自客
戶端發(fā)送的消息,但是上面的代碼只能接收客戶端發(fā)來的一條消息,因?yàn)樗呀?jīng)輸出了“輸
入Q鍵退出",說明程序已經(jīng)執(zhí)行完畢,無法再進(jìn)行任何動(dòng)作。此時(shí)如果我們?cè)匍_啟一個(gè)客
戶端,那么出現(xiàn)的情況是:客戶端可以與服務(wù)器建立連接,也就是netstat-a顯示為
ESTABLISHED,這是操作系統(tǒng)所知道的;但是由于服務(wù)端的程序已經(jīng)執(zhí)行到了最后一步,只
能輸入Q鍵退出,無法再采取任何的動(dòng)作。
回想一個(gè)上面我們需要一個(gè)服務(wù)器對(duì)應(yīng)多個(gè)客戶端時(shí),對(duì)AcceptTcpClient()方法的處
理辦法,將它放在了do/while循環(huán)中;類似地,當(dāng)我們需要一個(gè)服務(wù)端對(duì)同一個(gè)客戶端的
多次請(qǐng)求服務(wù)時(shí),可以將Read。方法放入到do/while循環(huán)中。
現(xiàn)在,我們大致可以得出這樣幾個(gè)結(jié)論:
如果不使用do/while循環(huán),服務(wù)端只有一個(gè)listener.AcceptTcpClient()
方法和一個(gè)TcpClient.GetStreamO,Read()方法,則服務(wù)端只能處理到同
-客戶端的條請(qǐng)求。
?如果使用一個(gè)do/while循環(huán),并將listener.AcceptTcpClient()方法和
TcpClient.GetStreamO.Read。方法都放在這個(gè)循環(huán)以內(nèi),那么服務(wù)端將
可以處理多個(gè)客戶端的?條請(qǐng)求。
?如果使用一個(gè)do/while循環(huán),并將listener.AcceptTcpClient()方法放
在循環(huán)之外,將TcpClient.GetStreamO.Read。方法放在循環(huán)以內(nèi),那么
服務(wù)端可以處理一個(gè)客戶端的多條請(qǐng)求。
?如果使用兩個(gè)do/while循環(huán),對(duì)它們進(jìn)行分別嵌套,那么結(jié)果是什么呢?
結(jié)果并不是可以處理多個(gè)客戶端的多條請(qǐng)求。因?yàn)槔飳拥膁o/while循環(huán)總
是在為一個(gè)客戶端服務(wù),因?yàn)樗鼤?huì)中斷在TcpClient.GetStreamO.ReadO
方法的位置,而無法執(zhí)行完畢。即使可以通過某種方式讓里層循環(huán)退出,
比如客戶端往服務(wù)端發(fā)去“exit”字符串時(shí),服務(wù)端也只能挨個(gè)對(duì)客戶端
提供服務(wù)。如果服務(wù)端想執(zhí)行多個(gè)客戶端的多個(gè)請(qǐng)求,那么服務(wù)端就需要
采用多線程。主線程,也就是執(zhí)行外層do/while循環(huán)的線程,在收到一個(gè)
TcpClient之后,必須將里層的do/while循環(huán)交給新線程去執(zhí)行,然后主
線程快速地重新回到listener.AcceptTcpClient()的位置,以響應(yīng)其它的
客戶端。
對(duì)于第四種情況,實(shí)際上是構(gòu)建一個(gè)服務(wù)端更為通常的情況,所以需要專門開辟一個(gè)
章節(jié)討論,這里暫且放過。而我們上面所做的,即是列出的第一種情況,接下來我們?cè)俜謩e
看一下第二種和第三種情況。
對(duì)于第二種情況,我們按照上面的敘述先對(duì)服務(wù)端進(jìn)行一下改動(dòng):
do{
//獲取一個(gè)連接,中斷方法
TcpClientremoteClient=listener.AcceptTcpClient();
//打印連接到的客戶端信息
Console.WriteLine(Z,C1ientConnected!{0}<一{1}”,
remoteClient.Client.LocalEndPoint,remoteClient.Client.RemoteEndPoint);
//獲得流,并寫入buffer中
NetworkStroanistreamToClient=remoteClient.GetStreamO;
byte[]buffer=newbyte[BufferSize];
intbytesRead=streamToClient.Read(buffer,0,BufferSize);
Console.WriteLine(^Readingdata,{0}bytes…〃,bytesRead);
//獲得清求的字符串
stringmsg=Encoding.Unicode.GetString(buffer,0,bytesRead);
Console.WriteLine(''Received:{0}”,msg);
}while(true);
然后啟動(dòng)多個(gè)客戶端,在服務(wù)端應(yīng)該可以看到下面的輸出(客戶端沒有變化):
Serverisrunning
StartListening
ClientConnected!127.0.0.1:8500<-:8196
Readingdata,52bytes
Received:"WelcomeToTraceFact.Netz,
ClientConnected!127.0.0.1:8500<—127.0.0.1:8199
Readingdata,52bytes
Received:"WelcomeToTraceFact.Net”
山第2種情況改為第3種情況,只需要將do向下挪動(dòng)兒行就可以了:
//獲取一個(gè)連接,中斷方法
TcpCIientremoteClient=listener.AcceptTcpClient();
//打印連接到的客戶端信息
Console.WriteLine(,zClientConnected!{0}<--{1}〃,
remoteClient.Client.LocalEndPoint,remoteClient.Client.RemoteEndPoint);
//獲得流,并寫入buffer中
NetworkStreanistreamToClient=remoteClient.GetStreamO;
do{
byte[]buffer=newbyte[BufferSize];
intbytesRead=streamToClient.Read(buffer,0,BufferSize);
Console.WriteLine("Readingdata,{0}bytes…“、bytesRead);
//獲得請(qǐng)求的字符串
stringmsg=Encoding.Unicode.GetString(buffer,0,bytesRead);
Console.WriteLine("Received:{0},,msg);
}while(true);
然后我們?cè)俑膭?dòng)一下客戶端,讓它發(fā)送多個(gè)請(qǐng)求。當(dāng)我們按下S的時(shí)候,可以輸入一
行字符串,然后將這行字符串發(fā)送到服務(wù)端;當(dāng)我們輸入X的時(shí)候則退出循環(huán):
NetworkStreamstreamToServer=client.GetStreamO;
ConsoleKeykey;
Console.WriteLine(Z/Menu:S-Send,X-Exit");
do{
key=Console.ReadKey(true).Key;
if(key==ConsoleKey.S){
//獲取輸入的字符串
Console.Write(,zInputthemessage:〃);
stringmsg=Console.ReadLineO;
byte口buffer=Encoding.Unicode.GetBytes(msg);//獲得緩存
streamToServer.Write(buffer,0,buffer.Length);//發(fā)往服務(wù)器
Console.WriteLine("Sent:{0}〃,msg);
)
}while(key!=ConsoleKey.X);
接下來我們先運(yùn)行服務(wù)端,然后再運(yùn)行客戶端,輸入一些字符串,來進(jìn)行測試,應(yīng)該
能夠看到下面的輸出結(jié)果:
//服務(wù)端
Serverisrunning
StartListening
ClientConnected!127.0.0.1:8500<-:11004
Readingdata,44bytes
Received:歡迎訪問我的博客:TraceFact.Net
Readingdata,14bytes
Received:我們一起進(jìn)步!
〃客戶端
ClientRunning
ServerConnected!127.0.0.1:11004一>127.0.0.1:8500
Menu:S-Send,X-Exit
Inputthemessage:歡迎訪問我的博客:TraceFact.Net
Sent:歡迎訪問我的博客:TraceFact.Net
Inputthemessage:我們一起進(jìn)步!
Sent:我們一起進(jìn)步!
這里還需要注意一點(diǎn),當(dāng)客戶端在TcpClient實(shí)例上調(diào)用Close。方法,或者在流上調(diào)
用Dispose。方法,服務(wù)端的streamToClient.Read()方法會(huì)持續(xù)地返回0,但是不拋出異
常,所以會(huì)產(chǎn)生一個(gè)無限循環(huán);而如果直接關(guān)閉掉客戶端,或者客戶端執(zhí)行完畢但沒有調(diào)用
stream.Dispose?;蛘逿cpClient.Close(),如果服務(wù)器端此時(shí)仍阻塞在Read。方法處,則
會(huì)在服務(wù)器端拋出異常:“遠(yuǎn)程主機(jī)強(qiáng)制關(guān)閉了一個(gè)現(xiàn)有連接”。因此,我們將服務(wù)端的
streamToClient.Read()方法需要寫在一個(gè)try/catch中。同理,如果在服務(wù)端已經(jīng)連接到
客戶端之后,服務(wù)端調(diào)用remoteClient.CloseO,則客戶端會(huì)得到異常”無法將數(shù)據(jù)寫入
傳輸連接:您的主機(jī)中的軟件放棄了一個(gè)已建立的連接。;而如果服務(wù)端直接關(guān)閉程序的
話,則客戶端會(huì)得到異?!盁o法將數(shù)據(jù)寫入傳輸連接:遠(yuǎn)程主機(jī)強(qiáng)迫關(guān)閉了一個(gè)現(xiàn)有的連
接。。因此,它們的讀寫操作必須都放入到try/catch塊中。
2.服務(wù)端回發(fā),客戶端接收并輸出
2.2服務(wù)端程序
我們接著再進(jìn)行進(jìn)一步處理,服務(wù)端將收到的字符串改為大寫,然后回發(fā),客戶端接
收后打印。此時(shí)它們的角色和上面完全進(jìn)行了一下對(duì)調(diào):對(duì)于服務(wù)端來說,就好像剛才的客
戶端一樣,將字符串寫入到流中;而客戶端則同服務(wù)端一樣,接收并打印。除此以外,我們
最好對(duì)流的讀寫操作加上lock,現(xiàn)在我們直接看代碼,首先看服務(wù)端:
classServer(
staticvoidMain(string1]args){
constintBufferSize=8192;//緩存大小,8192Bytes
ConsoleKeykey;
Console.WriteLine(,zServerisrunning…〃);
IPAddressip=newIPAddress(newbyte[]{127,0,0,1});
TcpListenerlistener=newTcpListener(ip,8500);
listener.Start();//開始偵聽
Console.WriteLine("StartListening
//獲取?個(gè)連接,同步方法,在此處中斷
TcpClientremoteClient=listener.AcceptTcpClient();
//打印連接到的客戶端信息
Console.WriteLine(''ClientConnected!{0}<一{1}〃,
remoteClient.Client.LocalEndPoint,remoteClient.Client.RemoteEndPoint);
//獲得流
NetworkstreamstreamToClient=remoteClient.GetStreamO;
do{
//寫入buffer中
byte[]buffer=newbyte[BufferSize];
intbytesRead;
try(
lock(streamToClient){
bytesRead=streamToClient.Read(buffer,0,BufferSize);
}
if(bytesRead==0)thrownewException(〃讀取到0字節(jié)〃);
Console.WriteLine(/zReadingdata,{0}bytes…bytesRead);
〃獲得請(qǐng)求的字符串
stringmsg=Encoding.Unicode.GetString(buffer,0,bytesRead);
Console.WriteLine(''Received:{0}”,msg);
//轉(zhuǎn)換成大寫并發(fā)送
msg=msg.ToUpper();
buffer=Hncoding.Unicode.GetBytes(msg);
lock(streamToClient){
streamToClient.Write(buffer,0,buffer.Length);
)
Console.WriteLine("Sent:{0}〃,msg);
}catch(Exceptionex){
Console.WriteLine(ex.Message);
break;
)
}while(true);
streamToClient.DisposeO;
remoteClient.Close();
Console.WriteLine(/z\n\n輸入\〃Q\"鍵退出。;
do{
key=Console.ReadKey(true).Key;
}while(key!=ConsoleKey.Q);
)
)
接下來是客戶端:
classClient{
staticvoidMain(string1]args){
Console.WriteLine(Z/C1ientRunning…;
TcpClientclient;
ConsoleKeykey;
constintBufferSize=8192;
try{
client=newTcpClient();
client.Connect(z,localhostz,,8500);//與服務(wù)器連接
}catch(Exceptionex){
Console.WriteLine(ex.Message);
return;
)
//打印連接到的服務(wù)端信息
Console.WriteLine("'ServerConnected!{0}一>{1}〃,
client.Client.LocalEndPoint,client.Client.RemoteEndPoint);
NetworkstreamstreamToServer=client.GetStreamO;
Console.WriteLine(z,Menu:S-Send,X-Exit");
do{
key=Console.ReadKey(true).Key;
if(key二二ConsoleKey.S){
//獲取輸入的字符串
Console.Write(z,Inputthemessage:〃);
stringmsg=Console.ReadLineO;
byte[]buffer=Encoding.Unicode.GetBytes(msg);//獲得緩存
try(
lock(streamToServer){
streamToServer.Write(buffer,0,buffer.Length);//發(fā)往服務(wù)器
)
Console.WriteLine("Sent:{0}〃,msg);
intbytesRead;
buffer=newbyte[BufferSize];
lock(streamToServer){
bytesRead=streamToServer.Read(buffer,0,BufferSize);
)
msg=Encoding.Unicode.GetString(buffer,0,bytesRead);
Console.WriteLine(''Received:{0}〃,msg);
}catch(Exceptionex){
Console.WriteLine(ex.Message);
break;
}
}
}while(key!=ConsoleKey.X);
streamToServer.DisposeO;
client.Close();
Console.WriteLine(〃\n\n輸入、"Q\〃鍵退出。;
do{
key=Console.ReadKey(true).Key;
}while(key!=ConsoleKey.Q);
)
)
最后我們運(yùn)行程序,然后輸入一串英文字符串,然后看一下輸出:
//客戶端
Clientisrunning
ServerConnected!127.0.0.1:12662一>127.0.0.1:8500
Menu:S-Send,X-Exit
Inputthemessage:Hello,I'mjimmyzhang.
Sent:Hello,I'mjimmyzhang.
Received:HELLO,I'MJIMMYZHANG.
//服務(wù)端
Serverisrunning
StartListening
ClientConnected!:8500<-:12662
Readingdata,46b
溫馨提示
- 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請(qǐng)下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請(qǐng)聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會(huì)有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
- 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫網(wǎng)僅提供信息存儲(chǔ)空間,僅對(duì)用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對(duì)用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對(duì)任何下載內(nèi)容負(fù)責(zé)。
- 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請(qǐng)與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對(duì)自己和他人造成任何形式的傷害或損失。
最新文檔
- 有機(jī)蔬菜怎樣種植
- 品牌策劃與營銷策略培訓(xùn)材料
- 電子商務(wù)物流時(shí)效分析對(duì)比表
- 婚姻考題復(fù)習(xí)試題含答案
- 三農(nóng)信息采集與共享平臺(tái)建設(shè)方案
- 農(nóng)業(yè)資源整合與可持續(xù)發(fā)展解決方案
- 出版行業(yè)數(shù)字化內(nèi)容管理系統(tǒng)設(shè)計(jì)
- 高效辦公實(shí)踐教程
- 通訊設(shè)備業(yè)5G基站建設(shè)與維護(hù)管理方案
- 農(nóng)業(yè)科技精準(zhǔn)種植與養(yǎng)殖技術(shù)推廣方案
- 文明交通知識(shí)培訓(xùn)課件
- 2025年亳州職業(yè)技術(shù)學(xué)院單招職業(yè)適應(yīng)性測試題庫完整
- 2025年公立醫(yī)院與心理咨詢機(jī)構(gòu)合作協(xié)議
- 2025年南京城市職業(yè)學(xué)院單招職業(yè)技能測試題庫完整版
- (統(tǒng)編版)2025年小升初語文模擬考試卷(附帶答案)
- 2024年廣東省中考數(shù)學(xué)試卷(附答案)
- 旅行社安全管理培訓(xùn)
- DB65T 8024-2024 建筑用室外氣象參數(shù)標(biāo)準(zhǔn)
- 《預(yù)制高強(qiáng)混凝土風(fēng)電塔筒生產(chǎn)技術(shù)規(guī)程》文本附編制說明
- ICD-11(國際疾病分類第十一修訂)重點(diǎn)基礎(chǔ)知識(shí)總結(jié)-
- 重慶市2025年初中學(xué)業(yè)水平暨高中招生考試數(shù)學(xué)試題預(yù)測卷(一)
評(píng)論
0/150
提交評(píng)論