当前位置: 首页 > news >正文

深入了解linux网络—— UDP网络通信

前言

了解了socket编程的基础知识,IP + port标识网络中的唯一进程、源IP/端口号、目的IP/端口号以及socket相关API

传输层的两大协议:TCP/UDP

这里来使用UDP相关接口来实现网络通信

服务端

1. 初始化

我们知道,在操作系统中并不是所有的进程都有端口号,所以在服务器端,就要做相关操作来让进程绑定唯一的端口号。

创建socket

对于服务器端,要先创建socketIP + port

创建socket要用到接口socket

int socket(int domain, int type, int protocol);

参数:

socket存在三个参数:int domainint typeint protocol

domain

在这里插入图片描述

简单来说该参数就是表面要进行的通信类型,这里我们要进行网络通信,参数就传AF_INET

type

在这里插入图片描述

对于这个参数也是介绍了一大堆,这里就暂且先关注UPD通信(也就是面向数据报),其中SOCK_DGRAM就表示面向数据报。

所有,在传参时,domainAF_INET(表示网络通信)、typeSOCK_DGRAM(表示面向数据报)就能够表示当前是UDP通信。

在这里插入图片描述

protocol

对于这个参数,这里暂时不做介绍,在使用时传递0即可。

返回值:

对于socket的返回值,就有意思了:

在这里插入图片描述

可以看到,如果调用socket创建成功,就会返回一个文件描述符(这里体现了Linux一切皆文件)

而我们还知道,一个进程默认会打开三个文件(012文件描述符被占用),所以默认(不关闭012socket返回的文件描述符是>=3的。

这里暂且先不探究其原理,后续再深入了解。

所以,要使用UDP通信,创建套接字,调用socket就要传递AF_INETSOCK_DGRAM0

socket创建成功会返回一个文件描述符,这里就设计一个类,将该文件描述符管理起来。

//udpserver.hpp
class UdpServer
{
public:UdpServer() : _sockfd(-1){}~UdpServer() {}void Init(){// 1. 创建socket_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // AF_INET 网络通信  SOCK_DGRAM  面向数据报if (_sockfd < 0){// _sockfd < 0 表示创建套接字失败,这里输出一条日志然后退出LOG(Level::DEBUG) << "socket error";exit(1);}LOG(Level::DEBUG) << "socket success, sockfd : " << _sockfd;}
private:int _sockfd;
};

这样实现UdpSrever,在服务器端只需创建UdpServer对象,然后调用Init(初始化)和运行成员函数即可。

//udpserver.cc
#include "udpserver.hpp"
int main()
{UdpServer usvr;usvr.Init();return 0;
}

在这里插入图片描述

可以看到,socket返回的文件描述符确实是从3开始的。

绑定IP/端口号

在上述操作中,通过socket创建了套接字,也获取了文件描述符;但是还没有IP和端口号啊,还是没办法在网络中找到该主机的该进程啊;

所以创建完套接字之后,还要进行绑定IP和端口号;就要调用bind方法

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数:

可以看到bind应该是哪个参数,其第一个参数是sockfd,就需要传递socket返回的文件描述符。

addraddrlen

首先const struct sockaddr* addr,这里我们要进行网络通信,就要传递一个struct sockaddr_in *的指针变量;

socklen_t addrlen则表示传递的第二个参数的长度。

所以,在调用bind时,我们要进行网络通信就要先有一个struct sockaddr_in类型的结构体对象;

  • 构建struct sockaddr_in对象

我们知道sockaddr_in结构体中存在三个字段:sin_familysin_addrsin_port;分别指标志位、IP地址和端口号。

这里要进行网络通信,标志位就传递AF_INET

这里在服务器端,就由外部指定要绑定的IP和端口号。(在运行程序时由命令行参数传递)

  • IP地址转换

在我们的认知中,IP地址都是10.0.16.12这种形式的,但是在这里struct sockaddr_in结构中的IP地址类型是uint32_t,也就是4字节整数;

那我们通过命令行参数获取到的IP地址是字符串形式的(.分隔开的数字),所以在这里就要进行IP地址的转换(由字符串形式转为4字节数字

此外,我们这里的sockaddr_in在未来进行通信时,是要发送到网络的,所以在这里我们还需要将本地字节序转换为网络字节序

这里我们可以自己实现字符串IP --> 4字节数字,本地字节序 --> 网络字节序。但是这里,我们也可以调用inet_addr(将字符串形式的IP地址转换为4字节数字,再转换为网络字节序)

  • 端口号转换

这里的struct sockaddr_in是要发送到网络的,所以端口号sin_port也要由本机字节序转换为网络字节序,这里就可以调用htons来进行转换。

返回值:

在这里插入图片描述

成功,0被返回;失败则返回-1,并且错误码被设置。

class UdpServer
{
public:UdpServer(const std::string &ip, uint16_t port) : _sockfd(-1), _ip(ip), _port(port){}~UdpServer() {}void Init(){// 1. 创建socket_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // AF_INET 网络通信  SOCK_DGRAM  面向数据报if (_sockfd < 0){// _sockfd < 0 表示创建套接字失败,这里输出一条日志然后退出LOG(Level::DEBUG) << "socket error";exit(1);}LOG(Level::DEBUG) << "socket success, sockfd : " << _sockfd;// 2.1 构建sockaddr_in对象struct sockaddr_in sockin;sockin.sin_family = AF_INET;sockin.sin_addr.s_addr = inet_addr(_ip.c_str());sockin.sin_port = htons(_port);// 2.2 绑定IP、端口号int n = bind(_sockfd, (struct sockaddr *)&sockin, sizeof(sockin));if (n < 0){// n < 0 表示绑定失败,这里输出一条日志然后退出LOG(Level::DEBUG) << "bind error";exit(2);}LOG(Level::DEBUG) << "socket success";}
private:int _sockfd;std::string _ip;uint16_t _port;
};

这样,服务器在创建UpdServer对象,调用Init初始化后才能进行通信。

//udpserver.cc
int main(int argc, char *argv[])
{if (argc != 3){std::cout << argv[0] << " ip  port" << std::endl;return -1;}std::string ip = argv[1];uint16_t port = std::stoi(argv[2]);UdpServer usvr(ip, port);usvr.Init();while (true){}return 0;
}

这样在程序运行起来之后,就可以在系统中查看到当前进程绑定的IP和端口号了

netstat可以用来查看网络相关信息

  • -u 表示UDP相关;
  • -p表示查看进程pidprogram name
  • -n表示用数字显示
  • -l表示查看监听状态的端口

在这里插入图片描述

2. 接收/发送信息

通过创建套接字、绑定IP和端口号,当前就已经具备了通信的条件,现在来实现接受和发送信息;

接受信息

对于UDP通信,接受信息要用到的接口就是recv系列的接口,这里使用recvfrom

ssize_t recvfrom(int sockfd, void buf[restrict .len], size_t len,int flags,struct sockaddr *_Nullable restrict src_addr,socklen_t *_Nullable restrict addrlen);

参数:

recvfrom存在6个参数:

  • sockfdsocket返回的文件描述符,指明通信使用的套接字(文件)
  • buflenbuf表示接受信息的缓冲区,len表示缓冲区大小
  • flags:标志为,这里传0即可**(表示阻塞式接受信息)**
  • src_addraddrlenrecvfrom不仅受到了对方发来的信息,还收到对方的struct sockaddr字段(IPport);

src_addr输出型参数,recvfrom接收到对方的struct sockaddr字段拷贝到该地址;

addrlen传参时表示src_addr的长度,(也是输出型参数,调用完成后,addrlen中的值表示实际读到的长度)

返回值:

recvfrom读取成功,返回实际读到信息的字节数;(buf)

发送信息

要发送信息,这里就要使用send系列接口,这里使用sendto

ssize_t sendto(int sockfd, const void buf[.len], size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);

参数:

参数和recvfrom一样:sockfd指创建socket时返回的文件描述符、buf表示要发送的信息,len表示信息的长度;

flag这里暂时传递0即可。

dest_addr表示要发送给谁,目的主机的struct sockaddr字段;addrlen表示dest_addr的长度(大小)。

返回值这里暂时不考虑。

所以,我们就可以让服务器端阻塞等待式的接受信息,收到信息之后,对信息稍作处理,然后再发送回来。

class UdpServer
{
public:void Start(){while (true){char buff[256];struct sockaddr_in peer;socklen_t len;// 接受信息int n = recvfrom(_sockfd, buff, sizeof(buff), 0, (struct sockaddr *)&peer, &len);if (n < 0){// 读取失败LOG(Level::WARNING) << "recvfrom error";continue;}buff[n] = '\0';std::cout << "recv massage :" << buff << std::endl;// 发送信息int m = sendto(_sockfd, buff, sizeof(buff), 0, (struct sockaddr *)&peer, len);if (m < 0){// 读取失败LOG(Level::WARNING) << "sendto error";continue;}}}
private:int _sockfd;std::string _ip;uint16_t _port;
};

这里,服务器端在接受到远端发送的信息,输出一条消息到标准输出,然后再信息发送给远端。

客户端

上面实现了服务端的代码,现在来实现客户端。

对于服务端,在通信之前,要先创建套接字,绑定IP和端口号;

那客户端呢?

客户端,只需要我们显示地去创建套接字即可,不需要显示绑定(在首次发送信息时会自动绑定IP地址和随机端口号)

所以,客户端只需要创建套接字,然后就可以给服务端发送信息了。

而发送信息,要知道给谁发吧,那就需要对方的IP地址和端口号;

这里就通过命令行参数,在执行程序时指定IP和端口号。

//udpclient.cc
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <strings.h>
int main(int agrc, char *argv[])
{if (agrc != 3){std::cout << argv[0] << " serverip  serverport" << std::endl;return -1;}// 创建服务端 struct sockaddr_instruct sockaddr_in server;bzero(&server, sizeof(server));server.sin_family = AF_INET;server.sin_addr.s_addr = inet_addr(argv[1]);server.sin_port = htons(std::stoi(argv[2]));// 创建套接字int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){std::cerr << "socket error" << std::endl;return -1;}// 无需显示绑定while (true){std::string massage;std::getline(std::cin, massage);// 发送信息sendto(sockfd, massage.c_str(), massage.size(), 0, (struct sockaddr *)&server, sizeof(server));std::cout << "send massage : " << massage << std::endl;// 接受信息struct sockaddr_in peer;bzero(&peer, sizeof(peer));socklen_t len = sizeof(len);char buff[256];int n = recvfrom(sockfd, buff, sizeof(buff), 0, (struct sockaddr *)&peer, &len);if (n < 0){// 读取失败std::cerr << "recvfrom error";continue;}buff[n] = '\0';std::cout << "recv : " << buff << std::endl;}return 0;
}

这里clientserver都只是简单的接受和发送数据,并没有进行数据处理。

在这里插入图片描述

本机IP

在上述操作中,使用的10.0.16.12IP地址是云服务器的子网IP,我们如果尝试去连接公网IP,可以发现是连不上的(服务器公司做相关保护)

而在服务器中还存一种IP地址,就是本机IP:

使用ifconfig命令可以查看到:

在这里插入图片描述

就是上图中的127.0.0.1,如果我们一台机器上的serverclient通信使用这种IP地址,数据就不会传输到网络,而是通过操作系统发送到对方。

这种IP 地址也通常用来测试网络代码。

在这里插入图片描述

可以看到也是可以通信的。

但是,如果这里一端使用10.0.16.12地址,另外一端使用127.0.0.1呢?

在这里插入图片描述

在这里插入图片描述

可以看到,client和server绑定不同的IP地址,虽然是一台主机的IP地址,但却无法完成通信。

而发送到127.0.0.110.0.16.12的信息都是发送给该主机的啊,按理来说应该是能够收到的。

这里,要想让服务端接收到发送给该主机的所有信息,就不能让server去绑定某个IP地址,而是让server绑定INADDR_ANY

在这里插入图片描述

INADDR_ANY本质上就是0

所以,在server端,就不需要通过命令行参数传递IP地址,也不需要存储IP地址了,直接绑定INADDR_ANY即可。

class UdpServer
{
public:// UdpServer(const std::string &ip, uint16_t port) : _sockfd(-1), _ip(ip), _port(port)// {// }UdpServer(uint16_t port) : _sockfd(-1), _port(port){}~UdpServer() {}void Init(){// 1. 创建socket_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // AF_INET 网络通信  SOCK_DGRAM  面向数据报if (_sockfd < 0){// _sockfd < 0 表示创建套接字失败,这里输出一条日志然后退出LOG(Level::DEBUG) << "socket error";exit(1);}LOG(Level::DEBUG) << "socket success, sockfd : " << _sockfd;// 2.1 构建sockaddr_in对象struct sockaddr_in sockin;bzero(&sockin, sizeof(sockin));sockin.sin_family = AF_INET;// sockin.sin_addr.s_addr = inet_addr(_ip.c_str());sockin.sin_addr.s_addr = INADDR_ANY;//绑定INADDR_ANY 接受发送给该主机的所有信息sockin.sin_port = htons(_port);// 2.2 绑定IP、端口号int n = bind(_sockfd, (struct sockaddr *)&sockin, sizeof(sockin));if (n < 0){// n < 0 表示绑定失败,这里输出一条日志然后退出LOG(Level::DEBUG) << "bind error";exit(2);}LOG(Level::DEBUG) << "socket success";}void Start(){while (true){char buff[256];struct sockaddr_in peer;socklen_t len;// 接受信息int n = recvfrom(_sockfd, buff, sizeof(buff), 0, (struct sockaddr *)&peer, &len);if (n < 0){// 读取失败LOG(Level::WARNING) << "recvfrom error";continue;}buff[n] = '\0';std::cout << "recv massage :" << buff << std::endl;// 发送信息int m = sendto(_sockfd, buff, sizeof(buff), 0, (struct sockaddr *)&peer, len);if (m < 0){// 读取失败LOG(Level::WARNING) << "sendto error";continue;}}}
private:int _sockfd;// std::string _ip;uint16_t _port;
};

到这里本篇文章内容就结束了,感谢各位大佬的支持
我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=2oul0hvapjsws

http://www.dtcms.com/a/406930.html

相关文章:

  • 招商加盟的网站应该怎么做宝坻做网站哪家好
  • 视频网站开发工具网站备案中是什么意思
  • 物理媒介和分组交换原理
  • Linux常用命令53——file
  • 西双版纳 网站建设网络建设与运维初级
  • 【Python】文件处理(一)
  • win10怎么做网站wordpress wooyun
  • 织梦网站登录网上做网站赚钱吗
  • Linux数据安全与备份策略完全指南
  • 哈尔滨网站建设服务公司暴雪游戏服务中心
  • wordpress 关闭评论网站优化排名提升
  • 硅基计划5.0 MySQL 壹 初识MySQL 初版
  • Linux之挂载新的硬盘(超详细!)
  • 部署 GitLab 服务器
  • C++项目:仿muduo库高并发服务器-------connection模块
  • 网站建设需要的资质互联网保险的发展现状
  • 8-机器学习与大模型开发数学教程-第0章 预备知识-0-8 编程与数值计算基础(浮点数精度、溢出、数值稳定性)
  • php网站开发书微信公众号手机网站开发
  • 做网站需要工商执照吗代人做网站
  • Go基础:模块化管理为什么能够提升研发效能?
  • 合肥专业做网站公司wd wordpress
  • IR 680LT Maleimide,IR 680LT马来酰亚胺用于蛋白质标记与定量分析
  • 打工人日报#20250925
  • Kubernetes Pod 的生命周期与故障排查
  • Java List列表创建方法大总结
  • 河南工信建设网站市场营销师报名官网
  • 没有文字的网站怎么优化建立旅游网站的目的
  • Spring Boot用户登录注册系统设计与实现
  • 筑牢AI安全防线:阿里云AI安全护栏
  • seo网站优化教程如何把自己电脑做网站服务器吗