一、V1版本-Echo Server

网络通信中必须包含的三个头文件:

描述网络套接字的结构体:

填充字段,保证结构体符合指定长度


为什么需要16位地址类型AFinet:第一次传入AFinet是为了告诉操作系统我要创建网络套接字,而第二次传入是为了让未来Socket接口函数能够识别出我是用于网络通信的。afinet:


宏中的##功能:拼接a##1,拼接后即为:a1

EchoServer.hpp
cpp
#ifndef __ECHOSERVER_HPP
#define __ECHOSERVER_HPP
#include <iostream>
#include <string>
#include <cstring>
#include <cstdint> // uint16_t 类型依赖,原代码用到uint16_t必须加
#include <cstdlib> // exit() 函数依赖,Init中exit退出程序
#include <sys/types.h>// socket/recvfrom/sendto 底层类型前置依赖
// Linux网络系统调用头文件
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h> // sockaddr_in、INADDR_ANY 定义核心头文件,缺失会报INADDR_ANY未定义
#include "Mutex.hpp"
#include "Logger.hpp"
const static int default_fd = -1;
const static uint16_t default_port = 8080;
enum {
SUCCESS = 0,
SOCKET_ERR,
BIND_ERR, // 补充bind错误码
USAGE_ERR // 补充用法错误码
};
class UdpServer {
public:
UdpServer(uint16_t port = default_port)
: _port(port),
_sockfd(default_fd)
{
}
~UdpServer() {
if (_sockfd != default_fd)
close(_sockfd);
}
void Init() {
// 第一步:创建socket
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0) {
LOG(LogLevel::FATAL) << "create socket error";
exit(SOCKET_ERR);
}
LOG(LogLevel::INFO) << "create socket success, sockfd: " << _sockfd;
// 第二步:填充网络信息并bind
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port); // 主机序 -> 网络序
//local.sin_addr.s_addr = inet_addr(_ip.c_str());
//字符串 IP -> 4字节 + 网络序,由于服务器开发时不建议显示bind IP,所以不使用这种方式来绑定IP,而是采用下面的方式
local.sin_addr.s_addr = INADDR_ANY;//最佳实践:任意IP地址bind
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0) {
LOG(LogLevel::FATAL) << "bind socket error";
exit(BIND_ERR);
}
LOG(LogLevel::INFO) << "bind socket success, ip: " << _ip << ", port: " << _port;
}
void Start()
{
// 传递的是字符串,echo server
char inbuffer[1024];
while(true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 1. 用户发来的数据
// 2. 用户的socket信息
ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer)-1, 0, (struct sockaddr *)&peer, &len);
if(n > 0)
{
// 用户的IP + Port信息,是recvfrom从网络中获取到的数据,网络序列(大端) -> 转换成为主机序列
// peer.sin_port; // peer client port
// peer.sin_addr; // peer client i
// 把网络序端口转为主机序,得到客户端真实端口号
uint16_t client_port = ntohs(peer.sin_port);
// 将sockaddr_in里4字节网络序IP数值,转换成人类可读的点分十进制IP字符串
std::string client_ip = inet_ntoa(peer.sin_addr);
// 拼接格式化客户端地址,格式为 [ip:端口]
std::string client_address = "[" + client_ip + ":" + std::to_string(client_port) + "]";
// 用户发来的数据
inbuffer[n] = 0;
LOG(LogLevel::DEBUG) << "cilent say# " << inbuffer;
std::string echo_string = "server echo# ";
echo_string += inbuffer;
// h to n
sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&peer, len);
}
else
{
LOG(LogLevel::ERROR) << "recvfrom error";
}
}
}
private:
int _sockfd;
uint16_t _port;
//std::string _ip;
//由于使用的是任意IP,不需要绑定固定IP,可以把std::string _ip去掉了,只需要固定端口号即可
};
#endif
上面的代码中,服务端在进行网络通信时,需要先创建套接字,填充本机 IP 与端口地址信息,再调用 bind 函数完成地址绑定;很多人会产生疑问,客户端通信时是否也需要专属的套接字地址(IP + 端口)?
答案是肯定的!但是客户端在绑定时不能显示地bind IP和端口号。原因是,服务端的端口必须明确且固定,这样各个客户端发起通信时,才能精准找到服务器上对应的服务进程;而客户端端口没有固定要求,只要本机内端口唯一不冲突就行。
如果我们强行给客户端显式绑定固定端口,后续客户端去连接不同服务端的时候,很容易出现端口占用冲突问题,一旦该端口被其他程序占用,客户端就无法正常收发数据了。
那么客户端如何设置自己的IP和端口呢?
客户端默认走随机端口这套逻辑,全部交给操作系统分配,不用我们手动操作。拿 UDP 客户端来说,程序第一次调用sendto发数据的时候,系统会自动完成隐式bind,随机挑一个本机空闲端口,同时绑定本机可用 IP,自动生成属于这个客户端的 IP + 端口。
EchoClient.cc
cpp
#include <iostream>
#include <string>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
// 打印使用提示
static void Usage(const std::string &process)
{
std::cerr << "Usage:\n\t";
std::cerr << process << " server_ip server_port" << std::endl;
}
// ./client_udp server_ip server_port
int main(int argc, char *argv[])
{
// 参数校验:必须传入服务端IP、端口两个参数,总参数数量为3
if(argc != 3)
{
Usage(argv[0]);
exit(1);
}
// 从命令行读取目标服务端IP、端口
std::string server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);
// 1. 创建UDP socket
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
std::cerr << "socket error" << std::endl;
exit(2);
}
// 2. 填充目标服务端地址信息
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 message;
// 1. 获取用户输入
std::cout << "Please Enter# ";
std::getline(std::cin, message);
// 2. client 发送数据给 server,首次发送即自动bind
ssize_t n = sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));
if(n > 0)
{
// recvfrom 接收服务端回包
char inbuffer[1024] = {0};
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t m = recvfrom(sockfd, inbuffer, sizeof(inbuffer)-1, 0, (struct sockaddr*)&temp, &len);
if(m > 0)
{
inbuffer[m] = 0;
std::cout << inbuffer << std::endl;
}
}
}
return 0;
}
EchoServerMain.cc
cpp
#include "EchoServer.hpp"
#include <memory>
#include <iostream> // std::cerr、std::endl 依赖
#include <string> // std::string、std::stoi 依赖
#include <cstdlib> // exit() 函数依赖
#include <cstdint> // uint16_t 类型依赖
// 打印程序使用帮助
static void Usage(const std::string &process)
{
std::cerr << "Usage:\n\t";
std::cerr << process << " local_port" << std::endl;
}
// 程序运行格式:./server_udp 端口号
int main(int argc, char *argv[])
{
// 参数校验:必须传入1个端口参数,总参数数量为2
if(argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
// 开启控制台日志输出策略(自定义日志库宏)
ENABLE_CONSOLE_LOG_STRATEGY();
// 把命令行传入的端口字符串转为数字端口
uint16_t server_port = std::stoi(argv[1]);
// 使用智能指针创建UDP服务端对象,传入指定监听端口
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(server_port);
// 初始化socket、bind端口
usvr->Init();
// 启动循环收发echo数据
usvr->Start();
return 0;
}
如果测试时绑定本地环回 IP 127.0.0.1、端口 8080,流量只会在本机内核协议栈内部流转,报文不会出网卡、不会发到外部物理网络,只能本机内部进程互相访问,局域网、公网其他设备都无法连接这个服务。
服务端开发则有一套最佳实践,云服务器千万不能显式 bind 公网 IP,日常写服务代码也不建议手动指定固定IP去 bind。一旦显式绑定指定 IP,系统只会接收目标地址和该绑定 IP 完全匹配的报文,云厂商公网 IP 经过NAT转发后流量目的地址实际是内网IP,外部请求根本进不来。但禁止显式绑定公网 IP 不代表公网 IP 不会参与通信,外部用户访问服务走的依旧是公网 IP,稳妥的写法直接bind 0.0.0.0(INADDR_ANY),监听本机全部网卡地址就能兼容所有访问场景。
所以服务端开发时最佳实践是随机bind任意IP地址,而客户端开发最佳实践是随机bind任意端口号。
二、V2版本-Dict Server
前面的网络通信服务只能完成IO功能,也就是只能收到或发送信息,下面通过封装一层业务软件层来实现一个简单的英译汉英文字典
Dict.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
#include "Logger.hpp"
static const std::string default_dict = "./dict.txt";
static const std::string sep = ": ";
using namespace NS_LOG_MODULE;
class Dict
{
public:
Dict(const std::string &dict_path = default_dict) : _dict_path(dict_path)
{
LoadDict();
}
~Dict()
{
}
void LoadDict()
{
std::ifstream in(_dict_path);
if (!in.is_open())
{
LOG(LogLevel::FATAL) << " open " << _dict_path << " error";
exit(1);
}
std::string line;
while (std::getline(in, line))
{
LOG(LogLevel::FATAL) << "load: " << line << " success";
// apple: 苹果
auto pos = line.find(sep);
if (pos == std::string::npos)
{
LOG(LogLevel::WARNING) << "Format: " << line << " error";
continue;
}
std::string k = line.substr(0, pos); // [)
std::string v = line.substr(pos + sep.size());
_dict.insert(std::make_pair(k, v));
}
in.close();
LOG(LogLevel::INFO) << "load done....";
}
std::string Translate(std::string word)
{
auto iter = _dict.find(word);
if(iter != _dict.end())
{
return iter->second;
}
else
{
return "None";
}
}
private:
std::string _dict_path;
std::unordered_map<std::string, std::string> _dict;
};
dict.txt:
cpp
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天
三、V3版本-Chat Server
在网络通信模块中,UDPServer会接收到客户端发送的所有信息,也会收到所有的客户端。如果要将这些信息和客户端管理起来,就必须先描述,在组织!用链表,vector这类数据结构来对这些收到的信息和客户端进行管理。
在上面的V1版本中,整个服务器都是单进程的,工作方式也是串行进行的。而由于UDP Server会以回调的方式调用上层逻辑,所以在V1版本-Echo Server的基础上,我们可以设计一种转发逻辑,形成一个新的数据转发的任务------route_message(),将客户端收到的消息,转发给所有的在线用户。为了完成信息转发,我们可以将这些转发任务放入线程池中,经过线程池处理之后,由线程池将数据转发给所有人。虽然还是单进程对收到的数据进行收发处理,但是经过任务封装和线程池之后,我们就能并发地读和写,多线程处理收到的任务。这也就是V3版本聊天服务器Chat Server的核心思想。
由于需要使用到线程池,同时需要引入线程池,条件变量的hpp文件:Thread.hpp、Threadpool.hpp、Cond.hpp(在前面的博客中有)
下面是V3版本Chat Server的核心代码:
ChatClient.cc
cpp
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Thread.hpp"
#include "InetAddr.hpp"
using namespace NS_THREAD_MODULE;
int sockfd = 0;
std::string server_ip;
uint16_t server_port = 0;
std::string nickname;
static void Usage(const std::string &proc)
{
std::cout << "Usage:\n\t";
std::cout << proc << " server_ip server_port" << std::endl;
}
static void Online(InetAddr &serveraddr)
{
std::cout << "Please Set Your Nick Name# ";
std::getline(std::cin, nickname);
std::string online_message = nickname + " online!";
ssize_t n = sendto(sockfd, online_message.c_str(), online_message.size(), 0,
(struct sockaddr *)serveraddr.GetNetAddress(), serveraddr.Len());
(void)n;
}
void RecvMessage()
{
while (true)
{
// recvfrom
char inbuffer[1024] = {0};
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t m = recvfrom(sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&temp, &len);
if (m > 0)
{
inbuffer[m] = 0;
std::cerr << inbuffer << std::endl; // 2
}
}
}
void SendMessage()
{
InetAddr serveraddr(server_port, server_ip);
Online(serveraddr);
while (true)
{
std::string message;
// 1. 获取用户输入
std::cout << "Please Enter# "; // 1
std::getline(std::cin, message);
message = nickname + "# " + message;
// 2. clinet 发送数据给 server,首次发送即自动bind
ssize_t n = sendto(sockfd, message.c_str(), message.size(), 0,
(struct sockaddr *)serveraddr.GetNetAddress(), serveraddr.Len());
(void)n;
}
}
// 我怎么知道server对方的IP和端口啊, 类似IP+Port 是被内置到client的!!!
// ./client_udp server_ip server_port
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
server_ip = argv[1];
server_port = std::stoi(argv[2]);
// 1. 创建socket
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
std::cerr << "socket error" << std::endl;
exit(2);
}
Thread recver(RecvMessage);
Thread sender(SendMessage);
recver.Start();
sender.Start();
recver.Join();
sender.Join();
return 0;
}
ChatServerMain.cc
cpp
#include "ThreadPool.hpp" // 执行者,执行处理动作的人
#include "Route.hpp" // 任务
#include "UdpServer.hpp" // 获取事件
#include <memory>
static void Usage(const std::string &process)
{
std::cerr << "Usage:\n\t";
std::cerr << process << " local_port" << std::endl;
}
using namespace NS_THREAD_POOL_MODULE;
using task_t = std::function<void()>;
// ./server_udp port
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
ENABLE_CONSOLE_LOG_STRATEGY();
uint16_t server_port = std::stoi(argv[1]);
// 线程池模块
auto thread_pool = ThreadPool<task_t>::Instance();
// 路由模块
Route r;
// 网络模块
UdpServer usvr(server_port);
usvr.Init();
usvr.RegisterService(
[&r](const InetAddr &addr){
r.CheckUser(addr);
},
[&r, thread_pool](int sockfd, std::string msg){
auto t = std::bind(&Route::Broadcast, &r, sockfd, msg);
thread_pool->Enqueue(t);
// thread_pool->Enqueue([&r, &sockfd, &msg](){
// r.Broadcast(sockfd, msg);
// });
}
);
usvr.Start();
return 0;
}
InetAddr.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// 对客户端进行先描述
class InetAddr
{
public:
InetAddr(const struct sockaddr_in &address):_address(address), _len(sizeof(address))
{
_ip = inet_ntoa(_address.sin_addr);
_port = ntohs(_address.sin_port);
}
InetAddr(uint16_t port, const std::string &ip = "0.0.0.0"):_ip(ip), _port(port)
{
bzero(&_address, sizeof(_address));
_address.sin_family = AF_INET;
_address.sin_port = htons(_port); // h->n
_address.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1. 字符串ip->4字节IP 2. hton
_len = sizeof(_address);
}
bool operator == (const InetAddr &addr)
{
return (this->_ip == addr._ip) && (this->_port == addr._port);
}
std::string ToString()
{
return "[" + _ip + ":" + std::to_string(_port) + "]";
}
InetAddr()
{}
struct sockaddr_in *GetNetAddress()
{
return &_address;
}
socklen_t Len()
{
return _len;
}
~InetAddr()
{}
private:
// net address
struct sockaddr_in _address;
socklen_t _len;
// host address
std::string _ip;
uint16_t _port;
};
UpdServer.hpp
cpp
#ifndef __ECHOSERVER_HPP
#define __ECHOSERVER_HPP
#include <iostream>
#include <string>
#include <cstdlib>
#include <strings.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <functional>
#include <arpa/inet.h>
#include "InetAddr.hpp"
#include "Logger.hpp"
using namespace NS_LOG_MODULE;
const static int default_fd = -1;
const static int default_port = 8888;
using handler_addr_t = std::function<void (const InetAddr &)>;
using handler_msg_t = std::function<void (int sokcfd, std::string msg)>;
enum
{
SUCCESS = 0,
USAGE_ERR,
SOCKET_ERR,
BIND_ERR,
};
class UdpServer
{
public:
// UdpServer(const std::string &ip, uint16_t port = default_port)
UdpServer(uint16_t port = default_port)
: _port(port),
_sockfd(default_fd)
{
}
~UdpServer()
{
close(_sockfd);
}
void Init()
{
// 第一步: 创建socket, 本质: 打开网卡 --- 系统特性
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "create socket error";
exit(SOCKET_ERR);
}
LOG(LogLevel::INFO) << "create socket success, sockfd: " << _sockfd;
InetAddr local(_port);
// 第三步:bind socket 信息
int n = bind(_sockfd, (struct sockaddr *)(local.GetNetAddress()), local.Len());
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind socket error";
exit(BIND_ERR);
}
LOG(LogLevel::INFO) << "bind socket success"<< ", port: " << _port;
}
void RegisterService(handler_addr_t handler_addr, handler_msg_t handler_msg)
{
_handler_addr = handler_addr;
_handler_msg = handler_msg;
}
void Start()
{
// 传递的是字符串,echo server
char inbuffer[1024];
while (true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 1. 用户发来的数据
// 2. 用户的socket信息
ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (n > 0)
{
inbuffer[n] = 0;
// 1. 检测新用户
InetAddr clientaddress(peer);
std::string tips = clientaddress.ToString();
std::string message = tips + inbuffer;
LOG(LogLevel::DEBUG) << message;
_handler_addr(clientaddress);
// 2. 转发消息
// nickname# message -> [ip:port]-nickname# message
_handler_msg(_sockfd, message);
}
else
{
LOG(LogLevel::ERROR) << "recvfrom error";
}
// sleep(3);
}
}
private:
int _sockfd;
// std::string _ip; // "192.168.2.2"(字符串风格的点分十进制IP地址, 让人看的) && 4字节IP ???
uint16_t _port; // 用户设置好的,server port必须是固定的!
handler_addr_t _handler_addr;
handler_msg_t _handler_msg;
};
#endif
UserManager.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include "InetAddr.hpp"
#include "Logger.hpp"
using namespace NS_LOG_MODULE;
class UserManager
{
public:
UserManager(){}
void AddUser(const InetAddr &addr)
{
if(SearchUser(addr))
return;
_users.push_back(addr);
}
void DelUser(const InetAddr &addr)
{
for(auto iter = _users.begin(); iter != _users.end(); iter++)
{
if(*iter == addr)
{
_users.erase(iter);
break;
}
}
}
bool SearchUser(const InetAddr &addr)
{
for(auto &user : _users)
{
if(user == addr)
{
return true;
}
}
return false;
}
bool ModUser(const InetAddr &addr)
{
DelUser(addr);
AddUser(addr);
return true;
}
std::vector<InetAddr> &Users()
{
return _users;
}
~UserManager(){}
private:
std::vector<InetAddr> _users;
};
Route.hpp
cpp
#pragma once
#include <iostream>
#include <memory>
#include <string>
#include <sys/socket.h>
#include "Mutex.hpp"
#include "UserManager.hpp"
class Route
{
public:
Route():_uma(std::make_unique<UserManager>())
{}
void CheckUser(const InetAddr &addr)
{
LockGuard lockguard(_lock);
_uma->AddUser(addr);
}
void OfflineUser(const InetAddr &addr)
{
LockGuard lockguard(_lock);
_uma->DelUser(addr);
}
void Broadcast(int sockfd, std::string message)
{
LockGuard lockguard(_lock);
auto &users = _uma->Users();
for(auto &user : users)
{
sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)user.GetNetAddress(), user.Len());
}
}
~Route()
{}
private:
std::unique_ptr<UserManager> _uma;
Mutex _lock;
};