前面两篇文章所使用的范例都是传输字符串,有的时候咱们可能会想在服务端和客户端之间传递文件。好比,考虑这样一种状况,假如客户端显示了一个菜单,当咱们输入S一、S2或S3(S为Send缩写)时,分别向服务端发送文件Client01.jpg、Client02.jpg、Client03.jpg;当咱们输入R一、R2或R3时(R为Receive缩写),则分别从服务端接收文件Server01.jpg、Server02.jpg、Server03.jpg。那么,咱们该如何完成这件事呢?此时可能有这样两种作法:html
如今咱们只关注于上面的数据端口,回忆一下在第二篇中咱们所总结的,能够得出:当咱们使用上面的方法一时,服务端的数据端口能够为多个客户端的屡次请求服务;当咱们使用方法二时,服务端只为一个客户端的一次请求服务,可是由于每次请求都会从新开辟端口,因此实际上仍是至关于能够为多个客户端的屡次请求服务。同时,由于它只为一次请求服务,因此咱们在数据端口上传输文件时无需采用异步传输方式。但在控制端口咱们仍然须要使用异步方式。正则表达式
从上面看出,第一种方式要好得多,可是咱们将采用第二种方式。至于缘由,你能够回顾一下Part.1(基本概念和操做)中关于聊天程序模式的讲述,由于接下来一篇文章咱们将建立一个聊天程序,而这个聊天程序采用第三种模式,因此本文的练习实际是对下一篇的一个铺垫。数组
咱们先看一下发送文件的状况,若是咱们想将文件client01.jpg由客户端发往客户端,那么流程是什么:缓存
此时,咱们订立的发送文件协议为:[file=Client01.jpg, mode=send, port=8005]。可是,因为它是一个普通的字符串,在上一篇中,咱们采用了正则表达式来获取其中的有效值,但这显然不是一种好办法。所以,在本文及下一篇文章中,咱们采用一种新的方式来编写协议:XML。对于上面的语句,咱们能够写成这样的XML:服务器
<protocol><file name="client01.jpg" mode="send" port="8005" /></protocol>网络
这样咱们在服务端就会好处理得多,接下来咱们来看一下接收文件的流程及其协议。多线程
NOTE:这里说发送、接收文件是站在客户端的立场说的,当客户端发送文件时,对于服务器来收,则是接收文件。app
接收文件与发送文件实际上彻底相似,区别只是由客户端向网络流写入数据,仍是由服务端向网络流写入数据。异步
和上面一章同样,在开始编写实际的服务端客户端代码以前,咱们首先要编写处理协议的类,它须要提供这样两个功能:一、方便地帮咱们获取完整的协议信息,由于前面咱们说过,服务端可能将客户端的屡次独立请求拆分或合并。好比,客户端连续发送了两条控制信息到服务端,而服务端将它们合并了,那么则须要先拆开再分别处理。二、方便地获取咱们所想要的属性信息,由于协议是XML格式,因此还须要一个类专门对XML进行处理,得到字符串的属性值。ide
咱们先看下ProtocalHandler,它与上一篇中的RequestHandler做用相同。须要注意的是必须将它声明为实例的,而非静态的,这是由于每一个TcpClient都须要对应一个ProtocalHandler,由于它内部维护的patialProtocal不能共享,在协议发送不完整的状况下,这个变量用于临时保存被截断的字符串。
public class ProtocolHandler {
private string partialProtocal; // 保存不完整的协议
public ProtocolHandler() {
partialProtocal = "";
}
public string[] GetProtocol(string input) {
return GetProtocol(input, null);
}
// 得到协议
private string[] GetProtocol(string input, List<string> outputList) {
if (outputList == null)
outputList = new List<string>();
if (String.IsNullOrEmpty(input))
return outputList.ToArray();
if (!String.IsNullOrEmpty(partialProtocal))
input = partialProtocal + input;
string pattern = "(^<protocol>.*?</protocol>)";
// 若是有匹配,说明已经找到了,是完整的协议
if (Regex.IsMatch(input, pattern)) {
// 获取匹配的值
string match = Regex.Match(input, pattern).Groups[0].Value;
outputList.Add(match);
partialProtocal = "";
// 缩短input的长度
input = input.Substring(match.Length);
// 递归调用
GetProtocol(input, outputList);
} else {
// 若是不匹配,说明协议的长度不够,
// 那么先缓存,而后等待下一次请求
partialProtocal = input;
}
return outputList.ToArray();
}
}
由于如今它已经不是本文的重点了,因此我就不演示对于它的测试了,本文所附带的代码中含有它的测试代码(我在ProtocolHandler中添加了一个静态类Test())。
由于XML是以字符串的形式在进行传输,为了方便使用,咱们最好构建一个强类型来对它们进行操做,这样会方便不少。咱们首先能够定义FileRequestMode枚举,它表明是发送仍是接收文件:
public enum FileRequestMode {
Send = 0,
Receive
}
接下来咱们再定义一个FileProtocol结构,用来为整个协议字符串提供强类型的访问,注意这里覆盖了基类的ToString()方法,这样在客户端咱们就不须要再手工去编写XML,只要在结构值上调用ToString()就OK了,会方便不少。
public struct FileProtocol {
private readonly FileRequestMode mode;
private readonly int port;
private readonly string fileName;
public FileProtocol
(FileRequestMode mode, int port, string fileName) {
this.mode = mode;
this.port = port;
this.fileName = fileName;
}
public FileRequestMode Mode {
get { return mode; }
}
public int Port {
get { return port; }
}
public string FileName {
get { return fileName; }
}
public override string ToString() {
return String.Format("<protocol><file name=\"{0}\" mode=\"{1}\" port=\"{2}\" /></protocol>", fileName, mode, port);
}
}
这个类专用于将XML格式的协议映射为咱们上面定义的强类型对象,这里我没有加入try/catch异常处理,由于协议对用户来讲是不可见的,并且客户端应该老是发送正确的协议,我以为这样可让代码更加清晰:
public class ProtocolHelper {
private XmlNode fileNode;
private XmlNode root;
public ProtocolHelper(string protocol) {
XmlDocument doc = new XmlDocument();
doc.LoadXml(protocol);
root = doc.DocumentElement;
fileNode = root.SelectSingleNode("file");
}
// 此时的protocal必定为单条完整protocal
private FileRequestMode GetFileMode() {
string mode = fileNode.Attributes["mode"].Value;
mode = mode.ToLower();
if (mode == "send")
return FileRequestMode.Send;
else
return FileRequestMode.Receive;
}
// 获取单条协议包含的信息
public FileProtocol GetProtocol() {
FileRequestMode mode = GetFileMode();
string fileName = "";
int port = 0;
fileName = fileNode.Attributes["name"].Value;
port = Convert.ToInt32(fileNode.Attributes["port"].Value);
return new FileProtocol(mode, port, fileName);
}
}
OK,咱们又耽误了点时间,下面就让咱们进入正题吧。
咱们仍是将一个问题分红两部分来处理,先是发送数据,而后是接收数据。咱们先看发送数据部分的服务端。若是你从第一篇文章看到了如今,那么我以为更多的不是技术上的问题而是思路,因此咱们再也不将重点放到代码上,这些应该很容易就看懂了。
class Server {
static void Main(string[] args) {
Console.WriteLine("Server is running ... ");
IPAddress ip = IPAddress.Parse("127.0.0.1");
TcpListener listener = new TcpListener(ip, 8500);
listener.Start(); // 开启对控制端口 8500 的侦听
Console.WriteLine("Start Listening ...");
while (true) {
// 获取一个链接,同步方法,在此处中断
TcpClient client = listener.AcceptTcpClient();
RemoteClient wapper = new RemoteClient(client);
wapper.BeginRead();
}
}
}
public class RemoteClient {
private TcpClient client;
private NetworkStream streamToClient;
private const int BufferSize = 8192;
private byte[] buffer;
private ProtocolHandler handler;
public RemoteClient(TcpClient client) {
this.client = client;
// 打印链接到的客户端信息
Console.WriteLine("\nClient Connected!{0} <-- {1}",
client.Client.LocalEndPoint, client.Client.RemoteEndPoint);
// 得到流
streamToClient = client.GetStream();
buffer = new byte[BufferSize];
handler = new ProtocolHandler();
}
// 开始进行读取
public void BeginRead() {
AsyncCallback callBack = new AsyncCallback(OnReadComplete);
streamToClient.BeginRead(buffer, 0, BufferSize, callBack, null);
}
// 再读取完成时进行回调
private void OnReadComplete(IAsyncResult ar) {
int bytesRead = 0;
try {
lock (streamToClient) {
bytesRead = streamToClient.EndRead(ar);
Console.WriteLine("Reading data, {0} bytes ...", bytesRead);
}
if (bytesRead == 0) throw new Exception("读取到0字节");
string msg = Encoding.Unicode.GetString(buffer, 0, bytesRead);
Array.Clear(buffer,0,buffer.Length); // 清空缓存,避免脏读
// 获取protocol数组
string[] protocolArray = handler.GetProtocol(msg);
foreach (string pro in protocolArray) {
// 这里异步调用,否则这里可能会比较耗时
ParameterizedThreadStart start =
new ParameterizedThreadStart(handleProtocol);
start.BeginInvoke(pro, null, null);
}
// 再次调用BeginRead(),完成时调用自身,造成无限循环
lock (streamToClient) {
AsyncCallback callBack = new AsyncCallback(OnReadComplete);
streamToClient.BeginRead(buffer, 0, BufferSize, callBack, null);
}
} catch(Exception ex) {
if(streamToClient!=null)
streamToClient.Dispose();
client.Close();
Console.WriteLine(ex.Message); // 捕获异常时退出程序
}
}
// 处理protocol
private void handleProtocol(object obj) {
string pro = obj as string;
ProtocolHelper helper = new ProtocolHelper(pro);
FileProtocol protocol = helper.GetProtocol();
if (protocol.Mode == FileRequestMode.Send) {
// 客户端发送文件,对服务端来讲则是接收文件
receiveFile(protocol);
} else if (protocol.Mode == FileRequestMode.Receive) {
// 客户端接收文件,对服务端来讲则是发送文件
// sendFile(protocol);
}
}
private void receiveFile(FileProtocol protocol) {
// 获取远程客户端的位置
IPEndPoint endpoint = client.Client.RemoteEndPoint as IPEndPoint;
IPAddress ip = endpoint.Address;
// 使用新端口号,得到远程用于接收文件的端口
endpoint = new IPEndPoint(ip, protocol.Port);
// 链接到远程客户端
TcpClient localClient;
try {
localClient = new TcpClient();
localClient.Connect(endpoint);
} catch {
Console.WriteLine("没法链接到客户端 --> {0}", endpoint);
return;
}
// 获取发送文件的流
NetworkStream streamToClient = localClient.GetStream();
// 随机生成一个在当前目录下的文件名称
string path =
Environment.CurrentDirectory + "/" + generateFileName(protocol.FileName);
byte[] fileBuffer = new byte[1024]; // 每次收1KB
FileStream fs = new FileStream(path, FileMode.CreateNew, FileAccess.Write);
// 从缓存buffer中读入到文件流中
int bytesRead;
int totalBytes = 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();
}
// 随机获取一个图片名称
private string generateFileName(string fileName) {
DateTime now = DateTime.Now;
return String.Format(
"{0}_{1}_{2}_{3}", now.Minute, now.Second, now.Millisecond, fileName
);
}
}
这里应该没有什么新知识,须要注意的地方有这么几个:
咱们如今先不着急实现客户端S一、R1等用户菜单,首先完成发送文件这一功能,实际上,就是为上一节SendMessage()加一个姐妹方法SendFile()。
class Client {
static void Main(string[] args) {
ConsoleKey key;
ServerClient client = new ServerClient();
string filePath = 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);
}
}
public class ServerClient {
private const int BufferSize = 8192;
private byte[] buffer;
private TcpClient client;
private NetworkStream streamToServer;
public ServerClient() {
try {
client = new TcpClient();
client.Connect("localhost", 8500); // 与服务器链接
} catch (Exception ex) {
Console.WriteLine(ex.Message);
return;
}
buffer = new byte[BufferSize];
// 打印链接到的服务端信息
Console.WriteLine("Server Connected!{0} --> {1}",
client.Client.LocalEndPoint, client.Client.RemoteEndPoint);
streamToServer = client.GetStream();
}
// 发送消息到服务端
public void SendMessage(string msg) {
byte[] temp = Encoding.Unicode.GetBytes(msg); // 得到缓存
try {
lock (streamToServer) {
streamToServer.Write(temp, 0, temp.Length); // 发往服务器
}
Console.WriteLine("Sent: {0}", msg);
} catch (Exception ex) {
Console.WriteLine(ex.Message);
return;
}
}
// 发送文件 - 异步方法
public void BeginSendFile(string filePath) {
ParameterizedThreadStart start =
new ParameterizedThreadStart(BeginSendFile);
start.BeginInvoke(filePath, null, null);
}
private void BeginSendFile(object obj) {
string filePath = obj as string;
SendFile(filePath);
}
// 发送文件 -- 同步方法
public void SendFile(string filePath) {
IPAddress ip = IPAddress.Parse("127.0.0.1");
TcpListener listener = new TcpListener(ip, 0);
listener.Start();
// 获取本地侦听的端口号
IPEndPoint endPoint = listener.LocalEndpoint as IPEndPoint;
int listeningPort = endPoint.Port;
// 获取发送的协议字符串
string fileName = Path.GetFileName(filePath);
FileProtocol protocol =
new FileProtocol(FileRequestMode.Send, listeningPort, fileName);
string pro = protocol.ToString();
SendMessage(pro); // 发送协议到服务端
// 中断,等待远程链接
TcpClient localClient = listener.AcceptTcpClient();
Console.WriteLine("Start sending file...");
NetworkStream stream = localClient.GetStream();
// 建立文件流
FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);
byte[] fileBuffer = new byte[1024]; // 每次传1KB
int bytesRead;
int totalBytes = 0;
// 建立获取文件发送状态的类
SendStatus status = new SendStatus(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();
}
}
接下来咱们来看下这段代码,有这么两点须要注意一下:
下面是SendStatus的内容:
// 即时计算发送文件的状态
public class SendStatus {
private FileInfo info;
private long fileBytes;
public SendStatus(string filePath) {
info = new FileInfo(filePath);
fileBytes = info.Length;
}
public void PrintStatus(int sent) {
string percent = GetPercent(sent);
Console.WriteLine("Sending {0} bytes, {1}% ...", sent, percent);
}
// 得到文件发送的百分比
public string GetPercent(int sent){
decimal allBytes = Convert.ToDecimal(fileBytes);
decimal currentSent = Convert.ToDecimal(sent);
decimal percent = (currentSent / allBytes) * 100;
percent = Math.Round(percent, 1); //保留一位小数
if (percent.ToString() == "100.0")
return "100";
else
return percent.ToString();
}
}
接下里咱们运行一下程序,来检查一下输出,首先看下服务端:
接着是客户端,咱们可以看到发送的字节数和进度,能够想到若是是图形界面,那么咱们能够经过扩展SendStatus类来建立一个进度条:
最后咱们看下服务端的Bin\Debug目录,应该能够看到接收到的图片:
原本我想这篇文章就能够完成发送和接收,不过如今看来无法实现了,由于若是继续下去这篇文章就太长了,我正尝试着尽可能将文章控制在15页之内。那么咱们将在下篇文章中再完成接收文件这一部分。
出处:http://www.cnblogs.com/JimmyZhang/archive/2008/09/16/1291857.html