- 浏览: 2101167 次
- 性别:
- 来自: 深圳
文章分类
最新评论
-
wahahachuang5:
web实时推送技术使用越来越广泛,但是自己开发又太麻烦了,我觉 ...
细说websocket - php篇 -
wahahachuang8:
挺好的,学习了
细说websocket - php篇 -
jacking124:
学习了!支持你,继续
初窥Linux 之 我最常用的20条命令 -
aliahhqcheng:
应该是可以实现的,没有看过源码。你可以参考下:http://w ...
Jackson 框架,轻易转换JSON
C#网络编程(订立协议和发送文件) - Part.4
文件传输
前面两篇文章所使用的范例都是传输字符串,有的时候我们可能会想在服务端和客户端之间传递文件。比如,考虑这样一种情况,假如客户端显示了一个菜单,当我们输入S1、S2或S3(S为Send缩写)时,分别向服务端发送文件Client01.jpg、Client02.jpg、Client03.jpg;当我们输入R1、R2或R3时(R为Receive缩写),则分别从服务端接收文件Server01.jpg、Server02.jpg、Server03.jpg。那么,我们该如何完成这件事呢?此时可能有这样两种做法:
- 类似于FTP协议,服务端开辟两个端口,并持续对这两个端口侦听:一个用于接收字符串,类似于FTP的控制端口,它接收各种命令(接收或发送文件);一个用于传输数据,也就是发送和接收文件。
- 服务端只开辟一个端口,用于接收字符串,我们称之为控制端口。当接到请求之后,根据请求内容在客户端开辟一个端口专用于文件传输,并在传输结束后关闭端口。
现在我们只关注于上面的数据端口,回忆一下在第二篇中我们所总结的,可以得出:当我们使用上面的方法一时,服务端的数据端口可以为多个客户端的多次请求服务;当我们使用方法二时,服务端只为一个客户端的一次请求服务,但是因为每次请求都会重新开辟端口,所以实际上还是相当于可以为多个客户端的多次请求服务。同时,因为它只为一次请求服务,所以我们在数据端口上传输文件时无需采用异步传输方式。但在控制端口我们仍然需要使用异步方式。
从上面看出,第一种方式要好得多,但是我们将采用第二种方式。至于原因,你可以回顾一下Part.1(基本概念和操作)中关于聊天程序模式的讲述,因为接下来一篇文章我们将创建一个聊天程序,而这个聊天程序采用第三种模式,所以本文的练习实际是对下一篇的一个铺垫。
1.订立协议
1.1发送文件
我们先看一下发送文件的情况,如果我们想将文件client01.jpg由客户端发往客户端,那么流程是什么:
- 客户端开辟数据端口用于侦听,并获取端口号,假设为8005。
- 假设客户端输入了S1,则发送下面的控制字符串到服务端:[file=Client01.jpg, mode=send, port=8005]。
- 服务端收到以后,根据客户端ip和端口号与该客户端建立连接。
- 客户端侦听到服务端的连接,开始发送文件。
- 传送完毕后客户端、服务端分别关闭连接。
此时,我们订立的发送文件协议为:[file=Client01.jpg, mode=send, port=8005]。但是,由于它是一个普通的字符串,在上一篇中,我们采用了正则表达式来获取其中的有效值,但这显然不是一种好办法。因此,在本文及下一篇文章中,我们采用一种新的方式来编写协议:XML。对于上面的语句,我们可以写成这样的XML:
<protocol><file name="client01.jpg" mode="send" port="8005" /></protocol>
这样我们在服务端就会好处理得多,接下来我们来看一下接收文件的流程及其协议。
NOTE:这里说发送、接收文件是站在客户端的立场说的,当客户端发送文件时,对于服务器来收,则是接收文件。
1.2接收文件
接收文件与发送文件实际上完全类似,区别只是由客户端向网络流写入数据,还是由服务端向网络流写入数据。
- 客户端开辟数据端口用于侦听,假设为8006。
- 假设客户端输入了R1,则发送控制字符串:<protocol><file name="Server01.jpg" mode="receive" port="8006" /></protocol>到服务端。
- 服务端收到以后,根据客户端ip和端口号与该客户端建立连接。
- 客户端建立起与服务端的连接,服务端开始网络流中写入数据。
- 传送完毕后服务端、客户端分别关闭连接。
2.协议处理类的实现
和上面一章一样,在开始编写实际的服务端客户端代码之前,我们首先要编写处理协议的类,它需要提供这样两个功能:1、方便地帮我们获取完整的协议信息,因为前面我们说过,服务端可能将客户端的多次独立请求拆分或合并。比如,客户端连续发送了两条控制信息到服务端,而服务端将它们合并了,那么则需要先拆开再分别处理。2、方便地获取我们所想要的属性信息,因为协议是XML格式,所以还需要一个类专门对XML进行处理,获得字符串的属性值。
2.1 ProtocalHandler辅助类
我们先看下ProtocalHandler,它与上一篇中的RequestHandler作用相同。需要注意的是必须将它声明为实例的,而非静态的,这是因为每个TcpClient都需要对应一个ProtocalHandler,因为它内部维护的patialProtocal不能共享,在协议发送不完整的情况下,这个变量用于临时保存被截断的字符串。
publicclassProtocolHandler{
privatestringpartialProtocal;//
保存不完整的协议
publicProtocolHandler() {
partialProtocal ="";
}
publicstring[] GetProtocol(stringinput) {
returnGetProtocol(input,null);
}
// 获得协议
privatestring[] GetProtocol(stringinput, List<string>
outputList) {
if(outputList ==null)
outputList =newList<string>();
if(String.IsNullOrEmpty(input))
returnoutputList.ToArray();
if(!String.IsNullOrEmpty(partialProtocal))
input = partialProtocal + input;
stringpattern ="(^<protocol>.*?</protocol>)";
// 如果有匹配,说明已经找到了,是完整的协议
if(Regex.IsMatch(input, pattern)) {
// 获取匹配的值
stringmatch =Regex.Match(input, pattern).Groups[0].Value;
outputList.Add(match);
partialProtocal ="";
// 缩短input的长度
input = input.Substring(match.Length);
// 递归调用
GetProtocol(input, outputList);
}else{
// 如果不匹配,说明协议的长度不够,
// 那么先缓存,然后等待下一次请求
partialProtocal = input;
}
returnoutputList.ToArray();
}
}
因为现在它已经不是本文的重点了,所以我就不演示对于它的测试了,本文所附带的代码中含有它的测试代码(我在ProtocolHandler中添加了一个静态类Test())。
2.2 FileRequestType枚举和FileProtocol结构
因为XML是以字符串的形式在进行传输,为了方便使用,我们最好构建一个强类型来对它们进行操作,这样会方便很多。我们首先可以定义FileRequestMode枚举,它代表是发送还是接收文件:
publicenumFileRequestMode{
Send = 0,
Receive
}
接下来我们再定义一个FileProtocol结构,用来为整个协议字符串提供强类型的访问,注意这里覆盖了基类的ToString()方法,这样在客户端我们就不需要再手工去编写XML,只要在结构值上调用ToString()就OK了,会方便很多。
publicstructFileProtocol{
privatereadonlyFileRequestModemode;
privatereadonlyintport;
privatereadonlystringfileName;
publicFileProtocol
(FileRequestMode mode,intport,stringfileName) {
this.mode = mode;
this.port = port;
this.fileName = fileName;
}
publicFileRequestMode Mode {
get {returnmode; }
}
publicintPort{
get {returnport; }
}
publicstringFileName {
get {returnfileName; }
}
publicoverridestringToString() {
returnString.Format("<protocol><file name=\"{0}\" mode=\"{1}\"
port=\"{2}\" /></protocol>", fileName, mode, port);
}
}
2.3 ProtocolHelper辅助类
这个类专用于将XML格式的协议映射为我们上面定义的强类型对象,这里我没有加入try/catch异常处理,因为协议对用户来说是不可见的,而且客户端应该总是发送正确的协议,我觉得这样可以让代码更加清晰:
publicclassProtocolHelper{
privateXmlNodefileNode;
privateXmlNoderoot;
publicProtocolHelper(stringprotocol) {
XmlDocumentdoc =newXmlDocument();
doc.LoadXml(protocol);
root = doc.DocumentElement;
fileNode = root.SelectSingleNode("file");
}
// 此时的protocal一定为单条完整protocal
privateFileRequestMode GetFileMode() {
stringmode = fileNode.Attributes["mode"].Value;
mode = mode.ToLower();
if(mode =="send")
returnFileRequestMode.Send;
else
returnFileRequestMode.Receive;
}
// 获取单条协议包含的信息
publicFileProtocol GetProtocol() {
FileRequestModemode = GetFileMode();
stringfileName ="";
intport = 0;
fileName = fileNode.Attributes["name"].Value;
port = Convert.ToInt32(fileNode.Attributes["port"].Value);
returnnewFileProtocol(mode, port, fileName);
}
}
OK,我们又耽误了点时间,下面就让我们进入正题吧。
3.客户端发送数据
3.1 服务端的实现
我们还是将一个问题分成两部分来处理,先是发送数据,然后是接收数据。我们先看发送数据部分的服务端。如果你从第一篇文章看到了现在,那么我觉得更多的不是技术上的问题而是思路,所以我们不再将重点放到代码上,这些应该很容易就看懂了。
classServer{
staticvoidMain(string[] args) {
Console.WriteLine("Serverisrunning ... ");
IPAddressip = IPAddress.Parse("127.0.0.1");
TcpListenerlistener =newTcpListener(ip, 8500);
listener.Start();// 开启对控制端口 8500 的侦听
Console.WriteLine("Start Listening ...");
while(true) {
// 获取一个连接,同步方法,在此处中断
TcpClientclient = listener.AcceptTcpClient();
RemoteClientwapper =newRemoteClient(client);
wapper.BeginRead();
}
}
}
publicclassRemoteClient{
privateTcpClientclient;
privateNetworkStreamstreamToClient;
privateconstintBufferSize
= 8192;
privatebyte[] buffer;
privateProtocolHandlerhandler;
publicRemoteClient(TcpClient client) {
this.client = client;
// 打印连接到的客户端信息
Console.WriteLine("\nClient Connected!{0} <-- {1}",
client.Client.LocalEndPoint, client.Client.RemoteEndPoint);
// 获得流
streamToClient = client.GetStream();
buffer =newbyte[BufferSize];
handler =newProtocolHandler();
}
// 开始进行读取
publicvoidBeginRead() {
AsyncCallbackcallBack =newAsyncCallback(OnReadComplete);
streamToClient.BeginRead(buffer, 0, BufferSize, callBack,null);
}
// 再读取完成时进行回调
privatevoidOnReadComplete(IAsyncResult ar) {
intbytesRead = 0;
try{
lock(streamToClient) {
bytesRead = streamToClient.EndRead(ar);
Console.WriteLine("Readingdata, {0} bytes ...", bytesRead);
}
if(bytesRead == 0)thrownewException("读取到0字节");
stringmsg =Encoding.Unicode.GetString(buffer,
0, bytesRead);
Array.Clear(buffer,0,buffer.Length);// 清空缓存,避免脏读
// 获取protocol数组
string[] protocolArray = handler.GetProtocol(msg);
foreach(stringproinprotocolArray) {
// 这里异步调用,不然这里可能会比较耗时
ParameterizedThreadStartstart =
newParameterizedThreadStart(handleProtocol);
start.BeginInvoke(pro,null,null);
}
// 再次调用BeginRead(),完成时调用自身,形成无限循环
lock(streamToClient) {
AsyncCallbackcallBack =newAsyncCallback(OnReadComplete);
streamToClient.BeginRead(buffer, 0, BufferSize, callBack,null);
}
}catch(Exceptionex) {
if(streamToClient!=null)
streamToClient.Dispose();
client.Close();
Console.WriteLine(ex.Message);// 捕获异常时退出程序
}
}
// 处理protocol
privatevoidhandleProtocol(objectobj) {
stringpro = objasstring;
ProtocolHelperhelper =newProtocolHelper(pro);
FileProtocolprotocol = helper.GetProtocol();
if(protocol.Mode == FileRequestMode.Send) {
// 客户端发送文件,对服务端来说则是接收文件
receiveFile(protocol);
}elseif(protocol.Mode == FileRequestMode.Receive) {
// 客户端接收文件,对服务端来说则是发送文件
// sendFile(protocol);
}
}
privatevoidreceiveFile(FileProtocol protocol) {
// 获取远程客户端的位置
IPEndPointendpoint = client.Client.RemoteEndPointasIPEndPoint;
IPAddressip = endpoint.Address;
// 使用新端口号,获得远程用于接收文件的端口
endpoint =newIPEndPoint(ip, protocol.Port);
// 连接到远程客户端
TcpClientlocalClient;
try{
localClient =newTcpClient();
localClient.Connect(endpoint);
}catch{
Console.WriteLine("无法连接到客户端 --> {0}", endpoint);
return;
}
// 获取发送文件的流
NetworkStreamstreamToClient = localClient.GetStream();
// 随机生成一个在当前目录下的文件名称
stringpath =
Environment.CurrentDirectory +"/"+ generateFileName(protocol.FileName);
byte[]fileBuffer =newbyte[1024];//
每次收1KB
FileStreamfs =newFileStream(path, FileMode.CreateNew,
FileAccess.Write);
// 从缓存buffer中读入到文件流中
intbytesRead;
inttotalBytes = 0;
do{
bytesRead = streamToClient.Read(buffer, 0, BufferSize);
fs.Write(buffer, 0, bytesRead);
totalBytes += bytesRead;
Console.WriteLine("Receiving {0} bytes ...", totalBytes);
}while(bytesRead > 0);
Console.WriteLine("Total {0} bytes received, Done!", totalBytes);
streamToClient.Dispose();
fs.Dispose();
localClient.Close();
}
// 随机获取一个图片名称
privatestringgenerateFileName(stringfileName) {
DateTimenow = DateTime.Now;
returnString.Format(
"{0}_{1}_{2}_{3}", now.Minute, now.Second, now.Millisecond, fileName
);
}
}
这里应该没有什么新知识,需要注意的地方有这么几个:
- 在OnReadComplete()回调方法中的foreach循环,我们使用委托异步调用了handleProtocol()方法,这是因为handleProtocol即将执行的是一个读取或接收文件的操作,也就是一个相对耗时的操作。
- 在handleProtocol()方法中,我们深切体会了定义ProtocolHelper类和FileProtocol结构的好处。如果没有定义它们,这里将是不堪入目的处理XML以及类型转换的代码。
- handleProtocol()方法中进行了一个条件判断,注意sendFile()方法我屏蔽掉了,这个还没有实现,但是我想你已经猜到它将是后面要实现的内容。
- receiveFile()方法是实际接收客户端发来文件的方法,这里没有什么特别之处。需要注意的是文件存储的路径,它保存在了当前程序执行的目录下,文件的名称我使用generateFileName()生成了一个与时间有关的随机名称。
3.2客户端的实现
我们现在先不着急实现客户端S1、R1等用户菜单,首先完成发送文件这一功能,实际上,就是为上一节SendMessage()加一个姐妹方法SendFile()。
classClient{
staticvoidMain(string[] args) {
ConsoleKeykey;
ServerClientclient =newServerClient();
stringfilePath = Environment.CurrentDirectory +"/" + "Client01.jpg";
if(File.Exists(filePath))
client.BeginSendFile(filePath);
Console.WriteLine("\n\n输入\"Q\"键退出。");
do{
key =Console.ReadKey(true).Key;
}while(key != ConsoleKey.Q);
}
}
publicclassServerClient{
privateconstintBufferSize
= 8192;
privatebyte[] buffer;
privateTcpClientclient;
privateNetworkStreamstreamToServer;
publicServerClient() {
try{
client =newTcpClient();
client.Connect("localhost", 8500);// 与服务器连接
}catch(Exceptionex) {
Console.WriteLine(ex.Message);
return;
}
buffer =newbyte[BufferSize];
// 打印连接到的服务端信息
Console.WriteLine("Server Connected!{0} --> {1}",
client.Client.LocalEndPoint, client.Client.RemoteEndPoint);
streamToServer = client.GetStream();
}
// 发送消息到服务端
publicvoidSendMessage(stringmsg) {
byte[] temp =Encoding.Unicode.GetBytes(msg);// 获得缓存
try{
lock(streamToServer) {
streamToServer.Write(temp, 0, temp.Length);// 发往服务器
}
Console.WriteLine("Sent: {0}", msg);
}catch(Exceptionex) {
Console.WriteLine(ex.Message);
return;
}
}
// 发送文件 - 异步方法
publicvoidBeginSendFile(stringfilePath) {
ParameterizedThreadStartstart =
newParameterizedThreadStart(BeginSendFile);
start.BeginInvoke(filePath,null,null);
}
privatevoidBeginSendFile(objectobj) {
stringfilePath = objasstring;
SendFile(filePath);
}
// 发送文件 -- 同步方法
publicvoidSendFile(stringfilePath) {
IPAddressip = IPAddress.Parse("127.0.0.1");
TcpListenerlistener =newTcpListener(ip, 0);
listener.Start();
// 获取本地侦听的端口号
IPEndPointendPoint = listener.LocalEndpointasIPEndPoint;
intlisteningPort = endPoint.Port;
// 获取发送的协议字符串
stringfileName = Path.GetFileName(filePath);
FileProtocolprotocol =
newFileProtocol(FileRequestMode.Send, listeningPort, fileName);
stringpro = protocol.ToString();
SendMessage(pro);// 发送协议到服务端
// 中断,等待远程连接
TcpClientlocalClient = listener.AcceptTcpClient();
Console.WriteLine("Start sending file...");
NetworkStreamstream = localClient.GetStream();
// 创建文件流
FileStreamfs =newFileStream(filePath,
FileMode.Open, FileAccess.Read);
byte[]fileBuffer =newbyte[1024];//
每次传1KB
intbytesRead;
inttotalBytes = 0;
// 创建获取文件发送状态的类
SendStatusstatus =newSendStatus(filePath);
// 将文件流转写入网络流
try{
do{
Thread.Sleep(10);// 为了更好的视觉效果,暂停10毫秒:-)
bytesRead = fs.Read(fileBuffer, 0, fileBuffer.Length);
stream.Write(fileBuffer, 0, bytesRead);
totalBytes += bytesRead;// 发送了的字节数
status.PrintStatus(totalBytes);// 打印发送状态
}while(bytesRead > 0);
Console.WriteLine("Total {0} bytes sent, Done!", totalBytes);
}catch{
Console.WriteLine("Server has lost...");
}
stream.Dispose();
fs.Dispose();
localClient.Close();
listener.Stop();
}
}
接下来我们来看下这段代码,有这么两点需要注意一下:
- 在Main()方法中可以看到,图片的位置为应用程序所在的目录,如果你跟我一样处于调试模式,那么就在解决方案的Bin目录下的Debug目录中放置三张图片Client01.jpg、Client02.jpg、Client03.jpg,用来发往服务端。
- 我在客户端提供了两个SendFile()方法,和一个BeginSendFile()方法,分别用于同步和异步传输,其中私有的SendFile()方法只是一个辅助方法。实际上对于发送文件这样的操作我们几乎总是需要使用异步操作。
- SendMessage()方法中给streamToServer加锁很重要,因为SendFile()方法是多线程访问的,而在SendFile()方法中又调用了SendMessage()方法。
- 我另外编写了一个SendStatus类,它用来记录和打印发送完成的状态,已经发送了多少字节,完成度是百分之多少,等等。本来这个类的内容我是直接写入在Client类中的,后来我觉得它执行的工作已经不属于Client本身所应该执行的领域之内了,我记得这样一句话:当你觉得类中的方法与类的名称不符的时候,那么就应该考虑重新创建一个类。我觉得用在这里非常恰当。
下面是SendStatus的内容:
// 即时计算发送文件的状态
publicclassSendStatus{
privateFileInfoinfo;
privatelongfileBytes;
publicSendStatus(stringfilePath) {
info =newFileInfo(filePath);
fileBytes = info.Length;
}
publicvoidPrintStatus(intsent) {
stringpercent = GetPercent(sent);
Console.WriteLine("Sending {0} bytes, {1}% ...", sent, percent);
}
// 获得文件发送的百分比
publicstringGetPercent(intsent){
decimalallBytes = Convert.ToDecimal(fileBytes);
decimalcurrentSent = Convert.ToDecimal(sent);
decimalpercent = (currentSent / allBytes) * 100;
percent =Math.Round(percent, 1);//保留一位小数
if(percent.ToString() =="100.0")
return"100";
else
returnpercent.ToString();
}
}
3.3程序测试
接下里我们运行一下程序,来检查一下输出,首先看下服务端:
接着是客户端,我们能够看到发送的字节数和进度,可以想到如果是图形界面,那么我们可以通过扩展SendStatus类来创建一个进度条:
最后我们看下服务端的Bin\Debug目录,应该可以看到接收到的图片:
本来我想这篇文章就可以完成发送和接收,不过现在看来没法实现了,因为如果继续下去这篇文章就太长了,我正尝试着尽量将文章控制在15页以内。那么我们将在下篇文章中再完成接收文件这一部分。
相关推荐
C#网络编程(订立协议和发送文件) - Part.4.doc
C#网络编程(订立协议和发送文件)-Part.4[收集].pdf
C#网络编程(订立协议和发送文件)
C#网络编程:基本概念和操作、同步传输字符串、订立协议和发送文件、接收文件
《Visual C# .NET精彩编程实例集锦》配套光盘文件-3031.part1.rar
《Visual C# .NET精彩编程实例集锦》配套光盘文件-3031.part2.rar
《Visual C# .NET精彩编程实例集锦》配套光盘文件-3031.part3.rar
C#网络编程-4.doc
C#网络编程---第1章_进程、线程与网络协议.ppt C#网络编程---第2章_TCP应用编程.ppt C#网络编程---第3章_UDP应用编程.ppt C#网络编程---第4章_P2P应用编程.ppt C#网络编程---第5章_SMTP与POP3应用编程.ppt C#网络...
myeclipse-2019.4.7s破解文件,亲测可用,附带破解过程;
C#实验--- A.3 矩阵并行计算练习,《C#网络应用编程》课后实验,也是广工C#网络编程实验
有关于C#窗体编程,比较详细,不容错过。。。。
C#数据库编程-例子.rar
ASP_NET 2_0入门经典:C#编程篇-源码.part03.rar
ASP_NET 2_0入门经典:C#编程篇-源码.part02.rar
[C#游戏编程].C-#.Game.Programming.-.For.Serious.Game.Creation
C#实验--A.1 视频动态绘制练习,《C#网络应用编程》课后实验,也是广工C#网络编程实验
第3部分(第10章~第12章)介绍协议类应用开发技术,包括P2P应用开发、网络数据加解密技术等,最后用一个网络综合应用开发实例作为对《C#网络应用编程(第2版)》编程技术的总结。 《C#网络应用编程(第2版)》提供配套的...