简介:VB6.0局域网内传输文件的实例是一份面向VB初学者的网络编程学习源码,围绕局域网内点对点上传原理,演示了从好友列表管理、消息发送到图片传输与接收的完整链路。资源体积仅84KB,共包含20个文件:窗体文件frm与对应二进制资源frx占据主体,用于搭建好友列表、消息发送、图片查看、个人设置等界面;bas标准模块承载公共过程与逻辑;vbp/vbw工程文件负责组织项目结构,另有SCC与XML记录源码控制信息。已有601人次浏览学习,非常适合希望理解VB网络通信原理、快速上手文件传输开发的初学者。通过研读整个工程,可以学习如何使用Winsock等控件实现文件分块上传与实时接收,同时熟悉事件驱动下多窗体协作的典型写法;还能参考作者对界面布局和消息提示的处理方式,为后续开发类似QQ的实用工具打下基础。整体代码结构清晰,关键交互注释明确,便于对照学习。 前阵子翻U盘,翻出一个几年前用VB6.0写的局域网传文件小工程,顺手打开跑了一遍,居然还能正常收发。这个场景其实很常见:公司内网里两台电脑要互传一个几十兆的安装包,不想登QQ、不想拔U盘、公司又不让装第三方网盘工具,这时候一个自己写的、一百多KB的传文件小工具反而最顺手。今天就把这个VB6.0局域网内传输文件的实例完整拆一遍,从Winsock控件的选型、数据包结构设计,到完整的发送端/接收端代码,再到实际运行中踩过的坑,全部分享出来。
这个实例本身不复杂,但麻雀虽小五脏俱全。它涉及了TCP连接管理、二进制文件分包、进度反馈、异常恢复这几个关键点,学完这套逻辑,后面你想改成传输文件夹、加断点续传、甚至做成一个简单的局域网聊天工具,都只是在这个基础上做加法。适合正在学VB6的初学者,也适合在工控、教育、企业内部维护老系统的朋友直接拿去改。
1. 实例整体设计与思路拆解
1.1 这个实例到底解决了什么问题
局域网传文件这件事,Windows自带的共享文件夹其实能做,但实际操作里经常遇到两类问题:跨网段访问权限不好配,再加上有些精简版系统把共享组件砍掉了,点开网络邻居一片空白。QQ和微信传文件倒方便,但公司网络策略经常把这类端口禁掉,而且走外网服务器绕一圈,几十兆文件反而比局域网直连慢得多。
自己写一个传文件工具,本质就是绕过所有中间环节,让两台机器通过网卡直连。VB6里的Winsock控件是封装好的TCP/UDP网络组件,不需要懂socket底层细节,拖两个控件、写几行事件代码就能跑通。这个实例的核心思路就是:发送端读文件、分包、用TCP发给接收端;接收端收包、拼文件、落盘。
1.2 为什么选Winsock + TCP而不是UDP
Winsock控件支持两种协议:TCP(sckTCPProtocol,值0)和UDP(sckUDPProtocol,值1)。传文件这个场景,我建议直接用TCP。原因很直接:TCP自带确认重传机制,数据丢了会重发,接收端收到的字节顺序和发送端一致,不需要自己处理乱序;UDP只负责发,丢不丢、乱不乱序不管,传小报文或者做广播探测挺好,但拿它传文件,一旦网络稍微有波动,轻则文件损坏,重则程序卡死。
有人可能会问:TCP不是有黏包问题吗?这个确实有,后面我会专门讲怎么处理。这是TCP传文件的核心难点,也是很多初学者第一次写Winsock传文件翻车的地方。UDP表面上省了这个麻烦,但换来的是丢包、乱序,对传文件来说更致命。
所以结论是:传输文件用TCP,UDP只适合做同网段设备发现、在线状态广播这类辅助功能。
1.3 数据包结构设计
写传文件程序,数据包结构是第一关。我当时设计的包分三种:
| 包类型 | 内容 | 说明 |
|---|---|---|
| 文件头包 | 文件名 + 文件大小 | 接收端先收这个包,知道接下来要收多少数据 |
| 数据包 | 字节数组 | 文件二进制数据,分包发送 |
| 结束包 | 固定标识 | 可选,用于确认接收完成 |
文件头包我用的是字符串格式:FILE_NAME=xxx|FILE_SIZE=12345678,接收端收到后做一次解析。数据包就是纯字节流,一次发一块。结束包的作用是让接收端在收到完整数据后可以明确知道“传输完成”,或者在文件头和实际数据大小对不上的时候,能立刻判断出传输异常。
VB6里处理二进制数据最常用的是Byte数组。字符串在内存里是Unicode,直接传文件内容会出乱码,必须走Byte数组,这一点新手特别容易踩。
2. 界面布局与代码模块规划
2.1 发送端窗体
发送端界面很简单,我放了这些控件:
- TextBox显示待发送的文件路径,配一个“浏览”按钮调用CommonDialog选文件
- TextBox填接收端IP,默认填
192.168.1.100 - TextBox填端口,默认
8000 - 一个“连接并发送”按钮
- ProgressBar显示发送进度
- Label显示当前状态
发文件逻辑上不用单独开一个接收线程。Winsock的SendData本身是异步的,控件内部自己处理发送缓冲。你要做的就是把文件读成块、调用SendData、然后维护一个发送索引,把进度条往前走。
2.2 接收端窗体
接收端更简单:
- Winsock控件设为监听模式,端口固定
- TextBox显示保存路径
- ProgressBar显示接收进度
- Label显示文件名、文件大小、当前状态
接收端的核心在DataArrival事件里。这个事件在每次数据到达时触发,参数bytesTotal告诉你有多少字节可以读。你需要在事件里调用GetData把数据取出来,然后判断这个数据是文件头包、数据包、还是结束包。所有状态管理都在这个事件里完成。
2.3 状态机与全局变量
传文件过程是一个典型的状态机,我用模块级变量保存状态:
m_State:0=空闲,1=已收到文件头,2=正在收数据,3=传输完成m_FileName:保存接收文件名m_FileSize:保存文件总大小m_Received:记录当前已接收字节数m_FileNum:文件句柄
为什么必须搞状态机?因为TCP数据到达的时机是随机的,你可能一次事件里只收到了文件头的一部分,或者一次收到了文件头加一截数据内容。没有状态判断,数据就全乱了。有了状态机,每个分支都清楚现在处于哪个阶段,该干什么。
3. 核心代码实现与关键流程
3.1 连接建立与监听
发送端连接代码:
Private Sub cmdSend_Click() If Winsock1.State <> sckClosed Then Winsock1.Close Winsock1.RemoteHost = txtIP.Text Winsock1.RemotePort = Val(txtPort.Text) Winsock1.Connect lblStatus.Caption = "正在连接..." End Sub接收端监听代码:
Private Sub Form_Load() Winsock1.LocalPort = 8000 Winsock1.Listen lblStatus.Caption = "监听中..." End SubWinsock的Listen是异步的,监听后不阻塞界面,有连接进来时触发ConnectionRequest事件:
Private Sub Winsock1_ConnectionRequest(ByVal requestID As Long) ' 如果之前有连接没断,先关闭再接受新连接 If Winsock1.State <> sckClosed Then Winsock1.Close Winsock1.Accept requestID lblStatus.Caption = "已连接,等待文件..." End Sub有一点要注意:接收端在Accept之前要重新Listen,否则只能接一个连接。这个实例里一台机器同时只服务一个连接,够用了。
3.2 发送端分包与传输
发送端在Connect事件触发后,开始读文件分包发送:
Private Sub Winsock1_Connect() Dim f As Long Dim bHeader As String m_FileSize = FileLen(txtFile.Text) m_Sent = 0 ' 发送文件头,告诉接收端文件名和大小 bHeader = "FILE_NAME=" & GetFileName(txtFile.Text) & "|FILE_SIZE=" & m_FileSize Winsock1.SendData bHeader ' 用二进制方式打开文件 f = FreeFile Open txtFile.Text For Binary Access Read As #f ' 分块读取文件内容,每次64KB ReDim m_Buffer(65535) As Byte Do ' 这一轮实际能读到的字节数 Dim bytesRead As Long If m_FileSize - m_Sent < 65536 Then bytesRead = m_FileSize - m_Sent Else bytesRead = 65536 End If Get #f, , m_Buffer Dim bSend() As Byte ReDim bSend(bytesRead - 1) As Byte ' 只取有效数据 For i = 0 To bytesRead - 1 bSend(i) = m_Buffer(i) Next i Winsock1.SendData bSend m_Sent = m_Sent + bytesRead ProgressBar1.Value = (m_Sent / m_FileSize) * 100 lblStatus.Caption = "已发送 " & m_Sent & " / " & m_FileSize DoEvents Loop While m_Sent < m_FileSize Close #f ' 发个结束标识,方便接收端判断 Winsock1.SendData "FILE_END" lblStatus.Caption = "发送完成" End Sub分包大小选64KB是我测试下来比较稳的值。太小了,比如1KB,网络交互次数太多,CPU占用高,速度上不去;太大了,比如1MB,一次SendData的数据会超出TCP分段大小,底层要拆成多个包传,一旦网络拥堵,重传的成本很高。64KB是一个在效率和稳定性之间比较平衡的选择。
注意Get的用法。Get #f, , m_Buffer是一次性读满整个m_Buffer数组,但如果文件剩余字节不够65536,Get会读出少于数组长度的数据。所以我先算出bytesRead,再从m_Buffer里截取有效部分塞进bSend,否则会把没读到的脏数据也发出去,接收端文件就会变大、内容错位。
3.3 接收端重组与落盘
接收端核心在DataArrival事件。这里有个必须处理的关键问题:TCP是流式协议,一次接收到的数据不一定对应你发送时的一次SendData。
用生活化的方式理解:你往快递站寄了10个包裹,但快递员是按车送的,可能一车拉走3个,也可能一车拉走10个,你收到的是一堆流水线下来的包裹,得自己判断哪些是文件头、哪些是数据、哪些是结束包。这就是黏包问题。
我的处理方案是加一个接收缓冲区:
Private m_RecvBuf As String Private m_bHeaderDone As Boolean Private m_FileName As String Private m_FileSize As Long Private m_Received As Long Private m_FileNum As Integer Private Sub Winsock1_DataArrival(ByVal bytesTotal As Long) Dim bData() As Byte Dim sData As String ' 取原始字节数据 Winsock1.GetData bData, vbArray + vbByte ' 如果是文件头阶段,暂时把数据当字符串处理 If Not m_bHeaderDone Then ' 注意:这里不能用StrConv直接转,要处理Unicode问题 ' 简单做法:把字节还原成ASCII字符串 sData = StrConv(bData, vbUnicode) m_RecvBuf = m_RecvBuf & sData ' 检查是否收到完整的文件头 Dim posEnd As Long posEnd = InStr(m_RecvBuf, "|FILE_SIZE=") If posEnd > 0 Then Dim posSep As Long posSep = InStr(m_RecvBuf, "|FILE_SIZE=") m_FileName = Mid(m_RecvBuf, 10, posSep - 10) m_FileSize = Val(Mid(m_RecvBuf, posSep + 11, Len(m_RecvBuf) - posSep - 10)) m_bHeaderDone = True ' 打开文件准备写入 m_FileNum = FreeFile Open App.Path & "\" & m_FileName For Binary Access Write As #m_FileNum lblStatus.Caption = "开始接收: " & m_FileName End If Else ' 数据阶段:直接把字节写入文件 Dim lLen As Long lLen = UBound(bData) + 1 ' 小端序判断,VB默认按数组写入即可 Put #m_FileNum, , bData m_Received = m_Received + lLen ProgressBar1.Value = (m_Received / m_FileSize) * 100 lblStatus.Caption = "已接收 " & m_Received & " / " & m_FileSize ' 如果接收字节数超过文件大小,说明文件头解析可能有偏差 If m_Received >= m_FileSize Then Close #m_FileNum lblStatus.Caption = "接收完成" Winsock1.Close Winsock1.Listen End If End If End Sub这里我简化了黏包处理:文件头如果一次没来全,先把数据攒在m_RecvBuf里,等下次事件继续拼。但要注意,文件头用字符串拼接有个坑——文件头之后紧接着的二进制数据,如果用StrConv转成Unicode再拼,二进制里的某些字节会被破坏。所以更稳的做法是:文件头阶段也严格按字节处理,把接收到的字节先放进缓冲区,然后用InStrB和MidB这类字节函数来解析。
不过,为了让实例代码可读性高,当初我偷了个懒:数据包阶段不再做字符串拼接,一律走Byte数组入盘;只有文件头阶段做简单的字符串判断。文件头很短,一次到达的概率很高,所以这个方案实际跑下来也基本没问题。稳妥起见,我建议你参考下面这个改进版:
' 文件头解析用字节方式 Private Sub ParseHeader(bData() As Byte) Dim i As Long Dim sAcc As String ' 把字节转ASCII For i = LBound(bData) To UBound(bData) If bData(i) = 0 Then Exit For sAcc = sAcc & Chr(bData(i)) Next i ' 然后解析 sAcc End Sub这样避免StrConv转换导致二进制数据变乱。
4. 常见的坑与排查技巧实录
4.1 黏包半包问题
刚才说了,TCP是流协议,SendData一次调用和DataArrival一次触发不是一一对应关系。文件头和数据包粘在一起是常见现象,数据包之间粘在一起也常见。如果不做处理,接收端轻则报错,重则文件损坏。
我的排查经验是:写代码之前先在接收端的DataArrival里加一个断点,打印出bytesTotal和请求的字节数,观察一段时间,你就能直观看到数据到达的分布。别一上来就急着写逻辑,这个观察能省掉你后面大量调试时间。
处理黏包通常有两种思路:一种是像我上面那样,把所有接收数据统一塞进一个接收队列,再按自定义协议从队列里一段段切出来;另一种是用Winsock的PeekData先查看数据再决定是否GetData。第二种在Winsock控件里支持有限,处理不当容易漏数据,我推荐第一种:统一缓冲区+状态机解析。
4.2 大文件内存溢出
第一次写这个实例时,我犯过一个低级错误:用Open File For Binary读完后,直接把整个文件塞进一个大的Byte数组再SendData。一个100MB的文件,直接占用100MB内存,加上Windows对VB6进程可用内存的限制,文件稍大就报“内存溢出”,或者程序直接卡死无响应。
正确做法就是上面代码里的分块读取:用一个64KB的循环缓冲,读一块发一块。这样内存占用始终恒定在几十KB级别,发多大文件都不怕。同理,接收端也是边收边往磁盘写,不要攒在内存里最后统一写。
4.3 传输速度慢如何优化
默认参数下,Winsock传文件的极限速度大约在5MB/s到15MB/s之间。想再快,可以调整以下几个点:
- 增大SendData的大块尺寸。64KB是我们测过的稳定值,你可以在自己内网试128KB或256KB
- 关闭Winsock的
SetLingerState里一些缓冲选项,但这个控件直接暴露的接口不多,一般不建议动 - 接收端写入磁盘用
Put #配合二进制方式,注意不要每收到一个包就写一次进度条并调用DoEvents,这会严重拖慢速度。进度条可以每收2MB更新一次
如果你的内网是千兆交换机,但传输速度只有几MB/s,多半是接收端的事件处理太慢,尤其是进度条频繁刷新导致界面线程阻塞。把进度刷新频率降低以后,速度能明显提升。
4.4 防火墙、权限与环境兼容问题
局域网传文件失败,第一个嫌疑就是Windows防火墙。程序启动后会弹窗询问是否允许网络访问,如果当时点了取消,后续每次连接都会被拦。解决办法是到控制面板的防火墙设置里手动添加程序到允许列表。
另外,Win10/11下跑VB6编译出来的exe,要注意以管理员身份运行,否则可能会因为权限不足导致网络监听失败。还有一点:如果两台电脑不在同一网段,比如一个是192.168.1.x,一个是192.168.2.x,中间隔着路由器或三层交换机,Winsock的TCP照样能通,前提是路由策略没有拦截。这个实例里填IP时要填对方网卡的实际IP,可以在对方机器上打开cmd输入ipconfig查看。
5. 常见问题速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 发送端Connect不成功 | IP填错、端口被占用、防火墙拦截 | 在接收端用netstat -an看8000端口有没有监听 |
| 接收端收了文件但打不开 | 文件头解析错误、数据字节错乱 | 对比文件大小,如果接收字节数和发送端FileLen不一致,基本是黏包问题 |
| 传大文件时界面卡死 | 主线程频繁DoEvents和进度刷新 | 降低进度刷新频率,或者把发送循环放到独立逻辑里 |
| 第一次连接成功,第二次连不上 | 接收端Listen没有重新执行 | 在ConnectionRequest里关闭旧连接后必须重新Listen |
| 发送完文件后程序没退出 | Winsock没有Close | 传输完成后调用Winsock1.Close释放连接 |
这张表是实际项目里最常遇到的问题,我建议先把这些记下来,比背代码管用。
6. 扩展方向与我的实操心得
这个实例做完以后,我自己给它加过几个小功能,你可以参考:
- 支持多文件传输:把文件头包扩展成“文件名列表”,然后逐个传
- 断点续传:接收端记录已接收字节数,发送端从断点位置继续读文件
- 自动发现设备:发送端启动时向局域网内发一个UDP广播包,接收端收到后回应自己的IP,发送端自动填入,省去手输IP的麻烦
- 传输完成后自动校验:接收端根据文件大小和文件尾标志做简单校验,需要更严格的话可以增加MD5校验算法
我个人在实际操作中最大的体会是:VB6虽然老,但它的Winsock控件把网络编程的底层细节封装得足够简单,非常适合用来理解TCP通信的本质。写这个实例的过程中,我终于把“三次握手”“窗口机制”“黏包”这些概念真正搞懂了——不是背概念,而是看着自己的代码在网络环境里跑,数据一包一包地到达,问题一个一个地暴露,然后解决,这些知识就再也忘不掉了。
最后再分享一个打包发布的小经验:VB6程序在本机能跑,拷到别的机器上提示“控件未注册”,多半是Winsock.ocx没有注册。发布给同事用的时候,把msvbvm60.dll和ms winsock.ocx一起打进去,或者在目标机器上运行一次regsvr32 ms winsock.ocx注册组件。这个细节看似不起眼,实际使用中能帮你省掉很多“为什么我这边跑不了”的沟通成本。
本文还有配套的精品资源,点击获取