136-基于 UDP 协议的通信

来源:互联网 发布:知党章党规,系列讲话 编辑:程序博客网 时间:2024/06/06 09:51

前面我们所实现的通信程序,是基于 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 sockaddr *dest_addr, socklen_t addrlen);

上面的函数中,除了 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


这里写图片描述
图1 基于 UDP 协议的大写转换服务器

4. 总结

  • 掌握基于 UDP 协议的基本编程方法
0 0
原创粉丝点击