linux【网络编程】之UDP网络程序模拟实现
一、开发环境
本次实验是在腾讯云服务器上进行
二、服务端实现
做完这次实验,感受最深的就是函数接口方面的问题,我们先来介绍一下需要用到的接口。
2.1 接口认识
2.1.1 socket创建网络通信套接字
#include<sys/types.h>/* See NOTES */#include<sys/socket.h>intsocket(int domain,int type,int protocol);
参数domain:域,未来套接字是进行网络通信(AF_INET)还是本地通信(AF_UNIX, AF_LOCAL)
参数type:套接字提供服务的类型,如SOCK_STREAM:流式服务TCP策略,SOCK_DGRAM:数据报服务,UDP策略
参数protocol:缺省为0,可由前两个类型确定
返回值:失败返回-1,成功返回文件描述符
2.1.2 bind:绑定Ip和端口号
绑定端口号和ip
#include<sys/types.h>/* See NOTES */#include<sys/socket.h>intbind(int sockfd,conststructsockaddr*addr,socklen_t addrlen);
参数sockfd:文件描述符,也就是调用socket的返回值
参数addr:利用structsockaddr_in强转
参数addrlen:结构体的大小
返回值:成功返回0,失败返回-1
2.1.3 sockaddr_in结构体
structsockaddr_in{__SOCKADDR_COMMON(sin_);//协议家族,对应AF_INET
in_port_t sin_port;//端口号,in_port_t是对port的重命名structin_addr sin_addr;//IP地址,in_addr结构体里封装了一个32位整数/* Pad to size of `struct sockaddr'. */unsignedchar sin_zero[sizeof(structsockaddr)-
__SOCKADDR_COMMON_SIZE -sizeof(in_port_t)-sizeof(structin_addr)];};
#define__SOCKADDR_COMMON(sa_prefix)\sa_family_t sa_prefix##family//sa_family_t //16位整数typedefuint16_t in_port_t;typedefuint32_t in_addr_t;structin_addr{
in_addr_t s_addr;};
宏中的**##**是将两个字符串合并成一个新字符串,也就是将接收到的sa_prefix与family合并起来,形成了sa_prefix_family
创建结构体后要先清空数据
#include<strings.h>voidbzero(void*s, size_t n);
2.1.4 IP地址转换函数:inet_addr、inet_ntoa
#include<sys/socket.h>#include<netinet/in.h>#include<arpa/inet.h>
inet_addr内部完成两件事情:1.把字符串转化成整数;2.再把整数转化成对应的网络序列**
in_addr_t inet_addr(constchar*cp);//const char*cp:点分十进制风格的IP地址//1.网络->主机 2.uint32_t -> 点分十进制char*inet_ntoa(structin_addr in);
2.1.5 recvfrom:读取数据
#include<sys/types.h>#include<sys/socket.h>
ssize_t recvfrom(int sockfd,void*buf, size_t len,int flags,structsockaddr*src_addr, socklen_t *addrlen);
sockfd:特定的套接字,buf:读取到特定缓冲区,len:结构体大小
flags:读取的方式,默认为0,阻塞读取
src_addr:输入输出型参数,收到消息除了本身,还得知道是那个IP+port发过来的数据
len:大小是多少
返回-1表示失败,成功返回字节数
2.2 头文件udpServer.hpp
头文件中:构造负责初始化参数
initServer函数负责初始化服务器:1.创建套接字;2.将套接字与IP地址和端口号绑定。
start()函数服务器本质是一个死循环,在start启动的时候,通过
recvfrom
读取通过网络发来的数据、源ip+源端口号分别保存到缓冲区和struct sockaddr这个结构体中
#pragmaonce#include<iostream>#include<string>#include<sys/types.h>#include<sys/socket.h>#include<cerrno>#include<cstring>#include<cstdlib>#include<unistd.h>#include<arpa/inet.h>#include<netinet/in.h>#include<functional>namespace Server
{usingnamespace std;//默认的点分十进制IPstaticconst string defaultIP="0.0.0.0";staticconstint gnum=1024;//缓冲区大小enum{USAGE_ERR=1,SOCKET_ERR,BIND_ERR};//退出码typedef function<void(string,uint16_t,string)> func_t;classudpServer{public:udpServer(const func_t& callback,constuint16_t&port,const string& ip=defaultIP):callback_(callback),port_(port),ip_(ip),sockfd_(-1){}voidinitServer(){//****创建UDP网络通信端口****
sockfd_=socket(AF_INET,SOCK_DGRAM,0);if(sockfd_==-1){
cerr <<"socket error: "<< errno <<" : "<<strerror(errno)<< endl;exit(SOCKET_ERR);}
cout <<"socket success: "<<" : "<< sockfd_ << endl;//******绑定port,ip****//服务器要明确绑定port,不能随意改变,需要程序员显示指明structsockaddr_in local;bzero(&local,sizeof(local));//对结构体数据清0//填充结构体
local.sin_family=AF_INET;//大小端转换,给别人发消息,端口号和IP地址也要发给对方
local.sin_port=htons(port_);//端口号转网络//1.string->uint32_t 2.htonl()---->两个工作统一交给inet_addr
local.sin_addr.s_addr=inet_addr(ip_.c_str());//ip转网络// local.sin_addr.s_addr=htonl(INADDR_ANY);//任意地址绑定,服务器真正写法,上面与这个选一个就行int n=bind(sockfd_,(structsockaddr*)&local,sizeof(local));if(n==-1){
cerr <<"bind error: "<< errno <<" : "<<strerror(errno)<< endl;exit(BIND_ERR);}}voidstart(){char buffer[gnum];for(;;){//****读取数据****//服务器的本质就是一个死循环structsockaddr_in peer;//做输入输出型参数
socklen_t len=sizeof(peer);//必填//数据获取:IP地址+端口号+数据
ssize_t s=recvfrom(sockfd_,buffer,sizeof(buffer)-1,0,(structsockaddr*)&peer,&len);if(s>0){
buffer[s]=0;//1.网络->主机 2.uint32_t -> 点分十进制//string clientip=peer.sin_addr.s_addr;需要转两步,不方便
string clientip=inet_ntoa(peer.sin_addr);uint16_t clientport=ntohs(peer.sin_port);
string message=buffer;
cout<<clientip<<"["<<clientport<<"]#"<<message<<endl;//利用回调方法处理数据,实现解耦callback_(clientip,clientport,message);}}}~udpServer(){}private:uint16_t port_;//端口号
string ip_;//IP地址int sockfd_;//文件描述符
func_t callback_;//回调};}
2.3 绑定IP和port问题
- 一般情况下,服务器不会绑定某一个确定的IP,避免因绑定一个确定的 IP而漏掉另一个ip发过来的数据,实际情况是,将ip设置为全0,任何提交到服务器的开放端口的数据都会被该服务器处理
- 服务器不需要绑定一个固定IP(目的ip有多个,可能是本地环回、内网IP、公网IP),只要是访问服务器上的某个开放的端口,都会把数据拿过来处理,但这并不意味着客户端不需要绑定IP
- 这个IP是目的IP,是已经收到了数据向上交付的时候,不需要绑定IP,只需要看端口号就行了
本地环回:客户端与服务端在一台主机上,进行通信的时候数据贯穿协议栈流动,但不会到达物理层(出不去),仅测试
内网IP:真正属于这个服务器的IP,同一个品牌的服务器可以用内网ip通信
公网IP:云服务器是虚拟的,不能直接bind公网IP
虚拟机或者真正的linux可以绑定
内网IP可以被绑定
2.4 源文件udpServer.cc
服务器端进行测试的时候不需要提供IP地址
#include"udpServer.hpp"#include<memory>usingnamespace std;usingnamespace Server;staticvoidUsage(string proc){
cout <<"\nUsage:\n\t"<< proc <<" local_port\n\n";}voidhandlerMessage(string,uint16_t,string){//对message进行处理,完成server通信与业务逻辑解耦}intmain(int argc,char* argv[]){if(argc !=2){Usage(argv[0]);exit(USAGE_ERR);}uint16_t port=atoi(argv[1]);
std::unique_ptr<udpServer>usvr(newudpServer(handlerMessage,port));
usvr->initServer();
usvr->start();return0;}
三、客户端实现
3.1 接口认识
3.1.1 数据发送:sendto
#include<sys/types.h>#include<sys/socket.h>
ssize_t sendto(int sockfd,constvoid*buf, size_t len,int flags,conststructsockaddr*dest_addr, socklen_t addrlen);
sockfd:发送给哪个套接字,
buf:要发送的消息;len:消息的字节数;flags:0,有数据就发,没数据就阻塞
dest_addr:这个结构体也是由structsockaddr_in强转,指明向谁发,填充服务器的IP和端口
addrlen:输入型参数
3.2 头文件udpClient.hpp
在这个文件中:构造函数负责初始化变量
initClient函数负责创建套接字但是不需要程序员手动绑定端口号,OS会帮我们绑
服务端明确绑定是因为需要客户端知道,并且不能随便改变;未来是多个客户端访问一个服务端,客户端端口号是多少不重要,保证唯一性就行
run()函数负责发送数据到服务端,需要用到sendto函数,而sendto函数在首次发送数据的时候,OS发现客户端还未绑定端口号,会令sendto函数自动绑定一个端口号
#pragmaonce#include<iostream>#include<string>#include<sys/types.h>#include<sys/socket.h>#include<cerrno>#include<cstring>#include<cstdlib>#include<unistd.h>#include<arpa/inet.h>#include<netinet/in.h>namespace Client
{usingnamespace std;// //默认的点分十进制IP// static const string defaultIP="0.0.0.0";// static const int gnum=1024;enum{USAGE_ERR=1,SOCKET_ERR,BIND_ERR};//退出码classudpClient{public:udpClient(const string& serverip,constuint16_t&serverport):serverip_(serverip),serverport_(serverport),sockfd_(-1),quit_(false){}voidinitClient(){//****1.创建UDP网络通信端口****
sockfd_=socket(AF_INET,SOCK_DGRAM,0);if(sockfd_==-1){
cerr <<"socket error: "<< errno <<" : "<<strerror(errno)<< endl;exit(SOCKET_ERR);}
cout <<"socket success: "<<" : "<< sockfd_ << endl;//****Client必须要bind,但是Client不需要显示bind(不需要自己写)******///客户端端口是多少不重要,只要能保证唯一性就行,让OS自己去绑}voidrun(){/*********发送数据***********/structsockaddr_in server;memset(&server,0,sizeof(server));
server.sin_family=AF_INET;//主机序列转网络序列
server.sin_addr.s_addr=inet_addr(serverip_.c_str());
server.sin_port=htons(serverport_);
string message;while(!quit_){
cout<<"Please Enter# "<<endl;
cin>>message;//首次向服务器发送数据的时候,OS识别到还未绑定,sendto自动绑定ip+portsendto(sockfd_,message.c_str(),message.size(),0,(structsockaddr*)&server,sizeof(server));}}~udpClient(){}private:int sockfd_;
string serverip_;uint16_t serverport_;bool quit_;};}
3.3 源文件udpClient.cc
客户端进行测试的时候必须要提供IP地址和端口号
#include"udpClient.hpp"#include<memory>usingnamespace std;usingnamespace Client;staticvoidUsage(string proc){
cout <<"\nUsage:\n\t"<<proc <<" server_ip server_port\n\n";}//udpClient server_ip server_portintmain(int argc,char* argv[]){if(argc!=3){Usage(argv[0]);exit(USAGE_ERR);}//客户端必须要知道发给谁
string serverip=argv[1];uint16_t serverport=atoi(argv[2]);
unique_ptr<udpClient>ucli(newudpClient(serverip,serverport));
ucli->initClient();
ucli->run();return0;}
四、结果展示
**无论是客户端还是服务端的测试文件,所要传递的参数(
ip、port
)与头文件中
struct sockaddr_in
需要填充的ip、port没有关系**
这篇博客只是讲解一下UDP下网络通信的逻辑,关于它的应用方面会在近期推出,敬请关注!!!
版权归原作者 阿浩啊z 所有, 如有侵权,请联系我们删除。