面向无连接的协议直到这时,我们实际了忽略了套接口通信的大部分内容。相反,我们关注于创建套接口,绑定地址以及关闭套接口。现在我们要实际使用套接口了。
对于套接口有两种基本的通信模式。他们是面向无连接的通信与面向连接的通信。
在这一章,我们将会了解下面内容:
面向无连接通信与面向连接通信之间的区别
如何执行无连接的输入与输出操作
如何编写一个数据报服务器
如何编写一个数据报客户端
现在我们来关注一下面向无连接通信与面向连接通信之间的区别。
通信方法正如我们所想到的,面向无连接的通信在通信开始之前并不需要建立连接。这就像一个拿着护音器在嘈杂中向一个人喊话一样。对于每一次新的喊话,发送消息的人可以将他的话传递到另一个人,而不需要先前的同意。
相类似的,在我们创建一个无连接的套接口以后,我们可以向将会接收我们消息的任何套接口发送消息。此时并没有建立连接,而每一个消息可以直接发送给一个不同的接收套接口。
理解其优点面向无连接的通信与面向连接的通信相比提供了一些优点。这些优点包括:
简单:不需要建立连接
灵活:可以将消息发送到不同的带有消息发送操作的接收端
高效:不要求连接的建立与关闭,而这需要向网络添加大量的消息包
快速:因为不需要连接的建立也关闭,而只是发送消息本身
广播功能:将一个消息发送到多个接收端的功能
一个面向无连接的协议的许多优点都与高效与速度有关。为了建立连接,需要在数据交换之前在两个端点之间交换大量的数据包。而一个已建立的通信频道的关闭则需要额外的数据包交换。而这产生额外的时间花费。
无连接协议还有一个优点就是广播功能。他可以将一个消息发送到多个接收端。这高效的利用了网络带度。
在这一章,我们将会学习UDP协议(用户数据报协议),这是一个面向无连接的协议。这个协议充分利用了我们前面所谈到的所有优点。
理解无连接通信的缺点既然无连接的协议可以提供这些优点,我们也许想要知道为什么不总是使用这种协议。任何事物,即使是无连接的通信也有他的缺点。
UDP协议是一个非常简单的传输层协议,而且他是无连接的。对于UDP协议,存在着下面的缺点:
协议不可靠
没有多个数据报的序列
消息尺寸存在限制
可靠性问题对于大多数程序来说是最严重的限制。我们的程序可以将UDP数据报发送到网络,但是并不能保证接收端可以接收到。这个消息也许被丢弃,而没有同一网络的接收端所接收。相应的,由于网络路径中多个路由器中的一个不能接收没有检验码错误的消息,所发送的消息也会丢失。当一个数据包接收出错时,UDP包只是简单的丢弃并且永远的丢失。
其他的问题也会造成UDP数据包的丢失。如果接收主机或是路由器不能分配足够的缓冲空间来存放UDP数据包,这些数据包也会丢失。因此,如果我们的UDP数所包需要传输很长的距离,这些数据包就很有可能沿途丢失。
在我们决定为我们的程序使用UDP协议之前,还有另外一个问题需要考虑。如果我们成功的向我们的目的地发送了两个或是多个数据报,很有可能这些数据报并不是顺序接收的。UDP协议使用IP协议来传输最终的数据报。IP数据包可以由每一个传输进行不同的路由,而路由是依据当前的网络环境而变化的。这通常会造成一些数据包在另一些数据包之前到达目的地。也就说UDP数据包会并不会按顺序到达。
最后存在数据报尺寸的问题。理论上一个数据报的最大长度要小于64KB。然而,许多UNIX主机所支持的最大长度仅为32KB。其他的UNIX内核内建的尺寸限制更小,只有8KB。最后,接收套接口程序会将这个尺寸限制为接收缓冲区的尺寸。因为这个原因,一些程序将UDP的消息尺寸限制为512字节或是更小。
如果发送了一个大的UDP数据包,则必须将这个数据包分解为一些小的IP片段,然后在接收端重新进行组合。组合过程需要分配缓冲区来存放接收到的IP片段。这通常会指定一个超时时限,直到整个数据包重新进行组合。缓冲区的完整会造成重组的无约束,而这会造成我们的UDP数据包的丢失。UDP数据报在成功的到达目的地之前必须要经历些风险。
对于一些程序来说,这会将使得我们转向更为可靠的面向连接的协议,例如TCP/IP。这些于其他的程序来说并不是严重的限制。当然只有我们来决定UDP是否适用我们的程序。
执行数据报输入/输出操作在前面的介绍中,我们看到了当我们要从套接口中读取或是向套接口中写入时,我们使用了read(2)和write(2)函数。在当时我们并没有指现,socketpair(2)函数使用了面向连接的协议来创建一对套接口。相应的,我们可以使用我们所熟悉的read(2)与write(2)函数来在这些套接口上执行I/O操作。
然而,当要发送与接收数据报时,需要一对不同的函数。这是因为每一个要发送的消息实际上要发送到一个不同的目的地址。发送数据报的函数允许我们指定目的接收端的地址。同样,当我们要接收一个数据报时,我们也需要指明他的来源。这些新函数必须为我们提供一个合适的方法来确定发送者的地址。
简介sendto(2)函数sendto(2)函数允许我们写入一个数据报,同时指定目的接收端的地址。函数概要如下:
#include
#include
int sendto(int s,
const void *msg,
int len,
unsigned flags,
const struct sockaddr *to,
int tolen);
这些参数描述如下:
1 第一个参数s为套接口号。我们可以从socket函数得到这个值。
2 参数msg为指向存放我们将要发送的消息的缓冲区的指针
3 参数len为字节形式长度
4 flags允放我们指定一些选项位。在许多情况下,我们只需指定为0
5 参数to为指向我们已经建立的通用套接口的指针。这是数据报接收端的地址
6 参数tolen为参数to地址的长度
当sendto函数执行成功时,会返回写入的字节数。当发生错误时,则会返回-1,同时错误号会记录在errno中。
参数to指定了数据报将要发送到的地址。参数to必须指向一个可用的套接口地址,而参数tolen应包含地址的正确长度。我们已经在前面的章节中成为格式化地址的专家,所以在这里我们应感到得心应手了。
flags可用值如下所示,当然在大多数时候我们都将其指定为0。
标志 十六进制 意义
0 0x0000 普通-没有选项
MSG_OOB 0x0001 处理超过边界的数据
MSG_DONTROUTE 0x0004 旁路路由,使用直接接口
MSG_DONTWAIT 0x0040 不缓冲,直接写入
MSG_NOSIGNAL 0x4000 当端点断开时不发送信号
简介recvfrom(2)函数
与sendto函数相对就是recvfrom函数。这个函数与read函数的不同就在于他允许我们同时指定我们要接收的数据报的发送者的地址。函数概要如下:
#include
#include
int recvfrom(int s,
void *buf,
int len,
unsigned flags,
struct sockaddr *from,
int *fromlen);
参数描述如下:
1 套接口s指定要从中接收数据报的套接口
2 buf指向开始接收数据报的缓冲区
3 最大长度len指定了buf的长度
4 选项标志flags
5 指向接收套接口的地址缓冲,这将会接收发送者的地址
6 指向最大长度fromlen的指针
与正常的read函数相类似,接收缓冲区buf必须足够大来接收数据报。最大长度是通过len指定的。
如果函数执行失败则会返回-1,而错误号则会存放在errno中。否则,函数将会返回实际接收到的字节数。这将是我们接收到的数据报的尺寸。
然而在这里要特别注意的是,最后一个参数是一个指向接收地址结构长度的指针。指针所指向的int值必须包含接收地址结构from的最大字节尺寸。一旦由函数返回,返回的地址尺寸放置在int变量中。实际上,由fromlen所指向的值同时作为输入值和返回值。
可用的标志值如下所示:
标志 十六进制 意义
0 0x0000 普通
MSG_OOB 0x0001 处理超过边界的数据
MSG_PEEK 0x0002 读取一个数据报而不从内核接收队列中删除
MSG_WAITALL 0x0100 请求操作块直到全部请求都已满足
MSG_ERRQUEUE 0x2000 从错误队列获取一个数据包
MSG_NOSIGNAL 0x4000 当另一端断开连接时为流套接口关闭信号
编写一个UDP数据报服务器
现在我们已经有足够的知识来编写一个数据报客户端与服务器程序了。在这一部分,我们将从编写一个数据报服务的例子开始。这个程序接受一个strftime(3)格式字符串作为输入并且返回格式化的日期与时间字符串作为响应。在这一节我们将会编写一个数据报服务器,他可以独立运行,接受格式化字符串作为输入数据报。在服务器使用strftime函数格式化日期字符串之后,他会将结果返回给客户端程序。
/*
* dgramsrvr.c
*
* Example datagram server:
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
/*
* this function reports the error and
* exits back to the shell:
*/
static void bail(const char *on_what)
{
fputs(strerror(errno),stderr);
fputs(": ",stderr);
fputs(on_what,stderr);
fputs("\n",stderr);
exit(1);
}
int main(int argc,char **argv)
{
int z;
char *srvr_addr = NULL;
struct sockaddr_in adr_inet; /* AF_INET */
struct sockaddr_in adr_clnt; /* AF_INET */
int len_inet; /* length */
int s; /* socket */
char dgram[512]; /* recv buffer */
char dtfmt[512]; /* date/time result */
time_t td; /* current time and date */
struct tm tm; /* date time values */
/*
* use a server address from the command
* line,if one has been provided.
* otherwise,this program will default
* to using the arbitrary address
* 127.0.0.23:
*/
if(argc >=2)
{
/* addr on cmdline */
srvr_addr = argv[1];
}
else
{
/* use default address */
srvr_addr = "127.0.0.23";
}
/*
* create a UDP socket to use:
*/
s = socket(AF_INET,SOCK_DGRAM,0);
if(s==-1)
bail("socket()");
/*
* create a socket address,for use
* with bind:
*/
memset(&adr_inet,0,sizeof adr_inet);
adr_inet.sin_family = AF_INET;
adr_inet.sin_port = htons(9090);
adr_inet.sin_addr.s_addr = inet_addr(srvr_addr);
if(adr_inet.sin_addr.s_addr == INADDR_NONE)
bail("bad address");
len_inet = sizeof adr_inet;
/*
* bind a address to our socket,so that
* client program can contact this
* server:
*/
z = bind(s,(struct sockaddr *)&adr_inet,len_inet);
if(z==-1)
bail("bind()");
/*
* now wait for requests:
*/
for(;;)
{
/*
* block until the program receives a
* datagram at our address an port:
*/
len_inet = sizeof adr_clnt;
z = recvfrom(s, /* socket */
dgram, /* receiveing buffer */
sizeof dgram, /* max recv buf size */
0, /* flags */
(struct sockaddr *)&adr_clnt, /* addr */
&len_inet); /* addr len,in & out */
if(z<0)
bail("recvfrom()");
/*
* process the request:
*/
if(!strcasecmp(dgram,"QUIT"))
break; /* quit server */
/*
* get the current date and time:
*/
time(&td); /* get current time & date */
tm = *localtime(&td); /* get componets */
/*
* formate a new date and time string,
* based upon the input formate string:
*/
strftime(dtfmt, /* formatted result */
sizeof dtfmt, /* max result size */
dgram, /* input date/time format */
&tm); /* input date/time values */
/*
* send the formatted result back to the
* client program:
*/
z = sendto(s, /* socket to send result */
dtfmt, /* the datagram result to send */
strlen(dtfmt), /* the datagram length */
0, /* flags */
(struct sockaddr *)&adr_clnt, /* addr */
len_inet); /* client address length */
if(z<0)
bail("sendto()");
}
/*
* close the socket and exit:
*/
close(s);
return 0;
}
编写一个UDP数据报客户端
为了使用我们在前面所编写的数据报服务程序,我们需要一个数据报客户端程序。下面例子所提供的数据报客户端可以提示我们为strftime输入格式化文本。
/*
* dgramclnt.c
*
* Example datagram client:
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
/*
* This function reports the error and
* exits back to the shell:
*/
static void bail(const char *on_what)
{
fputs(strerror(errno),stderr);
fputs(": ",stderr);
fputs(on_what,stderr);
fputs("\n",stderr);
exit(1);
}
int main(int argc,char **argv)
{
int z;
int x;
char *srvr_addr = NULL;
struct sockaddr_in adr_srvr; /* AF_INET */
struct sockaddr_in adr; /* AF_INET */
int len_inet; /* length */
int s; /* socket */
char dgram[512]; /* recv buffer */
/*
* use a server address from the command
* line,if one has been provided.
* otherwise,this programe will default
* to using the arbitrary address
* 127.0.0.23:
*/
if(argc>=2)
{
/* addr on command line: */
srvr_addr = argv[1];
}
else
{
srvr_addr = "127.0.0.23";
}
/*
* create a socket address,to use
* to contact the server with:
*/
memset(&adr_srvr,0,sizeof adr_srvr);
adr_srvr.sin_family = AF_INET;
adr_srvr.sin_port = htons(9090);
adr_srvr.sin_addr.s_addr = inet_addr(srvr_addr);
if(adr_srvr.sin_addr.s_addr == INADDR_NONE)
bail("bad address");
len_inet = sizeof adr_srvr;
/*
* create a UDP socket to use:
*/
s = socket(AF_INET,SOCK_DGRAM,0);
if(s==-1)
bail("socket()");
for(;;)
{
/*
* prompt user for a date formate string:
*/
fputs("\nEnter format string: ",stdout);
if(!fgets(dgram,sizeof dgram,stdin))
break; /* EOF */
/*
* send format string to server:
*/
z = sendto(s, /* socket to send result */
dgram, /* the datagram result to snd */
strlen(dgram), /* the datagram length */
0, /* flags */
(struct sockaddr *)&adr_srvr, /* addr */
len_inet); /* server address length */
if(z<0)
bail("sendto()");
/*
* test if we asked for a server shutdown:
*/
if(!strncmp(dgram,"QUIT",strlen(dgram)-1))
break; /* yes,we quit too */
/*
* wait for a response:
*/
x = sizeof adr;
z = recvfrom(s, /* socket */
dgram, /* receiving buffer */
sizeof dgram, /* max recv buf size */
0, /* flags */
(struct sockaddr *)&adr, /* addr */
&x); /* addr len,in & out */
if(z<0)
bail("recvfrom()");
dgram[z]=0;
/*
* report result
*/
printf("Result from %s port %u:\n\t'%s'\n",
inet_ntoa(adr.sin_addr),
(unsigned)ntohs(adr.sin_port),
dgram);
}
/*
* close the socket and exit:
*/
close(s);
putchar('\n');
return 0;
}
测试数据报客户端与服务器
我们将会在这里进行的第一个测试适用于任何人,而无论你是否有可用的网络或是独立的PC。唯一的关键点就是我们需要将TCP/IP支持编译进入内核。如果我们正在运行一个独立的发行版本,例如RH系列,那么我们已经满足这些条件了。
要执行这个测试,我们需要执行下面的步骤:
1 启动服务器程序
2 启动客户端程序
3 输入客户程序输入
4 输入C-D来结束程序,或者是使用QUIT来退出程序。
启动服务器程序的输出结果如下:
$ ./dgramsrvr &
[1] 4405
$
字符&将服务器程序放置在后台运行,这样我们就可以继续使用当前的终端来运行客户端程序。
服务器程序启动运行以后,我们就可以使用客户程序来进行测试了。下面显示如何启动客户程序并进行测试:
$ ./dgramclnt
Enter format string: %D
Result from 127.0.0.23 port 9090 :
08/13/99'
Enter format string: %A %D %H:%M:%S
Result from 127.0.0.23 port 9090 :
Friday 08/13/99 22:14:02'
Enter format string: quit
[1]+ Done ./dgramsrvr
$
测试无服务器的情况
下面的输出显示没有服务器运行时运行客户程序的输出结果:
$ ./dgramclnt
Enter format string: %D
Connection refused: recvfrom(2)
$
在这里我们可以看到客户程序可以运行,并且可以创建套接口,要求输入。甚至sendto函数也报告执行成功。这就进一步确认了数据报的发送只是确保发送成功,而不确保接收成功。
在这种情况下,程序很幸运的得到了错误号来表明错误原因 。错误标识是通过recvfrom函数调用得到的。当客户与服务器程序独立运行在一个大的网络并有多个路由的情况下,也许就不会得到这个错误号。
在实际使用中,如果另一端并没有监听,我们不能依赖于得到错误码。因为这个原因,UDP程序通常包含定时器的使用,如果在一定的时间内没有收到响应则认为没有建立连接。
使用其他的IP进行测试
在前一节我们谈到可以在命令行指定IP地址。如果我们设置了我们自己的网络,我们可以试着在不同的主机上运行客户端与服务器程序。在下一个例子中,服务器程序运行在192.168.0.1的主机上,而客户端程序运行192.168.0.2的主机上。服务器程序的启动如下所示:
$ ./dgramsrvr 192.168.0.1 &
[1] 4416
$
服务器成功启动以后,就可以在另一个主机上调用客户端程序。客户端的输出结果如下所示:
$ ./dgramclnt 192.168.0.1
Enter format string: %D
Result from 192.168.0.1 port 9090 :
'08/13/99'
Enter format string: %A (%D)
Result from 192.168.0.1 port 9090 :
'Friday (08/13/99)'
Enter format string: QUIT
$
正如输出结果所示,通过在命令行指定地址通知客户程序服务器位于192.168.0.1主机上。在试验了两个例子以后,输入QUIT命令退出。
尽管这两个例子可以正确的运行,但是我们要认识到UDP是不可靠的。如果客户端程序不能由服务器得到响应,程序就会挂起。如果我们正在编写一个产品模型程序,我们需要提供超时代码。当原始的或是响应的数据报丢失时,这会允许程序修复响应缺失。
在客户程序中保留bind
一些读者也许已经注意到在上个例子中所创建的套接口的客户端程序并没有调用bind函数。如果bind函数调用可以消除,那么为什么还要使用呢?
我们也许还记得在前面的章节中,“绑定地址到套接口”,其中有一节名为“接口与地址”的部分,在其中解释了bind函数可用于限制用于执行通信的接口。其中的例子是一个防火墙程序,他只与信任的内部网络进行通信。如果现在这些对于我们来说有一些茫然,那么我们需要返回来查阅bind的相关内容。
而在我们的数据报例子程序中,bind函数调用被忽略了。这对于发送套接口有什么影响呢?正如我们在第五章所了解的,这实际表明这个程序可以接受任何流出的接口,这正是数据报到其目的地的路由所要求的。实际上,套接口据说有一个宽套接口地址。然后,当这个程序等待响应时,他也会接受从任何流入接口得到的输入数据报。在这里也要注意套接口端口号也是宽的。在这个特定的程序中,任何客户端口号都是可以接受的。
我们可以显式的使用bind函数来请求一个宽地址和端口。可以使用宽地址INADDR_NONE来完成这个工作。要请求一个宽端口号,可以将端口号指定为0。通过组合IP地址的INADDR_NONE和0端口号,我们已经请求bind显式的为我们提供一个宽地址,而这是与我们没有使用bind调用而得到的宽地址相同。
回应宽地址
如果客户程序的地址和端口号是宽的,我们也许想要知道服务器如何与回应特定的端口。毕竟,我们应如何缩写向没有一个特定IP地址和UDP端口号的客户程序返回响应?
问题的答案就在于:实际上当数据报发送时,就同时赋值了IP地址和端口号。我们前面的例子运行在IP地址为192.168.0.2的主机上。当客户程序调用sendto函数时,数据报知道将会发送到IP地址为192.168.0.1的主机上。路由表指明IP地址为192.168.0.2的以太网卡将会用于发送数据报。相应的,发送的数据报有一个192.168.0.2的from地址。这是在服务器端看到的地址。然而,端口号是宽的,并且将会所选择的IP地址选择任意的自由端口号。
如果另一个数据报发送到一个不同的网络,那么from IP地址将会是不同的值。from地址返应了用于发送数据报的网络接口的IP地址。
这是需要理解的一个重要概念,而且也许对于初学者来说也是最难理解的事情。如果我们现在并不能完全的理解这些内容,那么我们就需要回顾一下第五章的内容。作为练习,我们可以在我们的例子程序中加入下面的printf语句:
printf("Client from %s port %u;\n",
inet_ntoa(adr_clnt.sin_addr),
(unsigned)ntohs(adr_clnt.sin_port));
编译以后重新运行程序,程序的运行结果如下:
$ ./dgramsrvr &
[1] 733
$ ./dgramclnt
Enter format string: %D
Client from 127.0.0.23 port 1027;
Result from 127.0.0.23 port 9090 :
'08/15/99'
Enter format string: %A %D
Client from 127.0.0.23 port 1027;
Result from 127.0.0.23 port 9090 :
'Sunday 08/15/99'
Enter format string: QUIT
Client from 127.0.0.23 port 1027;
[1]+ Done ./dgramsrvr
$
在这里我们可以注意到所有的数据报发送到服务器,而数据报的from地址报告如下:
Client from 127.0.0.23 port 1027;
这再一次验证了当在客户端发送套接口没有使用bind函数时,会依据需求赋值合适的IP地址和最终的端口号。