在Codesys利用Socket操作Mysql
一、概述虽然Codesys有付费的mysql操作库,但是为了更大的自由度,我利用Codesys的socket自己去实现Mysql的操作。
操作Mysql的程序的逻辑很简单:建立连接->接收并解析认证包->发送数据库登录验证信息包->获得授权->发送Query命令->接收并解析结果->断开连接。其中认证过程,用到了SHA1加密。
Codesys上,需要用到"CAA Net Base Services" 和 "CAA Memory"两个公共库。
二、程序组织
三、代码展示
3.1AuthPacket
//作者:AlongWu
TYPE AuthPacket :
STRUCT
FrameDataLen :UDINT:=0;
Sqid :BYTE:=1;
ClientCap1 :WORD:=16#A68D;
ClientCap2 :WORD:=16#006F;
MaxSize :UDINT:=16#00FFFFFF;
CharSetCode :BYTE:=16#21;
UserName :STRING(50);
DataBaseName :STRING(50);
Password :ARRAY OF BYTE; //加密后的认证摘要
clientAuthPlugin :STRING(50):='mysql_native_password';
END_STRUCT
END_TYPE
3.2HandShakePacket
//作者:AlongWu
TYPE HandShakePacket :
STRUCT
FrameDataLen :UINT; //packet的内容区字节数
DataBaseMsg :STRING(50); //当前服务的mysql版本信息
Sqid :BYTE; //交互id
ProtoolId :BYTE; //协议号
ServerThreadId :UDINT; //服务器线程id
AuthBuffer :ARRAY OF BYTE; //20Byte的随机认证码
ServerCap :WORD; //当前服务器权能组(16位,每位代表1项能力)
CharSetCode :BYTE; //字符编码
ServerState :WORD; //当前服务器状态
END_STRUCT
END_TYPE
3.3MysqlConnectInfo
//作者:AlongWu
TYPE MysqlConnectInfo :
STRUCT
ServerIp :NBS.IP_ADDR:=(sAddr:='192.168.0.105'); //mysql服务器ip地址
Port :UINT:=3306; //服务器端口号
UserName :STRING(50):='SpinMachine'; //登录账号
Password :STRING(50):='123456'; //登录密码
DataBase :STRING(50):='spinmachine3'; //数据库名称
ip1 :BYTE:=192;
ip2 :BYTE:=168;
ip3 :BYTE:=0;
ip4 :BYTE:=105;
END_STRUCT
END_TYPE
3.4MysqlRowInfo
//作者:AlongWu
TYPE MysqlRowInfo :
STRUCT
ColumnCount :BYTE; //列数量
RowData :ARRAY OF BYTE; //行的字节数组
IsNull :BOOL; //数据表为空
END_STRUCT
END_TYPE
3.5MysqlFirstRowData_FB
//作者:AlongWu
FUNCTION_BLOCK MysqlFirstRowData_FB
VAR CONSTANT
abyEmpty :ARRAY OF BYTE; //复位buffer
END_VAR
VAR_INPUT
InputRxBytes :ARRAY OF BYTE; //接收字节数组
END_VAR
VAR_OUTPUT
FirstRowInfo :MysqlRowInfo; //解析后首行数据,结构体
END_VAR
VAR
columnCount :BYTE; //列数量
Addr :UINT; //地址
i :UINT;
TmpfieldSize :BYTE; //当前field字节数
END_VAR
(*
Result Set Header 返回数据的列数量
Field 返回数据的列信息(多个)
EOF 列结束
Row Data 行数据(多个)
EOF 数据结束
举例:
返回:3行3列的表格数据。
返回的数据帧1次, 该帧里面有 9个packet,即9个packet是在同一个报文里面。
第1个packet =Result Set Header
第2个packet =列1的field
第3个packet =列2的field
第4个packet =列3的field
第5个packet =EOF packet
第6个packet =行1的packet
第7个packet =行2的packet
第8个packet =行3的packet
第9个packet =EOF packet
*)
(*
Result Set Header 返回数据的列数量
packlen :3B
packid :1B
columnCount :1B
*)
(*
Field packet
packlen :3B
packid :1B
LengthEncodedString目录名称
LengthEncodedString数据库名称
LengthEncodedString数据表名称
LengthEncodedString数据表原始名称
LengthEncodedString列(字段)名称
LengthEncodedString列(字段)原始名称
int<1> 填充值
int<2> 字符编码
int<4> 列(字段)长度
int<1> 列(字段)类型
int<2> 列(字段)标志
int<1> 整型值精度
int<2> 填充值(0x00)
LengthEncodedString默认值
row Packet
packlen :3B
packid :1B
data1_len :1B = data的字节数
data :data1_len B
data2_len :1B = data的字节数
data :data2_len B
...... 多个字段值
*)
//列数量
columnCount := GVL.Mysql_abyRx;
//录入列数量
FirstRowInfo.ColumnCount :=columnCount;
//清空buffer
FirstRowInfo.RowData := abyEmpty;
//复位isNull标志
FirstRowInfo.IsNull := FALSE;
//初始化地址
Addr := 5;
columnCount := columnCount-1;
FOR i:=0 TO columnCount BY 1 DO
Addr := Addr + GVL.Mysql_abyRx+4;
END_FOR
//EOF packet
Addr := Addr + GVL.Mysql_abyRx+4;
//首个row的字符复制
IF GVL.Mysql_abyRx = 5 AND GVL.Mysql_abyRx = 16#FE THEN
//如果field的eof后面还是 eof,即行数据为空
FirstRowInfo.IsNull := TRUE;
ELSE
MEM.MemMove(pSource:= ADR(GVL.Mysql_abyRx)+Addr+4, pDestination:= ADR(FirstRowInfo.RowData), uiNumberOfBytes:=BYTE_TO_UINT(GVL.Mysql_abyRx));
END_IF
3.6MysqlQueryCmdPack_FB
//作者:AlongWu
FUNCTION_BLOCK MysqlQueryCmdPack_FB
VAR CONSTANT
EmptyResult : ARRAY OF BYTE;
END_VAR
VAR_INPUT
Cmd :BYTE; //命令码
InputBytes :ARRAY OF BYTE; //请求指令的内容
END_VAR
VAR_OUTPUT
Result :ARRAY OF BYTE;
ResultSize :UINT;
END_VAR
VAR
PacketLen :UDINT; //包的内容的字节长度 (总字节数 - 4)
tmp4byte :ARRAY OF BYTE;
i :UINT;
END_VAR
(*
命令码列表
0x00 COM_SLEEP (内部线程状态)
0x01 COM_QUIT 关闭连接
0x02 COM_INIT_DB 切换数据库
0x03 COM_QUERY SQL查询请求
0x04 COM_FIELD_LIST 获取数据表字段信息
0x05 COM_CREATE_DB 创建数据库
0x06 COM_DROP_DB 删除数据库
0x07 COM_REFRESH 清除缓存
0x08 COM_SHUTDOWN 停止服务器
0x09 COM_STATISTICS 获取服务器统计信息
0x0A COM_PROCESS_INFO 获取当前连接的列表
0x0B COM_CONNECT (内部线程状态)
0x0C COM_PROCESS_KILL 中断某个连接
0x0D COM_DEBUG 保存服务器调试信息
0x0E COM_PING 测试连通性
0x0F COM_TIME (内部线程状态)
0x10 COM_DELAYED_INSERT (内部线程状态)
0x11 COM_CHANGE_USER 重新登陆(不断连接)
0x12 COM_BINLOG_DUMP 获取二进制日志信息
0x13 COM_TABLE_DUMP 获取数据表结构信息
0x14 COM_CONNECT_OUT (内部线程状态)
0x15 COM_REGISTER_SLAVE 从服务器向主服务器进行注册
0x16 COM_STMT_PREPARE 预处理SQL语句
0x17 COM_STMT_EXECUTE 执行预处理语句
0x18 COM_STMT_SEND_LONG_DATA 发送BLOB类型的数据
0x19 COM_STMT_CLOSE 销毁预处理语句
0x1A COM_STMT_RESET 清除预处理语句参数缓存
0x1B COM_SET_OPTION 设置语句选项
0x1C COM_STMT_FETCH 获取预处理语句的执行结果
*)
//先复位
Result := EmptyResult;
//获取string的内容的字节长度,不含0结束符
i:=0;
WHILEInputBytes <> 0 AND i<999 DO
i:=i+1;
END_WHILE
//包的内容 = cmd + string ;
PacketLen :=UINT_TO_UDINT(1+i+1);
ResultSize :=1+i+1;
//包的内容长度赋值
MEM.MemMove(pSource:= ADR(PacketLen), pDestination:= ADR(Result), uiNumberOfBytes:=3 );
//sid
Result := 0;
//cmd
Result := Cmd;
//cmd内容赋值
MEM.MemMove(pSource:= ADR(InputBytes), pDestination:= ADR(Result)+5, uiNumberOfBytes:=ResultSize);
//包总字节数
ResultSize := ResultSize +4;
3.7SHA1_FB
//作者:AlongWu
FUNCTION_BLOCK SHA1_FB
VAR CONSTANT
Kt :ARRAY OF DWORD:=; //固定K常量
Ht :ARRAY OF DWORD:=; //固定H常量
emptpBuff :ARRAY OF BYTE;
END_VAR
VAR_INPUT
InputBytes :ARRAY OF BYTE; //输入字节数组
END_VAR
VAR_OUTPUT
ResultCode :ARRAY OF BYTE; ;
END_VAR
VAR
strSize :UINT;
i :UINT;
addr :UINT;
tmpBuffer :ARRAY OF BYTE;
tmp2Byte :ARRAY OF BYTE;
Ht_temp :ARRAY OF DWORD:=; //缓冲的H常量
Wt :ARRAY OF DWORD;
HtTmp :DWORD;
END_VAR
//Ht_temp,复位
Ht_temp := Ht;
tmpBuffer := emptpBuff;
i:=0;
//找出字符串字节长度
WHILEInputBytes <> 0 AND i<50 DO
i:=i+1;
END_WHILE
strSize:=i;
addr := i;
MEM.MemMove(pSource:= ADR(InputBytes), pDestination:= ADR(tmpBuffer), uiNumberOfBytes:=addr );
//字符最后byte的后面补 0x10
tmpBuffer := 16#80;
//字符的总位数, 字节数*8
strSize := strSize *8;
MEM.MemMove(pSource:= ADR(strSize), pDestination:= ADR(tmp2Byte), uiNumberOfBytes:=2 );
//在最后的2个byte,录入 输入字符的位数。
//高位在前,低位在后
tmpBuffer:= tmp2Byte;
tmpBuffer:= tmp2Byte;
//Wt 前 16个 32位,为tmpBuffer 64个8位
FOR i:=0 TO 15 BY 1 DO
addr := i*4;
MEM.MemMove(pSource:= ADR(tmpBuffer)+addr, pDestination:= ADR(Wt), uiNumberOfBytes:=4 );
Wt := MEM.ReverseBYTEsInDWORD (Wt);
END_FOR
//扩展到 80个32位
FOR i:=16 TO 79 BY 1 DO
addr := i*4;
Wt := Wt_Func(Wt,Wt,Wt,Wt);
END_FOR
FOR i:=0 TO 19 BY 1 DO
HtTmp :=Htemp_Func(Ht_temp,Ft1_Func(Ht_temp,Ht_temp,Ht_temp),Ht_temp,Wt,Kt);
Ht_temp := Ht_temp;
Ht_temp := Ht_temp;
Ht_temp := ROL(Ht_temp,30);
Ht_temp := Ht_temp;
Ht_temp := HtTmp;
END_FOR
FOR i:=20 TO 39 BY 1 DO
HtTmp :=Htemp_Func(Ht_temp,Ft2_Func(Ht_temp,Ht_temp,Ht_temp),Ht_temp,Wt,Kt);
Ht_temp := Ht_temp;
Ht_temp := Ht_temp;
Ht_temp := ROL(Ht_temp,30);
Ht_temp := Ht_temp;
Ht_temp := HtTmp;
END_FOR
FOR i:=40 TO 59 BY 1 DO
HtTmp :=Htemp_Func(Ht_temp,Ft3_Func(Ht_temp,Ht_temp,Ht_temp),Ht_temp,Wt,Kt);
Ht_temp := Ht_temp;
Ht_temp := Ht_temp;
Ht_temp := ROL(Ht_temp,30);
Ht_temp := Ht_temp;
Ht_temp := HtTmp;
END_FOR
FOR i:=60 TO 79 BY 1 DO
HtTmp :=Htemp_Func(Ht_temp,Ft2_Func(Ht_temp,Ht_temp,Ht_temp),Ht_temp,Wt,Kt);
Ht_temp := Ht_temp;
Ht_temp := Ht_temp;
Ht_temp := ROL(Ht_temp,30);
Ht_temp := Ht_temp;
Ht_temp := HtTmp;
END_FOR
FOR i:=0 TO 4 BY 1 DO
Ht_temp := Ht_temp + Ht;
Ht_temp := MEM.ReverseBYTEsInDWORD (Ht_temp);
END_FOR
MEM.MemMove(pSource:= ADR(Ht_temp), pDestination:= ADR(ResultCode), uiNumberOfBytes:=20 );
3.7.1Ft1_Func
Ft1_Func := (B AND C) OR ((NOT B) AND D);
3.7.2Ft2_Func
Ft2_Func := B XOR C XOR D;
3.7.3Ft3_Func
Ft3_Func :=(B AND C) OR (B AND D) OR (C AND D);
3.7.4Htemp_Func
Htemp_Func := ROL(A,5) +Ft + E + W + K;
3.7.5Wt_Func
temp := Wi_3 XOR Wi_8 XOR Wi_14 XOR Wi_16;
//循环左移1位
Wt_Func := ROL(temp,1);
3.8MysqlClient
//作者:AlongWu
PROGRAM MysqlClient
VAR CONSTANT
abyEmpty :ARRAY OF BYTE; //复位abyRx
ClearSha1Input :ARRAY OF BYTE;
END_VAR
VAR
CloseOrder :ARRAY OF BYTE:=; //关闭连接命令
Mysql_Init_Step :BYTE:=0; //mysql通信步骤,0-认证,1-登录,2-query
Enable_F_TRIG :F_TRIG; //mysql通信使能下降沿
Enable_R_TRIG :R_TRIG; //mysql通信使能上升沿
Recei_R_TRIG :R_TRIG; //接收上升沿
Close_R_TRIG :R_TRIG; //关闭连接上升沿
Send_R_TRIG :R_TRIG; //发送上升沿
HSP :HandShakePacket; //mysql认证报文
AuthP :AuthPacket; //client回复的申请报文
Daddr :UINT; //解码byte地址
i :UINT;
TempFrameLen :UDINT; //帧的内容区字节数
TempAddr :UINT; //缓冲地址
TempUint1 :UINT; //中间变量1,UINT
//TempInt1 :INT; //中间变量1,INT
tempSha1 :ARRAY OF BYTE; ;
tempSha2 :ARRAY OF BYTE; ;
tempSha3 :ARRAY OF BYTE; ;
tmpStrBytes :ARRAY OF BYTE;
LongSha1Bytes :ARRAY OF BYTE;
TempDw :DWORD;
SHA1 :SHA1_FB; //实例SHA1算法
ReplyFirstByte :BYTE; //mysql回复的内容的首个字节
WaitCount :UINT:=0; //空闲计时
TryConnect :UINT;
TryAuth :UINT;
END_VAR
3.9MysqlUserLogin
//作者:AlongWu3.1
PROGRAM MysqlUserLogin
VAR CONSTANT
EmptyResult :ARRAY OF BYTE;
EmptyAuthPStr :STRING(4):='';
END_VAR
VAR
WB_UserLogin AT %IX115.7 :BOOL; //账号登录
WB_UserExit AT %IX116.0 :BOOL; //账号注销
WHMI_Account AT %IW4850 :STRING(25); //输入操作者账号,utf-8编码,25个英文,25个byte
WHMI_Password AT %IW4900 :STRING(25); //输入操作者密码,utf-8编码,25个英文,25个byte
WB_UserLogin_R_TRIG :R_TRIG; //账号登录
WB_UserExit_R_TRIG :R_TRIG; //账号注销
B_Login_F_TRIG :F_TRIG; //账号下降沿
B_Login :BOOL:=FALSE; //启动登录
B_LoginDeny :BOOL; //登录账号密码错误
B_Login_R_TRIG :R_TRIG; //登录上升沿
LoginStep :BYTE; //登录步骤
QueryStr :STRING(100); //查询命令字符串
QueryBytes :ARRAY OF BYTE; //查询命令buffer
MysqlCmdPack :MysqlQueryCmdPack_FB; //Mysql指令实例
MysqlFirstRowInfo :MysqlRowInfo; //Mysql首行数据结构体
MysqlFirstRowDecode :MysqlFirstRowData_FB; //Mysql解析回复表格信息并提取首行数据
AuthpStr :STRING(4);
tmpUint :UINT;
END_VAR
WB_UserLogin_R_TRIG(CLK:=WB_UserLogin , Q=> ); //账号登录
WB_UserExit_R_TRIG(CLK:=WB_UserExit , Q=> ); //账号注销
IF WB_UserLogin_R_TRIG.Q THEN
//Mysql启动登录流程
B_Login := TRUE;
END_IF
IF WB_UserExit_R_TRIG.Q THEN
//注销账号
B_Login := FALSE;
END_IF
B_Login_R_TRIG(CLK:=B_Login , Q=> ); //账号登录上升沿
B_Login_F_TRIG(CLK:=B_Login , Q=> ); //账号登录下降沿
IF B_Login_F_TRIG.Q OR GVL.B_Mysql_AuthFalure THEN
GVL.Mysql_AccountAutp := 0;
GVL.RB_Mysql_Logining:=FALSE;
GVL.B_Mysql_Login := FALSE;
B_Login := FALSE;
END_IF
IF B_Login_R_TRIG.Q THEN
LoginStep := 0;
B_LoginDeny :=FALSE;
GVL.B_Mysql_Login := FALSE;
END_IF
IF B_Login THEN
CASE LoginStep OF
0:
GVL.RB_Mysql_Logining:=TRUE;
QueryBytes := EmptyResult; //指令buffer复位
IF GVL.B_Mysql_Inited THEN
LoginStep := 10; //已连接数据库
ELSE
LoginStep := LoginStep +1; //未连接数据库,下一步
END_IF
1:
GVL.B_Mysql_Enable :=TRUE;
LoginStep := LoginStep +1; //下一步
2:
IF GVL.B_Mysql_Inited THEN
LoginStep := 10; //已连接数据库
END_IF
IF GVL.B_Mysql_AuthFalure THEN //连接失败,推出登录
B_Login := FALSE;
END_IF
10:
GVL.B_Mysql_Result :=FALSE; //复位接收解析标志
// '要用$27来转义,特殊的符号,用$+16进制的acsii值
QueryStr :='SELECT AUTP FROM `login` WHERE STATE=1 AND USER=$27';
QueryStr := CONCAT(QueryStr,WHMI_Account);
QueryStr := CONCAT(QueryStr,'$27 AND PASSWORD=$27');
QueryStr := CONCAT(QueryStr,WHMI_Password);
QueryStr := CONCAT(QueryStr,'$27 LIMIT 1');
MEM.MemMove(pSource:= ADR(QueryStr), pDestination:= ADR(QueryBytes), uiNumberOfBytes:=INT_TO_UINT(LEN(QueryStr)));
MysqlCmdPack(Cmd:=3 , InputBytes:= QueryBytes, Result=> GVL.Mysql_abyTx, ResultSize=>GVL.Mysql_WriteSize );
GVL.B_Mysql_Send :=TRUE;
LoginStep := LoginStep +1; //下一步
11:
IF GVL.B_Mysql_FF OR GVL.B_Mysql_FE THEN
B_Login := FALSE; //数据库错误,退出登录
END_IF
IF GVL.B_Mysql_Result THEN
MysqlFirstRowDecode(InputRxBytes:=GVL.Mysql_abyRx , FirstRowInfo=>MysqlFirstRowInfo );
IF MysqlFirstRowInfo.IsNull THEN
//回复为空,即账号密码错误
B_Login := FALSE;
LoginStep:=99;
B_LoginDeny :=TRUE;
ELSE
//回复为真,账号密码正确,并获取授权值
GVL.B_Mysql_Login := TRUE;
LoginStep:=99;
AuthpStr :=EmptyAuthPStr;
tmpUint :=MysqlFirstRowInfo.RowData;
MEM.MemMove(pSource:= ADR(MysqlFirstRowInfo.RowData)+1, pDestination:= ADR(AuthpStr), uiNumberOfBytes:=tmpUint);
GVL.Mysql_AccountAutp :=INT_TO_BYTE( STRING_TO_INT(AuthpStr));
END_IF
END_IF
GVL.RB_Mysql_Logining:=FALSE;
ELSE
//NTHONG
END_CASE
ELSE
GVL.RB_Mysql_Logining:=FALSE;
END_IF
//登录提示
IF B_LoginDeny OR GVL.B_Mysql_FF OR GVL.B_Mysql_AuthFalure THEN
B_LoginDeny :=FALSE;
END_IF
四、重要提示
这里要特别提醒Codesys的2个bug。
1,多次修改结构体的变量顺序(struct)后,其内部的变量的字节排列是不确定的。需要及时通过CleanAll再GenerateCode,来保证结构体的变量的字节顺序是根据最新的顺序。当结构体字节顺序不正确,不仅使用 MEM.MemMove内存复制操作会出现字节顺序错误,会发现明显数据错乱,同时在日常普通程序运行过程,也会有出现不易发现的数据和位的错乱。请务必及时CleanAll。
2,Codesys的socket在连接状态下,如果出现断电,会导致Codesys的掉电保存动作失效。也就是PtVars的变量是没有及时更新为断电前一刻。所以,需要每次操作完mysql,及时断开socket。
五、总结
自己写Mysql操作库获取更大自由度在于以下2个方面。
1,Codesys的String操作函数所能操作的字符数为255个字符。变量的字符数超过255,无法正确实施字符操作。而实际应用过程,255个字符对于Insert,Update命令太少了。
2,对Query查询结果的解析和利用,可以更直接。
页:
[1]