c#TCP传输文件
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 发送文件效果图