前面我们所实现的通信程序,是基于 TCP 协议的,有连接的方式。也就是在创建 socket 的时候,socket 函数第二个参数总是指定为 SOCK_STREAM,第三个 protocol 参数指定为 0,即默认使用 TCP 协议。

有连接,是指像打电话一样,首先拨通电话(connect),对方接听(accept),然后各自拿着自己的话筒(socket)开始通信。

而无连接不一样,事先不需要建立连接,它类似于发信息。如果要使用无连接的方式通信,在创建 socket 的时候,把 socket 类型指定为 SOCK_DGRAM,同时第三个参数 protocol 参数指定为 0,表示使用默认的 UDP 协议。

有关 TCP 和 UDP 协议的细节,将在 Linux 网络编程学习笔记中详细讨论,所以这里我们只关心怎么编程就行了。

1. 使用 UDP 协议通信的步骤

同样的,UDP 协议通信也需要有一个服务器程序。由于 UDP 协议不需要建立连接,这意味着就不需要被动 socket,因此函数 listen 自然也就派不上用场了。

另一方面,无论是 TCP 通信还是 UDP 通信,socket 都要与套接字地址进行绑定。所以对于服务器来说,如果不事先绑定套接字地址,客户端将找不到它。

  • 服务器端程序编写步骤

(1) 创建 socket (socket 函数)
(2) 将 socket 与套接字地址绑定(bind 函数)
(3) 使用 recvfrom 函数接收数据
(4) 使用 sendto 函数发数据
(5) 关闭套接字

  • 客户端程序编写步骤

(1) 创建 socket(socket 函数)
(2) 使用 sendto 函数发数据
(3) 使用 recvfrom 函数接收数据
(5) 关闭套接字

在客户端中,我们并没有主动的去 bind 套接字地址,当然你完全可以主动绑定一个,但是这样并没有什么意义,如果在同一台机器中启动多个客户端就肯定会出错。所以,当使用 sendto 函数后,系统为自动为客户端 bind 一个套接字地址(该地址的端口号随机分配)。

2. recvfrom 和 sendto

在 UDP 编程中,最关键的就是这两个函数了,sendto 函数可以指定发送端的套接字地址,recvfrom 函数可以返回对端的套接字地址。这两个函数原型如下:

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct

上面的函数中,除了 flags 参数我们不懂,其它参数都有简单。目前我们只需要将 flags 参数设置为 0 就行了。

这两个函数在阻塞 IO 下,都可能会阻塞。比如 recvfrom 会一直阻塞直到有数据到来。sendto 函数也一样,如果缓冲区满的情况下,会阻塞。

3. 大写转换服务器程序的 UDP 版本

这里,我们将上一节的大写转换服务器程序改用 UDP 协议来实现。

3.1 serv 服务器程序

// serv.c
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>


#define ERR_EXIT(msg) do { perror(msg); exit(1); } while(0)

void upper(char* buf) {
char* p = buf;
while(*p) {
*p = toupper(*p);
++p;
}
}

int main() {
struct sockaddr_in servaddr, cliaddr;
int sockfd, clientfd, ret, n;
socklen_t cliaddrlen;
char buf[64];

// 1. create sockaddr
puts("1. create sockaddr");
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(8080);

// 2. create socket
puts("2. create socket");
// 注意第 2 个参数已经改成了 SOCK_DGRAM
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) ERR_EXIT("socket");

// 3. bind sockaddr
puts("3. bind sockaddr");
ret = bind(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
if (ret < 0) ERR_EXIT("bind");

while(1) {
// 注意 recvfrom 的最后一个参数 addrlen 既是输入参数,也是输出参数,所以这里必须要传一个值给它。
cliaddrlen = sizeof(cliaddr);
n = recvfrom(sockfd, buf, 63, 0, (struct sockaddr*)&cliaddr, &cliaddrlen);
// 打印对端的 ip 地址和端口号
printf("%s:%d come in\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
buf[n] = 0;
puts(buf);
upper(buf);
// 将转换后的数据发送给对端
sendto(sockfd, buf, n, 0, (struct sockaddr*)&cliaddr, cliaddrlen);
}

close(sockfd);

return 0;
}

3.2 cli 客户端程序

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>

#define ERR_EXIT(msg) do { perror(msg); exit(1); } while(0)

int main() {
int sockfd, ret, n;
char buf[64];
struct sockaddr_in servaddr;
struct sockaddr_in cliaddr;
socklen_t servaddrlen, cliaddrlen;

memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
servaddr.sin_port = htons(8080);

// 注意第 2 个参数已经改成了 SOCK_DGRAM
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) ERR_EXIT("socket");

while(1) {
scanf("%s", buf);
if (buf[0] == 'q') break;
// 将数据发送给服务器
sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr*)&servaddr, sizeof(servaddr));
// recvfrom 最后两个参数可以为空,表示我们并不关心对端的套接字地址(因为我们本来就知道……)
n = recvfrom(sockfd, buf, 63, 0, NULL, NULL);
buf[n] = 0;
puts(buf);
}

close(sockfd);
return 0;
}

3.3 编译和运行

$ gcc serv.c -o serv
$ gcc cli.c -o cli


136-基于 UDP 协议的通信_通信


图1 基于 UDP 协议的大写转换服务器


4. 总结

  • 掌握基于 UDP 协议的基本编程方法