还剩17页未读,继续阅读
本资源只提供10页预览,全部文档请下载后查看!喜欢就下载吧,查找使用更方便
文本内容:
网络编程(订立协议和发送文件)一c#Part.4文件传输前面两篇文章所使用的范例都是传输字符串,有的时候我们可能会想在服务端和客户端之间传递文件比如,考虑这样一种情况,假如客户端显示了一个菜单,当我们输入S
1、S2或S3(S为Send缩写)时,分别向服务端发送文件ClientOL jpg、Client
02.jpg、Cl i ent03o jpg;当我们输入RI、R2或R3时(R为Receive缩写),则分别从服务端接收文件ServerOljpg、Serv e r02jpg S e rvOe rO3jpg那么,我们该如何完成这件事呢?此时可能有这样两种做法o•类似于FTP协议,服务端开辟两个端口,并持续对这两个端口侦听一个用于接收字符串,类似于FTP的控制端口,它接收各种命令(接收或发送文件);一个用于传输数据,也就是发送和接收文件•服务端只开辟一个端口,用于接收字符串,我们称之为控制端口当接到请求之后,根据请求内容在客户端开辟一个端口专用于文件传输,并在传输结束后关闭端口现在我们只关注于上面的数据端口,回忆一下在第二篇中我们所总结的,可以得出当我们使用上面的方法一时,服务端的数据端口可以为多个客户端的多次请求服务;当我们使用方法二时,服务端只为一个客户端的一次请求服务,但是因为每次请求都会重新开辟端口,所以实际上还是相当于可以为多个客户端的多次请求服务同时,因为它只为一次请求服务,所以我们在数据端口上传输文件时无需采用异步传输方式但在控制端口我们仍然需要使用异步方式.从上面看出第一种方式要好得多,但是我们将采用第二种方式至于原因,你可以回顾一下Pa rt.1(基本概念和操作)中关于聊天程序模式的讲述,因为接下来一篇文章我们将创建一个聊天程序,而这个聊天程序采用第三种模式,所以本文的练习实际是对下一篇的一个铺垫s endFilep rot o col;总privatevoi dreceiveFi1e Fi1eP r o t oco1protoc o1{//获取远程客户端的位置I PEn dPoin t endpoint二Aclient.Clien t.Remot eEn dP o int a s IPEn d Point;I PAd dressi p=endpoint Addres s;//使用新端口号,获得o A远程用于接收文件的端口a e nd p oint=new IP End P o inti p,protocol.Port;AA//连接到远程客户端a TcpClientlo c a1Client;try{a1o ca1Clie n t=newTcpC1i ent;1o calClAi ento C o nn e c t endp oint;}cat ch{C onso1e W r iteLine无法连接到客户端--------〉{0},endp o iont return;A}a//获取发送文件的流Netw o rkStr e a m str e amT o Clie n t=localC1ient.G e tStreamO;a//随机生成一个在当前目录下的文件名称a st r ingp ath=a Enviro n ment.Cu rr entD irect o ry+”/〃+ge n e rateFileName protoco
1.Fi1eN a me;by t e[]fi1eBuf f e r=ne wbyte
[1024];//每次收1FKBAi le S tream f s=new F ileS t r e a mpath,File Mod e.Crea t eNew,Fi1e Access.Write//从缓存bu f fer中读入到文件流中int byte s Read;^int t ot aIByte s=0;d o bytesRea d=s tr e amToC liento Readb u ff e r,0,{ABuffe r S ize;fs Writebu ffer,0,b ytesR e ad;ototalBy t es+二by t esReadj^Co n s o1e Write Line Re coe i vi n g{0}by te s..t o t aIBytes}whi1eb ytesReC onsole Write L i n e nTo t a1{0}b ytesr ec ei ved,D one!〃,to tai Boyte s;streamT oCliento Disp o se;fso Dispos e;localCli e nt.C lo s e;A}//随机获取一个图片名称p r i va t estringge n era teF ileN a m e str i n g f ileName{a DateTime now=Date Time.Now r eturnS t r i n gForm a to”{0}—{1}{2}_{3}〃,n oWo Minute,now.Sec ond,n owoMillisecond,f i1eName^;A}A}这里应该没有什么新知识,需要注意的地方有这么几个•在OnReadComple te回调方法中的fo r eac h循环,我们使用委托异步调用了handle Pro toco1方法,这是因为hand1eProtoc o1即将执行的是一个读取或接收文件的操作,也就是一个相对耗时的操作•在hand1eProt o col方法中,我们深切体会了定义Protoc o1Hei p e r类和F i leProt0coi结构的好处如果没有定义它们,这里将是不堪入目的处理XML以及类型转换的代码.•ha nd1ePro t oco1方法中进行了一个条件判断,注意s e ndFile方法我屏蔽掉了,这个还没有实现,但是我想你已经猜到它将是后面要实现的内容.•receiveFile方法是实际接收客户端发来文件的方法,这里没有什么特别之处需要注意的是文件存储的路径,它保存在了当前程序执行的目录下,文件的名称我使用gen erateFileNa me生成了一个与时间有关的随机名称.客户端的实现3o2我们现在先不着急实现客户端SI、R1等用户菜单,首先完成发送文件这一功能,实际上,就是为上一节SendMessag e加一个姐妹方法Sen d Filec1a ssCl i ent{a staticvo i dMa i nst r ing[]ar gs{Co n so IeKey key;ServerCl ient client=n ewSe rverClie nt;A string filePathA二Envi r onme n t Curr entDirecto ry+”/〃+”Cl i ent01jpg〃;aoif Fi1e.Ex i s ts f i leP a thclient.BeginSend F ile filePath;a ConsoleWr iteLin e\n\n输入\Q\〃键退出”;d o{△key=Console.Read Keyt rueKey;}while key!=Con s oleKe yQ;o}}Apubli c cl assSe rv erCl i ent{pr i v atecon s ti ntB uffer S iz e=8192;priv a tebyte[]b uf fer;privat efcpCl i e nt client;pri v ateN etworkS t r eam streamT oSAe rver;n pub1ic S e rv e rCl i en t{a try1ien t={A CnewlcpClient;c1i e nt.Connec t1o c a Ihost,8500;//与服务器连接a}c a tc hExcept i on ex Cons o le.Wri teLi n ee x{AMe ss age;r e tu rn;}总buffer=newb yte[BufferSize];a//打印连接到的服务端信息C onso1e Wri t eLineS erv erCo nne cte d!{0}----------o{1}H,c1iento C1ie nt.Loc a1EndPo i nt,cl i ent.Client.ReAm o t eEn dPo i nt;ns treamTo Server=cli e nt.G etStre a m}//发送消息到服务端pub1icv oid S e ndMessage stringms g{Aa byte[]temp=Encoding.U nic o de.GetBytes msg;//获得缓存try{a lockstre a mToS e rv erstr eamT o{ASer ver.Write temp,0,t emp.L engt h;//发往服务器}a Consol e.Wr ite Line Sent:{0}〃,msg;}catAc hEx cept ionex{a Cons o1e.WriteLine ex.M e ss age;ret u rn;a}A}A//发送文件一异步方法pu blievoid BeginS en dF i1e s t r i n g f i lePathParam eterizedThre adStar{At start=new Par ame terized Threa dS t artBeginSendFile;start.Beginlnv oke fil e Path,null,nul1;闷a pr i v a te void B e ginSen d Fileobject obj{△s t ring f i1e P a t h=obj as string;^Se ndFilef ilePat h;}a//发送文件------同步方法p ub1icv oi dSendFile st ringAfilePath{IPA ddres s ip=I PAddress.ParseC/
127.
00.1j;oT c p Listene rlis te ner=new fcpL i stene r ip,0;1i stene rStar t;A//获取本地侦听的端口号a IPEndPoi n te ndPoint二li s tener.LocalEndp oint as IPEndPoin t;int listenin gPo r t=endPoi n to Por t;//获取发送的协议字符串s t ri n gfil eName=Pa th.G et Fi1eName filePath;Fi1ePro t oco lpr o tocol=new F i leProtoc o1File Requ es t Mod e.Send,1i s t eni ngPo rt,fi leName;t rin g pro=p rotocol.ToSt rin g;A Sa SendMessagepro;//发送协议到服务端//中断,等待远程连接T cpClient1o c a1Client=liste ner.Acc e ptTc pClient;Console.AWriteL ine,St a rt sendingfile.;oNetworkStr eam s tream=lo calCl ien t.G etStream;//创建文件流li1eS treamf s=newl;i1eStre amfil e PatAh,FileMo d e0p en,F i1e Acc ess Read;byte[]filo oeBuff e r=ne wby te
[1024];//每次传1KBint bytes Read inttota1Bytes=0;a//创建获取文件发送状态的类AS e nd Sta t us statu s=newSendSt at usfil e Path;//将文件流转写入网络流At ry doThread.Sie ep10;//为了更{A{A好的视觉效果,暂停10毫秒一■byte sR ea d=fsRe adf ileBsou ffe r,0,fi1e Buffe rLeng th;tream oWri tefi1e Buffer,0,byte sRead;tota1By tes+=AbytesRead;//发送了的字节数a st at us.Prin tS tatust ota1Byte s;//打印发送状态a}wh i le bytesRe a{0}b ytesse n t,D od0C onsole.Write L ineT ota1ne!,tota IBytes;}catc hCon sole.Wri teLine〃Se rver haslost;A4}s treamDi spo s e;fs Di spos e;A olo c alClie nt.C1oselist ene r.Stop}接下来我们来看下这段代码,有这么两点需要注意一下•在Main方法中可以看到,图片的位置为应用程序所在的目录,如果你跟我一样处于调试模式,那么就在解决方案的Bin目录下的D ebu g目录中放置三张图片Client
01.jpg、ClientO
2.jpg、Client03jpg,用来发往服务端•我在客户端提供了两个SendFi1e方法,和一个Be ginSendFi1e方法,分别用于同步和异步传输,其中私有的Se ndFile方法只是一个辅助方法实际上对于发送文件这样的操作我们几乎总是需要使用异步操作•SendMe ssage方法中给s tre amTo S erve r力口锁很重要,因为SendF ile方法是多线程访问的,而在SendFile方法中又调用了S end Message方法.•我另外编写了一个S e ndStatus类,它用来记录和打印发送完成的状态,已经发送了多少字节,完成度是百分之多少,等等本来这个类的内容我是直接写入在cii ent类中的,后来我觉得它执行的工作已经不属于C1i ent本身所应该执行的领域之内了,我记得这样一句话当你觉得类中的方法与类的名称不符的时候,那么就应该考虑重新创建一个类.我觉得用在这里非常恰当.下面是Su ndStatus的内容//即时计算发送文件的状态pu b1i ccl as sS endSt atus{△pri v ateFilelnf oinfo;p riv ate1ong fi1eBytes;publi cSendSta tuss tring fi lePathinf o=newF i1e InfofilePath;a f ileBy tes=i nfo.Le ngth;}岫publicv oi dPrintStatu sint sent{△st ri ng percen t=Ge tPer cents ent;Co nso1e Wri teL ine Z,S end i ng{0}by tes,{1}%..A oosent,per cent;A}M//获得文件发送的百分比p ub1i cstri ng G e tPerc entint sent{decima1al IBy tes=Co nver tToD ec imal fileBytesd ecimal currentSent=Con ver toToD ecimal sentod ecimal percent二cur ren tS ent/allBy tes*100;p ercent=M athRound percent,1;〃保留一位小数oifp erc ent.ToS trin g二二〃
100.0”r eturn“100”;els eret urn percentToString;o程序测试
3.3接下里我们运行一下程序,来检查一下输出,首先看下服务端:接着是客户端,我们能够看到发送的字节数和进度,可以想到如果是图形界面,那么我们可以通过扩展SendStatus类来创建一个进度条cT C\YIMD0YS\systeB32\cBd.exe-|n|xiSending240640bytes,
97.Sending241664bytes,
98.0zSending242688bytes」
98.5zSending243712bytes」
98.9zSending244736bytes,
99.3z
24576099.7zSending bytes」246478100z.Sending bytes」246478100Z.Sending bytes,Total246478bytes sent.Done■zdTn最后我们看下服务端的B in\D eb ug目录,应该可以看到接收到的图片:\ServerConsole\ServerConsole\bin\DebugI I名称▲大小类型£^2_26_468_C1ientO
1.jp^241KB JPEG图像叵)图像241KB JPEG6_5_92l_ClientO
1.jpg由241KB JPEG图像6_10_468_ClientO
1.jpg」241KB JPEG图像rj62_468_C1i entO
1.jpg图像fril6_30_906_Cli entO1,jpg241KB JPEG应用程序[3ServerConsole.exe11KB国24KB Program DebugServerConsole.pdbServerConsole.vshost.exe14KB应用程序本来我想这篇文章就可以完成发送和接收,不过现在看来没法实现了,因为如果继续下去这篇文章就太长了,我正尝试着尽量将文章控制在15页以内那么我们将在下篇文章中再完成接收文件这一部分订立协议1O发送文件
1.1我们先看一下发送文件的情况,如果我们想将文件cl i entOloj pg由客户端发往客户端,那么流程是什么
1.客户端开辟数据端口用于侦听,并获取端口号,假设为
80052.假设客户端输入了S1,则发送下面的控制字符串到服务端[file=Clie ntOlojpg,mod e=send,port=8005]
3.服务端收到以后,根据客户端ip和端口号与该客户端建立连接
4.客户端侦听到服务端的连接,开始发送文件
5.传送完毕后客户端、服务端分别关闭连接此时,我们订立的发送文件协议为[file=Client01jpg,mode=send,p ort=8005]o但是,由于它是一个普通的字符串,在上一篇中,我们采用了正则表达式来获取其中的有效值,但这显然不是一种好办法因此,在本文及下一篇文章中,我们采用一种新的方式来编写协议XML.对于上面的语句,我们可以写成这样的XML protocol file name=〃c1ient
01.jpg〃m ode=nsend”port二”8005〃//prot o col这样我们在服务端就会好处理得多,接下来我们来看一下接收文件的流程及其协议这里说发送、接收文件是站在客户端的立场说的,当客户端发送文件时,NOTE:对于服务器来收,则是接收文件1接收文件.2接收文件与发送文件实际上完全类似,区别只是由客户端向网络流写入数据,还是由服务端向网络流写入数据
1.客户端开辟数据端口用于侦听,假设为
80062.假设客户端输入了R1,则发送控制字符串V P r o tocol〉〈file name=ServerOl.j pg〃mod e=receiv eport=8006〃/〉〈/p rotocol到服务端.
3.服务端收到以后,根据客户端i P和端口号与该客户端建立连接.
4.客户端建立起与服务端的连接,服务端开始网络流中写入数据
5.传送完毕后服务端、客户端分别关闭连接协议处理类的实现
2.和上面一章一样,在开始编写实际的服务端客户端代码之前,我们首先要编写处理协议的类,它需要提供这样两个功能
1、方便地帮我们获取完整的协议信息,因为前面我们说过,服务端可能将客户端的多次独立请求拆分或合并比如,客户端连续发送了两条控制信息到服务端,而服务端将它们合并了,那么则需要先拆开再分别处理
2、方便地获取我们所想要的属性信息,因为协议是X ML格式,所以还需要一个类专门对XML进行处理,获得字符串的属性值辅助类201Proto calH a ndler我们先看下Pr o to calHandl er,它与上一篇中的Request Han d1er作用相同.需要注意的是必须将它声明为实例的,而非静态的,这是因为每个TcpC1ien t都需要对应一个Pro toea IHand1er,因为它内部维护的pati a1Pro toc a1不能共享,在协议发送不完整的情况下,这个变量用于临时保存被截断的字符串p ubli cc lass P roto coHIandler{p rivates tringpartialP rot oca1;//保存不完整的协议pub liePro toco1Ha ndie r{p arti aIProtoc a1二〃;}n publ i cs tr ing[]G etProtocols tringinpu t{retu rn Ge t Protocolinp u t,null;A//获得协议p riv ates tring[]Ge tProt o col st r ingin p ut,Li sAtstr ingoutputList{if outputLi st==nullo utp u tList=n ewLis tstring;if Stri ng.IsN u1A10r Empt y inpu treturnout putL ist.ToArray;if!Str ing.I sNul10r Emptyp ar ti aIP ro tocal△input=par tia1Proto cal+i npu t;str ingpatt ern=n Cpro tocol.*/pr ot oco1»n;a//如果有匹配,说明已经找到了,是完整的协议if RegexoIsMatch inpu t,pa ttern{岫//获取匹配的值as tringmatch=Reg ex.Match input,patt ernGroups
[0]V a1u e;out putListo Addm atc h;a partialProtocal=;//缩短inpu t的长度a input=in put.Su bst ri ngmatch.Length//递归调用a GetProtocolinpu t,o utpu tList;a}else//如果不匹配,说明协议的长度不够,4//那么先缓存,然后等待下一次请求a partial Protoca1=inpu t;ret u rn outputList.T oA rra y;A}因为现在它已经不是本文的重点了,所以我就不演示对于它的测试了,本文所附带的代码中含有它的测试代码我在Prot o colHan dler中添加了一个静态类T est枚举和结构2o2FileRequestTy p e FileProtoco1因为XML是以字符串的形式在进行传输,为了方便使用,我们最好构建一个强类型来对它们进行操作,这样会方便很多我们首先可以定义File Request Mode枚举,它代表是发送还是接收文件publice n um Fi1eRe q uestMode{aSend=0,a Receive^}接下来我们再定义一个Fi1ePro tocol结构,用来为整个协议字符串提供强类型的访问,注意这里覆盖了基类的ToStr ing方法,这样在客户端我们就不需要再手工去编写XML,只要在结构值上调用ToStringO就OK了,会方便很多.p ubli c struc tli1eProto c ol{priv atereadonlyFil e Reques tMode mo d e;p rivat ereadon1y int port;pr i vate readonlys tringfi1AeName;迎public FileProtoe oMFile Req ues tMo de mode,intport,str ingfileN ame{th isomode=mo de;th is.port=port;A thisofileName=file Name;A}z pu b1i cFi1eReque stMode Mode{△get{retur nmode;}总}publicint Port{get{ret urn po rt;}a}pu b1i cstringFile Name{get{re turn fileName;}public overri des tring ToStringre turnS tring.Fo r{Amatn p rotocolfilen a me=\n{0}\v mode=、”{1}\port=\v{2}\//p rotocol〃,fi1eName,mode,p ort}A}辅助类
2.3P rotocolHe1per这个类专用于将XML格式的协议映射为我们上面定义的强类型对象,这里我没有加入行y/catch异常处理,因为协议对用户来说是不可见的,而且客户端应该总是发送正确的协议,我觉得这样可以让代码更加清晰publicc1assP roto colHel per沁priva teXm1Nod efile Node;p riv ateXmlNode root;public ProtocolHelperstringpr otAo colXmlDo cumentd oc=n ewXm1D ocumentdoc.Lo ad Xm1protocol;a root=do c.Do cu mentElementrootoSelec tSing1eNo de〃file”;a}a//此时的p rot ocal一定为单条完整protocaU p rivateFileRequestM odeGet FileMode{string mode=fileN odeAt tributes[m ode]Vai ue;omode=mod eToLower;oif mode==sen d””return FileRequestMode Send;oelse returnFile RequestMode.Rece ive;A}a//获取单条协议包含的信息pub1ic FileP rotoc o1Get Protoco1{Fil eReques tModemode=Get FileMod e;string fileName二”“;int po rt=0;n fileName=fileNode.At trib utes[name H.V alue;port=Convert.Tolnt32fileNod eAtt ri butes[〃port,z]oValue;nre turnn ew Fi leProtocolmode,port,f ileName;A}OK,我们又耽误了点时间,下面就让我们进入正题吧客户端发送数据3o服务端的实现
3.1我们还是将一个问题分成两部分来处理,先是发送数据,然后是接收数据我们先看发送数据部分的服务端如果你从第一篇文章看到了现在,那么我觉得更多的不是技术上的问题而是思路,所以我们不再将重点放到代码上,这些应该很容易就看懂了c1assServe r{a staticvoid Mainst ring[]arg s{Conso1e Wri teLine〃Serve risrunningo.o〃;oIPA ddre ssip=I PAddressParse“
127.
0.
0.1〃;oTcpList ener1iste ner=new IcpL istener ip,8500;a listenerStart;//开启对控制端口8500的侦听oCon sole WriteLine,StartListe ning...〃;o Awhi1e true{a//获取一个连接,同步方法,在此处中断TcpClient client=1i stener AcceptT cpCl ie not;RemoteClient wapper=newRemoteC1ientc1ie nt;wap per.Be ginRead}A}}ApublicclassRem o te C1i ent{privateTcpCli entclient;p rivateNetwo rkStr eamstreamAT oClient;a p rivatec onstintB ufferS iz e=8192privat ebyt ebuffer;p rivate ProtocolHand1er hand1er;public RemoteC1i entTcpCl ientcli ent{t his.c1ient=c1ient;a//打印连接到的客户端信息Co nso1e.WriteLine/z\n C1i entConnected!{0}-----------------{1}”,die ntoCli entL ocalEnd Point,c1ient.Cliento RemoteEn dPoint;//获得流streamToCl ient=c lient.GetStream;b uffer=n ewbyte[Buff erSize];h andler=n ewP rotoco IHand1er;A}A//开始进行读取publie void BeginRead{AsyncCall b a c k ca11B ack=new Asy nc Cal1b ack OnR e adComp1ete;streamT oClientB eginReadbuffer,A0,Buffe rSize,ca11B ack,null;}g//再读取完成时进行回调privatevo id0nReadComple teI As yncResult ar{a intb ytesRead=0;a try,lock str eamToC lient{bytesRead=streamToClien tEndRead ar;Cons o1e WriteLi neReadi ngd ata,{0}by tes〃,b yt esRead}oi fbyt esRea d==0thro wnew Exception读取至U0字节”;string msg=Encoding.Uni code GetString buffer,0,bytesRe ad;aQArrayC learb uffer,0,b uffer.Length;//清空缓存,避免脏读//获取Prot ocol数组总str ing[]proto colArray=han dler.GetProt oco1msg;fore achstr ing proinp rotoco1Arra y{//这里异步调用,不然这里可能会比较耗时Paramete rAiz edTh re adSt artstart=new Parameteri zedTh readStarth and leProtocol;s tartoBeginlnvoke pro,n till,nu11}//再次调用BeginRead,完成时调用自身,形成无限循环loc kstr eamT oClientAsyncC a11backcallBa{Ack=newAsync CalJ back OnRea dComp lete;streamToClie nt BeginRea dbuffer,0,BufferSize,callBack,null;o}catch Exceptionex{}Ai fstream ToC1ien t!=nu11streamToCl ientDisp ose;c1ient.C1ose;Console.Wri teLine exMessa ge;//捕获异常时退出程序o}//处理pro tprivate vo idhandlePr otoco1object ob j{string pro=objOCOIAa sstring;ProtocolHe1perhe Iper=n ewProtoe olHelperpro;F iAleProtocolpro toco1=hel per.Get Protocol;nif protocol.Mod e==Fil eRequ estModeo Send{a//客户端发送文件,对服务端来说则是接收文件rec eiv eFi1e protocol}el seif protocol.Mod e==Fi1eRequestModeRec eive{a//客户端接收文件,对服务端来说则是发送文o件。
个人认证
优秀文档
获得点赞 0