Linux网络编程 | Socket编程:UDP服务端/客户端的实现


客户端、服务器端

网络通信是两端主机的通信,通信两端被分为:客户端,服务器端。

客户端:发送请求的一端,主动发起请求的一端

服务器端:提供服务的一端,被动接收请求的一端


UDP协议

UDP协议 :用户数据报协议

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

应用场景:传输实时性要求高于安全性要求的场景---视频传输

UDP的通信流程

在这里插入图片描述因为UDP是无连接的,所以只要创建套接字绑定地址信息,就可以进行通信。

客户端一般不会主动绑定地址信息。 如果不绑定地址信息,系统会选择合适的地址信息进行绑定,若我们手动绑定,可能会绑定到已经使用的端口,这样就会发生端口冲突。还有如果在客户端代码中绑定地址信息,第一个客户端会绑定成功,因为客户端不止一个,当第二个客户端绑定信息时就会失败,因为第一个客户端已经绑定地址和端口,所以就不会主动绑定地址信息。


Socket编程

Sockaddr

struct sockaddr是通用的套接字地址,根据所使用的协议不同,选择不同的结构,通信时再将我们所使用的结构强转为sockaddr*,这样就能保证数据格式的一致。
在这里插入图片描述

  • IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址.
  • IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址, 不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
  • socket API可以都用struct sockaddr *类型表示, 在使用的时候需要将具体类型强制转化成struct sockaddr *; 这样的好 处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;

通常我们使用IPv4,所以一般使用sockaddr_in结构体,这个结构体主要描述了ip地址和端口。

sockaddr_in结构体:

struct sockaddr_in
{
 __SOCKADDR_COMMON (sin_);
 in_port_t sin_port;         /* Port number.  */
 struct in_addr sin_addr;    /* Internet address.  */
 
 /* Pad to size of `struct sockaddr'.  */
 unsigned char sin_zero[sizeof (struct sockaddr) -
            __SOCKADDR_COMMON_SIZE -
            sizeof (in_port_t) -
            sizeof (struct in_addr)];
};

sin_port存储端口号(使用网络字节顺序)
sin_addr存储IP地址,sin_addr是一个结构体,结构体中只有一个成员s_addr
sin_zero是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节。


字节序

主机字节序(由cpu架构决定):一台主机上cpu对内存中数据按照字节为单位进行存取的顺序,也就是通常说的大端小端问题。

若两通信主机字节序不同,则有可能在网络通信中会产生数据二义,想要避免因为主机字节序不同而导致的数据二义,则需要在网络中统一字节序标准,就是网络字节序,网络字节序是大端字节序,也就意味着如果你的主机是小端,则在网络通信时需要将数据转换为网络字节序后进行发送。

arpa/inet.h这个头文件中,也为我们提供了一套字节序的转换接口。

uint32_t htonl(uint32_t hostlong);32位数据主机到网络字节序转换 

uint16_t htons(uint16_t hostshort);16位数据主机到网络字节序转换

uint32_t ntohl(uint32_t netlong);32位数据网络到主机字节序转换 

uint16_t ntohs(uint16_t netshort);16位数据网络到主机字节序转换

port端口转换使用uint16_t类型,ip地址转换使用uint32_t,两者不能混用。


地址转换

通常我们输入的ip地址一般都是点分十进制的,但网络通信需要的是网络字节序的整数ip地址。

#include <arpa/inet.h>

int_addr_t inet_addr(const char* cp); 将字符串点分十进制IP地址转换为整形网络字节序IP地址

char* inet_ntoa(struct int_addr in); 将网络字节序IP地址转换为字符串点分十进制IP地址

socket相关接口

1.创建套接字
int socket(int domain, int type, int protocol);
    domain:地址域类型   AF_INET-IPv4通信,使用IPv4的地址结构
    type:套接字类型; SOCK_STREAM 字节流 /SOCK_DGRAM 数据报
          udp通信使用SOCK_DGRAM
    protocol:本次通信协议   IPPROTO_TCP-6/IPPROTO_UDP-17
    返回值:返回一个文件描述符-操作句柄   失败返回-1

2.为套接字绑定地址信息
int bind(int sockfd, struct sockaddr* addr, socklen_t addrlen)
    sockfd:创建套接字返回的操作句柄
    addr:当前绑定的地址信息
    socklen_t:地址信息长度
    返回值:成功返回0,失败返回-1

3.接收数据
ssize_t recvfrom(int sockfd, void* buf, int len, int flag, 
                 struct  sockaddr* srcaddr, socklen_t *addrlen)
    sockfd:操作句柄    
    buf:空间地址,用于存放接收的数据
    len:要接收的数据长度   
    flag:选项标志---默认0--表示阻塞接收
    srcaddr:本条数据的源端地址信息
    addrlen:输入输出参数-指定要接收多长的地址结构,并且返回实际接收的地址长度
    返回值:返回实际接收到的数据长度;失败返回-1

4.发送数据
ssize_t sendto(int sockfd, void* data, int len, int flag, 
               struct sockaddr* peeraddr, socklen_t addrlen)
     sockfd:操作句柄
     data:要发送的数据的空间首地址
     len:要发送的数据长度
     flag:默认0---阻塞发送
     peeraddr:对端地址信息
     addrlen:地址结构长度
     返回值:成功返回实际发送的数据长度,失败返回-15.关闭套接字
int close(int sockfd);

UDPSocket封装

为了使用更加方便,我们对UDP进行了封装,使用的时候,调用接口即可。

#include<iostream>
using namespace std;
#include<sys/socket.h>
#include<unistd.h>
#include<string.h>
#include<arpa/inet.h>
#include<cstdio>
#include<netinet/in.h>
#define CHECK(p) if(p == false) {return -1;}

class UDPSocket
{
  public:

    UDPSocket() 
      :_sockfd(-1)
    {}

    //创建套接字
    bool Socket()
    {
      _sockfd = socket(AF_INET, SOCK_DGRAM,IPPROTO_UDP);
      if(_sockfd < 0)
      {
        perror("create socket error");
        return false;
      }
      return true;
    }


    //绑定信息
    bool Bind(const string& ip, const uint16_t& port)
    {
      struct sockaddr_in addr;
      addr.sin_family = AF_INET;
      //主机字节序转换为网络字节序
      addr.sin_port = htons(port);
      //将字符串的点分十进制ip地址转换为网络字节序的整数形式ip地址
      addr.sin_addr.s_addr = inet_addr(ip.c_str());
      int len = sizeof(addr);
      int ret = bind(_sockfd, (sockaddr*)&addr,len);
      if(ret < 0)
      {
        perror("bind error");
        return false;
      }
      return true;
    }


    //发送数据
    bool Send(string& data, const string& ip, const uint16_t& port)
    {
      sockaddr_in peeraddr;
      peeraddr.sin_family = AF_INET;
      peeraddr.sin_port = htons(port);
      peeraddr.sin_addr.s_addr = inet_addr(ip.c_str());
      socklen_t len = sizeof(sockaddr_in);
      int ret = sendto(_sockfd, data.c_str(), data.size(), 0, (sockaddr*)&peeraddr, len);
      if(ret < 0)
      {
        perror("send error");
        return false;
      }
      return true;
    }


    //接收数据
    bool Recv(string& data, string* ip = NULL, int* port = NULL)
    {
      char tmp[4096] = {0};
      sockaddr_in srcaddr;
      socklen_t len = sizeof(srcaddr);
      int ret = recvfrom(_sockfd, tmp, 4095, 0, (sockaddr*)&srcaddr,&len);
      if(ret < 0)
      {
        perror("recv error");
        return false;
      }
      
      data.assign(tmp, ret);

      if(port != NULL)
        *port = ntohs(srcaddr.sin_port);
      if(ip != NULL)
        *ip = inet_ntoa(srcaddr.sin_addr);
      return true;
    }

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

  private:
    int _sockfd;
};

UDP服务器

根据服务器的流程,调用其接口

1.创建套接字  socket()
2.绑定地址信息   bind()
3.接收数据   recvfrom()
4.发送数据   sendto()
5.关闭套接字   close()

#include"udp.hpp"

int main()
{
  UDPSocket sock;
  //创建套接字
  CHECK(sock.Socket());

  //绑定地址信息
  CHECK(sock.Bind("192.168.134.141",9000 ));

  while(1)
  {
    //接收数据
    string data;
    string ip;
    int port;
    bool ret = sock.Recv(data, &ip, &port);
    if(ret == false)
      continue;
    cout << "cli_ip:" << ip <<" port:" << port << " say:"<< data<< endl; 
    
    //发送数据
    data.clear();
    cout << "serve say:";
    getline(cin, data);
    ret = sock.Send(data, ip, port);
    if(ret == false)
    {
      cout << "serve say failed" << endl;
    }
  }

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

UDP客户端

1.创建套接字   socket()
2.绑定地址信息(不推荐)  bind()
3.发送数据  sendto()
4.接收数据  recvfrom()
5.关闭套接字  close()

#include"udp.hpp"

int main()
{
  UDPSocket sock_cli;
  //创建套接字
  CHECK(sock_cli.Socket());

  //绑定地址信息(不推荐)
  
  while(1)
  {
    //发送数据
    string buf;
    cout << "client say:";
    getline(cin, buf);
    CHECK(sock_cli.Send(buf, "192.168.134.141", 9000));

    buf.clear();
    
    //接收数据
    CHECK(sock_cli.Recv(buf));

    cout << "server say:" << buf << endl;
    
  }

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

示例:

服务端:
在这里插入图片描述客户端1:
在这里插入图片描述

客户端2:
在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值