已经进入Internet网络时代了,许多新出的软件都拥有网络功能。其实,在这些软件背后所依靠的技术基础就是一系列的Inernet网络协议标准,亦即TCP/IP系列协议。

  下面本人简要介绍一下在Delphi环境下,直接采用winsock套接字编程,应用SNTP协议开发出具有网络时间校准功能的应用,以此来说明如何在编程实践中实现应用层网络协议,相信感兴趣的读者能从中举一反三。

  一、程序原理

  1、 SNTP协议的运作机制 SNTP(简单网络时间协议)是在UDP协议基础上发展出来的应用协议,目前广泛应用于整个INTERNET上计算机时钟的同步,依据同步源的性能及网络路径的差异,提供1~50ms的较准精度(资料来源:RFC-2030)。

  SNTP报文封装见图三,有关IP报头和UDP报头的详细结构这里不再赘述,需要知道的是针对我们的SNTP应用,UDP报头的源端口和目的端口值均应设定为123,SNTP数据紧跟在UDP报头后(数据格式见图二),SNTP协议应用分服务器端和客户端,根据我们的应用需求只要考虑SNTP客户端单播模式(UNICAST MODE)的运作:在该模式下,客户端初始化NTP数据报,将SNTP数据头的VN(协议版本号)值设置成3(即Version3);Mode值设置成3(即客户端模式);数据区的Transmit Timestamp(传输时间戳)值设置成客户机当前时间;然后向一个特定的单播模式时间服务器发出该数据报并接受服务器的回答,收到回答报文后从其中的传输时间戳里获取同步时间。需要注意的是请求数据报初始化时其传输时间戳必须被设定为客户机当前时间,这样可以通过计算,消除服务器客户机之间的传播延迟,有效地将同步精度控制在10ms级范围内。

  2、用WINSOCK API实现SNTP 首先,调用socket(domain,type,protocol)建立套接字,其中domain 为 PF_INET(Internet域);type 为SOCK_DGRAM(UDP数据报); protpcol为IPPROTO_IP(IP协议)。其次,调用sendto(socket_id,buf,buflen,flags,to,tolen)发送SNTP客户端请求,其中,buf为SNTP数据;to为时间服务器名字,它是一结构,包含域名、端口号、32位主机地址等;socket_id为套接字号 。接着,调用recvfrom (socket_id,buf,buflen,flags,from,fromlen)接收SNTP数据回答,其中buf为SNTP数据回答。最后,将回答数据中的传输时间戳(Transmit Timestamp)转换成Delphi的时间格式,并调用WINDOWS API函数SetLocalTime同步本地计算机时间,结束时关闭套接字。

  二、编程步骤:

  在IDE中建立一个新工程,缺省的Form上放一个label控件、一个button控件,在button控件的OnClick事件句柄中键入执行同步时间函数及label控件时间显示代码,另外在单元文件中建立MySyncTime等5个有关时间同步函数(过程),最后,编译该工程。

  三、完整的单元文件如下:

unit Unit1;

interface

uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,Dialogs,
StdCtrls,winsock ;// 添加winsock单元,直接调用WINSOCK API;
type
TForm1 = class(TForm)
Button1: TButton;
Label1: TLabel;
procedure Button1Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
procedure MyConnect(host:string){ Private declarations };
Procedure MySend( s:string);
function MyReceive: string;
procedure MyDisconnect;
function MySyncTime(host:string):TDateTime;
public
{ Public declarations }
end;

var
Form1: TForm1;

implementation

{$R *.DFM}
type
// NTP 数据格式
tNTPGram = packed record
head1, head2, file://其中,head1为LI VN 及Mode(见图二);
head3, head4 : byte;
RootDelay : longint;
RootDisperson : longint;
RefID : longint;
Ref1, Ref2,
Org1, Org2,
Rcv1, Rcv2,
Xmit1, Xmit2 : longint;//Transmit Timestamp(传输时间戳)
end;

// 用于转换本机网络字节顺序;
lr = packed record
l1, l2, l3, l4 : byte;
end;
var MySocket:Tsocket;
fiMaxSockets:integer;
MyAddr: TSockAddrIn;
UDPSize:integer;
const Port=123;// SNTP端口号必须为123;

procedure TForm1.Button1Click(Sender: TObject);
begin
label1.Caption:= timetostr(MySyncTime('bernina.ethz.ch'));
end;

procedure TForm1.FormCreate(Sender: TObject); file://初始化套接字;
var sData:TWSAData; 
fsStackDescription:string;
begin
if WSAStartup($101, sData) = SOCKET_ERROR then
raise Exception.Create('Winsock Initialization Error.'); 
fsStackDescription := StrPas(sData.szDescription);
UDPSize := sData.iMaxUdpDg;
fiMaxSockets := sData.iMaxSockets;
MySocket:= INVALID_SOCKET ;

end;

procedure TForm1.MyConnect(host:string);//建立套接字,域名解析;
var fsPeerAddress:string;
function ResolveHost(const psHost: string; var psIP: string): u_long;//主机名解析成IP地址;
var
pa: PChar;
sa: TInAddr;
aHost: PHostEnt;
begin
psIP := psHost;
if CompareText(psHost, 'LOCALHOST') = 0 then begin
sa.S_un_b.s_b1 := #127;
sa.S_un_b.s_b2 := #0;
sa.S_un_b.s_b3 := #0;
sa.S_un_b.s_b4 := #1;
psIP := '127.0.0.1';
Result := sa.s_addr;
end else begin
Result := inet_addr(PChar(psHost));
if Result = u_long(INADDR_NONE) then begin
aHost := GetHostByName(PChar(psHost));
pa := aHost^.h_addr_list^;
sa.S_un_b.s_b1 := pa[0];
sa.S_un_b.s_b2 := pa[1];
sa.S_un_b.s_b3 := pa[2];
sa.S_un_b.s_b4 := pa[3];
psIP := IntToStr(Ord(sa.S_un_b.s_b1)) + '.' + IntToStr(Ord(sa.S_un_b.s_b2)) + '.'
+ IntToStr(Ord(sa.S_un_b.s_b3)) + '.' + IntToStr(Ord(sa.S_un_b.s_b4));
Result := sa.s_addr;
end;
end;
end;
begin
MySocket:=socket(PF_INET,SOCK_DGRAM, IPPROTO_IP);//建立套接字,采用UDP/IP协议;
if MySocket = INVALID_SOCKET then begin
raise Exception.Create('套接字建立失败!');
end;
try
with MyAddr do begin file://时间服务器名字;
sin_family := PF_INET;
sin_port := HToNS(Port);
sin_addr.S_addr := ResolveHost(host, fsPeerAddress);
end;
except
On E: Exception do begin
if MySocket <> INVALID_SOCKET then begin
CloseSocket(MySocket);
end;;
raise;
end;
end;
end;
procedure TForm1.MySend( s:string); file://发送 请求时间数据报;
begin
SendTo(MySocket, s[1], Length(s), 0,Myaddr , sizeof(Myaddr));
end;
function TForm1.MyReceive; file://接收服务器时间数据报;
var
AddrVoid: TSockAddrIn;
fsUDPBuffer:string;
i:integer;
begin
SetLength(fsUDPBuffer, UDPSize);
i:= SizeOf(AddrVoid) ;
result := Copy(fsUDPBuffer,1,Recvfrom(Mysocket, fsUDPBuffer[1], Length(fsUDPBuffer), 0, AddrVoid , i) );
end;
function flip(var n : longint) : longint; file://网络字节顺序与本机字节顺序转换;
var
n1, n2 : lr;
begin
n1 := lr(n);
n2.l1 := n1.l4;
n2.l2 := n1.l3;
n2.l3 := n1.l2;
n2.l4 := n1.l1;
flip := longint(n2);
end;
function tzbias : double; // 获取本地时间区与UTC时间偏差;
var
tz : TTimeZoneInformation;
begin
GetTimeZoneInformation(tz);
result := tz.Bias / 1440;
end;
const maxint2 = 4294967296.0;
// 将DELPHI的 TDateTime 格式转换成为 NTP 时间戳(timestamp)格式 ;
procedure dt2ntp(dt : tdatetime; var nsec, nfrac : longint);

var d, d1 : double;
begin
d := dt + tzbias - 2;
d := d * 86400;
d1 := d;
if d1 > maxint then begin
d1 := d1 - maxint2;
end;
nsec := trunc(d1);
d1 := ((frac(d) * 1000) / 1000) * maxint2;
if d1 > maxint then begin
d1 := d1 - maxint2;
end;
nfrac := trunc(d1);
end;

// 将NTP 时间戳(timestamp)格式转换成为DELPHI的 TDateTime 格式;
function ntp2dt(nsec, nfrac : longint) : tdatetime;
var
d, d1 : double;
begin
d := nsec;
if d < 0 then d := maxint2 + d - 1;

d1 := nfrac;
if d1 < 0 then d1 := maxint2 + d1 - 1;
d1 := d1 / maxint2;
d1 := trunc(d1 * 1000) / 1000;
result := (d + d1) / 86400;
result := result - tzbias + 2;
end;

function TForm1.MySyncTime(host:string):TDateTime;//获取时间服务器上的标准时间,同时同步本地时间;
var
ng : TNTPGram;
s : string;
SysTimeVar : TSystemTime;
begin
fillchar(ng, sizeof(ng), 0); file://将 SNTP数据报初始化;
ng.head1 := $1B; // 设置SNTP数据报头为SNTP 协议版本3,模式3(客户机),即二进制00011011;
dt2ntp(now, ng.Xmit1, ng.xmit2);//将本机时间转换为数据报时间格式;
ng.Xmit1 := flip(ng.xmit1);
ng.Xmit2 := flip(ng.xmit2);
setlength(s, sizeof(ng));
move(ng, s[1], sizeof(ng));
try
MyConnect(host);
MySend(s);
s := MyReceive;
move(s[1], ng, sizeof(ng));
result := ntp2dt(flip(ng.xmit1), flip(ng.xmit2));// 将收到的数据报时间格式转换为本机时间;
DateTimeToSystemTime( result, SysTimeVar) ;
SetLocalTime( SysTimeVar ); file://同步本地时间;
MyDisconnect;
except
MyDisconnect;
showmessage('时间同步失败!');
application.Terminate;

end;
end;

procedure TForm1.MyDisconnect; file://关闭套接字;
begin
if MySocket <> INVALID_SOCKET then begin
CloseSocket(MySocket);
end;
end;
end.

   
  程序在Delphi3+ win98se 上通过。

  附部分SMTP时间服务器网址:

augean.eleceng.adelaide.edu.au*
bernina.ethz.ch
biofiz.mf.uni-lj.si*
black-ice.cc.vt.edu
chime.utoronto.ca*
churchy.udel.edu (128.4.1.5) 
clepsydra.dec.com
clock.psu.edu
clock.tricity.wsu.edu (192.31.216.30) 
constellation.ecn.uoknor.edu
cuckoo.nevada.edu*
delphi.cs.ucla.edu
dominator.eecs.harvard.edu

Delphi开发环境中应用层网络协议的实现的更多相关文章

  1. Neutron VxLAN + Linux Bridge 环境中的网络 MTU

    1. 基础知识 1.1 MTU   一个网络接口的 MTU 是它一次所能传输的最大数据块的大小.任何超过MTU的数据块都会在传输前分成小的传输单元.MTU 有两个测量层次:网络层和链路层.比如,网络层 ...

  2. 将linux用在开发环境中

    我是如何将linux用在开发环境中的 1.为什么不直接安装Linux在主机 一直想深入学习一下linux的使用,于是将家里的笔记本装了linux系统,但是要将自己的系统打造一个适合开发的环境确实是一件 ...

  3. 如何将linux用在开发环境中的

    如何将linux用在开发环境中的 1.我为什么要写这篇文章 一直想深入学习一下linux的使用,于是将家里的笔记本装了linux系统,但是要将自己的系统打造一个适合开发的环境确实是一件费心费力的事,而 ...

  4. C# 动态加载组件后怎么在开发环境中调试

    动态加载组件 那就是简单的Assembly.Load动态加载dll而以.这网上资料也有不少.基本的思路基本上就是在本地上一个指定目录如[plugs]存在着一堆dll文件.主程序在初始运行时一般会把指定 ...

  5. Qemu搭建ARM vexpress开发环境(三)----NFS网络根文件系统

    Qemu搭建ARM vexpress开发环境(三)----NFS网络根文件系统 标签(空格分隔): Qemu ARM Linux 经过上一篇<Qemu搭建ARM vexpress开发环境(二)- ...

  6. Idea开发环境中搭建Maven并且使用Maven打包部署程序

    1.配置Maven的环境变量 a.首先我们去maven官网下载Maven程序,解压到安装目录,如图所示: b.配置M2_HOME的环境变量,然后将该变量添加到Path中 备注:必须要有JAVA_HOM ...

  7. Wabpack系列:在webpack+vue开发环境中使用echarts导致编译文件过大怎么办?

    现象,在一个webpack+vue的开发环境中,npm install echarts --save了echarts,然后在vue文件中直接使用 import echarts from 'echart ...

  8. 开发环境中biztalk项目设置注意事项(转)

      适用版本:biztalk 2006 适用环境:开发测试环境 在开发过程中,在开发环境中,一定会是一个对项目不断的修改.编译.部署.测试,查看测试结果,发现有问题,然后回到开发环境再修改.编译.部署 ...

  9. Spark Streaming揭秘 Day28 在集成开发环境中详解Spark Streaming的运行日志内幕

    Spark Streaming揭秘 Day28 在集成开发环境中详解Spark Streaming的运行日志内幕 今天会逐行解析一下SparkStreaming运行的日志,运行的是WordCountO ...

随机推荐

  1. MySQL5.7 GTID在线开启与关闭【转】

    当前场景   当前某些业务还有未开启GTID服务组,升级5.7后,如何检测是否符合开启GTID条件,如何在线修改切换使用GTID:已经升级5.7后,已经开启GTID,如何快速回滚后退: 线上gtid如 ...

  2. 使用 CasperJS 构建 Web 爬虫

    转载:https://www.oschina.net/translate/building-your-own-web-scraper-in-nodejs 从你的应用中收集数据有时候可能有点困难和艰辛. ...

  3. AUI-靠谱的移动前端框架

    在如何开发出优秀的APICloud应用中ApiCloud官方推荐我们使用轻量级的框架AUI,针对AUI官网没有提供体验地址 特意编译了一个APP供给大家体验 aui官方地址:http://www.au ...

  4. css部分复习整理

    CSS代码语法 css 样式由选择符和声明组成,而声明又由属性和值组成,如下图所示: 选择符:又称选择器,指明网页中要应用样式规则的元素,如本例中是网页中所有的段(p)的文字将变成蓝色,而其他的元素( ...

  5. jenkins的svn路径中文问题

    今天弄Jenkins,我们的SVN代码路径是中文的,他娘的坑死我了,很没面子弄了俩点,网上方案试了好多,说装插件,修改Tomcat server.xml,基本没用,后来看到一个帖子写的方案蛮实用的,分 ...

  6. C++ Primer读书笔记(2)

    getline(cin,string s)可以读取一整行,包括空白符.使用ctrl+Z结束 字符串字面值与string是不同的类型.两个字符串字面值不能直接相加. 处理string对象中的字符时,C+ ...

  7. .NetCore 实现分页控件(URL分页)实战

    上一篇文章介绍了分页控件的具体实现方式,接下来我们就来做一个分页控件 后台数据处理就过度的介绍,下面针对URL分页中的下面几点做说明: 1.搜索条件的状态保持 2.点击分页需要带上搜索条件 3.页码的 ...

  8. 实操一下<python cookbook>第三版1

    这几天没写代码, 练一下代码. 找的书是<python cookbook>第三版的电子书. *这个操作符,运用得好,确实少很多代码,且清晰易懂. p = (4, 5) x, y = p p ...

  9. Qwidget+opencv显示图像

    步骤 1. 设置opencv库路径 在.pro文件中添加 INCLUDEPATH += D:/opencv/OpencvMingw/opencv310/include LIBS += D:/openc ...

  10. 扩展BootstrapTable的treegrid功能

    扩展BootstrapTable的treegrid功能 阅读目录 一.效果预览 二.代码示例 三.组件需要完善的地方 四.总结 正文 前言:上篇  JS组件系列——自己动手封装bootstrap-tr ...