YStudio Y++

TCP类使用说明

Y++ / YStudio 开发文档 · 一点滴

TCP类 提供基于 TCP 的网络收发:可作服务端监听,也可作客户端连接。支持文本与字节集、可选分包、TLS,以及异步发送文件(带进度)。

类型名必须写 TCP类。声明变量时一般会自动创建会话,多数情况不必手写 创建TCP()

相关:WebSocket类(消息帧 / ws·wss)、HTTP类(请求响应)。


1. 创建与变量作用域

1.1 推荐写法:声明即创建

TCP类 srv
TCP类 cli

声明后即可调用方法、注册事件。需要显式赋值时再用:

TCP类 t = 创建TCP()

1.2 局部变量(函数内)

只在当前函数(或事件程序)内有效,离开函数后不可再使用该变量名。适合一次性短连接、临时探测。

函数 按钮探测.被单击() {
    TCP类 cli
    cli.置超时(3000)
    如果 (cli.连接_同步("127.0.0.1", 9000) == 假) {
        信息框(cli.错误信息, 0)
        返回()
    }
    cli.发送("PING")
    文本 resp = cli.接收(256)
    信息框(resp, 0)
    cli.断开()
}

注意:局部 TCP类 在函数结束后会话会随变量结束而不可再操作;长连接、要在多个事件里共用的实例,请用文件顶部变量或全局变量。

1.3 文件顶部变量(本源文件模块级)

写在 .yc 文件顶部、所有函数之外。本文件内任意函数、窗口事件都可访问,适合「一个窗口配套一套 TCP」。

' 文件:源代码/主窗口.yc(示意)
TCP类 g_srv
TCP类 g_cli

函数 服务进入(整数 id, 文本 IP, 整数 端口, 文本 IP端口) {
    输出("进入 " + 到文本(id) + " " + IP端口)
}

函数 主窗口.创建完毕() {
    g_srv.客户进入(&服务进入)
    g_srv.数据到达(&服务数据)
    g_srv.客户离开(&服务离开)
    如果 (g_srv.启动(9000, "127.0.0.1") == 假) {
        信息框(g_srv.错误信息, 0)
    }
}

函数 主窗口.销毁() {
    g_cli.断开()
    g_srv.关闭全部()
}

文件顶部声明 TCP类 g_srv / g_cli 即可跨本文件事件与按钮复用同一会话。

1.4 全局变量(跨源文件)

需要多个 .yc 共用同一套 TCP 时,写在工程的 源代码/global.yc(或约定的全局源文件)顶部:

' 文件:源代码/global.yc
TCP类 全局服务端
TCP类 全局客户端

其它源文件可直接使用同名变量(工程内全局可见),无需再声明一遍:

' 文件:源代码/网络逻辑.yc
函数 启动服务() {
    全局服务端.置分包模式(1)
    全局服务端.客户进入(&进入)
    全局服务端.数据到达(&收到)
    全局服务端.客户离开(&离开)
    全局服务端.启动(9000)
}

1.5 三种作用域对照

写法位置 可见范围 典型用途
函数内 TCP类 t 仅该函数 短连接、探测、用完即断
本文件顶部 TCP类 g_xxx 本文件全部函数/事件 单窗体长连接、回显服务
global.yc 顶部 整个工程源文件 多模块共用服务端/客户端

1.6 使用顺序(重要)

  1. 声明 TCP类 变量
  2. 按需配置:超时、NoDelay、KeepAlive、TLS、分包
  3. 先注册事件,再 启动(或 监听)/ 连接
  4. 在回调或其它逻辑里 发送 / 广播 / 发送文件
  5. 结束时 断开 / 关闭全部 / 重置

事件不是控件那种 函数 按钮1.被单击(),而是普通函数 + 对象.事件名(&函数名) 注册。


2. 连接 ID 约定

角色 连接 ID
服务端 每个客户端一个正整数 ID,在「客户进入 / 数据到达 / 客户离开」等回调的第一个参数给出;也可用 取连接ID列表()
客户端 本端唯一连接,ID 固定为 1

服务端发数据必须带 ID:srv.发送(id, "hi")
客户端发数据不要带 ID:cli.发送("hi")


3. 事件

事件均在 界面线程 回调。回调内不要长时间阻塞(大循环、同步死等);重活请放到线程类或尽快返回。

0 可清除该事件回调。

3.1 客户进入(推荐;旧名「用户进入」)

注册: 对象.客户进入(子程序指针 回调)(或 用户进入
回调形参: 函数 名(整数 连接ID, 文本 IP, 整数 端口, 文本 IP端口)

  • 服务端:有客户端接入时触发;IP/端口/IP端口 为对端地址(接入时缓存,后续包复用)。
  • 客户端:仅 异步 连接() 成功时触发;ID 为 1。
  • 同步 连接_同步 成功 不会 触发本事件。
  • IP / IP端口 指针仅在本回调内有效。
函数 进入(整数 id, 文本 IP, 整数 端口, 文本 IP端口) {
    输出("进入 id=" + 到文本(id) + " " + IP端口)
}

srv.客户进入(&进入)
cli.客户进入(&进入)

3.2 数据到达

注册: 对象.数据到达(子程序指针 回调)
回调形参: 函数 名(整数 连接ID, 文本 IP, 整数 端口, 文本 IP端口, 文本 数据)

  • 分包模式 0:每次读到的 UTF-8 文本,可能粘包/半包。
  • 分包模式 1/2:已拆好的完整一帧文本。
  • 二进制请用「数据到达字节集」。可与文本回调同时注册,同一帧会各触发一次。
  • 对端信息与「客户进入」一致,按连接缓存,不必每包再查。
函数 收到(整数 id, 文本 IP, 整数 端口, 文本 IP端口, 文本 data) {
    srv.发送(id, data)          ' 回显(服务端)
}
srv.数据到达(&收到)

注意:回调参数 data / IP 只在本函数内有效,不要赋给全局文本后留到定时器再读。

3.3 数据到达字节集

注册: 对象.数据到达字节集(子程序指针 回调)
回调形参: 函数 名(整数 连接ID, 文本 IP, 整数 端口, 文本 IP端口, 字节集 数据)

适合文件块、协议体等非 UTF-8 数据。

函数 收到字节(整数 id, 文本 IP, 整数 端口, 文本 IP端口, 字节集 data) {
    srv.发送字节集(id, data)
}
srv.数据到达字节集(&收到字节)

3.4 客户离开(推荐;旧名「用户离开」)

注册: 对象.客户离开(子程序指针 回调)(或 用户离开
回调形参: 函数 名(整数 连接ID, 文本 IP, 整数 端口, 文本 IP端口)

对端关闭、本端 断开关闭全部、网络错误断开时都会触发。

函数 离开(整数 id, 文本 IP, 整数 端口, 文本 IP端口) {
    输出("离开 " + 到文本(id) + " " + IP端口)
}
srv.客户离开(&离开)

3.5 连接失败

注册: 对象.连接失败(子程序指针 回调)
回调形参: 函数 名(文本 原因)

仅客户端 异步 连接() 失败时触发。连接_同步 失败请看返回值与 取错误() / 错误信息

函数 失败(文本 原因) {
    信息框(原因, 0)
}
cli.连接失败(&失败)

3.6 发送进度

注册: 对象.发送进度(子程序指针 回调)
回调形参: 函数 名(整数 连接ID, 长整数 已发送, 长整数 总计)

发送文件 触发。开始时会先报一次 (0, 总计),之后每发出一块再报;空文件也会报 (0, 0)

函数 进度(整数 id, 长整数 sent, 长整数 total) {
    输出(到文本(sent) + "/" + 到文本(total))
}
cli.发送进度(&进度)

3.7 发送完毕

注册: 对象.发送完毕(子程序指针 回调)
回调形参: 函数 名(整数 连接ID, 逻辑 成功, 文本 错误)

发送文件 成功或失败都会触发。断开、关闭全部会取消进行中的发送并走失败。

函数 发完(整数 id, 逻辑 ok, 文本 err) {
    如果 (ok) {
        输出("发送完成")
    } 否则 {
        输出(err)
    }
}
cli.发送完毕(&发完)

4. 属性(只读,无括号)

属性与同名「取××」方法等价,可写成 t.超时t.取超时()

属性 类型 说明
超时 整数 当前超时毫秒数;写入请用 置超时
错误信息 文本 最近一次失败原因
模式 文本 "客户端" / "服务端" / "空闲"
是否已连接 逻辑 客户端是否仍保持连接
是否在监听 逻辑 服务端是否正在监听
是否连接中 逻辑 异步连接是否仍在进行
客户端数 整数 服务端当前在线连接数
端口 整数 服务端=监听端口;客户端=连接端口
绑定地址 文本 服务端监听绑定 IP
远程地址 文本 已连接远端 ip:port
连接地址 文本 客户端目标主机(不含端口)
TLS 逻辑 是否已启用 TLS 模式
TLS协议 文本 最近握手协商协议,如 TLS 1.2
要求客户端证书 逻辑 服务端是否要求客户端证书
分包模式 整数 0 / 1 / 2
整数 ms = t.超时
文本 e = t.错误信息
如果 (cli.是否已连接) {
    输出(cli.远程地址)
}

5. 方法说明

下列签名中,方括号表示可选参数。服务端/客户端参数差异已在说明中标出。

5.1 基础配置

置超时 / 取超时

对象.置超时([整数 毫秒=30000])
整数 = 对象.取超时()

≤0 时回退为 30000。影响连接与收发等待。

置NoDelay / 置KeepAlive

对象.置NoDelay([逻辑 启用=真])
对象.置KeepAlive([逻辑 启用=真])

默认均为开:NoDelay 降低小包延迟;KeepAlive 利于长连接保活。

5.2 TLS(须在监听/连接之前设置)

方法 签名 说明
置TLS 置TLS([逻辑 启用=真]) 后续连接走 TLS
置TLS验证 置TLS验证([逻辑 验证=真]) 客户端是否验证服务端证书;假仅建议测试
置TLS主机名 置TLS主机名(文本 主机名) 客户端 SNI/校验证书用;省略时常用连接地址
置TLS证书 置TLS证书(文本 证书文件, [文本 密码=""]) → 逻辑 服务端 PFX/P12
置TLS根证书 置TLS根证书(文本 根证书文件) → 逻辑 追加自签/自定义 CA(CER/DER)
置TLS客户端证书 置TLS客户端证书(文本 证书文件, [文本 密码=""]) → 逻辑 客户端双向认证
置TLS要求客户端证书 置TLS要求客户端证书([逻辑 要求=真], [逻辑 验证=真]) 服务端要求客户端证书
是否TLS 是否TLS() → 逻辑 或属性 TLS
是否要求客户端证书 是否要求客户端证书() → 逻辑 或属性 要求客户端证书
取TLS协议 取TLS协议() → 文本 或属性 TLS协议
srv.置TLS(真)
srv.置TLS证书("d:\\certs\\server.pfx", "password")
srv.启动(9443)

cli.置TLS(真)
cli.置TLS主机名("127.0.0.1")
cli.置TLS根证书("d:\\certs\\ca.cer")   ' 自签时
cli.连接("127.0.0.1", 9443)

5.3 分包(粘包拆包)

须在收发前设置;双方模式必须一致

模式 含义
0 原始字节流;数据到达 可能粘包/半包,需自拆
1 每帧前加 4 字节小端无符号长度(推荐二进制/JSON 消息)
2 按分隔符切帧(适合文本行协议)
对象.置分包模式([整数 模式=0])
整数 = 对象.取分包模式()
对象.置分包分隔符([文本 分隔符="\n"])   ' 仅模式 2
对象.置分包最大长度([整数 字节=1048576]) ' 单帧上限,默认 1MB
整数 = 对象.取分包最大长度()

模式 1/2 下发完整帧请用 发送分包;收完整帧走 数据到达 / 数据到达字节集
传大文件不要指望一帧塞完:用 发送文件,或自行循环 发送字节集;1MB 是单帧上限,不是整文件上限。

5.4 服务端:启动与连接管理

启动(推荐;同「监听」)

逻辑 = 对象.启动(整数 端口, [文本 绑定地址="0.0.0.0"], [整数 积压队列=0])
逻辑 = 对象.监听(整数 端口, [文本 绑定地址="0.0.0.0"], [整数 积压队列=0])  ' 旧名
  • 端口:165535
  • 绑定地址:0.0.0.0 全部网卡;本机联调常用 127.0.0.1
  • 积压队列:等待接入的连接个数(不是毫秒);0 或负数为系统默认。

返回真=已开始监听;假看 错误信息。须先注册相关事件。

停止 / 关闭全部 / 断开

对象.停止()                  ' 推荐;同「停止监听」——不再接受新连接;已有连接保留
对象.停止监听()              ' 旧名
对象.关闭全部()              ' 停止监听并断开全部连接
对象.断开(整数 连接ID)       ' 踢掉指定连接(服务端)

查询

整数 = 对象.取客户端数()
整数数组 = 对象.取连接ID列表()
文本 = 对象.取本地地址([整数 连接ID=1])
文本 = 对象.取客户IP([整数 连接ID=1])      ' 推荐;对端 IP
整数 = 对象.取客户端口([整数 连接ID=1])    ' 对端端口
文本 = 对象.取客户地址([整数 连接ID=1])    ' 「IP:端口」;同旧「取客户端地址」
文本 = 对象.取客户端地址([整数 连接ID=1])  ' 旧名
文本 = 对象.取绑定地址()
整数 = 对象.取端口()
逻辑 = 对象.是否在监听()

5.5 客户端:连接与同步收发

连接(异步)

逻辑 = 对象.连接(文本 地址, [整数 端口=0])
  • 地址可为 127.0.0.1、域名,或 IP:端口 / [IPv6]:端口
  • 端口为 0 或省略时从地址字符串解析。
  • 返回真只表示「已开始连接」,不代表已连上;成功走 客户进入,失败走 连接失败
  • 也可轮询 是否已连接 / 是否连接中

连接_同步

逻辑 = 对象.连接_同步(文本 地址, [整数 端口=0])

阻塞至成功或超时。返回真=已连上,可立刻 发送/接收;假看 错误信息
不会触发 客户进入 / 连接失败。适合短请求;长连接+事件请用 连接()

接收(同步)

文本 = 对象.接收([整数 最大长度=4096])
字节集 = 对象.接收字节集([整数 最大长度=4096])

单次读取上限默认 4096,硬上限 1MB。不等于完整消息;事件驱动场景请用 数据到达,勿在界面事件里死循环 接收

其它客户端查询

逻辑 = 对象.是否已连接()
逻辑 = 对象.是否连接中()
文本 = 对象.取远程IP()
整数 = 对象.取远程端口()
文本 = 对象.取远程地址()     ' 「IP:端口」;同 取客户地址()
文本 = 对象.取连接地址()
整数 = 对象.取连接端口()
对象.断开()                  ' 客户端省略连接ID

5.6 发送

发送 / 发送字节集

' 服务端
整数 = 对象.发送(整数 连接ID, 文本 数据)
整数 = 对象.发送字节集(整数 连接ID, 字节集 数据)

' 客户端
整数 = 对象.发送(文本 数据)
整数 = 对象.发送字节集(字节集 数据)

返回成功写出的字节数;≤0 失败,看 错误信息。文本按 UTF-8 写出。

发送分包

整数 = 对象.发送分包(整数 连接ID, 文本 数据)   ' 服务端
整数 = 对象.发送分包(文本 数据)               ' 客户端

按当前分包模式自动加长度前缀或分隔符。模式 0 时接近普通 发送

广播

整数 = 对象.广播(文本 数据)           ' 返回成功发送的连接数
整数 = 对象.广播字节集(字节集 数据)

仅服务端有意义:向全部在线客户端发送。

发送文件

异步按块发送本地文件(原始字节流,不套分包封装)。

逻辑 = 对象.发送文件(文本 路径)                              ' 客户端,默认分片 65536
逻辑 = 对象.发送文件(整数 连接ID, 文本 路径)                 ' 服务端,默认分片
逻辑 = 对象.发送文件(整数 连接ID, 文本 路径, 整数 分片大小) ' 自定义分片
逻辑 = 对象.发送文件(1, 文本 路径, 整数 分片大小)           ' 客户端自定义分片:连接ID 写 1
  • 返回真=已启动;假=立即失败(路径空、未连接等),看 错误信息
  • 启动后的失败只走「发送完毕」。
  • 进度走「发送进度」。
  • 建议双方 置分包模式(0),对端用「数据到达字节集」拼文件。
  • 同一会话再次 发送文件,或 断开 / 关闭全部,会取消进行中的发送。
  • 分片 ≤0 用 65536;上限 1MB。

5.7 状态与清理

文本 = 对象.取模式()       ' 或 对象.模式
文本 = 对象.取错误()       ' 或 对象.错误信息
对象.重置()                ' 关闭连接并恢复超时/NoDelay 等默认;事件回调也会清除,需重新注册

关闭全部 后可再次 监听 复用同一实例。


6. 综合实战示例

6.1 服务端回显(事件驱动)

文件顶部变量 + 先注册事件再监听。

TCP类 g_srv

函数 进入(整数 id, 文本 IP, 整数 端口, 文本 IP端口) {
    输出("[进] " + 到文本(id) + " " + IP端口)
}

函数 收到(整数 id, 文本 IP, 整数 端口, 文本 IP端口, 文本 data) {
    输出("[收] " + data)
    g_srv.发送(id, data)
}

函数 离开(整数 id, 文本 IP, 整数 端口, 文本 IP端口) {
    输出("[离] " + 到文本(id))
}

函数 主窗口.创建完毕() {
    g_srv.置超时(30000)
    g_srv.客户进入(&进入)
    g_srv.数据到达(&收到)
    g_srv.客户离开(&离开)
    如果 (g_srv.启动(9000, "127.0.0.1") == 假) {
        信息框(g_srv.错误信息, 0)
    }
}

函数 主窗口.销毁() {
    g_srv.关闭全部()
}

6.2 客户端异步连接 + 收发

TCP类 g_cli

函数 已连接(整数 id, 文本 IP, 整数 端口, 文本 IP端口) {
    输出("已连接 " + IP端口)
    g_cli.发送("HELLO")
}

函数 客户收到(整数 id, 文本 IP, 整数 端口, 文本 IP端口, 文本 data) {
    输出("回包: " + data)
}

函数 连失败(文本 原因) {
    信息框(原因, 0)
}

函数 按钮连接.被单击() {
    g_cli.客户进入(&已连接)
    g_cli.数据到达(&客户收到)
    g_cli.连接失败(&连失败)
    g_cli.客户离开(&客户离开)
    如果 (g_cli.连接("127.0.0.1:9000") == 假) {
        信息框(g_cli.错误信息, 0)
    }
}

函数 客户离开(整数 id) {
    输出("已断开")
}

函数 按钮断开.被单击() {
    g_cli.断开()
}

6.3 同步短请求(局部变量)

适合「连上 → 发 → 收 → 断」的一次性逻辑,不必注册连接成功事件。

函数 按钮查询.被单击() {
    TCP类 cli
    cli.置超时(5000)
    如果 (cli.连接_同步("127.0.0.1", 9000) == 假) {
        信息框(cli.错误信息, 0)
        返回()
    }
    cli.发送("TIME?\r\n")
    文本 resp = cli.接收(1024)
    如果 (resp == "") {
        信息框(cli.错误信息, 0)
    } 否则 {
        信息框(resp, 0)
    }
    cli.断开()
}

6.4 长度前缀分包(双方一致)

TCP类 g_srv
TCP类 g_cli

函数 服务帧(整数 id, 文本 IP, 整数 端口, 文本 IP端口, 文本 data) {
    g_srv.发送分包(id, data)
}

函数 客户帧(整数 id, 文本 IP, 整数 端口, 文本 IP端口, 文本 data) {
    输出("完整帧: " + data)
}

函数 开始长度分包演示() {
    g_srv.置分包模式(1)
    g_cli.置分包模式(1)

    g_srv.客户进入(&进入)
    g_srv.数据到达(&服务帧)
    g_srv.客户离开(&离开)
    g_srv.启动(9001, "127.0.0.1")

    g_cli.客户进入(&已连接)
    g_cli.数据到达(&客户帧)
    g_cli.连接失败(&连失败)
    g_cli.连接("127.0.0.1", 9001)
}

' 连接成功后:
' g_cli.发送分包("AAA")
' g_cli.发送分包("BBB")

分隔符模式将双方改为 置分包模式(2),可选 置分包分隔符("\r\n"),发送仍用 发送分包

6.5 广播通知

函数 按钮广播.被单击() {
    整数 n = g_srv.广播("NOTICE: 服务器将维护\r\n")
    输出("已发给 " + 到文本(n) + " 个客户端")
}

函数 按钮踢人.被单击() {
    整数数组 ids = g_srv.取连接ID列表()
    整数 i = 0
    循环 (数组长度(ids), i) {
        g_srv.断开(ids[i])
    }
}

6.6 发送文件 + 进度

发送端与接收端建议 置分包模式(0);接收端用字节集回调拼文件(此处仅演示接收计数)。

TCP类 g_cli
TCP类 g_srv
长整数 g_已收

函数 进度(整数 id, 长整数 sent, 长整数 total) {
    如果 (total > 0) {
        输出("进度 " + 到文本(sent * 100 / total) + "%")
    }
}

函数 发完(整数 id, 逻辑 ok, 文本 err) {
    如果 (ok) {
        输出("文件发送完成")
    } 否则 {
        信息框(err, 0)
    }
}

函数 服务收字节(整数 id, 字节集 data) {
    g_已收 = g_已收 + 字节集长度(data)
}

函数 按钮传文件.被单击() {
    g_已收 = 0
    g_srv.置分包模式(0)
    g_srv.数据到达字节集(&服务收字节)
    g_srv.客户进入(&进入)
    g_srv.客户离开(&离开)
    g_srv.启动(9002, "127.0.0.1")

    g_cli.置分包模式(0)
    g_cli.发送进度(&进度)
    g_cli.发送完毕(&发完)
    g_cli.客户进入(&已连接)
    g_cli.连接失败(&连失败)
    如果 (g_cli.连接_同步("127.0.0.1", 9002) == 假) {
        信息框(g_cli.错误信息, 0)
        返回()
    }
    ' 默认分片
    如果 (g_cli.发送文件("d:\\data\\demo.bin") == 假) {
        信息框(g_cli.错误信息, 0)
    }
    ' 自定义分片(客户端连接ID 固定为 1):
    ' g_cli.发送文件(1, "d:\\data\\demo.bin", 262144)
}

服务端向某个客户端推文件:

g_srv.发送进度(&进度)
g_srv.发送完毕(&发完)
g_srv.发送文件(id, "d:\\data\\push.bin", 65536)

6.7 全局变量跨文件

源代码/global.yc

TCP类 聊天服务
TCP类 聊天客户

源代码/服务.yc

函数 启动聊天服务() {
    聊天服务.置分包模式(2)
    聊天服务.置分包分隔符("\n")
    聊天服务.客户进入(&聊天进入)
    聊天服务.数据到达(&聊天收到)
    聊天服务.客户离开(&聊天离开)
    聊天服务.启动(9010)
}

源代码/客户.yc

函数 连接聊天室() {
    聊天客户.置分包模式(2)
    聊天客户.置分包分隔符("\n")
    聊天客户.客户进入(&客户进房)
    聊天客户.数据到达(&客户收消息)
    聊天客户.连接失败(&客户失败)
    聊天客户.连接("127.0.0.1", 9010)
}

函数 发送聊天(文本 行) {
    如果 (聊天客户.是否已连接) {
        聊天客户.发送分包(行)
    }
}

7. 常见注意点

  1. 先事件,后监听/连接;否则会漏掉进入、数据等回调。
  2. 服务端发送必带连接 ID;客户端发送不要带 ID(自定义分片的 发送文件(1, 路径, 分片) 除外)。
  3. 回调在界面线程:不要 延时 死等,也不要长时间占满 CPU。
  4. 连接()连接_同步() 事件行为不同;按场景二选一。
  5. 分包模式双方一致;传文件优先 发送文件 + 数据到达字节集,不要用 1MB 单帧硬塞整文件。
  6. 长连接实例放文件顶部或 global.yc;局部变量只适合短请求。
  7. 失败统一查 对象.错误信息对象.取错误()
  8. 客户端 断开() 会停止并回收接收线程;关窗前也会自动清理全部 TCP 会话。
  9. 支持库面板路径:扩展类库 → TCP类 → 事件 / 方法 / 属性。