【Linux】socket套接字编程----UDP

socket套接字编程

  • 概念
  • socket----TCP/UDP
  • socket接口
      • 1. 创建套接字
      • 2. 为套接字绑定地址信息
        • sockaddr结构体
      • 3. 发送数据
        • 4. 接收数据
      • 5. 关闭套接字
  • 代码编写
      • 服务端
      • 客户端
  • 切记

概念

  • socket是一套网络编程接口,类似于中间件
  • 上层用户可以通过这些接口简单的完成网络通信传输,而不需要过于关心内部的实现过程
  • 套接字编程讲的就是使用socket的接口实现网络通信

socket----TCP/UDP

传输层有两个协议:TCP/UDP,这两个协议特性各有不同,因此实现流程也稍有差别;因此需要分开来讲。

  • UDP(用户数据报协议):无连接,不可靠,面向数据报
    应用场景:数据实时性大于安全性的场景----视频传输
  • TCP(传输控制协议):面向连接,可靠传输,面向字节流
    应用场景:数据安全性大于实时性的场景----文件传输

面向数据报:无连接的,不可靠的,无序的,有最大长度限制的数据传输服务
面向字节流:基于连接的,可靠的,有序的,双向的字节流传输服务,以字节为单位,不限制上层传输数据大小的传输方式

网络通信,是网络中的两端主机上的进程之间的通信,这两端有个名称: 客户端/服务器端

  • 客户端:是主动发出请求一方的主机
  • 服务器端:是被动接受请求一方的主机
    永远都是客户端主机先向服务端主机发送请求

UDP服务器端和客户端事务流程图:
【Linux】socket套接字编程----UDP_第1张图片

【Linux】socket套接字编程----UDP_第2张图片

socket接口

1. 创建套接字

int socket(int domain, int type, int protocol);
domain:地址域----不同的网络地址结构  AF_INET即IPv4地址域
type:套接字类型----流式套接字(SOCK_STREAM)/数据报套接字(SOCK_DGRAM)
protocol:使用协议  0表示不同套接字下的默认协议(流式套接字为TCP,数据报套接字为UDP)
		  IPPROTO_TCP -- TCP协议 IPPROTO_UDP -- UDP协议
返回值:套接字的操作句柄----文件描述符

2. 为套接字绑定地址信息

int bind(int sockfd, struct sockaddr *addr, socklen_t len);
sockfd:创建套接字返回的操作句柄
addr:要绑定的地址信息结构体
len:地址信息的长度
返回值:成功返回0,失败返回-1

sockaddr结构体

为什么要使用sockaddr结构体
【Linux】socket套接字编程----UDP_第3张图片

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

用户先定义sockaddr_in的IPv4地址结构,强转之后传入bind中

3. 发送数据

int sendto(int sockfd, char *data, int data_len, int flag, struct sockaddr *dest_addr, socklen_t addr_len);
sockfd:套接字句柄
data:要发送数据的首地址
data_len:要发送数据的长度
flag:选项参数  默认为0--表示当前操作是阻塞操作  
			  MSG_DONTWAIT--设置为非阻塞
			  若发送数据的时候,socket发送缓冲区已经满了,设置为0就阻塞等待,非阻塞就报错返回
dest_addr:目的端地址信息结构--表示数据要发给谁
addr_len:地址信息长度
返回值:成功返回实际发送的数据字节数;失败返回-1
			   

4. 接收数据

int recvfrom(int sockfd, char *buf, int len, int flag, struct sockaddr *src_addr, socklen_t *addr_len);
sockfd:套接字操作句柄
buf:缓冲区的首地址,用于存放接收到的数据,从内核socket接收缓冲区中取出数据放入这个buf用户态缓冲区中
len:用户想要读取的数据长度,但是不能大于buf缓冲区的长度
flag:0--默认阻塞操作,若缓冲区没有数据则一直等待
		MSG_DONTWAIT--非阻塞
dest_addr:接收到的数据的源端地址--表示这个数据是谁发的,从哪来的,以此地址为回复地址
addr_len:输入输出型参数,用于指定想要获取多长的地址信息;成功地址信息的实际长度,失败返回-1

5. 关闭套接字

int close(int fd);

代码编写

服务端

#include 
#include 
#include 
#include 
#include  // sockaddr结构体 / IPPROTO_UDP
#include  // 包含一些字节序转换的接口
#include  // 套接字接口头文件

int main(int argc, char *argv[])
{
     
  // argc表示参数个数,通过argv向程序传递端口参数
  if(argc != 3){
     
    printf("./udp_srv ip port em: ./udp_srv 127.0.0.1 9000\n");
    return -1;
  }
  const char *ip_addr = argv[1];
  uint16_t port_addr = atoi(argv[2]);

  // socket(地址域, 套接字类型, 协议类型);
  int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
  if(sockfd < 0){
     
    perror("socket error!\n");
    return -1;
  }

  // bind(套接字描述符, 地址结构, 地址长度);
  // struct sockaddr_in IPv4地址结构
  // struct in_addr{ uint32_t s_addr }
  struct sockaddr_in addr;
  addr.sin_family = AF_INET;
  addr.sin_port = htons(port_addr);
  // inet_addr 将一个点分十进制的字符串IP地址转换为网络字节序的整数IP地址
  addr.sin_addr.s_addr = inet_addr(ip_addr);
  socklen_t len = sizeof(struct sockaddr_in); // 获取IPv4地址结构长度
  int ret = bind(sockfd, (struct sockaddr*)&addr, len);
  if(ret < 0){
     
    perror("bind error!\n");
    return -1;
  }

  while(1){
     
    char buf[1024] = {
     0};
    struct sockaddr_in cli_addr;
    socklen_t len = sizeof(struct sockaddr_in);
    // recvfrom(描述符, 缓冲区, 长度, 参数, 客户端地址信息, 地址信息长度);
    // 阻塞接收数据,将数据放入buf中,将发送端的地址放入cli_addr中
    int ret = recvfrom(sockfd, buf, 1023, 0, (struct sockaddr*)&cli_addr, &len);
    if(ret < 0){
     
      perror("recvfrom error!\n");
      return -1;
    }
    printf("clicent say : %s\n", buf);
    printf("server say : ");
    fflush(stdout);
    memset(buf, 0x00, 1024); // 清空buf中的数据
    scanf("%s", buf);

    sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr*)&cli_addr, len);
    
    if(ret < 0){
     
      perror("sendto error!\n");
      close(sockfd);
      return -1;
    }
  }
}

客户端

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

class UdpSocket{
     
  public:
    UdpSocket():_sockfd(-1){
     
    }
    // 1.创建套接字
    bool Socket(){
     
      _sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
      if(_sockfd < 0){
     
        perror("socket error!\n");
        return false;
      }
      return true;
    }
    // 2.为套接字绑定地址信息
    bool Bind(const std::string &ip, uint32_t port){
     
      // 1.定义IPv4地址结构
      struct sockaddr_in addr;
      addr.sin_family = AF_INET;
      addr.sin_port = htons(port);
      addr.sin_addr.s_addr = inet_addr(ip.c_str());
      // 2.绑定地址
      socklen_t len = sizeof(struct sockaddr_in);
      int ret = bind(_sockfd, (struct sockaddr*)&addr, len);
      if(ret < 0){
     
        perror("bind erroe!\n");
        return false;
      }
      return true;
    }
    // 3.发送数据
    bool Send(const std::string &data, const std::string &ip, uint16_t port){
     
      // 1.定义对端地址信息的IPv4地址结构
      struct sockaddr_in addr;
      addr.sin_family = AF_INET;
      addr.sin_port = htons(port);
      addr.sin_addr.s_addr = inet_addr(ip.c_str());
      // 2.向这个地址发送数据
      socklen_t len = sizeof(struct sockaddr_in);
      int ret = sendto(_sockfd, data.c_str(), data.size(), 0, (struct sockaddr*)&addr, len);
      if(ret < 0){
     
        perror("send error!\n");
        return false;
      }
      return true;
    }
    // 4.接收数据
    bool Recv(std::string *buf, std::string *ip = NULL, uint16_t *port = NULL){
     
      struct sockaddr_in addr;
      socklen_t len = sizeof(struct sockaddr_in);
      int ret;
      char tmp[4096] = {
     0};
      ret = recvfrom(_sockfd, tmp, 4096, 0, (struct sockaddr*)&addr, &len);
      if(ret < 0){
     
        perror("recvfrom erroe!\n");
        return false;
      }
      buf->assign(tmp, ret); // 给buf申请ret大小空间,从tmp中拷贝ret长度的数据进去
      if(ip != NULL){
     
        *ip = inet_ntoa(addr.sin_addr); // 将网络字节序整数IP地址转换为字符串地址
      }
      if(port != NULL){
     
        *port = ntohs(addr.sin_port);
      }
      return true;
    }
    // 5.关闭套接字
    void Close(){
     
      close(_sockfd);
      _sockfd = -1;
      return;
    }
  private:
      int _sockfd;
};

// 客户端要给服务端发送数据,那么就需要知道服务端的地址信息
// 因此通过程序运行参数传入服务端的地址信息
int main(int argc, char *argv[])
{
     
  if(argc != 3){
     
    printf("em : ./udp_cli 127.0.0.1 9000\n");
    return -1;
  }
  std::string ip_addr = argv[1];
  uint16_t port_addr = atoi(argv[2]);

  UdpSocket sock;
  sock.Socket();
  while(1){
     
    std::cout << "client say : ";
    std::string buf;
    std::cin >> buf;
    sock.Send(buf, ip_addr, port_addr);

    buf.clear();
    sock.Recv(&buf);
    std::cout << "server say : " << buf << std:: endl;
  }
  sock.Close();
  return 0;
}

切记

  • 客户端通常不推荐用户自己绑定地址信息
  1. 如果不主动绑定,操作系统会选择一个合适的地址信息进行绑定(当前没有使用的端口)
  2. 一个端口只能被一个进程占用,若用户自己指定端口以及地址进行绑定,有可能这个端口以及被使用了,则会绑定失败
    让操作系统选择合适的端口信息,可以尽最大能力减小端口冲突概率
  3. 对于客户端来说,其实并不关心使用什么源端地址将数据发送出去,只要能够发送数据并且接收数据就可以了
  • 服务端可不可以也不主动绑定地址?
    不可以
    客户端无法获得新的服务端的地址信息,访问不到服务端

你可能感兴趣的:(网络,socket,网络通信,c++,linux)