65.9K
CodeProject 正在变化。 阅读更多。
Home

适合初学者的简单 UDP 时间服务器和客户端

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.78/5 (39投票s)

2005 年 9 月 23 日

8分钟阅读

viewsIcon

490768

downloadIcon

35496

如何使用WinSock实现一个简单的UDP时间服务器和客户端。

引言

这个项目包含一个简单的UDP服务器和客户端程序。如果您从未编写过使用UDP的程序,这是一个理想的入门项目。服务器运行在本地计算机上,等待远程计算机发送过来的请求数据报,请求获取服务器的当前时间。然后,服务器将当前的日期时间返回给客户端,客户端再将其显示出来。

背景

UDP代表用户数据报协议。客户端向服务器发送一个数据报,服务器处理信息后返回响应。本文演示了如何使用sendtorecvfrom函数。

服务器程序

服务器程序是一个简单的UDP服务器,它等待来自客户端的数据报。当它接收到包含文本"GET TIME\r\n"的数据报时,它会将当前服务器时间返回给客户端。

打开Windows连接

在使用任何套接字函数之前,必须先打开Windows连接。这可以通过WSAStartup函数完成。

/* Open windows connection */
if (WSAStartup(0x0101, &w) != 0)
{
    fprintf(stderr, "Could not open Windows connection.\n");
    exit(0);
}

十六进制数0x0101是所要使用的WinSock版本,而变量w是一个WSADATA类型的结构。

打开数据报套接字

下一步是为UDP打开一个数据报套接字。这可以通过socket函数完成,该函数返回一个套接字描述符。

/* Open a datagram socket */
sd = socket(AF_INET, SOCK_DGRAM, 0);
if (sd == INVALID_SOCKET)
{
    fprintf(stderr, "Could not create socket.\n");
    WSACleanup();
    exit(0);
}

AF_INET指定要使用的地址族,而SOCK_DGRAM告诉函数我们想要使用UDP而不是TCP/IP。

设置服务器信息

在此之后,有必要在类型为sockaddr_instruct中填写一些关于服务器的信息。首先,清除struct中的内存。然后设置服务器的地址族,这始终是AF_INET。然后使用htons函数设置端口号。根据输入的命令行参数,服务器将尝试获取自己的IP地址(这是首选的操作方式),或者如果不行,也可以手动指定。要自动获取服务器计算机的地址,需要调用gethostname函数,然后gethostbyname函数返回一个指向类型为hostentstruct的指针。最后,将地址的每个部分(xxx.xxx.xxx.xxx格式)复制到server struct中。

/* Clear out server struct */
memset((void *)&server, '\0', sizeof(struct sockaddr_in));

/* Set family and port */
server.sin_family = AF_INET;
server.sin_port = htons(port_number);

/* Set address automatically if desired */
if (argc == 2)
{
    /* Get host name of this computer */
    gethostname(host_name, sizeof(host_name));
    hp = gethostbyname(host_name);

    /* Check for NULL pointer */
    if (hp == NULL)
    {
        fprintf(stderr, "Could not get host name.\n");
        closesocket(sd);
        WSACleanup();
        exit(0);
    }

    /* Assign the address */
    server.sin_addr.S_un.S_un_b.s_b1 = hp->h_addr_list[0][0];
    server.sin_addr.S_un.S_un_b.s_b2 = hp->h_addr_list[0][1];
    server.sin_addr.S_un.S_un_b.s_b3 = hp->h_addr_list[0][2];
    server.sin_addr.S_un.S_un_b.s_b4 = hp->h_addr_list[0][3];
}
/* Otherwise assign it manually */
else
{
    server.sin_addr.S_un.S_un_b.s_b1 = (unsigned char)a1;
    server.sin_addr.S_un.S_un_b.s_b2 = (unsigned char)a2;
    server.sin_addr.S_un.S_un_b.s_b3 = (unsigned char)a3;
    server.sin_addr.S_un.S_un_b.s_b4 = (unsigned char)a4;
}

变量a1a2a3a4是服务器地址的各个部分,它们是以xxx.xxx.xxx.xxx形式在命令行上输入的。

将地址绑定到套接字

下一步是将服务器地址绑定到socket函数创建的套接字上。这可以通过bind函数完成,如果发生错误,它将返回-1。

/* Bind address to socket */
if (bind(sd, (struct sockaddr *)&server, 
                     sizeof(struct sockaddr_in)) == -1)
{
    fprintf(stderr, "Could not bind name to socket.\n");
    closesocket(sd);
    WSACleanup();
    exit(0);
}

从客户端获取数据报

服务器现在已准备好监听来自客户端的数据报。这可以通过recvfrom函数完成。buffer是用于存储接收到的数据报的缓冲区,而BUFFER_SIZE是最大接收字节数。client是一个类型为sockaddr_instruct,其中包含发送数据报的客户端信息,包括客户端地址。

client_length = (int)sizeof(struct sockaddr_in);

/* Receive bytes from client */
bytes_received = recvfrom(sd, buffer, BUFFER_SIZE, 0, 
            (struct sockaddr *)&client, &client_length);
if (bytes_received < 0)
{
    fprintf(stderr, "Could not receive datagram.\n");
    closesocket(sd);
    WSACleanup();
    exit(0);
}

发送响应

一旦服务器接收到数据报,它会将数据报中的信息与字符串"GET TIME\r\n"进行比较。如果字符串匹配,则服务器返回时间。否则,请求将被视为无效请求而被丢弃。使用sendto函数将时间发送回客户端。

/* Check for time request */
if (strcmp(buffer, "GET TIME\r\n") == 0)
{
    /* Get current time */
    current_time = time(NULL);
            
    /* Send data back */
    if (sendto(sd, (char *)&current_time, 
         (int)sizeof(current_time), 0, 
         (struct sockaddr *)&client, client_length) != 
                                 (int)sizeof(current_time))
    {
        fprintf(stderr, "Error sending datagram.\n");
        closesocket(sd);
        WSACleanup();
        exit(0);
    }
}

之后,服务器将返回到recvfrom 函数,进入无限循环。

客户端程序

客户端程序是一个简单的UDP客户端,它向服务器发送一个请求以获取当前时间,并接收返回的时间。首先,打开Windows连接。然后打开一个套接字。接下来,将服务器地址复制到server struct中。这段代码与服务器的代码非常相似。

获取客户端地址

在UDP客户端中,有必要知道客户端计算机的IP地址。这可以自动完成,也可以通过命令行开关手动指定。代码几乎与为服务器分配地址的代码相同,只是客户端的端口号设置为零client.sin_port = htons(0);,其中client是一个类型为sockaddr_instruct

将客户端地址绑定到套接字

下一步是将客户端地址绑定到套接字。这段代码与服务器的代码几乎相同。

传输获取时间的请求

现在是时候向服务器发送获取当前时间的请求了。使用sendto函数来完成此操作。

/* Tranmsit data to get time */
server_length = sizeof(struct sockaddr_in);
if (sendto(sd, send_buffer, (int)strlen(send_buffer) + 1, 
       0, (struct sockaddr *)&server, server_length) == -1)
{
    fprintf(stderr, "Error transmitting data.\n");
    closesocket(sd);
    WSACleanup();
    exit(0);
}

send_buffer是一个包含字符串"GET TIME\r\n"并在末尾加上一个空终止符的字符串。

接收时间

发送获取时间的请求后,将从服务器发送响应。这通过recvfrom函数完成。

/* Receive time */
if (recvfrom(sd, (char *)&current_time, 
               (int)sizeof(current_time), 0, 
               (struct sockaddr *)&server, 
               &server_length) < 0)
{
    fprintf(stderr, "Error receiving data.\n");
    closesocket(sd);
    WSACleanup();
    exit(0);
}

current_time是一个time_t类型的变量,用于存储时间。

关闭套接字和清理

程序几乎完成了。最后一步是关闭套接字和Windows连接。使用closesocket函数关闭套接字,并调用WSACleanup来关闭Windows连接。

closesocket(sd);
WSACleanup();

使用服务器程序

服务器程序的语法是timeserv [server_address] portport是运行服务器的端口号。建议选择大于1023的端口号,因为较低的端口号可能已被其他协议占用。可选的server_address参数是服务器计算机的本地IP地址,以xxx.xxx.xxx.xxx格式输入。如果省略此参数,程序将尝试自动获取地址。大多数情况下这都可以正常工作,但如果您有多个网络连接,例如以太网卡和无线网络卡,程序可能会选择错误的连接。在这种情况下,请在命令提示符下键入ipconfig来确定您的地址,并在命令行中输入。例如,要在端口5000上使用自动本地地址生成来运行程序,您应该键入:timeserv 5000;要在本地地址为192.168.1.102的计算机上运行,您应该键入timeserv 192.168.1.102 5000。要退出服务器程序,请按住CTRL键并按C。

使用客户端程序

在启动客户端程序之前,请确保服务器程序已在服务器计算机上运行。客户端程序的语法是timecli server_address port [client_address]server_address是服务器计算机运行的地址,以xxx.xxx.xxx.xxx格式表示。port参数是服务器运行的端口。可选的client_address参数将覆盖客户端计算机的自动本地地址生成,其操作方式与上面列出的服务器程序类似。例如,要连接到地址为192.168.1.102、运行在端口5000的服务器,您应该键入timecli 192.168.1.102 5000

关注点

防火墙和防病毒软件

某些防火墙和防病毒程序可能不允许您在计算机上运行服务器和/或客户端程序。这只是它们保护您免受间谍软件侵害的一种尝试。这些程序可能会错误地将此项目识别为某种间谍软件。如果您的防火墙或防病毒程序阻止了其中任何一个程序,您可能需要暂时禁用它们。但是,在使用完这些程序后,请务必重新启用它们。

链接代码

编译源代码文件时,请务必将其与库文件wsock32.lib链接。否则,链接器将生成大量关于未定义函数的错误。

我为什么选择用C语言编写这个程序?

我可能会被问到的一个问题是:“你为什么选择用C语言而不是C++来编写这个程序?”答案很简单。这个程序的重点在于展示WinSock编程的基本框架,而不是提供最小巧、最现代化的代码。这就是为什么我选择不使用MFC的CSocket类来实现程序。不过不用担心,源代码仍然可以使用C++编译器进行编译。我想演示像socketbindsendto这样的基本函数的用法。我最初是在UNIX系统上学习套接字编程的,并希望来自该环境的程序员能够看到在Windows编程中如何使用类似的功能。这是一个面向WinSock编程初学者的基本示例程序,而不是为高级程序员设计的程序,他们可能已经知道所有这些内容了。

在没有网络连接的计算机上运行程序

可以通过使用环回地址(loopback address)在没有网络连接的计算机上测试此服务器和客户端。环回地址是127.0.0.1,并且不与任何网络硬件相关联。要在环回地址上运行服务器,请键入timeserv 127.0.0.1 5000;要在同一台计算机上运行客户端,请键入timecli 127.0.0.1 5000 127.0.0.1

历史

  • 目前还没有历史记录。
© . All rights reserved.