在实现这个实战前,先复习几个函数
inet_addr
inet_addr
是一个在 C 语言标准库 <arpa/inet.h>
中定义的函数,用于将一个以点分十进制字符串表示的 IPv4 地址(例如 “192.168.1.1”)转换为网络字节序的 in_addr_t
类型的整数值
参数说明:
-
cp
:指向一个以点分十进制表示的 IPv4 地址字符串的指针。
返回值:
-
如果转换成功,函数返回一个网络字节序的
in_addr_t
类型整数值,这可以存储在一个struct in_addr
结构体中。 -
如果输入的字符串不是一个有效的 IPv4 地址,函数返回
INADDR_NONE
(通常定义为in_addr_t
类型的-1
)。
inet_ntoa
inet_ntoa
是一个在 C 语言标准库 <arpa/inet.h>
中定义的函数,用于将网络字节序的 in_addr
结构体表示的 IPv4 地址转换成点分十进制字符串形式。
参数说明:
-
in
:一个struct in_addr
结构体,其中包含了网络字节序的 IPv4 地址。
返回值:
-
函数返回一个指向点分十进制字符串的指针,这个字符串表示了输入的 IPv4 地址。
关于inet_ntoa
Inet_ntoa这个函数返回了一个char* ,很显然这个函数自己在内部为我们申请了一块内存来保存ip的结果,那么是否需要调用者手动释放呢?
man手册上说,inet_ntoa函数,是把这个返回结果放到了静态存储区,这个时候不需要我们手动进行释放
那么问题来了,如果我们调用多次这个函数,会有什么样的效果呢?
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main() {
struct sockaddr_in addr1;
struct sockaddr_in addr2;
addr1.sin_addr.s_addr = 0;
addr2.sin_addr.s_addr = 0xffffffff;
char* ptr1 = inet_ntoa(addr1.sin_addr);
char* ptr2 = inet_ntoa(addr2.sin_addr);
printf("ptr1: %s, ptr2: %s\n", ptr1, ptr2);
return 0;
}
运行结果如下:
因为inet_ntoa 把结果放到自己内部的一个静态存储区,这样第二次调用时的结果会覆盖上一次的结果
思考:如果有多个线程调用 inet_ntoa 是否会出现异常情况呢?
在APUE中,明确提出inet_ntoa不是线程安全函数;
但是在centos7上测试,并没有出现问题,可能内部实现了互斥锁
在多线程环境下,推荐使用inet_ntop,这个函数由调用者提供一个缓冲区来保存结果,可以规避线程安全的问题
inet_ntop
inet_ntop
函数是用来将网络地址转换成字符串形式的。这是 POSIX 标准中定义的函数,用于替代较旧的 inet_ntoa
函数,因为它支持IPv6并且更加灵活
-
int af
: 地址族。例如,AF_INET
表示IPv4,AF_INET6
表示IPv6。 -
const void *src
: 指向包含原始网络地址的结构(如struct in_addr
或struct in6_addr
)的指针。 -
char *dst
: 指向目标缓冲区的指针,该缓冲区用于存储转换后的字符串形式的地址。 -
socklen_t size
: 目标缓冲区的大小。
所以我们InetAddr.hpp中就可以这样设计
UdpServer.hpp
我们这里要写一个服务端
我们首先肯定需要创建套接字,绑定套接字,我们用类来封装; 下面初始化了一些参数,其中UdpServer继承了nocopy类,这个主要目的是禁止拷贝构造和赋值拷贝,使用继承可以进行解耦合,并且使用继承可以复用模版,这样就不需要为每一个类写禁止拷贝构造和赋值拷贝了
class UdpServer : nocopy
{
private:
int _sockfd;
uint16_t _localport;
std::string _localip;
bool _isrunning;
public:
UdpServer(std::string loaclip, uint16_t port = gport)
:_localip(loaclip),
_localport(port),
_isrunning(false)
{
}
};
nocopy.hpp
#pragma once
#include <iostream>
class nocopy
{
public:
nocopy(){};
nocopy(const nocopy&) = delete;
const nocopy& operator=(const nocopy&) = delete;
~nocopy(){};
};
udp服务器初始化
创建套接字 和 将本地ip地址和端口号与套接字关联起来
void Init()
{
// 1.创建套接字
_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);//3
if (_sockfd < 0)
{
LOG(FATAL,"socket error\n");
exit(SOCKET_ERROR);
}
LOG(DEBUG,"socket create successs, _sockfd is %d\n",_sockfd);
//2.将文件和网络绑定起来
//将本地ip地址和端口号 与 _sockfd关联起来
//填写本地信息
struct sockaddr_in local;
memset(&local,0,sizeof(local));
//sin --> socker ip net
local.sin_family = AF_INET;
local.sin_port = htons(_localport);
local.sin_addr.s_addr = inet_addr(_localip.c_str());
int n = bind(_sockfd,(struct sockaddr*)&local,sizeof(local));
if(n < 0)
{
LOG(FATAL,"socket bind error\n");
exit(BIND_ERROR);
}
LOG(DEBUG,"socket bind success\n");
}
云服务器上我们不建议bind自己的IP,我们应该绑定能够接受任意ip的ip,那么服务器端我们应该把自己的ip绑定为0,这样服务器未来就能接收任意客户端发来的信息;
这样我们就需要修改代码,我们不需要ip了,我们只需要端口号就可以了
使用INADDR_ANY可以使服务器端,进行任意IP地址绑定
启动服务:这个服务需要一直挂着,不断进行从服务器收消息
我们是echo项目需要回显回去,所以我们收到消息后还要把消息发出去
其中这个peer是来自远方的结构体信息,存放谁发的地址信息,这样我们后续就可以通过peer再把信息发到对方
void Start()
{
_isrunning = true;
char inbuffer[1024];
while(_isrunning)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
//收消息
ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer)-1, 0,(struct sockaddr*)&peer,&len);
if(n > 0)
{
inbuffer[n] = 0;
std::string echo_string = "[Udp_server_echo]### ";
echo_string += inbuffer;
std::cout << "[client say => ]" << echo_string << std::endl;
sendto(_sockfd, echo_string.c_str(),echo_string.size(),0, (struct sockaddr*)&peer ,len);
}
else
{
std::cout << "recvfrom failed" << std::endl;
}
}
}
UdpServerMain
UdpClientMain
对于客户端
客户端一定要先访问服务器 所以客户端一定要知道服务器地址和端口号
所以我们需要输入客户端的服务器地址和端口号
client的端口号,一般不让用户自己设定,而是让client OS随机选择
client 需要 bind它自己的IP和端口, 但是client 不需要 “显示” bind它自己的IP和端口,
client 在首次向服务器发送数据的时候,OS会自动给client bind它自己的IP和端口
#include <iostream>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
// struct sockaddr头文件
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
using namespace log_ns;
// 客户端一定要先访问服务器 所以客户端一定要知道服务器地址和端口号
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cerr << "Usage: " << argv[0] << " server-ip server-port" << std::endl;
exit(0);
}
EnableScreen();
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
LOG(FATAL, "socket create fail \n");
exit(0);
}
LOG(DEBUG, "socket creat success , socketfd is %d \n", sockfd);
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);
while (true)
{
std::string line;
std::cout << "Please Enter# ";
std::getline(std::cin, line);
//std::cout << line << std::endl;
// 发消息
int n = sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr *)&server, sizeof(server));
if (n > 0)
{
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
char buffer[1024];
int m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&temp, &len);
if (m > 0)
{
buffer[m] = 0;
std::cout << buffer << std::endl;
}
else
{
std::cout << "recvfrom error " << std::endl;
}
}
else
{
std::cout << "sendto error" << std::endl;
}
}
::close(sockfd);
return 0;
}
测试及改进
我们之前说客户端不需要显示绑定自己的ip,我们来验证一下
正如我们之前所说,客户端可以自动绑定自己的网络地址信息,并且客户端的端口号是随机的
这样的代码是没有问题但是不够优雅,未来这个模块我们可能会经常用到(将收到的地址信息转化为本地端口号和本地地址),所以我们重新设计一个类封装这个模块(网络地址的类)
InetAddr
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
class InetAddr
{
private:
uint16_t _port;
std::string _ip;
struct sockaddr_in _addr;
public:
InetAddr(struct sockaddr_in &addr)
:_addr(addr)
{
ToHost(addr);
}
~InetAddr()
{
}
uint16_t Port()
{
return _port;
}
std::string Ip()
{
return _ip;
}
private:
void ToHost(const struct sockaddr_in &addr)
{
_port = ntohs(addr.sin_port);
_ip = inet_ntoa(addr.sin_addr);
}
};