程序简介
基于网友的提议,最近有点时间,便打算给之前的聊天程序增加一个功能-文件发送.
原理
文件发送跟字符串信息发送的原理其实是一样的,都是通过将需要发送的数据转换成计算机可以识别的字节数组来发送.当然,计算机本身并不知道你发送的是字符串信息还是文件,所以我们首先需要告诉计算机哪个发送的是文件,哪个是字符串信息;这里分别给它们的字节数组附加了一个类型标识符:字符串信息的字节数组标识符为0,文件的字节数组标识符为1.当一端将文件发送过去后,另一端则首先判断发送过来的类型标识符(1或者0),然后再调用相应的方法将获取的字节数组转换成人可以看懂的字符串信息或文件.
界面设计 - 客户端
这里新增了3个控件,用于实现文件发送功能.
Textbox: 文件名name: txtFileName
Button: 选择文件name: btnSelectFile 发送文件name: btnSendFile
代码实施 - 客户端
首先,我们需要写一个选择发送文件的方法,这里使用了最常见OpenFileDialog方法,用于选取需要发送的文件.
string filePath = null; //文件的全路径 string fileName = null; //文件名称(不包含路径) //选择要发送的文件 private void btnSelectFile_Click(object sender, EventArgs e) { OpenFileDialog ofDialog = new OpenFileDialog(); if (ofDialog.ShowDialog(this) == DialogResult.OK) { fileName = ofDialog.SafeFileName; //获取选取文件的文件名 txtFileName.Text = fileName; //将文件名显示在文本框上 filePath = ofDialog.FileName; //获取包含文件名的全路径 } }
选取文件之后,通过FileStream来读取文件字节数组,然后在读到的文件字节数组的索引为0的位置上增加了一个文件标识符1,目的是告知计算机该字节数组为文件字节数组.这里在向服务端发送文件的同时也发送了一个文件名(字符串信息),目的是在服务端成功接收文件后,自动将原文件名附加上去.
/// <summary> /// 发送文件的方法 /// </summary> /// <param name="fileFullPath">文件全路径(包含文件名称)</param> private void SendFile(string fileFullPath) { if (fileFullPath == null) { MessageBox.Show("请选择需要发送的文件!"); return; } else if (fileFullPath != null) { //创建文件流 FileStream fs = new FileStream(fileFullPath, FileMode.Open); //创建一个内存缓冲区 用于临时存储读取到的文件字节数组 byte[] arrClientFile = new byte[10 * 1024 * 1024]; //从文件流中读取文件的字节数组 并将其存入到缓冲区arrClientFile中 int realLength = fs.Read(arrClientFile, 0, arrClientFile.Length); //realLength 为文件的真实长度 byte[] arrClientSendedFile = new byte[realLength + 1]; //给新增标识符(实际要发送的)字节数组的索引为0的位置上增加一个标识符1 arrClientSendedFile[0] = 1; //告诉机器该发送的字节数组为文件 //将真实的文件字节数组完全拷贝到需要发送的文件字节数组中,从索引为1的位置开始存放,存放的字节长度为realLength. //实际发送的文件字节数组 arrSendedFile包含了2部分 索引为0位置上的标识符1 以及 后面的真实文件字节数组 Buffer.BlockCopy(arrClientFile, 0, arrClientSendedFile, 1, realLength); //调用发送信息的方法 将文件名发送出去 ClientSendMsg(fileName); socketClient.Send(arrClientSendedFile); txtMsg.AppendText("SoFlash:" + GetCurrentTime() + "\r\n您发送了文件:" + fileName + "\r\n"); } }
代码实施 - 服务端
由于新增了一个类型标识符,这里便将之前服务端接收信息的方法稍微改了下. 当服务端接收到含有标识符为0的字节数组,则直接将字节数组转换成字符串,并附加到聊天信息文本框上.若接收到的字节数组含有标识符1(即文件),则调用保存文件的方法SaveFile()将其保存为原文件;
string strSRecMsg = null; /// <summary> /// 接收客户端发来的信息 /// </summary> /// <param name="socketClientPara">客户端套接字的委托对象</param> private void ServerRecMsg(object socketClientPara) { Socket socketServer = socketClientPara as Socket; while (true) { int length = 0; //创建一个接收用的内存缓冲区 大小为10M字节数组 byte[] arrServerRecMsg = new byte[10 * 1024 * 1024]; try { //获取接收的数据,并存入内存缓冲区 返回一个字节数组的长度 length = socketServer.Receive(arrServerRecMsg); } catch (Exception ex) { txtMsg.AppendText("系统异常消息:" + ex.Message); break; } //判断发送过来的数据是文件还是普通文字信息 if (arrServerRecMsg[0] == 0) //0为文字信息 { //将字节数组 转换为人可以读懂的字符串 strSRecMsg = Encoding.UTF8.GetString(arrServerRecMsg, 1, length - 1);//真实有用的文本信息要比接收到的少1(标识符) //将接收到的信息附加到文本框txtMsg上 txtMsg.AppendText("SoFlash:" + GetCurrentTime() + "\r\n" + strSRecMsg + "\r\n"); } //如果发送过来的数据是文件 if (arrServerRecMsg[0] == 1) { SaveFile(arrServerRecMsg, length - 1);//同样实际文件长度需要-1(减去标识符) } } }
SaveFile()方法里包含了FileStream的Write()方法,用于将接收到的文件字节数组保存为实际文件,这里Write()方法传入了3个参数,文件的字节数组,需要拷贝文件字节数组的初始位置以及拷贝的字节数组的长度[具体介绍可以看这里].在获取到文件的同时,这里也获取了文件名(字符串信息),用于附加到另存为对话框的文件名上;同时截取了文件名的后缀,作为需要保存的文件类型.最后,在文件成功保存到服务端所在的计算机的同时,在聊天内容文本框中附加了成功接收的文件名和文件的保存路径.
/// <summary> /// 保存接收文件的方法 包含一个字节数组参数 和 文件的长度 /// </summary> /// <param name="arrFile">字节数组参数</param> /// <param name="fileLength">文件的长度</param> private void SaveFile(byte[] arrFile, int fileLength) { SaveFileDialog sfDialog = new SaveFileDialog(); //创建一个用于保存文件的对话框 sfDialog = new SaveFileDialog(); //获取文件名的后缀 比如文本文件后缀 .txt string fileNameSuffix = strSRecMsg.Substring(strSRecMsg.LastIndexOf(".")); sfDialog.Filter = "(*" + fileNameSuffix + ")|*" + fileNameSuffix + ""; //文件类型 sfDialog.FileName = strSRecMsg; //文件名 //如果点击了对话框中的保存文件按钮 if (sfDialog.ShowDialog(this) == DialogResult.OK) { string savePath = sfDialog.FileName; //获取文件的全路径 //保存文件 FileStream fs = new FileStream(savePath, FileMode.Create); //Write()方法需要传入3个参数,文件的字节数组,开始写入字节的索引位置以及字节数组的长度 fs.Write(arrFile, 1, fileLength); string fName = savePath.Substring(savePath.LastIndexOf("\\") + 1); //文件名 不带路径 string fPath = savePath.Substring(0, savePath.LastIndexOf("\\")); //文件路径 不带文件名 txtMsg.AppendText("天之涯:" + GetCurrentTime() + "\r\n您成功接收了文件" + fName + "\r\n保存路径为:" + fPath + "\r\n"); } }
运行程序
首先,启动服务端并持续监听客户端对其的连接,当客户端成功连接上服务端之后,两端便可以开始通信了.
两端建立连接之后,便可以开始互相通信了.
简单的两端对聊之后, 本人便打算发送个文件过去.
选取了一本张道真的语法书,后缀为.pdf(文件类型)
当点击发送文件按钮后,客户端聊天内容中显示"您发送了文件:张道真实用英语语法.pdf".
这时服务端收到文件后,程序弹出一个另存为对话框,用于保存接收到的文件.这里我们可以看到系统自动附加上了文件名和保存类型.
当服务端用户接收并保存文件之后,聊天内容里显示"您成功接收了文件张道真实用英语语法.pdf" 以及文件的保存路径.
附上源代码
服务端 ChatServer2.zip 客户端 ChatClient2.zip