1.UDP --socket编程基础
1.理解源IP地址和⽬的IP地址
数据传输到主机不是⽬的,⽽是⼿段。到达主机内部,在交给主机内的进程,才是⽬的。
- 源ip地址相当于是起始点,目的ip地址则是终点!!!!!!二者一旦建立就不会改变
- 红框中显示的就是ip地址
2.理解端口号
socket=IP+ port
所以,⽹络通信的本质,也是进程间通信,本质是两个互联⽹进程代表⼈来进⾏通信,{srcIp,srcPort,dstIp,dstPort} 这样的4元组就能标识互联⽹中唯⼆的两个进程
3.网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有⼤端和⼩端之分,磁盘⽂件中的多字节数据相对于⽂件中的偏移地址也有⼤端⼩端之分,⽹络数据流同样有⼤端⼩端之分.那么如何定义⽹络数据流的地址呢?
、
4.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
结构:
- 这几种结构体都会作为上述函数的参数-----------------即*会被强制转换为const struct sockaddr address 类型------------这样就可以统一传送了,函数会自行区分你是网络通信还是本地通信
- 其实就是一种继承---------
使用子类(sockaddr_in,sockaddr_un)来初始化父类的指针(sockaddr)
服务端::1.socket函数--------创建终端
- 返回值是文件描述符!!!一定》0,如果小于0则证明创建失败
- 网络的本质依旧是文件!!!!!!!因为返回的是文件描述符!!!!!
服务端::2.bind函数--------绑定ip和端口号!!!!(需要从外部得到ip和port
)
详细解释sockaddr_in
结构:
- 正好对应着
16位地址类型,16位端口号,32位IP地址!!!!
--------填充位不考虑!! - 所以在网络通信中的ip地址一共就四个字节!!!!
3.服务端::recvfrom函数--------接受消息
4.服务端::sendto函数--------发送消息
5.ip地址介绍
1.本地环回!!!!!!
2.内网ip
2.v1版本的 echo server
UdpServer.hpp:
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
using namespace LogModule;
using func_t = std::function<std::string(const std::string&)>;//函数指针
const int defaultfd = -1;
// 你是为了进行网络通信的!
class UdpServer
{
public:
UdpServer(uint16_t port, func_t func)//端口号是16位无符号整数
: _sockfd(defaultfd),//socket函数返回的文件描述符
// _ip(ip), //不在需要提供固定ip
_port(port),//端口号
_isrunning(false),//运行状态
_func(func)
{
}
void Init()
{
// 1. 创建套接字,相当于打开了一个网络文件
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);//AF_INET-----网络地址类型,SOCK_DGRAM--------
if (_sockfd < 0)//创建UDP套接字失败
{
LOG(LogLevel::FATAL) << "socket error!";
exit(1);
}
LOG(LogLevel::INFO) << "socket success, sockfd : " << _sockfd;//日志打印
// 2. 绑定socket信息,ip和端口, ip(比较特殊,后续解释)
// 2.1 填充sockaddr_in结构体
struct sockaddr_in local;//结构体,包含16位地址类型(sin_family),16位端口号(sin_port),32位ip地址(sin_addr.s_addr,注意,sin_addr是结构体,里面的s_drr才是ip地址)
bzero(&local, sizeof(local));//清空local
local.sin_family = AF_INET;// 1.地址类型
// 我会不会把我的IP地址和端口号发送给对方?
// IP信息和端口信息,一定要发送到网络!
// 本地格式->网络序列
local.sin_port = htons(_port); //2.端口号,我们自己是使用整数,需要转换成网络序列,需要使用htons函数
// IP也是如此,1. IP转成4字节 -------> 2. 4字节转成网络序列 -> in_addr_t inet_addr(const char *cp);
//local.sin_addr.s_addr = inet_addr(_ip.c_str()); // TODO
local.sin_addr.s_addr = INADDR_ANY;//要求给出的ip地址定义为INADDR_ANY宏-----即0x000000,千万不要绑定特定的ip!!!!!!!!!!设置为0,表示可以接收任何ip地址的信息
// 那么为什么服务器端要显式的bind呢?IP和端口必须是众所周知且不能轻易改变的!
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));//链接端口号和ip地址,记得local要强制类型转换
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind error";
exit(2);
}
LOG(LogLevel::INFO) << "bind success, sockfd : " << _sockfd;//成功打印日志
}
void Start()
{
_isrunning = true;
while (_isrunning)
{
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 1. 收消息, client为什么要个服务器发送消息啊?不就是让服务端处理数据。
ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);//这里的peer是客户端的套接字,客户端发来内容后客户端信息被捕捉到peer中
//第一个参数是文件描述符,后两个是缓冲区和大小,0表示i/o,最后两个表示客户端的套接字
if (s > 0)
{
//网络序列翻转数据!!!!
int peer_port = ntohs(peer.sin_port); // 从网络中拿到的!网络序列翻转
std::string peer_ip = inet_ntoa(peer.sin_addr); //4字节网络风格的IP -> 点分十进制的字符串风格的IP
buffer[s] = 0;
std::string result = _func(buffer);
LOG(LogLevel::DEBUG) << "[" << peer_ip << ":" << peer_port<< "]# " << buffer; // 1. 消息内容 2. 谁发的??
// 2. 发消息
// std::string echo_string = "server echo@ ";
// echo_string += buffer;
sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr*)&peer, len);
}
}
}
~UdpServer()
{
}
private:
int _sockfd;
uint16_t _port;
// std::string _ip; // 用的是字符串风格,点分十进制, "192.168.1.1" !!!不需要传ip,因为已经设置为0,即任何ip的信息都可以接收
bool _isrunning;
func_t _func; // 服务器的回调函数,用来进行对数据进行处理
};
UdpServer.cc:
#include <iostream>
#include <memory>
#include "UdpServer.hpp"
// 仅仅是用来进行测试的
std::string defaulthandler(const std::string &message)
{
std::string hello = "hello, ";
hello += message;
return hello;
}
// 翻译系统,字符串当成英文单词
// ./udpserver port --------不需要提供服务端ip!!!!!
int main(int argc, char *argv[])
{
if(argc != 2)
{
std::cerr << "Usage: " << argv[0] << " port" << std::endl;
return 1;
}
// std::string ip = argv[1];
uint16_t port = std::stoi(argv[1]);
Enable_Console_Log_Strategy();//启动日志
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, defaulthandler);
usvr->Init();
usvr->Start();
return 0;
}
UdpClient.cc:
#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
// ./udpclient server_ip server_port
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
return 1;
}
std::string server_ip = argv[1];//从命令行中得到服务端的ip
uint16_t server_port = std::stoi(argv[2]);//从命令行中得到服务端的端口号!!!!
// 1. 创建socket本质就是一个管道!!!!!!!!!!
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
std::cerr << "socket error" << std::endl;
return 2;
}
// 2. 本地的ip和端口是什么?要不要和上面的“文件”关联呢?
// 问题:client要不要bind?需要bind.
// client要不要显式的bind?不要!!首次发送消息,OS会自动给client进行bind,OS知道IP,端口号采用随机端口号的方式
// 为什么?一个端口号,只能被一个进程bind,为了避免client端口冲突
// client端的端口号是几,不重要,只要是唯一的就行!
// 填写服务器信息
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
while(true)
{
std::string input;
std::cout << "Please Enter# ";
std::getline(std::cin, input);
int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr*)&server, sizeof(server));//服务端的ip和端口号从命令行中获得并存储!!!!
(void)n;
char buffer[1024];//一个客户端可能访问多个服务端
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int m = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);//从服务端接收信息并写入客户端!!!!!这里没有对peer服务端做处理,只用作占位!!
if(m > 0)
{
buffer[m] = 0;
std::cout << buffer << std::endl;
}
}
return 0;
}
Log.hpp:
#ifndef __LOG_HPP__
#define __LOG_HPP__
// 头文件保护宏,防止重复包含
// 标准库头文件包含
#include <iostream> // 标准输入输出流
#include <cstdio> // C标准IO
#include <string> // 字符串类
#include <filesystem> // 文件系统操作(C++17)
#include <sstream> // 字符串流
#include <fstream> // 文件流
#include <memory> // 智能指针
#include <ctime> // 时间处理
#include <unistd.h> // POSIX API(获取进程ID)
#include <mutex> // 标准库互斥锁//这次不使用pthread.h库
namespace LogModule {
// 全局行分隔符(Windows风格)
const std::string gsep = "\r\n";
/* 日志策略基类(抽象接口) */
class LogStrategy {
public:
virtual ~LogStrategy() = default; // 虚析构函数
// 纯虚函数:同步日志消息,下文需要重写
virtual void SyncLog(const std::string &message) = 0;
};
/* 控制台日志策略(具体实现) */
class ConsoleLogStrategy : public LogStrategy {
public:
ConsoleLogStrategy() = default; // 默认构造函数
// 实现基类的日志同步接口
void SyncLog(const std::string &message) override {
// 使用lock_guard自动加锁/解锁
std::lock_guard<std::mutex> lock(_mutex);
// 输出到标准输出,直接打印到显示器上
std::cout << message << gsep;
}
~ConsoleLogStrategy() = default; // 默认析构函数
private:
std::mutex _mutex; // 互斥锁保证线程安全
};
/* 文件日志策略(具体实现) */
const std::string defaultpath = "./log"; // 默认日志目录
const std::string defaultfile = "my.log"; // 默认日志文件名
class FileLogStrategy : public LogStrategy {
public:
// 构造函数,可自定义路径和文件名
FileLogStrategy(const std::string &path = defaultpath,
const std::string &file = defaultfile)
: _path(path), _file(file) {
std::lock_guard<std::mutex> lock(_mutex);//如果使用pthread库的pthread_mutex_lock(&_mutex),那么记得初始化pthread_mutex_t _mutex = PTHREAD_MUTEX_INITIALIZER;
// 检查目录是否存在 //,还有记得要加unlock解锁,
if (std::filesystem::exists(_path)) { //判断路径是否存在,因为文件可以自动新建,但是路径不行
return;
}
try {
// 递归创建目录
std::filesystem::create_directories(_path); //创建对应的路径
} catch (const std::filesystem::filesystem_error &e) {
// 异常处理
std::cerr << e.what() << '\n';
}
}
// 实现基类的日志同步接口
void SyncLog(const std::string &message) override {
std::lock_guard<std::mutex> lock(_mutex);
// 构造完整文件路径,这样才能打印到文件中!!!!!!!!
std::string filename = _path +
(_path.back() == '/' ? "" : "/") + _file;
// 以追加模式打开文件
std::ofstream out(filename, std::ios::app); //!!!!!!!记得使用追加!!!
if (!out.is_open()) {
return; // 文件打开失败则返回
}
// 写入日志内容
out << message << gsep;
out.close(); // 关闭文件
}
~FileLogStrategy() = default; // 默认析构函数
private:
std::string _path; // 日志存储路径
std::string _file; // 日志文件名
std::mutex _mutex; // 互斥锁保证线程安全
};
/* 日志等级枚举 */
enum class LogLevel {
DEBUG, // 调试信息
INFO, // 普通信息
WARNING, // 警告信息
ERROR, // 错误信息
FATAL // 致命错误
};
/* 日志等级枚举转字符串 */
std::string Level2Str(LogLevel level) {
switch (level) {
case LogLevel::DEBUG: return "DEBUG";
case LogLevel::INFO: return "INFO";
case LogLevel::WARNING: return "WARNING";
case LogLevel::ERROR: return "ERROR";
case LogLevel::FATAL: return "FATAL";
default: return "UNKNOWN";
}
}
/* 获取当前时间戳 */
std::string GetTimeStamp() {
time_t curr = time(nullptr); // 获取当前时间
struct tm curr_tm;
localtime_r(&curr, &curr_tm); // 转换为本地时间
char timebuffer[128];
// 格式化时间为字符串
snprintf(timebuffer, sizeof(timebuffer),
"%4d-%02d-%02d %02d:%02d:%02d",
curr_tm.tm_year+1900, // 年(从1900开始)
curr_tm.tm_mon+1, // 月(0-11)
curr_tm.tm_mday, // 日
curr_tm.tm_hour, // 时
curr_tm.tm_min, // 分
curr_tm.tm_sec); // 秒
return timebuffer;
}
/* 日志器主类 */
class Logger {
public:
Logger() {
// 默认使用控制台输出策略
EnableConsoleLogStrategy();
}
// 启用文件日志策略
void EnableFileLogStrategy() {
std::lock_guard<std::mutex> lock(_mutex);
// 创建文件策略实例
_fflush_strategy = std::make_unique<FileLogStrategy>(); //类型转换
}
// 启用控制台日志策略
void EnableConsoleLogStrategy() {
std::lock_guard<std::mutex> lock(_mutex);
// 创建控制台策略实例
_fflush_strategy = std::make_unique<ConsoleLogStrategy>(); //一样的类型转换
}
/* 日志消息类(RAII实现) */
class LogMessage {
public:
// 构造函数,收集日志元信息--前缀建立
LogMessage(LogLevel level, const std::string &src_name,
int line_number, Logger &logger)
: _curr_time(GetTimeStamp()), // 当前时间
_level(level), // 日志等级
_pid(getpid()), // 进程ID
_src_name(src_name), // 源文件名
_line_number(line_number), // 行号
_logger(logger) { // 所属日志器
// 构造日志前缀信息
std::stringstream ss;
ss << "[" << _curr_time << "] "
<< "[" << Level2Str(_level) << "] "
<< "[" << _pid << "] "
<< "[" << _src_name << "] "
<< "[" << _line_number << "] "
<< "- ";
_loginfo = ss.str(); // 保存前缀
}
// 重载<<操作符,支持链式调用
template <typename T>
LogMessage &operator<<(const T &info) {
std::stringstream ss;
ss << info;
_loginfo += ss.str(); // 追加日志内容
return *this;
}
// 析构函数,实际执行日志写入
~LogMessage() {
if (_logger._fflush_strategy) {
// 调用策略写入日志
_logger._fflush_strategy->SyncLog(_loginfo);//message析构之后会写入
}
}
private:
std::string _curr_time; // 日志时间
LogLevel _level; // 日志等级
pid_t _pid; // 进程ID
std::string _src_name; // 源文件名
int _line_number; // 行号
std::string _loginfo; // 完整日志信息
Logger &_logger; // 所属日志器引用
};
// 重载()操作符创建日志消息
LogMessage operator()(LogLevel level, const std::string &name, int line) {
return LogMessage(level, name, line, *this);
}
~Logger() = default; // 默认析构函数
private:
std::unique_ptr<LogStrategy> _fflush_strategy; // 当前日志策略
std::mutex _mutex; // 保护策略切换的互斥锁
};
// 全局日志器实例
Logger logger;
/* 用户接口宏 */
#define LOG(level) logger(level, __FILE__, __LINE__) // 创建日志条目,获取当前路径和行号!!!!!
#define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy() // 切换控制台输出
#define Enable_File_Log_Strategy() logger.EnableFileLogStrategy() // 切换文件输出
}
#endif // __LOG_HPP__
Makefile:
.PHONY:all
all:udpclient udpserver
udpclient:UdpClient.cc
g++ -o $@ $^ -std=c++17 -static
udpserver:UdpServer.cc
g++ -o $@ $^ -std=c++17
.PHONY:clean
clean:
rm -f udpclient udpserver
运行示例:
我们以vscode远程登录为服务端,xshell端为客户端:
服务端:
- 可以看到,客户端输入了
what can i say man!
接下来看看客户端:
- 因为给服务端设置过ip(任意,只要端口号是8080就能读取),所以ip地址是多少无所谓!!!
具体步骤:
1.每一个进程都要有自己的socket,这个是必须的,相当于把这个文件添加进内核,才能进行网络通信!!!!
-
- 每一个进程都要有自己的structaddr结构体,存储自己的IP地址和ip地址类型,和struct file 一致
---------------
建立之后要把该结构体和socket的fd描述符使用bind函数绑定!!!!(silent会自动绑定,所以不需要显示建立structaddr,例如,但服务端server需要自己绑定和建立!!!!!!
)-
- 上述两个进程还都建立了额外的structaddr结构体–peer—用来存储发送内容到当前进程的其他进程的structaddr,
这样才能使用sendto函数把内容写回其他进程!!!!
- 上述两个进程还都建立了额外的structaddr结构体–peer—用来存储发送内容到当前进程的其他进程的structaddr,
-
- 例如客户端初始化时给了服务端的ip和端口,这样才能使用sendto函数,因为当前进程只知道自己的ip和端口,在命令行给出服务端的ip和端口才能输出!!!