首先,罗列几个即将用到的自定义函数。
以下是Read函数原型:
ssize_t
Read(int fd, void *ptr, size_t nbytes)
{
ssize_t n;
if ( (n = read(fd, ptr, nbytes)) == -1)
err_sys("read error");
return(n);
}
以下是err_sys函数的原型:
void
err_sys(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
err_doit(1, LOG_ERR, fmt, ap);
va_end(ap);
exit(1);
}
以下是err_doit函数的原型:
static void
err_doit(int errnoflag, int level, const char *fmt, va_list ap)
{
int errno_save, n;
char buf[MAXLINE + 1];
errno_save = errno;
#ifdef HAVE_VSNPRINTF
vsnprintf(buf, MAXLINE, fmt, ap);
#else
vsprintf(buf, fmt, ap);
#endif
n = strlen(buf);
if (errnoflag)
snprintf(buf + n, MAXLINE - n, ": %s", strerror(errno_save));
strcat(buf, "\n");
if (daemon_proc) {
syslog(level, buf);
} else {
fflush(stdout);
fputs(buf, stderr);
fflush(stderr);
}
return;
}
有了上面的函数之后,我们先给出没有调用connect的udp的c/s源程序
udp回射服务器程序 :
#include "unp.h"
int
main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in servaddr, cliaddr;
sockfd = Socket(AF_INET, SOCK_DGRAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(sockfd, (SA *) &servaddr, sizeof(servaddr));
dg_echo(sockfd, (SA *) &cliaddr, sizeof(cliaddr));
}
这里有个dg_echo函数,原型如下:
void
dg_echo(int sockfd, SA *pcliaddr, socklen_t clilen)
{
int n;
socklen_t len;
char mesg[MAXLINE];
for ( ; ; ) {
len = clilen;
n = Recvfrom(sockfd, mesg, MAXLINE, 0, pcliaddr, &len);
Sendto(sockfd, mesg, n, 0, pcliaddr, len);
}
}
udp回射客户端程序:
#include "unp.h"
int
main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in servaddr;
if (argc != 2)
err_quit("usage: udpcli ");
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
sockfd = Socket(AF_INET, SOCK_DGRAM, 0);
dg_cli(stdin, sockfd, (SA *) &servaddr, sizeof(servaddr));
exit(0);
}
这里的dg_cli函数原型如下:
void
dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
{
int n;
char sendline[MAXLINE], recvline[MAXLINE + 1];
while (Fgets(sendline, MAXLINE, fp) != NULL) {
Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
n = Recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);
recvline[n] = 0;
Fputs(recvline, stdout);
}
}
运行udp的c/s程序,会发现,当服务器进程处于进行状态时,客户端程序发送的数据总能得到回射,然而,当我们不启动服务器的前提下启动客户,如果我们这么做后在客户上键入一行文本,那么什么也不发生。客户永远阻塞于它的recvfrom调用,等待一个永不出现的服务器应答。
当客户端发送一个udp数据报时,服务器主机响应以一个"port unreachable"(端口不可达)ICMP消息。不过这个ICMP错误不返回给客户进程,这个ICMP出错消息包含引起错误的数据报的IP首部和UDP首部。客户需要知道引发该错误的数据报的目的地址以区分究竟是哪一个数据报引发了错误。内核如何将该信息返回给客户进程呢?recvfrom 可以返回的信息仅有errno值,它没有办法返回出错数据报的目的IP地址和目的UDP端口号。因此做出决定:仅在进程已将其UDP套接字连接到恰恰一个对端后,这些异步错误才返回给进程。然而,也许你会问,这样不是跟TCP一样了吗?事实上并非如此,它没有三次握手过程,它只是记录对端的IP地址和端口号,纯粹的本地操作过程。
那么对于一个已连接的UDP套接字,它与默认未连接UDP套接字的变化,主要有三个:
-
我们再也不能给输出操作指定目的IP地址和端口号。也就是说,我们不使用sendto(也可以使用,但是不能指定目的地址),而改用write或send。写到已连接的UDP套接字上的任何内容都自动发送到由connect指定的协议地址。
-
我们不使用recvfrom以获悉数据报的发送者,而改用read、recv或recvmsg。在一个已连接UDP套接字上,它有且仅能与一个IP地址交换数据报。
-
由已连接UDP套接字引发的异步错误会返回给它们所在的进程,而未连接UDP套接字不接收任何异步错误。
下面只对dg_cli函数做简单的修改,如下:
void
dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
{
int n;
char sendline[MAXLINE], recvline[MAXLINE + 1];
Connect(sockfd, (SA *) pservaddr, servlen);
while (Fgets(sendline, MAXLINE, fp) != NULL) {
Write(sockfd, sendline, strlen(sendline));
n = Read(sockfd, recvline, MAXLINE);
recvline[n] = 0;
Fputs(recvline, stdout);
}
}
运行程序,我们注意到,当启动客户进程时我们并没有收到这个错误。该错误只是在我们发送第一个数据报给服务器之后才发生。正是发送该数据报引发了来自服务器主机的ICMP错误。然而当一个TCP进程调用connect,指定一个不在运行服务器进程的服务器主机时,connect将返回同样的错误,因为调用connect会造成TCP三次握手,而其中第一个分节导致服务器TCP返回RST。该ICMP错误被内核映射为ECONNREFUSED错误,对应于由err_sys函数输出的消息串:"Connection refused"(连接被拒绝)。