网络socket编程: UDP通信流程以及代码实现

其实我们听TCP, UDP听得很多了, 想不想自己实现一个简易的UDP通信呢?
那么我们这次来学习网络socket编程, 话不多说, 直接开整



1. UDP通信流程

UDP – 用户数据报协议

无连接, 不可靠, 面向数据报

udp协议用于实时性要求大于安全性的场景 — 视频/音频数据传输

了解客户端和服务端的概念

客户端: 主动发起请求的一方 (一般是用户)
服务端: 被动接收请求的一方 (向用户提供服务的一方)

下面给出UDP通信中客户端和服务端的各自的流程
在这里插入图片描述


2. socket接口

头文件<sys/socket.h>

创建套接字

int socket(int domain, int type, int protocol);
domain: 地址域类型 -- AF_INET  (ipv4地址域)
type: 套接字类型 -- SOCK_STREAM 字节流服务 /  SOCK_DGRAM数据报服务
protocol: 协议类型,  0 默认类型/  IPPROTO_TCP/ IPPROTO_UDP
	字节流服务默认TCP协议, 数据报服务默认是UDP服务
返回值: 成功返回一个非负整数 -- 套接字描述符,  失败返回-1

为套接字绑定地址信息

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd: 创建套接字返回的操作句柄
addr:  struct socketaddr结构体,  不同的地址域有对应不同的地址结构
addrlen: 实际地址结构的长度
返回值: 成功返回0, 失败返回-1

发送数据

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
		const struct sockaddr *dest_addr, socklen_t addrlen);
sockfd: 套接字操作句柄,   buf: 要发送数据的空间首地址   len: 数据长度
flags: 标志位, 默认给0--阻塞发送,   dest_addr: 接收端地址信息   addrlen: 地址信息长度
返回值:  成功返回实际发送数据的字节长度, 失败返回-1.

接收数据

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
               struct sockaddr *src_addr, socklen_t *addrlen); 
sockfd:  套接字操作句柄,  buf: 放置数据的缓冲区空间首地址,  
len: 要接收的数据长度 (不能大于上面缓冲区长度)
flags: 默认0 -- 阻塞接收(有数据取, 没数据等待)   src_addr:  发送端的地址信息  
socklen_t *addrlen  输入输出型参数 -- 指定想要的地址长度, 返回实际的地址长度
返回值:  成功返回实际接收的数据长度,  失败返回-1

关闭套接字

int close(sockfd);

地址转换接口

htons/htonl:  主机字节序到网络字节序的整数转换  short-16/int-32
ntohs/ntohl:  网络字节序到主机字节序的整数转换  short-16/int-32
点分十进制字符串IP地址到网络字节序IP地址的转换
   in_addr_t inet_addr(const char *cp); 
   
网络字节序IP地址到点分十进制字符串IP地址的转换
   char *inet_ntoa(struct in_addr in);
   
int inet_pton(int af, const char *src, void *dst);
af: 地址域类型 -- AF_INET,   src: 字符串IP地址,    
dst: 返回转换后的整数地址


const char *inet_ntop(int af, const void *src,  char *dst, socklen_t size);
af: 地址域类型,  src: 网络字节序整数IP,  
dst: 返回转换后的字符串,  size: dst空间长度

看完接口之后我们就要实现了, 大家要把接口好好理解, 否则很容易搞乱~


3. 代码实现UDP通信

封装socket接口

创建udpsocket.hpp头文件, 进行接口的封装

#include <iostream>
#include <string>
#include <unistd.h>
#include <arpa/inet.h> //地址转换接口头文件
#include <netinet/in.h> // 地址结构类型定义头文件
#include <sys/socket.h> //套接字接口头文件

using namespace std;


class UdpSocket {
  public:
    UdpSocket():_socket(-1) {}
    
    //创建套接字
    bool Socket() {
      //int socket(int domain, int type, int protocol);
      _socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
      if(_socket < 0) {
        perror("socket error");
        return false;
      }
      return true;
    }

    //为套接字绑定地址信息
    bool Bind(const string& ip, int port) {
      //定义ipv4地址结构 struct socketaddr_in
      struct sockaddr_in addr;
      addr.sin_family = AF_INET;
      //htons:将主机字节序短整型数据转换为网络字节序数据
      addr.sin_port = htons(port);
      //将字符串IP地址转换为网络字节序
      addr.sin_addr.s_addr = inet_addr(ip.c_str());

      //int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
      socklen_t len = sizeof(struct sockaddr_in);
      int ret = bind(_socket, (struct sockaddr*)&addr, len);
      if(ret < 0) {
        perror("bind error");
        return false;
      }
      return true;
    }

    bool Send(string& data, string& ip, int port) {
      //sento(套接字句柄,数据首地址,数据长度,标志位,对端地址信息,地址信息长度)
      struct sockaddr_in addr;
      addr.sin_family = AF_INET;
      addr.sin_port = htos(port);
      addr.sin_addr.s_addr = inet_addr(ip.c_str());
      socklen_t len = sizeof(struct sockaddr_in);
      int ret = sendto(_socket, data.c_str(), data.size(), 0, (struct sockaddr*)&addr, len);
      if(ret < 0) {
        perror("sendto error");
        return false;
      }
      return true;
    }

    //接收数据,获取发送端的地址信息
    bool Recv(string* buf, string* ip = nullptr, uint16_t* port = nullptr) {
      //recvfrom(套接字句柄,接收缓冲区,数据长度,标志,源端地址,地址长度)
      struct sockaddr_in peer_addr;
      socklen_t len = sizeof(struct sockaddr_in);
      char tmp[4096] = {0};
      int ret = recvfrom(_socket, tmp, 4096, 0, (struct sockaddr*)&peer_addr, &len);
      if(ret < 0) {
        perror("recvfrom error");
        return false;
      }
      //从指定字符串中截取指定长度的数据到buf中
      buf->assign(tmp, ret);
      if(port != nullptr) {
        //网络字节序到主机字节序的转换
        *port = ntohs(peer_addr.sin_port);
      }
      if(ip != nullptr) {
        //网络字节序到字符串IP地址的转换
        *ip = inet_ntoa(peer_addr.sin_addr);
      }
      return true;
    }

    //关闭套接字
    bool Close() {
      if(_socket > 0) {
        close(_socket);
        _socket = -1;
      }
      return true;
    }

  private:
    int _socket;
};

OK, 这样我们就封装好了一个UDP协议的接口, 接下来我们只要根据流程调用接口即可.


模拟实现服务端

创建udp_server.cpp文件, 进行服务端的实现

#include <iostream>
#include <string>
#include "udpsocket.hpp"

using namespace std;

//#define CHECK_RET(q) if((q)==false) {return false;}

int main(int argc, char* argv[]) {
  //argc表示程序运行参数的个数
  if(argc != 3) {
    cout << "Usage: ./udp_server ip prot" << endl;
    return -1;
  }
  uint16_t port = stoi(argv[2]);
  string ip = argv[1];

  UdpSocket srv_sock;
  //创建套接字
  if (srv_sock.Socket() == false) {
    return -1;
  }
  //绑定地址信息
  if (srv_sock.Bind(ip, port) == false) {
    return -1;
  } 
  while(1) {
    //接收数据
    string buf;
    string peer_ip;
    uint16_t peer_port;
    if (srv_sock.Recv(&buf, &peer_ip, &peer_port) == false) {
      break;
    }
    cout << "client[" << peer_ip << ":" << peer_port << "] say: " << buf << endl;

    //回复数据
    string data;
    cout << "server say: ";
    cin >> data;
    if(srv_sock.Send(data, peer_ip, peer_port) == false) {
      break;
    }
  }
  
  //关闭套接字
  srv_sock.Close();

  return 0;
}


模拟实现客户端

创建udp_client.cpp文件, 进行服务端的实现

#include <iostream>
#include <string>
#include "udpsocket.hpp"

using namespace std;

int main(int argc, char* argv[]) {
  //客户端参数获取的ip地址是服务端绑定的地址, 也就是客户端发送的目标地址
  //不是为了自己绑定的
  if (argc != 3) {
    cout << "Usage: ./udp_cli ip port" << endl;
    return -1;
  }
  string srv_ip = argv[1];
  uint16_t srv_port = stoi(argv[2]);

  UdpSocket cli_sock;
  //创建套接字
  if(cli_sock.Socket() == false) {
    return -1;
  }
  //绑定地址(不推荐)
  while(1) {
    //发送数据
    cout << "client say:";
    string data;
    cin >> data;
    if (cli_sock.Send(data, srv_ip, srv_port) == false) {
      return -1;
    }
    //接收数据
    string buf;
    //客户端不需要关心服务器的ip和port信息, 只需要接收数据就行了
    if (cli_sock.Recv(&buf) == false) {
      return -1;
    }
    cout << "server say: " << buf << endl;
  }

  //关闭套接字
  cli_sock.Close();
  return 0;
}

OK, 到这里我们就实现完毕啦, 下面就可以编译测试了~


makefile文件编写

all:udp_client udp_server
udp_client:udp_client.cpp
	g++ -std=c++11 $^ -o $@
udp_server:udp_server.cpp
	g++ -std=c++11 $^ -o $@

编译后开两个渠道, 一个运行服务端, 一个运行客户端, 就可以开始对话啦~

  • 5
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

殇&璃

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值