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

Socket编程udp

目录

udp协议

网络字节序

库函数

socket编程接口

sockaddr结构

sockaddr_in结构

struct in_addr结构

Echo Server

InetAddr.hpp

Comm.hpp

UdpServer.hpp

UdpClientMain.cc

测试

Dict Server

Dict.hpp

UdpServer.hpp

UdpServer.cc

测试


udp协议

UDP(user datagram protocal用户数据报协议)

1. 传输层

2.无连接

3.不可靠传输

4.面向数据报

网络字节序

内存中多字节数据有大端字节序,小端字节序的区别,网络数据流中同样有大端小端之分

1.发送主机通常将发送缓冲区上的数据按内存地址从低到高的顺序发出

2.接受主机也是按照从低到高的顺序保存

3.因此网络数据流这样规定:先发出的数据是低地址,后发出的是高地址

4.TCP/IP协议规定,网络字节序应采用大端字节序,即低地址高字节

5.不管这台主机是大端还是小端,都会按照TCP/IP协议规定的网络字节序来接受与发送数据

6.如果发送数据的机器是小端,那么需要先转为大端再发送

库函数

我们可以调用以下库函数做网络字节序与主机字节序的转换

h:host,主机

n:net,网络

s:short,短整数

l:long,长整数

这样我们就可以很方便的记忆这些函数了

主机/网络+"to"+网络/主机+短/长

至于这里的短整型与长整型主要是因为

port:端口号,是一个16字节的短整数

ip:地址,是一个32字节的长整数

如果是小端机器,那么这些函数会进行转换

如果是大端,则不需要进行转换

socket编程接口

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,socklen_t address_len);// 开始监听 socket (TCP, 服务器)
int listen(int socket, int backlog);// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

sockaddr结构

1.IPv4与IPv6的地址格式定义在netinet/in.h头文件中,IPv4地址用sockaddr_in结构体,包括16位地址类型,16位端口号与32位IP地址

2.IPv4与IPv6分别定义AF_INET与AF_INET6常数,这样的话只需要取到某种sockaddr结构体的首地址,不需要知道具体类型,就可以根据地址类型字段确定结构体中的内容

3.socket API 可以都用 struct sockaddr *类型表示, 在使用的时候需要强制转化成
sockaddr_in; 这样的好处是程序的通用性, 可以接收 IPv4, IPv6, 以及 UNIX Domain
Socket 各种类型的 sockaddr 结构体指针做为参数

sockaddr_in结构

struct in_addr结构

Echo Server

这是一个客户端向服务器发送数据,服务器打印收到的数据,再将数据发送回给客户端的简单样例

接下来我们会用到地址转换函数

InetAddr.hpp

这个头文件是对网络地址的封装,即对ip和port的封装

我们将port保存为16位整型,ip保存为字符串

这里我们可以看到inet_nota是将网络字节序的in_addr结构转换为本地字符串

#pragma once
#include<iostream>
#include<string>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
class InetAddr
{public:InetAddr(struct sockaddr_in& addr):_in(addr){_port=ntohs(addr.sin_port);//网络转主机_ip=inet_ntoa(addr.sin_addr);//将uint32的ip转字符串}std::string Ip(){return _ip;}uint16_t Port(){return _port;}std::string PrintDebug(){std::string info=_ip;info+=':';info+=std::to_string(_port);return info;}~InetAddr(){}private:std::string _ip;uint16_t _port;struct sockaddr_in _in;
};

Comm.hpp

定义创建socket时的各种状态

#pragma once
enum
{Usage_Err,Socket_Err,Bind_Err
};

UdpServer.hpp

我们从这里开始封装我们的UdpServer类

1.首先将所需要的头文件包进来,然后确定默认端口号,默认socketfd与默认接受(发送)缓冲区大小

2.创建服务器,我们遵循以下步骤

1.首先创建socket文件,最后一项默认为0即可,第一项为ip类型,ipv4还是ipv6,ipv4为AF_INET,ipv6为AF_INET6。第二项为确认传输协议,是字节流流传输还是数据报传输。

tcp协议为面向字节流,我们这里是udp协议,面向字节流,采用SOCK_DGRAM。

2.确认socket创建完成之后我们开始绑定服务器信息,还是使用struct sockaddr结构

我们这里使用sockaddr_in,之后强转回sockaddr即可

第一项协议簇为AF_INET,即ipv4

第二项端口号,由于是服务器,端口号不可变,因此我们在前面就已经确定好了默认端口号

第三项ip地址,我们可以手动选择本地地址或者网络地址,当然,我们也可以选择填入

IN_ADDRANY(全0),填0的话就表示任意一个地址都可以,不管是本地还是网络地址

全部完成以后我们绑定socket文件与对应的sockaddr结构即可。

以上做完之后我们就完成了服务器的初始化

3.

服务器启动,我们创建一个缓冲区用来帮我们存放数据

ssize_t recvfrom (int __fd, void *__restrict __buf, size_t __n, int __flags,__SOCKADDR_ARG __addr, socklen_t *__restrict __addr_len)功能为从socket文件获取从网络传输过来的数据第一项为sock文件fd第二项为缓冲区起始地址第三项为缓冲区大小第四项默认填0即可第五项为sockaddr结构体,由于是接收数据,所以我们是获取目标主机结构,里面会把发送消息的主机
相关信息,即ip与port放在里面,我们可以通过这个结构体向该主机法案送消息最后一项为sockaddr结构的大小返回值:接收到数据的长度

 

ssize_t sendto (int __fd, const void *__buf, size_t __n,int __flags, __CONST_SOCKADDR_ARG __addr,socklen_t __addr_len);其他的与recvfrom函数一样,唯一不同的是这里的sockaddr结构体我们需要
填入目标主机的相关信息,即IP与portrecvfrom与sendto的sockaddr分辨方法很简单作为接收方,我们需要知道数据是谁发来的作为发送方,我们需要知道数据是发给谁的
#pragma once
#include<iostream>
#include<cstring>
#include<cerrno>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include"nocopy.hpp"
#include"Comm.hpp"
#include"Log.hpp"
#include"InetAddr.hpp"
#include<unistd.h>
const static uint16_t defaultport=8888;
const static int defaultfd=-1;
const static int defaultsize = 1024;using namespace LogModule;class UdpServer:public nocopy
{public:UdpServer(uint16_t port=defaultport):_port(port),_socketfd(defaultfd){}void Init(){//创建socket_socketfd=socket(AF_INET,SOCK_DGRAM,0);if(_socketfd<0){LOG(LogModule::LogLevel::ERROR)<<"socket error,"<<errno<<":"<<strerror(errno);exit(Socket_Err);}LOG(LogModule::LogLevel::INFO)<<"socket success,socketfd:"<<_socketfd;//绑定struct sockaddr_in local;bzero(&local,sizeof(local));local.sin_family=AF_INET;local.sin_port=htons(_port);//一定要本地转网络local.sin_addr.s_addr=INADDR_ANY;//任意一个ip都可以//local.sin_addr.s_addr=inet_addr(_ip.c_str());//设置为指定ipint n=::bind(_socketfd,(sockaddr*)&local,sizeof(local));if(n!=0){LOG(LogModule::LogLevel::ERROR)<<"bind error,"<<errno<<":"<<strerror(errno);exit(Bind_Err);}}void start(){LOG(LogModule::LogLevel::INFO)<<"服务器开始运行";char buffer[defaultsize];while(1){struct sockaddr_in peer;socklen_t len=sizeof(peer);ssize_t n=recvfrom(_socketfd,buffer,sizeof(buffer),0,(sockaddr*)&peer,&len);LOG(LogModule::LogLevel::INFO)<<"收到消息";if(n>0){InetAddr addr(peer);buffer[n]=0;std::cout << "[" << addr.PrintDebug() << "]# " <<buffer << std::endl;sendto(_socketfd,buffer,strlen(buffer),0,(sockaddr*)&peer,len);}}}~UdpServer(){}private:std::string _ip;uint16_t _port;int _socketfd;
};

UdpClientMain.cc

这里我们就不对客户端进行封装了

客户端比起服务器少了绑定这一步骤。

因为服务器我们需要保证每一次启动时我们的端口号相同,所以我们需要手动绑定

但是客户端我们并不需要保证我们的端口号相同,所以只需要交给操作系统,让操作系统自动绑定即可

#include<iostream>
#include<cstring>
#include<cerrno>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include"Comm.hpp"
#include"nocopy.hpp"
#include"Log.hpp"
#include<unistd.h>using namespace LogModule;void Usage(const std::string & process)
{std::cout << "Usage: " << process << " server_ip server_port"<< std::endl;
}
int main(int argc,char* argv[])
{if(argc!=3){Usage(argv[0]);return 1;}std::string serverip=argv[1];//ip样例:118.25.148.178uint16_t serverport=std::stoi(argv[2]);//端口号就是个数字//创建socketint sock=socket(AF_INET,SOCK_DGRAM,0);if(sock<0){LOG(LogModule::LogLevel::ERROR)<<"socket error,"<<errno<<":"<<strerror(errno);exit(Socket_Err);}std::cout<<"create socket success"<<std::endl;//client 一定要绑定port,但是不需要显示绑定,因为在第一次发数据时会自动绑定//server的 port众所周知不能更改,而client不需要,因此自动绑定即可struct sockaddr_in server;bzero(&server,sizeof(server));server.sin_family=AF_INET;server.sin_port=htons(serverport);//一样需要网络转主机//上面的inet_ntoa为将ip由uint32转回字符串,这里的inet_addr则反过来server.sin_addr.s_addr=inet_addr(serverip.c_str());//将字符串ip转为uint32while(true){std::string inbuffer;std::cout<<"please enter#:";std::getline(std::cin,inbuffer);ssize_t n=sendto(sock,inbuffer.c_str(),inbuffer.size(),0,(sockaddr*)&server,sizeof(server));if(n>0){char buffer[1024];struct sockaddr_in temp;socklen_t len=sizeof(temp);ssize_t m=recvfrom(sock,buffer,sizeof(buffer)-1,0,(sockaddr*)&temp,&len);if(m>0){buffer[m]=0;std::cout<<"server echo#"<<buffer<<std::endl;}elsebreak;}elsebreak;}close(sock);return 0;
}

测试

Dict Server

这里我们再用UDP写一个字典

比起上面大致内容差不多,只是多了一个初始化字典以及处理方法

Dict.hpp

我们采用哈希表,从文件中按照分隔符每行一对键值对的方式,将英文与中文的对应关系放入哈希表中

#pragma once
#include<iostream>
#include<string>
#include<fstream>
#include<unordered_map>const std::string sep=":";
const std::string default_path="./dictionary";
class Dict
{public:Dict(const std::string& confpath=default_path):_confpath(confpath){LoadDict();}~Dict(){}void LoadDict(){std::ifstream in(_confpath);if(!in.is_open()){std::cerr<<"open file error:"<<std::endl;return;}std::string line;while(getline(in,line)){if(line.empty())continue;auto pos=line.find(sep);if(pos==std::string::npos)continue;std::string key=line.substr(0,pos);std::string val=line.substr(pos+sep.size());_dict[key]=val;}in.close();}std::string Translate(const std::string word){auto iter=_dict.find(word);if(iter==_dict.end())return "UnKnow";return iter->second;}private:std::string _confpath;std::unordered_map<std::string,std::string> _dict;
};

UdpServer.hpp

其实差不多,我们只是多了一个处理方法而已

该处理方法没有返回值,我们只需要在构造函数时将对应的方法传入对象即可

第一个参数为我们的‘键’,第二个参数则是我们根据字典,查找对应的‘值’

我们接下来只需要在服务器中对字典进行初始化,并传入方法即可

UdpServer.cc

#include"UdpServer.hpp"
int main()
{Dict dict;dict.LoadDict();std::unique_ptr<UdpServer> server(std::make_unique<UdpServer>([&dict](const std::string& word,std::string& tran){tran=dict.Translate(word);}));server->Init();server->start();
}

服务器只有一份,我们采用unique_ptr智能指针的方式创建服务器

然后我们使用lamda表达式,实现查询并传入查找结果的方法

测试

http://www.xdnf.cn/news/1057861.html

相关文章:

  • 学习 Protobuf:序列化、反序列化及与 JSON 的对比
  • Java中间件使用方式与实战应用
  • Oracle 的 TCP.SEND_TIMEOUT 参数
  • 【沉浸式解决问题】优化MySQL中多表union速度慢的问题
  • 【MATLAB去噪算法】基于VMD联合小波阈值去噪算法(第六期)
  • VS2022 C++动态库制作和使用指南
  • 【深度学习】TensorFlow全面指南:从核心概念到工业级应用
  • 【C++】vector的模拟实现(详解)
  • 记一次用飞算JavaAI助力项目部分重构的过程
  • 从C++编程入手设计模式——外观模式
  • 0616---0617C#实训课总结摘要
  • 【前端基础】摩天之建的艺术:html(上)
  • MIT 6.S081 2020 Lab8 locks 个人全流程
  • <script setup> 和在 <script> 中使用 setup() 函数有什么区别
  • vite的分包
  • 使用 React-i18next 在 TypeScript 的 Next.js 应用中实现国际化
  • ARM单片机启动流程(一)(万字解析,纯干货分享)
  • CVPR 2025最佳论文详解|VGGT:纯前馈Transformer架构,3D几何感知「大一统」模型来了!
  • 精益数据分析(108/126):媒体网站用户参与时间优化与分享行为解析
  • 【Unity笔记】Unity URP 渲染中的灯光数量设置— 场景、使用方法与渲染原理详解
  • Python 列表与元组的性能差异:选择合适的数据结构
  • 人机交互的趋势判断-范式革命的推动力量
  • SCRM客户关系管理软件的界面设计原则:提升用户体验与交互效率
  • 【Mysql】MySQL的MVCC及实现原理,核心目标与全流程图解
  • 获取ip地址安全吗?如何获取静态ip地址隔离ip
  • 常见航空数码相机
  • 基于SpringBoot的民宿管理平台-037
  • 【Linux指南】文件内容查看与文本处理
  • 操作系统引导和虚拟机(包含os结构,选择题0~1题无大题)
  • 编译链接实战(27)动态库实现变了,可执行程序需要重新编译吗