一:预备工作
我们先把编程当中需要用到且重要的函数给认识一下
创建socket文件描述符函数
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int socket(int domain, int type, int protocol);domain:协议家族,常用的有AF_INET (IPv4),AF_INET6(IPv6)。在编程当中我们使用IPv4协议。
type:套接字类型,常用的有SOCK_STREAM(面向连接的字节流,基于 TCP(可靠、按序、双向)) SOCK_DGRAM(无连接的数据报,基于 UDP(不可靠、有边界))
由于这节课是UDP编程,所以函数当中我们使用SOCK_DGRAM。
protocol(具体协议):表示套接字使用的具体协议,通常设置为0,表示根据domain和type自动选择函数
绑定端口号函数
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);sockfd:套接字描述符,由刚刚的socket函数返回。
addr:指向通用地址结构体的指针,实际需要传入具体协议族的地址结构
其核心作用是绑定ip和端口号。
addrlen:表示通用结构体的大小。
补充:通用结构struct sockaddr以及常用的地址结构体
通用结构体struct sockaddr
#include <sys/socket.h> struct sockaddr { sa_family_t sa_family; // 地址族 (Address Family) char sa_data[14]; // 地址数据 (可变长度,实际不足14字节用0填充) };IPv4地址结构
struct sockaddr_in { sa_family_t sin_family; // 地址族: AF_INET in_port_t sin_port; // 端口号 (网络字节序) struct in_addr sin_addr; // IP地址 (网络字节序) char sin_zero[8]; // 填充字段 }; struct in_addr { in_addr_t s_addr; // 32位IPv4地址 };Unix域地址(本地通信)
struct sockaddr_un { sa_family_t sun_family; // 地址族: AF_UNIX/AF_LOCAL char sun_path[108]; // 路径名 };在这里,我们看到,不同的地址结构体类型可以用一个通用结构体struct sockaddr类型来接收,是不是觉得比较熟悉?这里不就是C++当中继承与多态的特性吗,struct sockaddr就相当于是基类,而其他地址结构体都是struct sockaddr的子类,子类可以被父类的指针类型指向,也可以以父类类型作为函数参数接收子类的传值。
网络进程发消息函数
#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:用来传递目的网络进程的ip和端口号
addrlen:struct sockaddr通用结构体的大小
返回值:当发送失败时返回-1,发送成功时返回发送信息的字节数
网络进程收消息函数
#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和端口号
addrlen:struct sockaddr通用结构体的大小
返回值:当发送失败时返回-1,发送成功时返回接受信息的字节数
那么接下来就开始正式的编程了。
二:一个简单的聊天室制作
将网络地址进行封装
inet.hpp
#pragma once #include <iostream> #include<string> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> class InetAddr { public: InetAddr(struct sockaddr_in &addr) : _addr(addr) { _port = ntohs(_addr.sin_port); _ip = inet_ntoa(_addr.sin_addr); } bool operator==(const InetAddr &addr) { return addr._ip == _ip && addr._port == _port; } std::string StringAddr() { return _ip+std::to_string(_port); } std::string Ip() { return _ip; } uint16_t Port() { return _port; } const struct sockaddr_in& Addr() { return _addr; } private: struct sockaddr_in _addr; uint16_t _port; std::string _ip; };在这里有两个函数较为重要:
1.ntohs
#include <arpa/inet.h> uint16_t ntohs(uint16_t netshort);这里ntohs函数的作用是将端口号从网络字节序(大端),转换成主机字节序。
2.inet_ntoa
#include <arpa/inet.h> char *inet_ntoa(struct in_addr in);这里inet_ntoa的作用是将网络字节序的IPv4地址转换成点分十进制的字符串,方便打印输出
例如:0x7F000001→"127.0.0.1"
对用户进行管理
Route.hpp
在这里实现了对聊天室用户的增加,删除,查找。通过STL中vector进行管理
#pragma once #include <iostream> #include <vector> #include "Log.hpp" #include "Inet.hpp" using namespace LogModle; class Route { bool IsExist(InetAddr &peer) { for (auto &user : _inline_users) { if (user == peer) return true; } return false; } public: Route() { } void RegisterUser(InetAddr&peer) { LOG(LogLevel::DEBUG)<<"注册用户"<<peer.Ip()<<" "<<peer.Port(); _inline_users.emplace_back(peer); } void DeleteUser(InetAddr&peer) { std::vector<InetAddr>::iterator it; for(it=_inline_users.begin();it!=_inline_users.end();it++) { if(*it==peer) { _inline_users.erase(it); break; } } } void MessageRoute(int sockfd, const std::string &message, InetAddr &peer) { if(!IsExist(peer)) { RegisterUser(peer); } std::string send_message=peer.StringAddr()+" #"+message; for(auto& user:_inline_users) { sendto(sockfd,send_message.c_str(),send_message.length(),0,(const sockaddr*)&user,sizeof(user)); } if(message == "QUIT") { LOG(LogLevel::INFO)<<"删除了一个用户"; DeleteUser(peer); } } private: std::vector<InetAddr> _inline_users; };这个文件基本上没什么难度,都是正常增加,删除的逻辑,进行查找也是对vector进行遍历。
服务端窗口的实现
UdpServer.hpp
服务端的作用本质是
1.收到其中某一个用户的消息,
2.将消息发送到全部的在线用户(这步我们通过回调函数实现,因为要遍历在线用户,不在server当中编写这个函数)让外部传入这个函数,我们再执行。
其中func_t声明的函数就是用于转发或数据处理的回调函数。
在该文件中还有一个重点,那就是server端口的ip通过INADDR_ANY初始化,这样初始化的好处是client不论通过哪种ip(本地??公网??),server都可以接收到发来的消息,不用再在服务端手动传入。
#pragma once #include "Log.hpp" #include "Route.hpp" #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <string> #include <strings.h> #include <functional> using namespace LogModle; const static uint16_t defaultport = 8888; const static int defaultfd = -1; const static int defaultsize = 1024; using func_t = std::function<void(int sockfd, const std::string &message, InetAddr &peer)>; class UdpServer { public: UdpServer(func_t fun, uint16_t port = defaultport) : //_ip(ip), _fun(fun), _port(port), _sockfd(defaultfd) { } void Init() { _sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (_sockfd < 0) { LOG(LogLevel::FATAL) << "socket error"; exit(1); } LOG(LogLevel::INFO) << "socket success"; // 绑定,套接字和ip struct sockaddr_in local; bzero(&local, sizeof(local)); local.sin_port = htons(_port); local.sin_family = AF_INET; // 1.把点分十进制ip转成四字节ip 2.4字节转换成网络序列 local.sin_addr.s_addr = INADDR_ANY; // local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 给服务器配置端口号 int n = ::bind(_sockfd, (sockaddr *)&local, sizeof(local)); if (n != 0) { LOG(LogLevel::FATAL) << "bind error"; exit(2); } LOG(LogLevel::INFO)<<"sock success sockfd:"<<_sockfd; } void Start() { char buffer[defaultsize]; for (;;) { struct sockaddr_in peer; socklen_t len = sizeof(struct sockaddr_in); // 这里是要收消息 ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len); if (n > 0) { InetAddr addr(peer); buffer[n] = 0; // 对消息进行特定的处理 _fun(_sockfd, buffer, addr); } } } ~UdpServer() { } private: // std::string _ip;// 点分十进制的ip uint16_t _port; // 端口号 int _sockfd; // UDP套接字 func_t _fun; // 回调函数 };UdpServer.cc
注意:r->MessageRoute(sockfd,message,addr);就是我们当时在server没有实现的回调函数,他这里通过仿函数的形式来对服务端进行初始化。
stoi
其作用是将字符串转换为整数。
#include <string> int stoi(const std::string& str, size_t* pos = 0, int base = 10); int stoi(const std::wstring& str, size_t* pos = 0, int base = 10);#include "UdpServer.hpp" #include "Route.hpp" int main(int argc, char *argv[]) { ENABLE_CONSOLE_LOG_STRATERY; if (argc != 2) { std::cerr << "Usage: " << argv[0] << " port " << std::endl; return 1; } uint16_t port = std::stoi(argv[1]); // Route route; std::unique_ptr<Route> r = std::make_unique<Route>(); std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>([&r](int sockfd,const std::string& message,InetAddr&addr) { r->MessageRoute(sockfd,message,addr); },port); usvr->Init(); usvr->Start(); return 0; }用户端窗口实现
UdpClient.cc
注意,客户端不用显示地进行bind,让操作系统来分配端口号。
其中收消息和发消息通过线程来进行维护,后续我就将线程,锁,日志的代码全部显示出来
#include <iostream> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <strings.h> #include <arpa/inet.h> #include <memory.h> #include <unistd.h> #include <cstdio> #include "Thread.hpp" #include "Inet.hpp" #include "Log.hpp" using namespace threadModlue; using namespace LogModle; struct ThreadData { ThreadData(int sockfd, struct sockaddr_in &serveraddr) : _sockfd(sockfd), _serveraddr(serveraddr) { } ~ThreadData() {} InetAddr _serveraddr; int _sockfd; }; void RecverRoutine(ThreadData &td) { char buffer[1024]; // 这里是收消息函数 while (true) { struct sockaddr_in peer; socklen_t len = sizeof(peer); int n = recvfrom(td._sockfd, buffer, sizeof(buffer) - 1, 0, (sockaddr *)&peer, &len); if (n > 0) { buffer[n] = 0; std::cerr << buffer << std::endl; } else { break; } } } void SendRoutine(ThreadData &td) { while (true) { std::string sendbuffer; std::cout << "Please Enter #"; getline(std::cin, sendbuffer); auto server = td._serveraddr; ssize_t n = sendto(td._sockfd, sendbuffer.c_str(), sendbuffer.size(), 0, (const sockaddr *)&server, sizeof(server)); if (n <= 0) { std::cout << "send error" << std::endl; } } } int main(int argc, char *argv[]) { ENABLE_CONSOLE_LOG_STRATERY; if (argc != 3) { std::cerr << "Usage: " << argv[0] << " ip port " << std::endl; return 1; } // 创建socket int sock = socket(AF_INET, SOCK_DGRAM, 0); if (sock < 0) { std::cerr << "socket error: " << strerror(errno) << std::endl; return 2; } std::cout << "socket create success" << std::endl; // client不需要显示地进行bind,让os自己bind,用随机的端口号 // 填充struct sockaddr_in server struct sockaddr_in server; socklen_t len = sizeof(server); bzero(&server, sizeof(server)); std::string ip = argv[1]; uint16_t port = std::stoi(argv[2]); // server.sin_port = port; server.sin_port = htons(port); server.sin_addr.s_addr = inet_addr(ip.c_str()); server.sin_family = AF_INET; ThreadData td(sock, server); std::string recvsername = "recvser"; Thread<ThreadData> recvser(recvsername, RecverRoutine, td); std::string sendername = "sender"; Thread<ThreadData> sender(sendername, SendRoutine, td); recvser.Start(); sender.Start(); recvser.join(); sender.join(); close(sock); return 0; }当我们两个端口实现完成后,会发现当Client使用公网ip链接Server时,Client发出的消息Server收不到,这个时候就要查看你的云服务器是否开放了端口。
打开云服务器,找到安全组。
点击添加规则
将端口范围给开放,就能够进行通信了。
当然还有一件细节,就是当你通信过后,过了一小段时间又不能通信了,这是因为udp基于无连接的通信,也就是你发送过一次消息之后,就不会在保持任何联系了,
那么从你的公网到你的另外一个机器上,一路要经过路由器,这些路由器会保存你的请求ip 请求端口,和源ip和源端口,到net表,但是这个表有一段时间没有通信的话,他就会给你刷新掉,那么再次去请求的时候,找不到映射关系了,就无法通信了。
Log.hpp
#pragma once #include <iostream> #include <unistd.h> #include <sstream> #include <fstream> #include <string> #include <ctime> #include <chrono> #include <iomanip> #include <filesystem> //c++17 #include "Mutex.hpp" namespace LogModle { using namespace Mutex; const std::string defaultpath = "./log"; const std::string defaultname = "log.txt"; enum class LogLevel { DEBUG, INFO, WARNING, ERROR, FATAL }; std::string LevelToString(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 GetCurTimeAndToString(const std::string &formant = "%Y-%m-%d %H:%M:%S") { auto now = std::chrono::system_clock::now(); std::time_t time = std::chrono::system_clock::to_time_t(now); std::tm *local_time = std::localtime(&time); std::stringstream ss; ss << std::put_time(local_time, formant.c_str()); return ss.str(); } class LogStrategy { public: virtual ~LogStrategy() = default; virtual void SyncLog(const std::string &message) = 0; }; // 向显示器打印 class ConsoleLogStrategy : public LogStrategy { public: void SyncLog(const std::string &message) { LockGuard lock(_mutex); std::cout << message << std::endl; } private: MyMutex _mutex; }; // 向文件打印 class FileLogStrategy : public LogStrategy { public: FileLogStrategy(const std::string path = defaultpath, const std::string name = defaultname) : _log_path(path), _log_name(name) { LockGuard lock(_mutex); //???????为什么加锁 访问了临界资源 if (std::filesystem::exists(_log_path)) return; try { std::filesystem::create_directories(_log_path); } catch (const std::exception &e) { std::cerr << e.what() << '\n'; } } void SyncLog(const std::string &message) override { LockGuard lock(_mutex); std::string log = _log_path + (_log_path.back() == '/' ? "" : "/") + _log_name; std::ofstream fout(log.c_str(), std::ios::app); // 追加信息 if(!fout.is_open()) { return ; } fout << message << std::endl; fout.close(); } private: MyMutex _mutex; std::string _log_path; std::string _log_name; }; class Logger { public: Logger() { UseConsoleLogStrategy(); // 默认使用显示器策略 } ~Logger() { } void UseConsoleLogStrategy() { _Strategy = std::make_unique<ConsoleLogStrategy>(); } void UseFileLogStrategy() { _Strategy = std::make_unique<FileLogStrategy>(); } class LogMassage { public: LogMassage(LogLevel level, std::string name, int line, Logger &logger) : _name(name), _level(level), _line(line), _logger(logger) { _cur_time = GetCurTimeAndToString(); _pid = getpid(); std::stringstream ss; ss << "[" << _cur_time << "] " << "[" << LevelToString(_level) << "] " << "[" << _pid << "] " << "[" << _name << "] " << "[" << _line << "]" << "- "; _info = ss.str(); } template <typename T> LogMassage& operator<<(const T &data) { std::stringstream ss; ss << data; _info += ss.str(); return *this; } ~LogMassage() { if(_logger._Strategy) { _logger._Strategy->SyncLog(_info); } } private: std::string _cur_time; LogLevel _level; // 等级 pid_t _pid; std::string _name; int _line; // 行号 std::string _info; Logger &_logger; // 方便刷新日志 }; LogMassage operator()(LogLevel level, std::string name, int line) // 刷新日志 { return LogMassage(level, name, line, *this); } private: std::unique_ptr<LogStrategy> _Strategy; }; Logger logger; #define LOG(type) logger(type,__FILE__,__LINE__) #define ENABLE_CONSOLE_LOG_STRATERY logger.UseConsoleLogStrategy() #define ENABLE_FILE_LOG_STRATERY logger.UseFileLogStrategy() }Mutex.hpp
#pragma once #include<mutex> #include<pthread.h> namespace Mutex { class MyMutex { public: MyMutex() { int n = pthread_mutex_init(&lock,nullptr); } pthread_mutex_t* GetMutexOriginal() { return &lock; } void Lock() { int n = pthread_mutex_lock(&lock); (void)n; } void UnLock() { int n = pthread_mutex_unlock(&lock); (void)n; } private: pthread_mutex_t lock; }; class LockGuard { public: LockGuard(MyMutex& lock) :_lock(lock) { _lock.Lock(); } ~LockGuard() { _lock.UnLock(); } private: MyMutex& _lock; }; }Thread.hpp
#ifndef _THREAD_H_ #define _THREAD_H_ #include <iostream> #include <pthread.h> #include <string> #include <cstdio> #include <cstring> #include <functional> namespace threadModlue { static uint32_t number = 1; template <class T> class Thread { private: using func_t = std::function<void(T&)>; void EnableDetach() { std::cout << "线程被分离" << std::endl; _isdetach = true; } void EnanleRunning() { _isrunning = true; } static void *Routine(void *args) { Thread<T> *self = static_cast<Thread<T> *>(args); if (self->_isdetach) self->Detach(); self->EnanleRunning(); self->_func(self->_data); return args; } public: Thread(const std::string& name,func_t func, T data) : _tid(0), _isdetach(false), _isrunning(false), _res(nullptr), _func(func), _data(data), _name(name) { //_name = "thread-" + std::to_string(number++); } void Detach() { if (_isdetach) return; if (_isrunning) pthread_detach(_tid); EnableDetach(); } bool Start() { int n = pthread_create(&_tid, nullptr, Routine, this); if (n != 0) { std::cerr << "create thread error" << strerror(n) << std::endl; return false; } return true; } bool Stop() { if (_isrunning) { int n = pthread_cancel(_tid); if (n != 0) { std::cerr << "Stop thread error" << strerror(n) << std::endl; return false; } else { _isrunning = false; std::cout << _name << " stop" << std::endl; } } return true; } bool join() { if (_isdetach) { std::cout << "线程已分离,join失败" << std::endl; return false; } int n = pthread_join(_tid, &_res); if (n != 0) { std::cerr << "join thread error" << strerror(n) << std::endl; return false; } else { std::cout << "join seccuss!" << std::endl; } return true; } ~Thread() { } private: pthread_t _tid; std::string _name; bool _isdetach; bool _isrunning; void *_res; func_t _func; T _data; }; } #endif