月巴月巴白勺合鸟月半

月巴月巴白勺合鸟月半

最近有个网友在做gps的一个系统。简单的讲就是,系统接收卫星传回的每辆车的数据,并保存这些数据供以后查询。每条数据的包含信息一般由*MID,CMD,F,DATA#构成。'*'为开头(1个字节),'#'为结尾 (1个字节)。下面就是一条示例数据:
*000000,00,00,155705,A,2202.12,11307.001,10,0,211209#
车ID:000000(MID,6个字节)
命令标示:00(CMD,2个字节)
状态标示:00(超速报警,防盗报警。。。)(F,2个字节)
卫星时间(时分秒:155705)15:57:05(6个字节)
数据是否有效(A有效,V无效)(1个字节)
维度:2202.12
经度:11307.001
速度:10
方向:0(0-360)
卫星时间:(日月年211209)09-12-21 (6个字节)
 
每辆车大约每40秒有一条数据,一天大约有2000条记录。整个系统由多个服务器组成,设计要求是每台服务器大约负担5万辆车半年的数据插入存储和查询。这就是每天有1亿条数据插入,而且还要不经常查询这个系统实际上是网友(qq号是63731???放飞)的一个系统。他目前是用sql server 做数据存储的。采用定时bcp的方式来处理的,在存储上每个数据库服务器只能负担1万到2万左右的车辆,达不到服务器负担5万辆车的设计要求。其实只考虑bcp数据插入,用sql server 是可以承受更多的数据插入的,但是还要保证数据查询不要过慢,所以我的那个网友最好只好每台服务器负责1万辆车的数据了。他目前的系统每分钟只能提供100次的查询。这也是他最后决定每台服务器只负责1万辆车的原因。经过我对这个系统的分析后,我做了一些改动和优化,试每台服务器可以达到5万辆车的数据存储,数据查询上也达到了每分钟1500-2000次)查询左右。这个速度是5万辆车中随机不同的车号的查询,如果是同一车号的不同时间的查询,可以达到每分钟1万次以上。基本上就测试结果来看,在数据插入变为原来的5倍的情况下,查询也提高了10倍以上。下面我开始说说,我是如何处理的,我的方法其实不是一个好的方法,也没有用任何技术,用的写法是任何一个大学二年级的学生都曾经写过的写法(甚至是非计算机系的学生)。我为什么采用这种写法的原因是:我很懒,而且这种写法也刚好可以达到设计要求。
 
第一步,解析数据
所有数据是通过udp从卫星上收下来的,很多条保存成一个文本文件,然后交给负责数据插入的程序处理。这种写法为什么是这样,还有为什么是变长的文本数据而不是定长的2进制数据,这个只能说是个历史遗留问题。其实在做项目时,经常遇到很多旧的程序,有时要兼容这些是必须的。先分析原来的程序的写法,他原来的写法用的是下面这个函数
function Split(aValue: string; aDelimiter: Char; StringList: TStrings): string;
var
  X: Integer;
  S: string;
begin
  if StringList = nil then
    StringList := TStringList.Create;
  StringList.Clear;
  S := '';
  for X := 1 to Length(aValue) do
  begin
    if aValue[X] <> aDelimiter then
      S := S + aValue[X]
    else begin
      S := S + aDelimiter;
      StringList.Add(TrimLeft(S));
      S := '';
    end;
  end;
  Result := S;//未处理的数据
end;
 
其实这个函数比较中规中矩(后来和网友聊,据说这个也是我随手写给他的,不过我忘记了)。好了我们开始改这里吧。我有个不好的习惯就是不喜欢用string,尤其不喜欢字符串相加。上边的那个函数好像也是我写的,估计那是急着去洗手间的时候写的。下面就是新的代码了。调用的代码为ReadReciveData(ms_text.Memory, ms_text.Position, ms_dat, ReciveData);其中 ms_text和 ms_dat是TMemoryStream对象。
TGetReciveDataProc = procedure(const obj: Pointer; const ReciveData: TReciveData) of object;
function ReadReciveData(buf: pchar; len: integer; obj: Pointer; Proc: TGetReciveDataProc): integer;
var
  i, begin_i, ReciveData_bufflen: integer;
  p, begin_p: PChar;
  ReciveData: TReciveData;
begin
{ 解析文本 速度还可以  2009-12-23
  begin_Char: char = '*';
  end_Char: char = '#';
}
  Result := 0;
  begin_p := nil;
  p := buf;
  for i := 1 to len do
    begin
      if p^ = begin_Char then
        begin
          begin_p := p;
          begin_i := i;
        end
      else
        begin
          if p^ = end_Char then
            begin
              if begin_p <> nil then
                begin
                  inc(begin_p); //跳过  begin_Char
                  ReciveData_bufflen := i - begin_i - 1; //去掉  begin_Char 和  end_Char
                  ProcReciveData(begin_p, ReciveData_bufflen, ReciveData);
                  //解析文本 *000000,00,00,155705,A,2202.12,11307.001,10,0,211209#
                  Proc(obj, ReciveData);
                  Result := i; //返回值是 处理的字节数
                  begin_p := nil;
                end;
            end;
        end;
      inc(p);
    end;
end;
回调函数Proc是
procedure TFrm_dataInserter.ReciveData(const obj: Pointer;
  const ReciveData: TReciveData);
var
  ms: TMemoryStream;
begin
  //处理报警信息。。。
  ms := TMemoryStream(obj);
  ms.Write(ReciveData, sizeof(ReciveData));
end;
 
这个函数没有使用字符串。速度比原来的快了很多,而且基本不占用内存。其实,很久以前用C语言时就这样写了,这样写也是一种怀旧吧。字符串相加实际上是一个比较讨厌的事情,但是你写代码时要经常遇到。一般的情况下,我也是直接相加。只有对速度有特殊要求是我才会用其他写法。这个主要是因为字符串相加时需要重新分配内存,这就会影响速度。顺便说一下,我现在用C#,C#的Stringbuilder就是为了干这个事的,不过他比较凶狠,当实际的内容超出内存空间,他往往分配2倍大小内存。这一点也验证了“要想快,开内存”(貌似有“要想富,圈地皮”的嫌疑)的思想。在处理大量文本时,我的想法是,尽量用PChar来减少字符串传递,不过要慎重。一般情况下没必要这样写。我们在来看看ProcReciveData。这个函数是提取文本里的具体数据的,这里我一样没有用string,基本的写法和ReadReciveData相似,不过这里用了一个自己写的函数fromHex。
procedure ProcReciveData(buf: pchar; len: integer; var ReciveData: TReciveData);
var
  p, begin_p: PChar;
  i, dataIdx, begin_i, data_len: integer;
  AYear, AMonth, ADay, AHour, AMinute, ASecond: Word;
  dt: TDateTime;
  tmp_p: array[0..16] of char;
begin
  p := buf;
  begin_p := p;
  begin_i := -1;
  dataIdx := 0;
  for i := 1 to len do
    begin
      if (p^ = Separator_Char) or (i = len) then //  Separator_Char: char = ',';
        begin
          data_len := i - begin_i;
 
          case dataIdx of
            0: Move(begin_p[0], ReciveData.MID, MID_len); //车ID(MID,6个字节)
            1: Move(begin_p[0], ReciveData.CMD, CMD_len); //命令标示(CMD,2个字节)
            2: ReciveData.TrackData.status := fromHex(begin_p, 2); //状态标示(超速报警,防盗报警。。。)(F,2个字节)
            3:
              begin //155705:卫星时间(时分秒)15:57:05(6个字节)
                AHour := from10(begin_p, 2);
                inc(begin_p, 2);
                AMinute := from10(begin_p, 2);
                inc(begin_p, 2);
                ASecond := from10(begin_p, 2);
              end;
            4: ReciveData.TrackData.valid := begin_p^; // A:数据是否有效(A有效,V无效)(1个字节)
            5:
              begin //维度
                Move(begin_p[0], tmp_p[0], data_len);
                tmp_p[data_len] := #0;
                TextToFloat(tmp_p, ReciveData.TrackData.location.Dimension, fvCurrency)
              end;
            6:
              begin //经度
                Move(begin_p[0], tmp_p[0], data_len);
                tmp_p[data_len] := #0;
                TextToFloat(tmp_p, ReciveData.TrackData.location.Longitude, fvCurrency)
              end;
            7: ReciveData.TrackData.speed := from10(begin_p, data_len); //速度
            8: ReciveData.TrackData.Direction := from10(begin_p, data_len); //方向(0-360)
            9:
              begin //211209:卫星时间(日月年)09-12-21 (6个字节)
                ADay := from10(begin_p, 2);
                inc(begin_p, 2);
                AMonth := from10(begin_p, 2);
                inc(begin_p, 2);
                AYear := from10(begin_p, 2) + 2000;
              end;
          end;
          inc(dataIdx);
          begin_i := -1;
        end
      else
        begin
          if begin_i < 0 then
            if p^ <> ' ' then
              begin
                begin_p := p;
                begin_i := i;
              end;
        end;
      inc(p);
    end;
  if not TryEncodeDateTime(AYear, AMonth, ADay, AHour, AMinute, ASecond, 0, ReciveData.TrackData.SatelliteTime) then
    ReciveData.TrackData.SatelliteTime := now;
end;
 
fromHex这个函数实际上就是strtoint('$'+'FF'),只是速度快一点而已。这个函数实际上就是一个查表法的计算。其实这里没必要用这个写法,因为这里用fromHex还是用strtoint 对于整个项目而言占的比重太小了。
var
  Hex_Table: array[#0..#$FF] of Byte;
procedure init_Hex_Table;
var
  i: integer;
  ch: Char;
begin
  for ch := #0 to #$FF do
    Hex_Table[ch] := 0;
  for ch := '0' to '9' do
    Hex_Table[ch] := StrToInt('$' + ch);
  for ch := 'a' to 'f' do
    Hex_Table[ch] := StrToInt('$' + ch);
  for ch := 'A' to 'F' do
    Hex_Table[ch] := StrToInt('$' + ch);
end;
 
function fromHex(v: PChar; len: integer): Byte;
var
  i: integer;
  p: PChar;
begin
  p := v;
  Result := 0;
  for i := 1 to len do
    begin
      Result := Result * 16 + Hex_Table[p^];
      inc(p);
    end;
{
这种写法 只是 为了追求速度 2009-12-24
比   StrToInt 快40%
比   StrToInt('$'+s) 快20倍 左右
}
end;
 
进程加载后要调用init_Hex_Table,初始化Hex_Table,代码如下
initialization
  begin
    init_Hex_Table; 
  end;
end.
这样的写法以前在单片机上经常见到,有些比较追求速度的计算也用查表法,例如CRC校验的计算。说白了就是预计算和缓存结果。有时候查表法确实是不错的选择。到这里基本就对解析数据优化完了,单以解析数据而言,修改后的代码比原来的代码快不少具体多少我没测试,至少快5倍以上吧。因为解析这些文本文件,在整个流程中占得比重很小,所以我就随便改改了。

08-22 16:12