Quick.Data.Redis.pas 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. { ***************************************************************************
  2. Copyright (c) 2015-2020 Kike Pérez
  3. Unit : Quick.Data.Redis
  4. Description : Redis client
  5. Author : Kike Pérez
  6. Version : 1.0
  7. Created : 22/02/2020
  8. Modified : 02/03/2020
  9. This file is part of QuickLib: https://github.com/exilon/QuickLib
  10. ***************************************************************************
  11. Licensed under the Apache License, Version 2.0 (the "License");
  12. you may not use this file except in compliance with the License.
  13. You may obtain a copy of the License at
  14. http://www.apache.org/licenses/LICENSE-2.0
  15. Unless required by applicable law or agreed to in writing, software
  16. distributed under the License is distributed on an "AS IS" BASIS,
  17. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  18. See the License for the specific language governing permissions and
  19. limitations under the License.
  20. *************************************************************************** }
  21. unit Quick.Data.Redis;
  22. {$i QuickLib.inc}
  23. interface
  24. uses
  25. System.SysUtils,
  26. System.DateUtils,
  27. IdTCPClient,
  28. Quick.Commons;
  29. type
  30. IRedisResponse = interface
  31. ['{21EF7ABF-E678-4F18-AE56-8A7C6B817AE3}']
  32. function GetIsDone: Boolean;
  33. function GetResponse: string;
  34. procedure SetIsDone(const Value: Boolean);
  35. procedure SetResponse(const Value: string);
  36. property IsDone : Boolean read GetIsDone write SetIsDone;
  37. property Response : string read GetResponse write SetResponse;
  38. end;
  39. TRedisResponse = class(TInterfacedObject,IRedisResponse)
  40. private
  41. fIsDone : Boolean;
  42. fResponse : string;
  43. function GetIsDone: Boolean;
  44. function GetResponse: string;
  45. procedure SetIsDone(const Value: Boolean);
  46. procedure SetResponse(const Value: string);
  47. public
  48. property IsDone : Boolean read GetIsDone write SetIsDone;
  49. property Response : string read GetResponse write SetResponse;
  50. end;
  51. TRedisClient = class
  52. private
  53. fTCPClient : TIdTCPClient;
  54. fHost : string;
  55. fPort : Integer;
  56. fDataBaseNumber : Integer;
  57. fMaxSize : Int64;
  58. fPassword : string;
  59. fConnectionTimeout : Integer;
  60. fReadTimeout : Integer;
  61. fConnected : Boolean;
  62. procedure SetConnectionTimeout(const Value: Integer);
  63. procedure SetReadTimeout(const Value: Integer);
  64. function Command(const aCommand : string; const aArguments : string = '') : IRedisResponse; overload;
  65. function Command(const aCommand, aArgumentsFormat : string; aValues : array of const) : IRedisResponse; overload;
  66. function EscapeString(const json: string) : string;
  67. function IsIntegerResult(const aValue : string) : Boolean;
  68. public
  69. constructor Create;
  70. destructor Destroy; override;
  71. property Host : string read fHost write fHost;
  72. property Port : Integer read fPort write fPort;
  73. property DataBaseNumber : Integer read fDataBaseNumber write fDataBaseNumber;
  74. property MaxSize : Int64 read fMaxSize write fMaxSize;
  75. property Password : string read fPassword write fPassword;
  76. property ConnectionTimeout : Integer read fConnectionTimeout write SetConnectionTimeout;
  77. property ReadTimeout : Integer read fReadTimeout write SetReadTimeout;
  78. property Connected : Boolean read fConnected;
  79. function RedisSELECT(dbIndex : Integer) : Boolean;
  80. function RedisSET(const aKey, aValue : string; aTTLMs : Integer = -1) : Boolean;
  81. function RedisGET(const aKey : string; var vValue : string) : Boolean;
  82. function RedisRPUSH(const aKey, aValue : string) : Boolean;
  83. function RedisLPUSH(const aKey, aValue : string) : Boolean;
  84. function RedisLTRIM(const aKey : string; aFirstElement, aMaxSize : Int64) : Boolean;
  85. function RedisEXPIRE(const aKey : string; aTTLMs : Integer) : Boolean; overload;
  86. function RedisEXPIRE(const aKey : string; aExpireDate : TDateTime) : Boolean; overload;
  87. function RedisAUTH(const aPassword : string) : Boolean;
  88. function RedisPING : Boolean;
  89. function RedisQUIT : Boolean;
  90. procedure Connect;
  91. procedure Disconnect;
  92. end;
  93. ERedisConnectionError = class(Exception);
  94. ERedisAuthError = class(Exception);
  95. ERedisCommandError = class(Exception);
  96. implementation
  97. const
  98. DEF_REDIS_PORT = 6379;
  99. CRLF = #10#13;
  100. DEF_CONNECTIONTIMEOUT = 30000;
  101. DEF_READTIMETOUT = 10000;
  102. { TRedisResponse }
  103. function TRedisResponse.GetIsDone: Boolean;
  104. begin
  105. Result := fIsDone;
  106. end;
  107. function TRedisResponse.GetResponse: string;
  108. begin
  109. Result := fResponse;
  110. end;
  111. procedure TRedisResponse.SetIsDone(const Value: Boolean);
  112. begin
  113. fIsDone := Value;
  114. end;
  115. procedure TRedisResponse.SetResponse(const Value: string);
  116. begin
  117. fResponse := Value;
  118. end;
  119. { TRedisClient }
  120. constructor TRedisClient.Create;
  121. begin
  122. inherited;
  123. fConnected := False;
  124. fHost := 'localhost';
  125. fPort := DEF_REDIS_PORT;
  126. fDataBaseNumber := 0;
  127. fMaxSize := 0;
  128. fPassword := '';
  129. fConnectionTimeout := DEF_CONNECTIONTIMEOUT;
  130. fReadTimeout := DEF_READTIMETOUT;
  131. fTCPClient := TIdTCPClient.Create;
  132. end;
  133. destructor TRedisClient.Destroy;
  134. begin
  135. try
  136. if fTCPClient.Connected then RedisQUIT;
  137. fTCPClient.IOHandler.InputBuffer.Clear;
  138. fTCPClient.IOHandler.WriteBufferFlush;
  139. if fTCPClient.Connected then fTCPClient.Disconnect(False);
  140. fTCPClient.Free;
  141. except
  142. //avoid closing errors
  143. end;
  144. inherited;
  145. end;
  146. procedure TRedisClient.Disconnect;
  147. begin
  148. if fConnected then RedisQUIT;
  149. fConnected := False;
  150. end;
  151. procedure TRedisClient.Connect;
  152. begin
  153. if not fTCPClient.Connected then
  154. begin
  155. fTCPClient.Host := fHost;
  156. fTCPClient.Port := fPort;
  157. fTCPClient.ConnectTimeout := fConnectionTimeout;
  158. fTCPClient.ReadTimeout := fConnectionTimeout;
  159. end;
  160. try
  161. fTCPClient.Connect; //first connection
  162. //connect password and database
  163. if not fTCPClient.Connected then
  164. begin
  165. fTCPClient.Connect;
  166. if not fTCPClient.Connected then raise ERedisConnectionError.Create('Can''t connect to Redis Server!');
  167. end;
  168. fTCPClient.Socket.Binding.SetKeepAliveValues(True,5000,1000);
  169. if fPassword <> '' then
  170. begin
  171. if not RedisAUTH(fPassword) then raise ERedisAuthError.Create('Redis authentication error!');
  172. end;
  173. if fDataBaseNumber > 0 then
  174. begin
  175. if not RedisSELECT(fDataBaseNumber) then raise ERedisConnectionError.CreateFmt('Can''t select Redis Database "%d"',[fDataBaseNumber]);
  176. end;
  177. fConnected := True;
  178. except
  179. on E : Exception do raise ERedisConnectionError.CreateFmt('Can''t connect to Redis service %s:%d (%s)',[Self.Host,Self.Port,e.Message]);
  180. end;
  181. end;
  182. function TRedisClient.IsIntegerResult(const aValue: string): Boolean;
  183. begin
  184. Result := IsInteger(StringReplace(aValue,':','',[]));
  185. end;
  186. function TRedisClient.EscapeString(const json: string): string;
  187. begin
  188. Result := StringReplace(json,'\','\\',[rfReplaceAll]);
  189. Result := StringReplace(Result,'"','\"',[rfReplaceAll]);
  190. //Result := StringReplace(Result,'/','\/"',[rfReplaceAll]);
  191. end;
  192. procedure TRedisClient.SetConnectionTimeout(const Value: Integer);
  193. begin
  194. if fConnectionTimeout <> Value then
  195. begin
  196. fConnectionTimeout := Value;
  197. if Assigned(fTCPClient) then fTCPClient.ConnectTimeout := fConnectionTimeout;
  198. end;
  199. end;
  200. procedure TRedisClient.SetReadTimeout(const Value: Integer);
  201. begin
  202. if fReadTimeout <> Value then
  203. begin
  204. fReadTimeout := Value;
  205. if Assigned(fTCPClient) then fTCPClient.ConnectTimeout := fReadTimeout;
  206. end;
  207. end;
  208. function TRedisClient.Command(const aCommand, aArgumentsFormat : string; aValues : array of const) : IRedisResponse;
  209. begin
  210. Result := Command(aCommand,Format(aArgumentsFormat,aValues));
  211. end;
  212. function TRedisclient.Command(const aCommand : string; const aArguments : string = '') : IRedisResponse;
  213. function TrimResponse(const aResponse : string) : string;
  214. begin
  215. Result := Copy(aResponse,Low(aResponse) + 1, aResponse.Length);
  216. end;
  217. var
  218. res : string;
  219. begin
  220. Result := TRedisResponse.Create;
  221. try
  222. if not fTCPClient.Connected then Connect;
  223. fTCPClient.IOHandler.Write(aCommand + ' ' + aArguments + CRLF);
  224. if fTCPClient.IOHandler.CheckForDataOnSource(fReadTimeout) then
  225. begin
  226. res := fTCPClient.IOHandler.ReadLn;
  227. if not res.IsEmpty then
  228. case res[Low(res)] of
  229. '+' :
  230. begin
  231. if res.Contains('+OK') then
  232. begin
  233. Result.IsDone := True;
  234. end
  235. else Result.Response := TrimResponse(res);
  236. end;
  237. '-' : Result.Response := TrimResponse(res);
  238. ':' :
  239. begin
  240. Result.Response := TrimResponse(res);
  241. Result.IsDone := Result.Response.ToInteger > -1;
  242. end;
  243. '$' :
  244. begin
  245. Result.Response := TrimResponse(res);
  246. if IsInteger(Result.Response) then
  247. begin
  248. if Result.Response.ToInteger > -1 then Result.IsDone := True;
  249. end
  250. else Result.IsDone := True;
  251. end;
  252. '*' : Result.Response := TrimResponse(res);
  253. end;
  254. end;
  255. except
  256. on E : Exception do raise ERedisCommandError.CreateFmt('%s error: %s',[aCommand,e.message]);
  257. end;
  258. end;
  259. function TRedisClient.RedisRPUSH(const aKey, aValue : string) : Boolean;
  260. begin
  261. Result := Command('RPUSH','%s "%s"',[aKey,EscapeString(aValue)]).IsDone;
  262. end;
  263. function TRedisClient.RedisSELECT(dbIndex: Integer): Boolean;
  264. begin
  265. Result := Command('SELECT',dbIndex.ToString).IsDone;
  266. end;
  267. function TRedisClient.RedisSET(const aKey, aValue: string; aTTLMs: Integer = -1): Boolean;
  268. begin
  269. Result := Command('SET','%s "%s" PX %d',[aKey,EscapeString(aValue),aTTLMs]).IsDone;
  270. end;
  271. function TRedisClient.RedisLPUSH(const aKey, aValue : string) : Boolean;
  272. begin
  273. Result := Command('LPUSH','%s "%s"',[aKey,EscapeString(aValue)]).IsDone;
  274. end;
  275. function TRedisClient.RedisLTRIM(const aKey : string; aFirstElement, aMaxSize : Int64) : Boolean;
  276. begin
  277. Result := Command('LTRIM','%s %d %d',[aKey,aFirstElement,fMaxSize]).IsDone;
  278. end;
  279. function TRedisClient.RedisAUTH(const aPassword : string) : Boolean;
  280. begin
  281. Result := Command('AUTH',fPassword).IsDone;
  282. end;
  283. function TRedisClient.RedisEXPIRE(const aKey: string; aExpireDate: TDateTime): Boolean;
  284. begin
  285. Result := RedisEXPIRE(aKey,MilliSecondsBetween(Now(),aExpireDate));
  286. end;
  287. function TRedisClient.RedisEXPIRE(const aKey: string; aTTLMs: Integer): Boolean;
  288. begin
  289. Result := Command('PEXPIRE','%s %d',[aKey,aTTLMs]).IsDone;
  290. end;
  291. function TRedisClient.RedisGET(const aKey: string; var vValue: string): Boolean;
  292. begin
  293. if Command('GET','%s',[aKey]).IsDone then
  294. begin
  295. vValue := fTCPClient.IOHandler.ReadLn;
  296. Result := True;
  297. end;
  298. end;
  299. function TRedisClient.RedisPING : Boolean;
  300. begin
  301. Result := False;
  302. if Command('PING').IsDone then
  303. begin
  304. Result := fTCPClient.IOHandler.ReadLn = 'PONG';
  305. end;
  306. end;
  307. function TRedisClient.RedisQUIT : Boolean;
  308. begin
  309. try
  310. Result := Command('QUIT').IsDone;
  311. except
  312. Result := False;
  313. end;
  314. end;
  315. end.