136-基于 UDP 协议的通信

前面我们所实现的通信程序,是基于 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 协议的基本编程方法
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值