TCP是一種面向連接的,可靠的,基於字節流的傳輸層通信協議。TCP建立一個連接需要三次握手,而終止一個連接要經過四次握手。一旦通信雙方建立了TCP連接,連接中的任何一方都能向對方發送數據和接受對方發來的數據。TCP協議負責把用戶數據(字節流)按一定的格式和長度組成多個數據報進行發送,並在接收到數據報之后按分解順序重新組裝和恢復傳輸的數據。
使用TCP傳輸文件,可以直接使用socket進行傳輸,也可以使用TcpLister類和TcpClient類進行傳輸。其實TcpLister和TcpClient就是Socket封裝后的類,是.NET為了簡化編程復雜度而對套接字又進行了封裝。但是,TcpLister和TcpClient只支持標准協議編程。如果希望編寫非標准協議的程序,只能使用套接字socket來實現。
下面分別講解兩種方法進行文件傳輸:
因為和一些終端進行文件傳輸時,受發送緩沖區最大發送字節的影響,我這里每次發送512字節,循環發送,直到把文件傳輸完,然后關閉連接;接收文件時,同樣是每次接收512字節,然后寫入文件,當所有的數據都接收完時,最后關閉連接。
當然,最后一次發送和接收的數據,以實際計算的數據大小來發送或者接收,不會是512字節,以免造成數據空白。
一、直接使用socket進行文件傳輸
服務端和發送端Demo界面分別如圖1、2所示:
圖1 服務端界面圖
圖2 客戶端界面圖
1、服務器端代碼如下:
public ShakeHands() { InitializeComponent(); IPAddress[] ips = Dns.GetHostAddresses(Dns.GetHostName()); txtIp.Text = ips[1].ToString(); int port = 50001; txtPort.Text = port.ToString(); ListBox.CheckForIllegalCrossThreadCalls = false;//關閉跨線程對ListBox的檢查 } #region 啟動TCP監聽服務,開始接收連接和文件 private void btnBegin_Click(object sender, EventArgs e) { try { ReceiveFiles.BeginListening(txtIp.Text, txtPort.Text, lstbxMsgView, listbOnline); btnBegin.Enabled = false; btnCancel.Enabled = true; } catch (Exception ex) { ShwMsgForView.ShwMsgforView(lstbxMsgView, "監聽服務器出現了錯誤:"+ex.Message); } } #endregion
其中,啟動監聽,接收文件ReceiveFiles類代碼如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Net.Sockets; using System.Net; using System.Windows.Forms; using System.IO; namespace BusinessLogicLayer { public class ReceiveFiles { private static Thread threadWatch = null; private static Socket socketWatch = null; private static ListBox lstbxMsgView;//顯示接受的文件等信息 private static ListBox listbOnline;//顯示用戶連接列表 private static Dictionary<string, Socket> dict = new Dictionary<string, Socket>(); /// <summary> /// 開始監聽 /// </summary> /// <param name="localIp"></param> /// <param name="localPort"></param> public static void BeginListening(string localIp, string localPort, ListBox listbox, ListBox listboxOnline) { //基本參數初始化 lstbxMsgView = listbox; listbOnline = listboxOnline; //創建服務端負責監聽的套接字,參數(使用IPV4協議,使用流式連接,使用Tcp協議傳輸數據) socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //獲取Ip地址對象 IPAddress address = IPAddress.Parse(localIp); //創建包含Ip和port的網絡節點對象 IPEndPoint endpoint = new IPEndPoint(address, int.Parse(localPort)); //將負責監聽的套接字綁定到唯一的Ip和端口上 socketWatch.Bind(endpoint); //設置監聽隊列的長度 socketWatch.Listen(10); //創建負責監聽的線程,並傳入監聽方法 threadWatch = new Thread(WatchConnecting); threadWatch.IsBackground = true;//設置為后台線程 threadWatch.Start();//開始線程 //ShowMgs("服務器啟動監聽成功"); ShwMsgForView.ShwMsgforView(lstbxMsgView, "服務器啟動監聽成功"); } /// <summary> /// 連接客戶端 /// </summary> private static void WatchConnecting() { while (true)//持續不斷的監聽客戶端的請求 { //開始監聽 客戶端連接請求,注意:Accept方法,會阻斷當前的線程 Socket connection = socketWatch.Accept(); if (connection.Connected) { //向列表控件中添加一個客戶端的Ip和端口,作為發送時客戶的唯一標識 listbOnline.Items.Add(connection.RemoteEndPoint.ToString()); //將與客戶端通信的套接字對象connection添加到鍵值對集合中,並以客戶端Ip做為健 dict.Add(connection.RemoteEndPoint.ToString(), connection); //創建通信線程 ParameterizedThreadStart pts = new ParameterizedThreadStart(RecMsg); Thread thradRecMsg = new Thread(pts); thradRecMsg.IsBackground = true; thradRecMsg.Start(connection); ShwMsgForView.ShwMsgforView(lstbxMsgView, "客戶端連接成功" + connection.RemoteEndPoint.ToString()); } } } /// <summary> /// 接收消息 /// </summary> /// <param name="socketClientPara"></param> private static void RecMsg(object socketClientPara) { Socket socketClient = socketClientPara as Socket; while (true) { //定義一個接受用的緩存區(100M字節數組) //byte[] arrMsgRec = new byte[1024 * 1024 * 100]; //將接收到的數據存入arrMsgRec數組,並返回真正接受到的數據的長度 if (socketClient.Connected) { try { //因為終端每次發送文件的最大緩沖區是512字節,所以每次接收也是定義為512字節 byte[] buffer = new byte[512]; int size = 0; long len = 0; string fileSavePath = @"..\..\files";//獲得用戶保存文件的路徑 if (!Directory.Exists(fileSavePath)) { Directory.CreateDirectory(fileSavePath); } string fileName = fileSavePath + "\\" + DateTime.Now.ToString("yyyyMMddHHmmssffff") + ".doc"; //創建文件流,然后讓文件流來根據路徑創建一個文件 FileStream fs = new FileStream(fileName, FileMode.Create); //從終端不停的接受數據,然后寫入文件里面,只到接受到的數據為0為止,則中斷連接 DateTime oTimeBegin = DateTime.Now; while ((size = socketClient.Receive(buffer, 0, buffer.Length, SocketFlags.None)) > 0) { fs.Write(buffer, 0, size); len += size; } DateTime oTimeEnd = DateTime.Now; TimeSpan oTime = oTimeEnd.Subtract(oTimeBegin); fs.Flush(); ShwMsgForView.ShwMsgforView(lstbxMsgView,socketClient.RemoteEndPoint + "斷開連接"); dict.Remove(socketClient.RemoteEndPoint.ToString()); listbOnline.Items.Remove(socketClient.RemoteEndPoint.ToString()); socketClient.Close(); ShwMsgForView.ShwMsgforView(lstbxMsgView, "文件保存成功:" + fileName); ShwMsgForView.ShwMsgforView(lstbxMsgView, "接收文件用時:" + oTime.ToString()+",文件大小:"+len/1024+"kb"); } catch { ShwMsgForView.ShwMsgforView(lstbxMsgView, socketClient.RemoteEndPoint + "下線了"); dict.Remove(socketClient.RemoteEndPoint.ToString()); listbOnline.Items.Remove(socketClient.RemoteEndPoint.ToString()); break; } } else { } } } /// <summary> /// 關閉連接 /// </summary> public static void CloseTcpSocket() { dict.Clear(); listbOnline.Items.Clear(); threadWatch.Abort(); socketWatch.Close(); ShwMsgForView.ShwMsgforView(lstbxMsgView, "服務器關閉監聽"); } } }
顯示時時動態信息ShwMsgForView類代碼如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows.Forms; namespace BusinessLogicLayer { public class ShwMsgForView { delegate void ShwMsgforViewCallBack(ListBox listbox, string text); public static void ShwMsgforView(ListBox listbox, string text) { if (listbox.InvokeRequired) { ShwMsgforViewCallBack shwMsgforViewCallBack = ShwMsgforView; listbox.Invoke(shwMsgforViewCallBack, new object[] { listbox, text }); } else { listbox.Items.Add(text); listbox.SelectedIndex = listbox.Items.Count - 1; listbox.ClearSelected(); } } } }
2、客戶端發送文件代碼
首先連接服務器代碼:
#region 連接服務器 private void btnBegin_Click(object sender, EventArgs e) { IPAddress address = IPAddress.Parse(txtIp.Text.Trim()); IPEndPoint endpoint = new IPEndPoint(address, int.Parse(txtPort.Text.Trim())); //創建服務端負責監聽的套接字,參數(使用IPV4協議,使用流式連接,使用TCO協議傳輸數據) socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); socketClient.Connect(endpoint); if (socketClient.Connected) { ShowMgs(socketClient.RemoteEndPoint +"連接成功"); } } #endregion
連接服務器成功后,即可發送文件了,先選擇文件:
#region 選擇要發送的文件 private void btnSelectFile_Click(object sender, EventArgs e) { OpenFileDialog ofd = new OpenFileDialog(); if (ofd.ShowDialog() == System.Windows.Forms.DialogResult.OK) { txtFileName.Text = ofd.FileName; } } #endregion
發送文件代碼:
//使用socket向服務端發送文件 private void btnSendFile_Click(object sender, EventArgs e) { int i = Net.SendFile(socketClient, txtFileName.Text,512,1); if (i == 0) { ShowMgs(txtFileName.Text + "文件發送成功"); socketClient.Close(); ShowMgs("連接關閉"); } else { ShowMgs(txtFileName.Text + "文件發送失敗,i="+i); } }
其中,發送文件Net類的代碼如下:
using System; using System.Net; using System.Net.Sockets; using System.IO; namespace MyCharRoomClient { /// <summary> /// Net : 提供靜態方法,對常用的網絡操作進行封裝 /// </summary> public sealed class Net { private Net() { } /// <summary> /// 向遠程主機發送數據 /// </summary> /// <param name="socket">要發送數據且已經連接到遠程主機的 Socket</param> /// <param name="buffer">待發送的數據</param> /// <param name="outTime">發送數據的超時時間,以秒為單位,可以精確到微秒</param> /// <returns>0:發送數據成功;-1:超時;-2:發送數據出現錯誤;-3:發送數據時出現異常</returns> /// <remarks > /// 當 outTime 指定為-1時,將一直等待直到有數據需要發送 /// </remarks> public static int SendData(Socket socket, byte[] buffer, int outTime) { if (socket == null || socket.Connected == false) { throw new ArgumentException("參數socket 為null,或者未連接到遠程計算機"); } if (buffer == null || buffer.Length == 0) { throw new ArgumentException("參數buffer 為null ,或者長度為 0"); } int flag = 0; try { int left = buffer.Length; int sndLen = 0; while (true) { if ((socket.Poll(outTime * 100, SelectMode.SelectWrite) == true)) { // 收集了足夠多的傳出數據后開始發送 sndLen = socket.Send(buffer, sndLen, left, SocketFlags.None); left -= sndLen; if (left == 0) { // 數據已經全部發送 flag = 0; break; } else { if (sndLen > 0) { // 數據部分已經被發送 continue; } else { // 發送數據發生錯誤 flag = -2; break; } } } else { // 超時退出 flag = -1; break; } } } catch (SocketException e) { flag = -3; } return flag; } /// <summary> /// 向遠程主機發送文件 /// </summary> /// <param name="socket" >要發送數據且已經連接到遠程主機的 socket</param> /// <param name="fileName">待發送的文件名稱</param> /// <param name="maxBufferLength">文件發送時的緩沖區大小</param> /// <param name="outTime">發送緩沖區中的數據的超時時間</param> /// <returns>0:發送文件成功;-1:超時;-2:發送文件出現錯誤;-3:發送文件出現異常;-4:讀取待發送文件發生錯誤</returns> /// <remarks > /// 當 outTime 指定為-1時,將一直等待直到有數據需要發送 /// </remarks> public static int SendFile(Socket socket, string fileName, int maxBufferLength, int outTime) { if (fileName == null || maxBufferLength <= 0) { throw new ArgumentException("待發送的文件名稱為空或發送緩沖區的大小設置不正確."); } int flag = 0; try { FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read); long fileLen = fs.Length; // 文件長度 long leftLen = fileLen; // 未讀取部分 int readLen = 0; // 已讀取部分 byte[] buffer = null; if (fileLen <= maxBufferLength) { /* 文件可以一次讀取*/ buffer = new byte[fileLen]; readLen = fs.Read(buffer, 0, (int)fileLen); flag = SendData(socket, buffer, outTime); } else { /* 循環讀取文件,並發送 */ while (leftLen != 0) { if (leftLen < maxBufferLength) { buffer = new byte[leftLen]; readLen = fs.Read(buffer, 0, Convert.ToInt32(leftLen)); } else { buffer = new byte[maxBufferLength]; readLen = fs.Read(buffer, 0, maxBufferLength); } if ((flag = SendData(socket, buffer, outTime)) < 0) { break; } leftLen -= readLen; } } fs.Flush(); fs.Close(); } catch (IOException e) { flag = -4; } return flag; } } }
這樣,就可以進行文件的傳輸了,效果圖如圖3所示
圖3 文件傳輸效果圖
二、使用TcpLister和TcpClient進行文件傳輸
TcpLister和TcpClient進行文件傳輸相對來說就要簡單些,服務器Demo界面如圖4所示:
圖4 服務器界面圖
啟動監聽和接收文件的代碼如下:
TcpListener listener;
#region 服務器啟動監聽服務,並開始接收文件 private void btnBegin_Click(object sender, EventArgs e) { btnBegin.Enabled = false; listener = new TcpListener(IPAddress.Parse(txtIp.Text), int.Parse(txtPort.Text)); listener.Start(); ShwMsgForView.ShwMsgforView(lstbxMsgView, "服務器開始監聽"); Thread th = new Thread(ReceiveMsg); th.Start(); th.IsBackground = true; } public void ReceiveMsg() { while (true) { try { int size = 0; int len = 0; TcpClient client = listener.AcceptTcpClient(); if (client.Connected) { //向列表控件中添加一個客戶端的Ip和端口,作為發送時客戶的唯一標識 listbOnline.Items.Add(client.Client.RemoteEndPoint); ShwMsgForView.ShwMsgforView(lstbxMsgView, "客戶端連接成功" + client.Client.RemoteEndPoint.ToString()); } NetworkStream stream = client.GetStream(); if (stream != null) { SaveFileDialog sfd = new SaveFileDialog(); if (sfd.ShowDialog(this) == System.Windows.Forms.DialogResult.OK) { string fileSavePath = sfd.FileName;//獲得用戶保存文件的路徑 FileStream fs = new FileStream(fileSavePath, FileMode.Create, FileAccess.Write); byte[] buffer = new byte[512]; while ((size = stream.Read(buffer, 0, buffer.Length)) > 0) { fs.Write(buffer, 0, size); len += size; } fs.Flush(); stream.Flush(); stream.Close(); client.Close(); ShwMsgForView.ShwMsgforView(lstbxMsgView, "文件接受成功" + fileSavePath); } } } catch(Exception ex) { ShwMsgForView.ShwMsgforView(lstbxMsgView, "出現異常:" + ex.Message); } } } #endregion
客戶端選擇文件后,即可直接發送文件:
客戶端代碼如下:
//使用TcpLister和TcpClient向服務端發送文件 private void button1_Click(object sender, EventArgs e) { TcpClient client = new TcpClient(); client.Connect(IPAddress.Parse(txtIp.Text), int.Parse(txtPort.Text)); NetworkStream ns = client.GetStream(); FileStream fs = new FileStream(txtFileName.Text, FileMode.Open); int size = 0;//初始化讀取的流量為0 long len = 0;//初始化已經讀取的流量 while (len < fs.Length) { byte[] buffer = new byte[512]; size = fs.Read(buffer, 0, buffer.Length); ns.Write(buffer, 0, size); len += size; //Pro((long)len); } fs.Flush(); ns.Flush(); fs.Close(); ns.Close(); ShowMgs(txtFileName.Text + "文件發送成功"); }
其中發送文件效果圖如圖5所示:
圖5 發送文件效果圖