前文:Socket套接字编程
UDP协议特点
- 无连接:UDP在发送数据之前不需要建立连接,减少了开销和发送数据之前的时延。
- 尽最大努力交付:UDP不保证可靠交付,主机不需要维持复杂的连接状态表。
- 面向报文:UDP对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。
- 首部开销小:UDP的首部只有8个字节,比TCP的20个字节要短。
- 支持多种交互通信:UDP支持一对一、一对多、多对一和多对多的交互通信。
大体思路
将通过客户端与服务端的创建,实现客户端能够与服务端进行通信;
利用Socket编程接口,实现双端互通;
UdpServer.hpp
包含一个udp服务端的类,通过一些成员函数,能够实现对应创建类对象,进行初始化和启动对应服务端;
对于服务器来说,只需要创建对应的端口号,IP地址这里为本地地址(已经确定)。
代码
#pragma once
#include<iostream>
using namespace std;
#include"Log.hpp"
#include<string>
#include<cerrno>
#include<cstring>
#include<cstdlib>
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include"InetAddr.hpp"
//描述错误码
enum
{
SOCKET_ERROR =1,
BIND_ERROR,
USAGE_ERROR
};
const static int defaultfd = -1;//默认文件描述符为无
class UdpServer
{
public:
UdpServer(uint16_t port):
_sockfd(defaultfd),
_port(port),
_isrunning(false)
{}
void InitServer()
{
//1.创建udp socket 套接字
_sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(_sockfd<0)
{
LOG(FATAL,"socket error, %s, %d\n",strerror(errno),errno);
exit(SOCKET_ERROR);
}
LOG(INFO,"socket create success,sockfd: %d\n",_sockfd);
//2 先填充sockadd_in结构
struct sockaddr_in local;
bzero(&local,sizeof(local));
local.sin_family=AF_INET;
local.sin_port= htons(_port);//htons:将主机字节序转换为网络字节序
//local.sin_addr.s_addr = inet_addr(_ip.c_str());
local.sin_addr.s_addr=INADDR_ANY;
//INADDR_ANY告诉套接字监听来自任何IP地址的连接。这对于服务器来说非常有用,因为服务器通常需要能够接受来自客户端网络上任何位置的连接请求。
//再通过函数bind将套接字和网络信息连接起来
int n= bind(_sockfd,(struct sockaddr* )&local,sizeof(local));
if(n<0)
{
LOG(FATAL,"bind error,%s, %d\n",strerror(errno),errno);
exit(BIND_ERROR);
}
LOG(INFO,"socket bind success\n");
}
//通过循环不断接收客户端的数据,打印出对应的内容
void Start()
{
_isrunning = true;
while(true)
{
char buffer[1024];
struct sockaddr_in peer;
socklen_t len= sizeof(peer);
ssize_t n=recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr* )&peer,&len);
if(n>0)
{
buffer[n] = 0;
InetAddr addr(peer);//创建一个网络地址的类对象
LOG(DEBUG, "get message from [%s:%d]: %s\n", addr.Ip().c_str(),addr.Port(),buffer);
// 2. 我们要将server收到的数据,发回给对方,表示已接收
sendto(_sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&peer, len);
}
}
_isrunning = false;
}
private:
int _sockfd; //sock文件描述符
uint16_t _port; //端口号
bool _isrunning; //服务器是否启动
};
解释
利用socket函数创建socket编程接口,AF_INET表示IPv4,SOCK_DGRAM表示udp数据报套接字的;
INADDR_ANY
: 调用bind()
函数将套接字与一个特定的端口(以及可选的IP地址)绑定时,可以将IP地址设置为INADDR_ANY
(其值通常为0,表示“任何地址”)。这样做的好处是,UDP服务器将能够接收来自任何网络接口的连接请求,而不仅仅是绑定时指定的那一个。
这样bind之后就能让IP地址与端口号和Socket编程接口联系起来,让接口 “有名有性” ;
将接收的数据存储在buffer中,peer可以接收到发送端的地址信息;
只有n大于0,才表示接收到了具体数据;
将接收到的数据信息,可以经过一定的处理,然后发送回客户端,这里中间没有经过处理,只是将接收到的信息直接反馈给客户端而已;也就是将buffer中的内容发送到客户端中;
InetAddr.hpp
InetAddr类:将包含主机上一切有关地址的信息;
代码
#pragma once
#include<iostream>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
using namespace std;
class InetAddr
{
public:
InetAddr(const struct sockaddr_in& addr):
_addr(addr)
{
GetAddress(&_ip,&_port);
}
string Ip()
{
return _ip;
}
uint16_t Port()
{
return _port;
}
~InetAddr()
{
}
private:
//通过已知的sockaddr信息获取对应的ip地址信息和端口号
void GetAddress(string* ip,uint16_t* port)
{
*port=ntohs(_addr.sin_port);
*ip=inet_ntoa(_addr.sin_addr);
}
struct sockaddr_in _addr; //sockaddr的信息
string _ip; //IP地址
uint16_t _port; //端口号
};
main.cc
初始化服务端并启动服务端
#include<iostream>
#include<memory>
using namespace std;
#include"UdpServer.hpp"
void Usage(string proc)
{
cout<<"Usage:\n\t"<<proc<<" local_port\n"<<endl;
}
//示例: ./main.cc 8888
int main(int argc,char* argv[])
{
if(argc!=2)
{
Usage(argv[0]);
exit(USAGE_ERROR);
}
EnableScreen();//打印到屏幕
uint16_t port = stoi(argv[1]); //获取对应的端口号
unique_ptr<UdpServer> usvr=std::make_unique<UdpServer>(port);//智能指针
usvr->InitServer(); // 初始化服务端
usvr->Start(); // 启动服务端
return 0;
}
通过智能指针的性质,创建一个指向UdpServer类对象的指针,能够在周期结束时自动回收对应的智能指针;
UdpClient.cc
作为客户端能够不断给服务端发送数据并接收对应数据,构建Socket编程接口与上面一致。
#include<iostream>
using namespace std;
#include<string>
#include<cstring>
#include<cstdlib>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
void Usage(string proc)
{
cout<<"Usage:\n\t"<<proc<< " serverip serverport\n"<<endl;
}
//示例: ./UdpClient.cc 127.0.0.1 8080
int main(int argc,char* argv[])
{
if(argc!=3)
{
Usage(argv[0]);
exit(1);
}
string serverip=argv[1];//服务端ip
uint16_t serverport=stoi(argv[2]);//服务端端口号
//创建socket
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd<0)
{
cerr<<"socket error"<<endl;
}
//构建目标主机socket信息
struct sockaddr_in server;
memset(&server,0,sizeof(server));
server.sin_family=AF_INET;
server.sin_port=htons(serverport);
server.sin_addr.s_addr=inet_addr(serverip.c_str());
string message;
while(true)
{
cout<<"Please Enter# ";
getline(cin,message);//获取输入的一行字符串,空格亦可接收,输入到message中
sendto(sockfd,message.c_str(),message.size(),0,(struct sockaddr *)&server,sizeof(server));//发送给服务端
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
char buffer[1024];//接收存放数据
ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);//接收服务端发送数据
if(n > 0)
{
buffer[n] = 0;
std::cout << "server echo# " << buffer << std::endl;
}
}
}
127.0.0.1
127.0.0.1 是一个特殊的IP地址,被称为回环地址(Loopback Address)。在IPv4网络中,它被用于指向本机(即运行代码的计算机)。当你尝试访问 127.0.0.1 上的某个端口时,你的操作系统会识别这是一个内部请求,并将该请求直接路由回本机上的相应服务,而不会通过网络发送到外部。
这种机制使得开发者能够在本地测试网络服务,而无需担心外部网络的影响。
验证
我们可以通过命令行netstat来进行查询对应网络接口:
netstat
(网络统计)命令是Linux系统中用于显示网络连接、路由表、接口统计、伪装连接和多播成员等网络相关信息的强大工具。