☰
VB6 Winsock控件实现局域网TCP点对点文件传输实例详解
2026/10/13 7:09:07 网站建设 项目流程

简介:VB6.0局域网内文件传输实例,是一份面向VB初学者的网络编程学习源码包。项目基于点对点上传原理,实现同一网段内文件与图片的发送接收,界面与交互风格类似QQ,包含好友列表、消息发送与视图查看等典型窗体,但重点不在于实用,而在于演示VB中WinSock控件通信、文件读写与多窗体协作的完整思路。压缩包共20个文件,涵盖6个窗体模块(.frm)、5个窗体伴随数据文件(.frx)、3个标准模块(.bas)及工程/工作区文件(.vbp/.vbw)等,各模块职责明确,便于按学习路径拆解。整包仅84KB,十分轻量,下载学习门槛低。目前已有603人学习,适合想通过实战理解局域网传输机制、提升VB网络编程能力的开发者参考。

1. 局域网内两台机器互相传文件:这个 VB6 实例到底解决了什么

某公司运维给办公网络里的老电脑批量下发说明文档,U 盘来回插太慢,聊天工具又限制了单文件体积,共享文件夹权限改来改去还容易暴露其他目录。这时候最直接的诉求就是:写一个小工具,在一台机器上输入对方 IP,点一下按钮,把文件通过网络推过去。VB6.0 局域网内传输文件的实例.rar 就是围绕这个场景打包的一套完整示例,核心是 VB6 自带的 Winsock 控件,走 TCP 连接实现点对点文件传输。

这个实例能解决的问题很具体:内网隔离环境下不依赖第三方服务,程序主动把文件从发送端推到接收端,接收端自动存到指定目录。它适合三类人:还在维护 VB6 老系统、需要给内部工具补网络能力的开发者;刚接触网络编程、想搞懂 Winsock 收发流程的初学者;以及想在老代码里植入文件推送功能但不想引入整套通信框架的工程师。它不是什么黑匣子,就是一个典型的两端程序,看懂协议设计和收发边界,你完全能照着自己改出一版能用的工具。

2. 为什么用 Winsock:选型对比与这个实例的传输协议设计

2.1 三种常见传法选型:共享文件夹、HTTP 服务、Winsock 控件

写局域网传输工具,第一反应往往不是 Winsock,而是共享文件夹。把目录设成共享,另一台机器通过 UNC 路径直接拷贝,确实零代码,但问题也明显:权限配置在域环境里很麻烦,共享目录会把其他文件暴露出去,而且传输过程没有程序介入,没法做进度提示、自动更名、按需推送。共享文件夹适合日常人工操作,不适合做「程序化推送」。

第二种常见做法是在接收端架一个 HTTP 上传服务,发送端用浏览器或客户端 POST 文件。这套方案能跑,但对 VB6 程序来说偏重:接收端要挂 Web 服务、处理 multipart 协议、管理会话,代码量远超一个 Winsock 控件。而且内网机器不一定有权限装服务组件,运维成本不低。

Winsock 控件是 VB6 自带的标准网络控件,封装了 TCP/UDP,处理的是原始字节流,收发两端怎么写完全由你控制。对文件传输来说,TCP 可靠有序,不用自己处理丢包重传,适合文件场景;UDP 丢包不可控,除非自己实现确认重传,通常只适合流媒体类需求。这个实例用 TCP 是正确选择,微软的 Winsock 控件属性少、事件清晰,DataArrival、ConnectionRequest、SendComplete 这几个事件就能覆盖完整收发链路。

2.2 TCP 连接下的数据边界问题:为什么示例里要设计「头信息 + 确认 + 文件流」

选型定了,下一步就是设计协议。很多新手在这里翻车:拿到 Winsock 就直接把整个文件塞进 SendData,接收端在 DataArrival 里取出来写盘,结果文件损坏、大小不对、中文乱码全来了。

根因是 TCP 本身只保证字节有序到达,不保证「一次发送对应一次接收」。你在发送端调用一次 SendData,接收端可能触发一次 DataArrival,也可能拆成两次;你连调十次 SendData,接收端可能合成一次事件才触发。这就是流没有边界。所以要传文件,必须在应用层自己定义边界。

这个实例的协议设计分三段:

  1. 发送端先发一行头信息,格式是「文件名|文件大小」加回车换行,这是文本阶段。
  2. 接收端收到完整头信息后,创建文件,回发一个字符串「OK」。
  3. 发送端收到「OK」后,才开始把文件内容按固定块大小逐块发送,接收端每次收到的数据都当作二进制写入文件。

这里的关键点是:头信息和文件体严格分开,中间靠一个确认信号隔开。因为确认之前发送端不会发文件体,接收端就不会遇到「一次 DataArrival 里既有头信息又有文件内容」的边界问题。用固定块大小发送文件体,接收端只要把字节追加写入文件即可,不需要关心一包数据从哪里开始、到哪里结束。这个设计把最麻烦的边界问题规避掉了。

2.3 实例整体结构与运行流程速览

一套典型的 VB6 传文件项目由两个独立工程组成:发送端工程和接收端工程。发送端负责选择文件、建立 TCP 连接、发包;接收端负责监听端口、接收数据、落盘。两端如果要在传输时显示进度条,都需要在 VB6 的「工程-部件」里勾选 Microsoft Windows Common Controls 6.0 才能使用 ProgressBar 控件;发送端要选择文件的话,还需要引用 Microsoft Common Dialog Control 6.0。新手打开工程提示「找不到控件」时,先检查部件勾选,这个细节很多教程不提。

运行流程速览:

  • 接收端先启动,绑定一个固定端口进入监听状态。
  • 发送端输入接收端机器的局域网 IP 和端口,点击连接。
  • TCP 三次握手完成后,发送端发送头信息。
  • 接收端解析头信息,创建文件,回送 OK。
  • 发送端收到 OK,开始按块循环发送文件字节流。
  • 接收端边收边写盘,写完关闭文件,恢复监听等待下一次传输。

下面两章按接收端和发送端拆开讲,每段代码都说明参数含义和边界。建议你先在本机用 127.0.0.1 跑通,再上真机。

3. 接收端搭建:先写监听、头信息解析与文件落盘

3.1 接收端界面与 Winsock 关键属性

接收端界面很简单:一个 Winsock 控件、一个 ProgressBar、一个文本框显示日志,再配一个 Label 显示状态就够了。重点在 Winsock 控件属性:

  • Protocol 属性设为 0(sckTCPProtocol),走 TCP。
  • LocalPort 设为固定值,比如 65000,监听端口。注意这个端口要大于 1024,避免跟系统保留端口冲突。
  • RemoteHost、RemotePort 在接收端不需要设置,连接是发送端主动发起的。

Form_Load 里先初始化端口并启动监听:

Private Const LISTEN_PORT As Long = 65000 Private Sub Form_Load() ' 确保控件处于关闭状态再进入监听 If Winsock1.State <> sckClosed Then Winsock1.Close End If Winsock1.LocalPort = LISTEN_PORT Winsock1.Listen txtLog.Text = txtLog.Text & "监听端口 " & LISTEN_PORT & " ..." & vbCrLf End Sub

逻辑说明:Listen 之后 Winsock 进入监听状态,之后收到的连接请求会触发 ConnectionRequest 事件,Port 65000 是收发两端约定好的服务端口。这里要养成习惯:Listen 前先检查 State,因为 Winsock 在 ConnectionRequest 事件里 Accept 时,监听 Socket 本身已经不再是监听状态,重复 Listen 会报错。

注意:请不要把监听端口设成常见服务端口(如 8080、1433),内网其他机器可能已经在占用,端口冲突时 Listen 不会报错,但连接永远建立不起来,排错很费时间。

3.2 监听与接受连接:ConnectionRequest 事件的正确写法

连接请求进来时,Winsock 触发 ConnectionRequest,requestID 是操作系统分配这个连接的唯一标识。Accept 之后,这个 Winsock 控件就从「监听 Socket」变成「连接 Socket」,可以收发数据了。常用的做法是单个 Winsock 控件一会儿监听、一会儿连接,简单够用;如果需要同时服务多个客户端,才需要控件数组配合 Load 生成新实例。

Private Sub Winsock1_ConnectionRequest(ByVal requestID As Long) ' 旧的连接还没结束就先关掉,避免状态混乱 If Winsock1.State <> sckClosed Then Winsock1.Close End If Winsock1.Accept requestID txtLog.Text = txtLog.Text & "已接受连接: " & Winsock1.RemoteHostIP & vbCrLf ' 重置接收状态 bHeadDone = False sHeadBuf = "" m_recvLen = 0 m_lastPct = 0 End Sub

逻辑说明:Accept 是必须调用的一步,不要试图自己处理 requestID,它由控件内部维护。Close 之前检查 State 是防止对未连接状态的 Socket 执行 Close 引发运行时错误。RemoteHostIP 可以拿到发送端的内网地址,写入日志方便排错。重置接收状态的目的是支持连续传多个文件:传完一个、连接断开、重新 Listen,下一个连接进来时不能带着上一次的文件名和字节数。

这里有个细节:Accept 之后原来监听的 Socket 变成了连接 Socket,所以接收端同一时刻只能服务一个发送端。如果你需要多台机器同时向它推文件,要改成控件数组:接受新连接时 Load Winsock1(新下标),让监听实例始终保留。理解这个差异对后续维护很重要。

3.3 接收头信息并创建文件

头信息阶段拿到的都是文本。发送端发来的格式是「文件名|文件大小」加 vbCrLf,接收端在 DataArrival 事件里持续累积字符串,直到找到 vbCrLf 才认为一行完整。这里用 InStr 找换行符,比固定长度包头更容易理解,也不会因为文件名长短变化而出错。

Private Sub Winsock1_DataArrival(ByVal bytesTotal As Long) Dim sHeadPart As String Dim bData() As Byte Dim nPos As Long Dim sLine As String Dim sSize As String On Error GoTo ErrHandler If Not bHeadDone Then ' 阶段一:头信息是文本,用 String 方式取出 Winsock1.GetData sHeadPart, vbString, bytesTotal sHeadBuf = sHeadBuf & sHeadPart ' 找整数行结束符 vbCrLf(回车+换行) nPos = InStr(sHeadBuf, vbCrLf) If nPos > 0 Then ' 拆出文件名与大小 sLine = Left$(sHeadBuf, nPos - 1) sHeadBuf = Mid$(sHeadBuf, nPos + 2) m_fileName = Left$(sLine, InStr(sLine, "|") - 1) sSize = Mid$(sLine, InStr(sLine, "|") + 1) m_fileSize = Val(sSize) ' 创建文件,路径取当前程序目录 m_fileNum = FreeFile Open App.Path & "\" & m_fileName For Binary Access Write As #m_fileNum bHeadDone = True txtLog.Text = txtLog.Text & "开始接收文件: " & m_fileName & _ " (" & Format$(m_fileSize \ 1024) & " KB)" & vbCrLf If m_fileSize = 0 Then FinishReceive Exit Sub End If ' 通知发送端:头信息没问题,可以发文件体了 Winsock1.SendData "OK" End If Else ' 阶段二:文件体按二进制接收,见 3.4 节 End If Exit Sub ErrHandler: txtLog.Text = txtLog.Text & "接收出错: " & Err.Description & vbCrLf Close #m_fileNum Winsock1.Close End Sub

逻辑说明:GetData 指定 vbString 会把到达的字节按当前系统代码页转成字符串,适合头信息这种纯文本。字节数 bytesTotal 作为第三个参数传入,表示这次最多取多少字节。m_fileSize 用 Val 解析字符串里的数字,使整个大小字段不受 VB6 长整型长度限制的约束,文件落到 Currency 类型变量里,后续判断完成状态用 Currency 类型累加。

Open 语句用 For Binary Access Write 创建文件,Binary 模式不会做任何文本转换,写进去什么就是什么,这是文件体必须用的模式。文件名直接拼接在 App.Path 后面,如果发送端传的文件名里带了目录分隔符,这里会变成路径穿越,所以可靠的做法是用 Dir 函数只取文件名部分,发送端在下一章就是这么写的。

3.4 文件体落盘与完成判断

阶段二里出现的都是文件二进制内容,不要再转字符串,一字节都不能变。每次 DataArrival 把字节数组原样 Put 进文件,同时累加已接收长度,当累加长度大于等于文件大小时,调用 FinishReceive 收尾。

Private Sub Winsock1_DataArrival(ByVal bytesTotal As Long) Dim bData() As Byte If bHeadDone Then ' 文件体,强制按字节数组取出 Winsock1.GetData bData, vbArray + vbByte, bytesTotal If UBound(bData) >= 0 Then Put #m_fileNum, , bData m_recvLen = m_recvLen + UBound(bData) + 1 ' 按百分比刷新进度,避免每个包都刷新界面造成卡顿 If m_recvLen * 100 \ m_fileSize > m_lastPct Then m_lastPct = m_recvLen * 100 \ m_fileSize ProgressBar1.Value = m_lastPct lblState.Caption = "已接收 " & Format$(m_recvLen \ 1024) & _ " KB / " & Format$(m_fileSize \ 1024) & " KB" End If If m_recvLen >= m_fileSize Then FinishReceive End If End If End If End Sub

逻辑说明:GetData 第二个参数用 vbArray + vbByte 组合,明确告诉控件把数据作为 Byte 数组返回,这是二进制安全的关键。UBound(bData) + 1 是本次实际取到的字节数,TCP 流的累计长度 m_recvLen 就是判断文件是否收完的唯一依据,不能靠 DataArrival 触发次数判断——TCP 会合并和拆分数据段,事件次数与块数完全不对应。

Put 语句没有指定记录号,表示从文件当前指针位置连续写入。VB6 的 Binary 模式下文件指针自动后移,不需要手动 Seek。完成判断用 >= 而不是 =,为了兼容「接收长度恰好等于文件大小」和极端情况下多读出来几个字节两种情形。FinishReceive 内部把文件关闭、状态重置、重新 Listen,让程序可以连续接下一个发送端:

Private Sub FinishReceive() Close #m_fileNum bHeadDone = False m_recvLen = 0 m_lastPct = 0 txtLog.Text = txtLog.Text & "文件接收完成: " & m_fileName & vbCrLf ' 重新进入监听,等待下一次发送 Winsock1.Close Winsock1.LocalPort = LISTEN_PORT Winsock1.Listen End Sub

这里的 LocalPort 重新赋值是为了防止 Listener 控件被 Close 后端口状态异常,重新绑定一次更可靠。FinishReceive 还有一个细节:接收端在完成时不主动向发送端回「完成」消息,因为发送端按已发送字节数判断自己的进度,两端各自按计数收尾即可,不需要额外的完成握手。

4. 发送端搭建:选文件、连接、分块发送与进度反馈

4.1 发送端界面与连接参数

发送端界面比接收端多两个东西:一个文本框让用户填接收端 IP,一个 CommonDialog 控件让用户选文件。Winsock 的 RemoteHost 和 RemotePort 不在设计期写死,运行时从界面取值,这样工具才能给不同机器复用。

关键控件参数:

  • Winsock1.Protocol = 0(sckTCPProtocol)。
  • RemotePort 在运行时设为 65000,与接收端一致。
  • CommonDialog1 用于弹文件选择框,需要先勾选部件 Microsoft Common Dialog Control 6.0。

点击发送按钮的代码:

Private Sub cmdSend_Click() ' 选择要发送的文件 CommonDialog1.Filter = "所有文件 (*.*)|*.*" CommonDialog1.ShowOpen If CommonDialog1.FileName = "" Then Exit Sub m_sendPath = CommonDialog1.FileName m_totalSize = FileLen(m_sendPath) ' 打开文件准备读取,Access Read 只读 m_sendFile = FreeFile Open m_sendPath For Binary Access Read As #m_sendFile ' 发起 TCP 连接 Winsock1.RemoteHost = txtIP.Text Winsock1.RemotePort = LISTEN_PORT Winsock1.Connect lblState.Caption = "正在连接 " & txtIP.Text & "..." End Sub

逻辑说明:FileLen 返回 Long 类型,单文件超过约 2.1GB 会溢出,如果需要传超大文件,改用 Scripting.FileSystemObject 的 File.Size 更稳妥。Connect 是异步的,调用后立即返回,连接成功与否由后续事件判断。发送按钮里只做了「打开文件 + 发起连接」两件事,真正发送要等连接事件回调,这是异步编程的基本节奏。

注意:连接之前不要尝试发送数据。VB6 的 Winsock 在未连接状态下调用 SendData 会触发错误 40006(Wrong protocol or connection state),这是新手常犯的时序错误。

4.2 连接成功后的头信息发送与握手等待

Connect 事件触发表示 TCP 连接已建立,此刻先发头信息。头信息包含文件名和文件大小,用竖线分隔,末尾带 vbCrLf。用 Dir(m_sendPath) 而不是直接取 FileName,可以拿到纯文件名,避免把路径里的反斜杠传给接收端造成路径穿越。

Private Sub Winsock1_Connect() Dim sHead As String ' 头信息:文件名|大小,以换行结束,方便接收端判断一行完整 sHead = Dir(m_sendPath) & "|" & Format$(m_totalSize, "0") & vbCrLf Winsock1.SendData sHead lblState.Caption = "头信息已发送,等待接收端确认..." End Sub

逻辑说明:SendData 的参数是 String 时,控件会把它按当前代码页转成字节流发出。Format$(m_totalSize, "0") 确保数字不带千分位等格式后缀,接收端 Val 解析时不会出现多余的逗号。发送完头信息后,工作并没有结束,必须等接收端回 OK。

接收端确认消息在发送端这边同样触发 DataArrival 事件:

Private Sub Winsock1_DataArrival(ByVal bytesTotal As Long) Dim sMsg As String ' 接收端回的确认是纯文本 Winsock1.GetData sMsg If sMsg = "OK" Then bHandshakeOk = True Timer1.Enabled = True lblState.Caption = "开始发送文件..." End If End Sub

逻辑说明:如果跳过握手直接发文件,接收端可能还没创建文件,或者头信息和文件体在同一个 DataArrival 事件里到达,处理逻辑会变得复杂。有了 OK 确认,发送端能确定接收端已经准备就绪。这个握手是整套示例里最值得保留的设计,后续加断点续传、加鉴权都在这条链路上扩展。

4.3 定时器分块发送:块大小与节奏

文件体不能一次性 SendData,常见的错误是把整个文件读进一个超大 Byte 数组然后整体发出。这样做有两个后果:内存瞬间被大数组占满,大文件直接卡死;Winsock 发送缓冲区打满后报错 10055(缓冲区不足)。正确做法是固定块大小循环发送,推荐块大小 8192 字节(8KB),这个值在百兆和千兆局域网内都很稳。

Private Const CHUNK_SIZE As Long = 8192 Private Sub SendNextChunk() Dim bChunk() As Byte Dim nLeft As Long Dim nRead As Long If bSendDone Then Exit Sub If Not bHandshakeOk Then Exit Sub nLeft = m_totalSize - m_sendPos If nLeft <= 0 Then ' 所有字节都已交给控件,延迟关闭,让发送队列排空 bSendDone = True Timer1.Enabled = False tmrClose.Enabled = True Exit Sub End If If nLeft < CHUNK_SIZE Then nRead = nLeft Else nRead = CHUNK_SIZE End If ReDim bChunk(0 To nRead - 1) Get #m_sendFile, m_sendPos + 1, bChunk Winsock1.SendData bChunk m_sendPos = m_sendPos + nRead If m_sendPos * 100 \ m_totalSize > m_lastPct Then m_lastPct = m_sendPos * 100 \ m_totalSize ProgressBar1.Value = m_lastPct lblState.Caption = "已发送 " & Format$(m_sendPos \ 1024) & " KB" End If End Sub

定时器驱动:

Private Sub Timer1_Timer() SendNextChunk End Sub

逻辑说明:每触发一次 Timer1_Timer,只从文件读一个 8KB 块并发送,然后立即返回。这样做有两个目的:一是控制发送节奏,给 Winsock 发送缓冲区排队数据的时间,避免 10055;二是让 UI 线程能持续处理消息,界面不会假死。Timer 的 Interval 配到 10 到 20 毫秒即可,太快反而没有意义,Windows 定时器本身有精度下限。

nLeft 小于块大小时取实际剩余字节,这是最后一个块的处理。nRead 声明为 Long 是安全的,因为单块最大 8192 远小于 Long 上限。Get 语句按 m_sendPos + 1 定位读取位置,因为文件记录号从 1 开始,而 m_sendPos 是已经发送的累计字节数。发送完成后不能立刻 Close,要等 tmrClose 定时器触发后再关闭,给 TCP 缓冲区留出排空时间,这个小细节是文件尾部不少丢字节现象的根源。

注意:m_sendPos 用 Currency 类型,避免大文件超过 Long 上限。Currency 是 VB6 里常用的大整数存储方案,虽然它是定点小数类型,但整数运算部分足够大,文件最大约 9 千万 GB,根本用不完。

4.4 让进度条平滑并且不拖慢界面

进度条刷新有一个常见误用:在每次 DataArrival 或每次发送一个块之后立刻更新 ProgressBar.Value。8KB 一个块,传 100MB 要刷新一万多次,每次刷新都会触发控件的重绘,界面明显掉帧。示例里用百分比节流来解决:只有百分比整数部分变化时才刷新。

If m_sendPos * 100 \ m_totalSize > m_lastPct Then m_lastPct = m_sendPos * 100 \ m_totalSize ProgressBar1.Value = m_lastPct End If

逻辑说明:\ 是整数除法,结果是向下取整的整数。百分比从 49 变到 50 才更新一次,100MB 文件进度条大约刷新 100 次,对界面的压力几乎可以忽略。m_lastPct 在连接开始时重置为 0,避免上一次传输的进度残留。

大文件传输还有一个隐形问题:Text 控件不停 Append 日志会把内存越撑越大。日志框只在关键节点写入(连接成功、开始接收、完成、出错),不要把每个块的进度都写进 TextBox,这是运维排错时最容易被忽略的内存增长点。

5. 局域网传输避坑:五个高频问题的现象、原因与解决

5.1 先用回环地址在单机上验证协议

拿到整套示例,不要急着在两台电脑上联调。先把接收端和发送端跑在同一台机器上,发送端 IP 填 127.0.0.1,这样能把「协议逻辑问题」和「网络环境问题」分开排查。回环地址不走网卡,所以它能验证的是代码本身:头信息解析对不对、文件写盘是否完整、进度是否正常结束。如果回环测试文件都损坏,问题一定在代码或系统代码页,不在防火墙。

回环通过之后才上真机,接收端把 IP 换成实际局域网地址。此时如果连不上,按 5.2 的防火墙条目处理。这个顺序能省下大量两头跑的排查时间,是本地联调的基本功。

5.2 五个高频坑与对应处理

坑一:接收到的文件比源文件小几KB,末尾缺失。

现象:进度条到达 100%,接收端日志显示完成,但目标文件大小和源文件不一致,文件打不开或解压报错。

原因:发送端把最后一个块 SendData 后立刻执行 Close。TCP 的发送缓冲区里还排着没发出去的字节,Close 一旦发起,Winsock 会丢弃队列数据,接收端自然收不到尾部。

解决:发送端发完最后一个块后不要立刻关闭,用 tmrClose 延时 300 到 500 毫秒再 Close,让缓冲区排空。更稳妥的做法是等 SendComplete 事件触发后再关闭,第 6 章会展开。接收端这边则用「累计接收长度 >= 文件大小」判断完成,而不是依赖连接关闭事件。

坑二:大文件传到一半发送端卡死,内存占用飙到几百MB。

现象:传输刚开始正常,几十 MB 之后界面无响应,任务管理器里进程内存持续上涨。

原因:一次性把整个文件读入 Byte 数组再 SendData,或者发送循环里没有给 UI 线程让出时间片。VB6 是单线程模型,长循环不调用 DoEvents 或没有定时器介入,界面就会假死。一次性大数组还会触发系统大内存块分配,产生明显卡顿。

解决:严格分块读取,每块不超过 8192 字节。用 Timer 驱动发送而不是在按钮事件里写 While 循环。如果非要写成循环,必须在每次 SendData 后调用 DoEvents 并加入 Sleep 间隔,但定时器方式是更干净的做法。

坑三:接收端写出的文件名中文乱码,个别字节不对。

现象:发送端发的是「项目说明.txt」,接收端存下的是「椤圭洰璇存槑.txt」或问号。

原因:VB6 的 String 内部是 Unicode,Winsock 发送 String 时按发送端系统代码页转成字节;接收端 GetData String 时按接收端代码页转回 Unicode。两端代码页不一致(比如一台简体中文系统、一台英文系统),转换结果就错乱。文件体如果也走 String 而不是 Byte 数组,二进制字节会再次发生代码页偏移,数据直接损坏。

解决:文件名和文件体永远分开处理。文件体一律用 Byte 数组 GetData 和 Put,任何情况下都不转 String。文件名要在两端约定一致的编码,最简单的办法是发送端用 StrConv(fileName, vbFromUnicode) 转成 Byte 数组后作为头信息的一部分发送,接收端用 StrConv(bName, vbUnicode) 还原,两端代码页一致时效果很好。日常使用中保持两端系统语言一致,也能规避大部分乱码。

坑四:本机回环通,换一台电脑就连接失败。

现象:回环测试一切正常,换到局域网另一台机器后,发送端卡在「正在连接」,接收端没有任何日志。

原因:Windows 防火墙默认拦截外部入站连接。VB6 编译出的 exe 不在系统白名单里,目标端口 65000 也没有放行规则,TCP SYN 包被直接丢弃。

解决:接收端机器上打开防火墙高级设置,添加入站规则放行 TCP 65000 端口,或者给程序 exe 添加允许规则。放行后用「netstat -ano | findstr 65000」确认接收端确实在监听,再用发送端 telnet 目标 IP 65000 测试端口通断。临时关闭防火墙只适合实验环境,生产环境必须用白名单规则。

坑五:传输完成后 Socket 关闭时报错 10038 或「无效的控件操作」。

现象:FinishReceive 里调用 Close 时弹出运行时错误,或者第二次连接时报错,程序崩溃。

原因:对监听 Socket 执行了原本属于连接 Socket 的操作。单控件模式下 Accept 之后这个控件已经变成连接 Socket,关闭后再设置 LocalPort 并 Listen 的次序不对,就会触发状态冲突。另一个常见原因是 Close 后没有检查 State 就再次 Accept。

解决:操作前统一检查 State,只在 sckClosed 状态下执行 Listen 和 Accept。FinishReceive 里先 Close 再赋 LocalPort 再 Listen,顺序不要颠倒。如果错误发生在 Accept 那一刻,多半是前一次连接没有关闭干净,可以加一句状态判断:If Winsock1.State <> sckClosed Then Winsock1.Close。

6. 进阶:用 SendComplete 做流控、断点续传思路与最后的验证习惯

6.1 用 SendComplete 事件替代定时器实现背压

示例用 Timer 控制发送节奏,胜在简单,但吞吐上限受定时器精度限制,而且 Timer 间隔设置不好仍然可能触发 10055。更专业的做法是取消定时器,每次只发一块,等 SendComplete 事件触发后再发下一块。SendComplete 表示这一块数据已经被 Winsock 完整交给操作系统网络栈,此时发送缓冲区有空位,天然形成背压:

Private Sub Winsock1_SendComplete() If Not bSendDone Then SendNextChunk End If End Sub

SendNextChunk 和 4.3 节完全一样,只是不再由 Timer 触发。启动方式改为:握手成功后直接调用一次 SendNextChunk,剩下的由事件链自动驱动。这个模式的优点是不会撞缓冲区上限,吞吐量接近 TCP 窗口允许的上限,缺点是要确认事件一定触发——如果 Socket 发送缓冲区足够大,SendData 返回后并不保证 SendComplete 会立刻触发,但 TCP 数据传输总是会排空的,事件最终会来。改造时给 SendComplete 加一个累计次数日志,方便对照字节数验证触发频率。

6.2 断点续传与完整性校验的改造方向

断点续传的扩展思路是在头信息握手阶段加一条消息。接收端创建文件后,如果发现目标文件已经存在,先读当前文件字节数,在回 OK 时把数字带上,比如回「OK|12345」;发送端解析出这个数字后,把读取文件的起始位置向后偏移到该字节处,继续发送即可。注意接收端打开文件时要改用 Append 或先 Seek 到已写长度,避免新数据覆盖旧内容。这个改造只涉及头信息解析和打开模式,不动发送主循环,改动量很小。

完整性校验最省事的方式是传输完成后在接收端跑 fc /b 源文件 目标文件,但这对程序化使用不友好。纯 VB6 没有内置 MD5,不引第三方 DLL 的话,可以退一步做抽样校验:发送端每隔若干块记录一个块长度值,接收端在相同时刻统计块长度,全部吻合则基本可信。要严格校验就得调用系统 CryptoAPI 计算 MD5,代码量会明显增加,内网小工具通常没必要做到那一步。

6.3 我的收尾验证清单

我自己改这类示例时,最后会固定跑四组测试:回环 1MB 小文件、回环 300MB 大文件、真机局域网小文件、真机局域网大文件。小文件验证协议边界和文件名中文编码,大文件验证内存占用和进度条刷新,真机验证防火墙规则,大文件真机验证发送缓冲区排空逻辑。每组传完都用 fc /b 逐一比对文件,确认字节数完全一致才算通过。

断点续传和并发多连接不要一开始就做,先把单连接跑稳,再考虑扩展。一次只改一个变量,改完立刻用上面四组测试回归。这套习惯帮我避开了大量「上午能用下午坏了」的尴尬场景,也希望你能用同样的方式对待这个示例——先回环、后真机、每改必比对。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询