前面兩篇文章所使用的範例都是傳輸字符串,有的時候咱們可能會想在服務端和客戶端之間傳遞文件。好比,考慮這樣一種狀況,假如客戶端顯示了一個菜單,當咱們輸入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