一、开发环境
本次实验是在腾讯云服务器上进行
二、服务端实现
做完这次实验,感受最深的就是函数接口方面的问题,我们先来介绍一下需要用到的接口。
2.1 接口认识
2.1.1 socket创建网络通信套接字
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(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>
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
参数sockfd:文件描述符,也就是调用socket的返回值
参数addr:利用struct sockaddr_in强转
参数addrlen:结构体的大小
返回值:成功返回0,失败返回-1
2.1.3 sockaddr_in结构体
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);//协议家族,对应AF_INET
in_port_t sin_port; //端口号,in_port_t是对port的重命名
struct in_addr sin_addr;//IP地址,in_addr结构体里封装了一个32位整数
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) -
sizeof (struct in_addr)];
};
#define __SOCKADDR_COMMON(sa_prefix) \
sa_family_t sa_prefix##family
//sa_family_t //16位整数
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
宏中的**##**是将两个字符串合并成一个新字符串,也就是将接收到的sa_prefix与family合并起来,形成了sa_prefix_family
创建结构体后要先清空数据
#include <strings.h>
void bzero(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(const char *cp);
//const char*cp:点分十进制风格的IP地址
//1.网络->主机 2.uint32_t -> 点分十进制
char *inet_ntoa(struct in_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,struct sockaddr *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这个结构体中
#pragma once
#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
{
using namespace std;
//默认的点分十进制IP
static const string defaultIP="0.0.0.0";
static const int gnum=1024;//缓冲区大小
enum{USAGE_ERR=1,SOCKET_ERR,BIND_ERR};//退出码
typedef function<void(string,uint16_t,string)> func_t;
class udpServer
{
public:
udpServer(const func_t& callback,const uint16_t &port,const string& ip=defaultIP)
:callback_(callback),port_(port),ip_(ip),sockfd_(-1)
{}
void initServer()
{
//****创建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,不能随意改变,需要程序员显示指明
struct sockaddr_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_,(struct sockaddr*)&local,sizeof(local));
if(n==-1)
{
cerr << "bind error: " << errno << " : " << strerror(errno) << endl;
exit(BIND_ERR);
}
}
void start()
{
char buffer[gnum];
for(;;)
{
//****读取数据****
//服务器的本质就是一个死循环
struct sockaddr_in peer;//做输入输出型参数
socklen_t len=sizeof(peer);//必填
//数据获取:IP地址+端口号+数据
ssize_t s= recvfrom(sockfd_,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&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>
using namespace std;
using namespace Server;
static void Usage(string proc)
{
cout << "\nUsage:\n\t" << proc << " local_port\n\n";
}
void handlerMessage(string,uint16_t,string)
{
//对message进行处理,完成server通信与业务逻辑解耦
}
int main(int argc,char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port=atoi(argv[1]);
std::unique_ptr<udpServer> usvr(new udpServer(handlerMessage,port));
usvr->initServer();
usvr->start();
return 0;
}
三、客户端实现
3.1 接口认识
3.1.1 数据发送:sendto
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
sockfd:发送给哪个套接字,
buf:要发送的消息;len:消息的字节数;flags:0,有数据就发,没数据就阻塞
dest_addr:这个结构体也是由struct sockaddr_in强转,指明向谁发,填充服务器的IP和端口
addrlen:输入型参数
3.2 头文件udpClient.hpp
在这个文件中:构造函数负责初始化变量
initClient函数负责创建套接字但是不需要程序员手动绑定端口号,OS会帮我们绑
服务端明确绑定是因为需要客户端知道,并且不能随便改变;未来是多个客户端访问一个服务端,客户端端口号是多少不重要,保证唯一性就行
run()函数负责发送数据到服务端,需要用到sendto函数,而sendto函数在首次发送数据的时候,OS发现客户端还未绑定端口号,会令sendto函数自动绑定一个端口号
#pragma once
#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
{
using namespace std;
// //默认的点分十进制IP
// static const string defaultIP="0.0.0.0";
// static const int gnum=1024;
enum{USAGE_ERR=1,SOCKET_ERR,BIND_ERR};//退出码
class udpClient
{
public:
udpClient(const string& serverip,const uint16_t&serverport)
:serverip_(serverip),serverport_(serverport),sockfd_(-1),quit_(false)
{}
void initClient()
{
//****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自己去绑
}
void run()
{
/*********发送数据***********/
struct sockaddr_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+port
sendto(sockfd_,message.c_str(),message.size(),0,(struct sockaddr*)&server,sizeof(server));
}
}
~udpClient()
{}
private:
int sockfd_;
string serverip_;
uint16_t serverport_;
bool quit_;
};
}
3.3 源文件udpClient.cc
客户端进行测试的时候必须要提供IP地址和端口号
#include "udpClient.hpp"
#include <memory>
using namespace std;
using namespace Client;
static void Usage(string proc)
{
cout << "\nUsage:\n\t" <<proc << " server_ip server_port\n\n";
}
//udpClient server_ip server_port
int main(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(new udpClient(serverip,serverport));
ucli->initClient();
ucli->run();
return 0;
}
四、结果展示
无论是客户端还是服务端的测试文件,所要传递的参数(ip、port
)与头文件中struct sockaddr_in
需要填充的ip、port没有关系文章来源:https://www.toymoban.com/news/detail-438614.html
这篇博客只是讲解一下UDP下网络通信的逻辑,关于它的应用方面会在近期推出,敬请关注!!!文章来源地址https://www.toymoban.com/news/detail-438614.html
到了这里,关于linux【网络编程】之UDP网络程序模拟实现的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!