ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

川崎机器人AS语言实战:单线程实现多客户端TCP服务器

川崎机器人AS语言实战:单线程实现多客户端TCP服务器 1. 为什么要在川崎机器人上自己写TCP服务器车间里那台川崎机器人跑得好好的突然接到需求产线MES系统要实时读取机器人的关节角度和运行状态同时还要能下发简单的启停指令。找供应商问了一圈对方报价六位数交付周期三个月。这种场景下最经济的做法就是自己动手用川崎控制器自带的AS语言写一个TCP服务器让机器人直接和上位机对话。川崎机器人控制器无论是C系列还是E系列都内置了以太网口AS语言提供了TCP_LISTEN、TCP_ACCEPT、TCP_RECV、TCP_SEND这一套通信指令。很多人第一次翻手册看到这些指令时觉得挺简单但真正上手写多客户端并发的时候问题就来了AS语言没有线程、没有异步IO、没有select/poll机制怎么同时伺候多个客户端这就是本篇要解决的核心问题。我会从零开始把多客户端TCP服务器的完整实现思路、代码结构、踩坑经验全部摊开讲。适合有川崎机器人基础编程经验、需要做上位机通信的工程师也适合刚接触AS语言但想直接上手实战的朋友。代码可以直接复制到控制器里跑但建议先看完原理部分再动手否则出了问题不好排查。提示本文所有代码基于川崎AS语言标准指令集不同控制器型号如C56、E73等在指令支持上可能有细微差异建议先确认你的控制器固件版本是否支持TCP_ACCEPT的非阻塞模式。2. AS语言做TCP通信的底层限制与破局思路2.1 AS语言没有多线程这是最大的拦路虎如果你写过C语言或者Python的TCP服务器第一反应可能是开线程或者用asyncio。但AS语言是单线程顺序执行的程序从第一行跑到最后一行中间不会自动切换。这意味着你不能在一个循环里阻塞等待某个客户端的数据否则其他客户端全部卡死。我见过不少人的第一版代码是这样的TCP_LISTEN之后进入一个WHILE TRUE循环里面TCP_ACCEPT等新连接然后TCP_RECV等数据。结果就是第一个客户端连上之后第二个客户端根本连不进来因为程序卡在TCP_RECV上了。破局的核心思路是把所有TCP操作都改成非阻塞模式用一个主循环轮询所有socket的状态。AS语言的TCP_ACCEPT和TCP_RECV都支持超时参数把超时设成0或者很小的值就能实现非阻塞效果。2.2 非阻塞模式的具体实现方式AS语言里TCP_ACCEPT的基本语法是TCP_ACCEPT port, timeout, socket_id其中timeout单位是毫秒。如果设成0表示立即返回不等待。返回值会告诉你是否有新连接。类似地TCP_RECV也有超时参数TCP_RECV socket_id, timeout, recv_data把这两个超时都设成0主循环就可以快速轮询先检查有没有新连接再遍历所有已连接的客户端检查有没有数据。这就是单线程下实现多客户端并发的核心机制。但这里有个坑超时设成0的时候CPU占用率会很高因为主循环跑得飞快。实际项目中我一般设成10到50毫秒既能保证响应速度又不会让控制器CPU过载。2.3 客户端管理的核心数据结构AS语言没有结构体数组这种高级数据结构但可以用多个一维数组来模拟。我通常用这几个数组来管理客户端数组名类型用途client_socketINTEGER数组存储每个客户端的socket ID-1表示空闲client_activeINTEGER数组标记该位置是否被占用client_bufferSTRING数组每个客户端的接收缓冲区client_countINTEGER当前连接的客户端总数最大客户端数量根据实际需求定一般设8到16个就够了。设太多会占用大量内存AS语言的内存资源本来就紧张。注意AS语言的数组下标从1开始不是从0开始。这一点和C语言不同写循环的时候要特别注意否则会数组越界。3. 多客户端TCP服务器的完整代码拆解3.1 初始化部分监听端口与数组清零先看初始化代码; 初始化变量 max_clients 8 listen_port 5000 i 0 ; 初始化客户端数组 FOR i 1 TO max_clients client_socket[i] -1 client_active[i] 0 client_buffer[i] NEXT i ; 启动监听 TCP_LISTEN listen_port, 1这里TCP_LISTEN的第二个参数是1表示允许同时监听的连接数。实际上这个参数在不同固件版本里行为不太一样有的版本设成1就够有的需要设成max_clients。我一般直接设成max_clients保险一点。数组清零这一步看起来简单但绝对不能省。AS语言的全局变量默认值不一定是0如果不手动初始化client_socket里可能是随机值后面判断的时候就会出问题。我刚开始写的时候就吃过这个亏调试了半天才发现是数组没清零。3.2 主循环轮询新连接与已有客户端主循环是整个服务器的核心结构如下WHILE TRUE ; 检查新连接 TCP_ACCEPT listen_port, 10, new_socket IF new_socket 0 THEN ; 找空闲位置 FOR i 1 TO max_clients IF client_active[i] 0 THEN client_socket[i] new_socket client_active[i] 1 client_buffer[i] EXIT ENDIF NEXT i ENDIF ; 轮询已有客户端 FOR i 1 TO max_clients IF client_active[i] 1 THEN TCP_RECV client_socket[i], 10, recv_data IF recv_data THEN ; 处理数据 CALL ProcessData(i, recv_data) ENDIF ENDIF NEXT i ; 短暂延时降低CPU占用 DELAY 0.01 ENDWHILE这段代码的逻辑很清晰先看有没有新客户端要连进来有的话找个空位放进去然后遍历所有已连接的客户端看有没有数据要收。TCP_ACCEPT和TCP_RECV的超时都设成10毫秒整个循环一轮大概几十毫秒响应速度完全够用。这里有个细节TCP_ACCEPT返回的new_socket如果大于0表示有新连接。但有些固件版本返回的是0表示成功-1表示失败。这个一定要查你手头控制器的手册确认不同版本行为不一样。我遇到过C56控制器返回0表示成功的情况代码里判断 0就漏掉了新连接排查了好久。3.3 数据接收与缓冲区管理TCP_RECV一次不一定能收到完整的数据包。TCP是流式协议没有消息边界客户端发过来的数据可能被拆成多次接收。所以需要一个缓冲区来拼接SUB ProcessData(client_idx, data) client_buffer[client_idx] client_buffer[client_idx] data ; 检查是否有完整消息假设以换行符结尾 pos INSTR(client_buffer[client_idx], CHR(10)) WHILE pos 0 msg LEFT$(client_buffer[client_idx], pos - 1) client_buffer[client_idx] MID$(client_buffer[client_idx], pos 1) CALL HandleMessage(client_idx, msg) pos INSTR(client_buffer[client_idx], CHR(10)) ENDWHILE END SUB这里假设客户端发来的消息以换行符\n结尾这是最常见的做法。如果你的协议不是这样需要相应调整。缓冲区拼接的时候要注意AS语言的字符串操作函数INSTR找子串位置LEFT$取左边部分MID$取中间部分。这些函数和VB类似用起来还算顺手。提示缓冲区不能无限增长。如果客户端发了数据但一直不完整缓冲区会越来越大。建议设一个上限比如4096字节超过就清空或者断开连接。3.4 数据发送与客户端断开处理发送数据用TCP_SENDTCP_SEND client_socket[i], send_data如果发送失败返回值会告诉你。这时候需要把该客户端标记为空闲关闭socketTCP_CLOSE client_socket[i] client_socket[i] -1 client_active[i] 0 client_buffer[i] 客户端主动断开的时候TCP_RECV会返回空字符串或者特定的错误码。这时候也要做同样的清理工作。我一般会在TCP_RECV之后检查返回值如果连续多次收到空数据就认为连接已断开。这里有个经验不要等到TCP_SEND失败才清理客户端。有些客户端断开后TCP_SEND可能还能成功一两次数据进了缓冲区但后续就会失败。更好的做法是定期发送心跳包如果心跳发送失败就立即清理。4. 实际调试中遇到的五个典型问题4.1 客户端连上后收不到数据第一次跑通代码的时候客户端能连上但发数据过去机器人没反应。排查了半天发现是TCP_RECV的超时参数设得太大了。我一开始设的是1000毫秒结果主循环每轮要等1秒才检查下一个客户端8个客户端轮一遍要8秒响应慢得没法用。改成10毫秒之后问题解决。但这里有个权衡超时太小CPU占用高超时太大响应慢。实测下来10到50毫秒是比较合适的范围。如果你的控制器还要跑其他任务建议设大一点比如50毫秒。4.2 多个客户端同时发数据导致数据错乱有一次测试的时候两个客户端同时发数据结果机器人的回复发错了对象。查代码发现是ProcessData里用了全局变量存client_idx但主循环里又改了它。AS语言没有局部变量作用域的概念至少老版本没有所有变量都是全局的。解决办法很简单不要在子程序里依赖全局的循环变量。把client_idx作为参数传进去子程序里用参数值不要直接引用主循环的i。这个坑很隐蔽因为单客户端测试的时候完全正常只有多客户端并发才暴露。4.3 长时间运行后内存泄漏AS语言没有垃圾回收机制字符串拼接操作会不断申请新内存。如果client_buffer只增不减跑几天之后控制器就会报内存不足。我的做法是每次处理完完整消息后立即把缓冲区里已处理的部分截掉。另外设一个最大缓冲区限制超过就强制清空。还有一点TCP_RECV返回的recv_data变量在每次循环后最好手动清空虽然AS语言可能会自动处理但手动清空更保险。4.4 控制器重启后客户端无法重连这个问题困扰了我很久。控制器断电重启后之前的客户端连接全部失效但客户端程序不知道还在往旧socket发数据。机器人这边重新监听后客户端不主动重连就永远连不上。解决方案是在客户端加心跳机制定期发送心跳包。如果连续几次心跳没有回复客户端就主动断开重连。机器人这边也要定期检查所有客户端的心跳超时就清理掉。这样双方都能及时感知连接状态。4.5 不同固件版本的指令差异川崎控制器的固件版本更新比较频繁不同版本之间TCP_ACCEPT和TCP_RECV的行为可能有差异。我遇到过E73控制器上TCP_ACCEPT超时参数单位是秒而不是毫秒的情况代码移植过去之后响应变得极慢。建议在代码里加一个版本检测或者至少在注释里写清楚适配的固件版本。移植到新控制器之前先用简单的测试程序验证一下各个TCP指令的实际行为。5. 让服务器更稳的几个进阶技巧5.1 用状态机管理客户端生命周期简单的client_active标记只能区分“空闲”和“占用”但实际项目中客户端有更多状态正在连接、已连接、正在接收数据、正在发送数据、等待断开等。用状态机来管理会更清晰状态值含义处理逻辑0空闲可分配给新连接1已连接正常收发数据2接收中数据不完整等待后续3待断开发送失败或心跳超时准备清理状态机的好处是逻辑清晰排查问题的时候一眼就能看出每个客户端处于什么阶段。而且后续扩展功能比如加认证、加加密的时候只需要在状态转换里插入处理逻辑就行。5.2 心跳机制的具体实现心跳包不需要太复杂客户端每隔几秒发一个固定字符串比如PING服务器收到后回复PONG。服务器这边记录每个客户端最后一次心跳的时间超过阈值比如30秒就认为断开。AS语言里可以用TIMER指令获取当前时间。每次收到心跳就更新client_last_heartbeat[i]主循环里检查当前时间和上次心跳的差值。这个逻辑不复杂但能极大提升连接的可靠性。5.3 数据协议的简单设计虽然本文主要讲TCP服务器实现但协议设计也很重要。我一般用最简单的文本协议每条消息一行以换行符结尾字段之间用逗号分隔。比如GET_JOINT,1 SET_SPEED,50 START STOP这种协议的好处是可读性好调试的时候直接用telnet就能测试。缺点是传输效率低如果数据量大可以考虑二进制协议。但对于大多数工业场景文本协议完全够用。5.4 日志记录与问题追溯AS语言没有现成的日志库但可以用文件操作自己写一个简单的日志函数SUB WriteLog(msg) OPEN log.txt FOR APPEND AS #1 PRINT #1, TIME$ msg CLOSE #1 END SUB每次有新连接、断开、收到数据、发送数据的时候都记一条日志。出问题的时候翻日志比盯着屏幕猜要高效得多。注意日志文件要定期清理否则会占满控制器存储空间。6. 代码整合与部署注意事项6.1 完整代码结构把前面的片段整合起来完整的程序结构如下; 变量定义 max_clients 8 listen_port 5000 i 0 new_socket 0 recv_data send_data ; 数组初始化 FOR i 1 TO max_clients client_socket[i] -1 client_active[i] 0 client_buffer[i] client_last_heartbeat[i] 0 NEXT i ; 启动监听 TCP_LISTEN listen_port, max_clients ; 主循环 WHILE TRUE ; 接受新连接 TCP_ACCEPT listen_port, 10, new_socket IF new_socket 0 THEN FOR i 1 TO max_clients IF client_active[i] 0 THEN client_socket[i] new_socket client_active[i] 1 client_buffer[i] client_last_heartbeat[i] TIMER CALL WriteLog(Client STR$(i) connected) EXIT ENDIF NEXT i ENDIF ; 轮询客户端 FOR i 1 TO max_clients IF client_active[i] 1 THEN TCP_RECV client_socket[i], 10, recv_data IF recv_data THEN CALL ProcessData(i, recv_data) ENDIF ; 心跳超时检查 IF TIMER - client_last_heartbeat[i] 30000 THEN CALL DisconnectClient(i) ENDIF ENDIF NEXT i DELAY 0.01 ENDWHILE这个结构清晰明了主循环负责调度子程序负责具体处理。实际项目中可以根据需要往HandleMessage里添加业务逻辑。6.2 部署时的几个关键检查点代码写完之后部署到控制器之前建议按这个清单检查一遍确认控制器的以太网口配置正确IP地址和上位机在同一网段确认防火墙没有拦截监听端口有些控制器有内置防火墙确认max_clients不超过控制器允许的最大socket数量确认所有数组都正确初始化没有残留的随机值确认TCP_ACCEPT和TCP_RECV的超时参数单位正确毫秒还是秒确认日志文件路径可写存储空间充足6.3 性能调优的经验数据根据我在C56和E73控制器上的实测几个关键参数的经验值如下参数推荐值说明TCP_ACCEPT超时10ms再小CPU占用高再大响应慢TCP_RECV超时10ms同上主循环延时10ms配合超时使用降低CPU占用最大客户端数8超过8个建议用多控制器方案心跳间隔5s客户端发送频率心跳超时30s服务器判定断开的阈值缓冲区上限4096字节超过强制清空这些数值不是绝对的需要根据实际网络环境和业务需求调整。比如网络延迟大的时候超时可以适当加大业务数据量大的时候缓冲区上限要调高。6.4 从单客户端到多客户端的迁移建议如果你已经有一个单客户端的TCP程序在跑想升级到多客户端建议不要直接改原代码而是新写一个程序并行测试。因为多客户端的逻辑和单客户端差异很大直接改容易引入难以排查的bug。迁移步骤建议先写一个最简单的多客户端框架只做连接和断开不做业务逻辑用多个telnet客户端测试连接和断开是否正常逐步加入数据收发功能每加一个功能测试一轮最后把原单客户端程序的业务逻辑移植过来这样虽然看起来慢但实际上比直接改代码再反复调试要快得多。我在实际项目中用这个方法从单客户端迁移到8客户端只用了两天其中大部分时间花在测试上。6.5 一个容易被忽略的细节socket ID的复用AS语言里TCP_CLOSE之后socket ID可能会被系统回收下次TCP_ACCEPT的时候可能分配到相同的ID。如果你的代码里用socket ID作为客户端的唯一标识就会出现问题旧客户端断开后新客户端拿到相同的ID但缓冲区里可能还有旧数据。解决办法是不要用socket ID作为唯一标识而是用数组下标i作为客户端编号。socket ID只用来收发数据不参与逻辑判断。这个细节在客户端频繁断开重连的场景下特别重要。7. 写在最后的一些个人体会这套多客户端TCP服务器的方案我在三个不同的产线项目里用过最长的已经稳定运行了一年多。中间踩过的坑基本都写在上面了但肯定还有没遇到的情况。AS语言虽然限制多但把非阻塞轮询这个核心思路吃透之后实现多客户端并发并不难。如果你刚开始接触AS语言的TCP编程建议先用一个客户端把基本流程跑通再逐步增加客户端数量。不要一上来就写8个客户端的完整代码那样出了问题很难定位。另外调试的时候善用日志AS语言没有断点调试功能日志是唯一能追溯问题的手段。代码里的HandleMessage子程序我留空了因为每个项目的业务逻辑都不一样。你可以在里面解析客户端发来的指令控制机器人运动或者读取机器人状态返回给客户端。这部分就是你的业务代码根据实际需求来写就行。
返回列表