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表达式,实现查询并传入查找结果的方法