UDP聊天室,TCP服务站
- [0. 头文件准备](#0. 头文件准备)
- [1. UDP聊天室](#1. UDP聊天室)
-
- [1.1 需求分析](#1.1 需求分析)
- [1.2 实现服务端------UdpServer.hpp](#1.2 实现服务端——UdpServer.hpp)
- [1.3 实现客户端------UdpClient.cc](#1.3 实现客户端——UdpClient.cc)
- [1.4 测试](#1.4 测试)
- [2. TCP服务站](#2. TCP服务站)
-
- [2.1 需求分析](#2.1 需求分析)
- [2.2 Translate任务模块------Translater.hpp](#2.2 Translate任务模块——Translater.hpp)
- [2.3 服务器端------TcpServer.hpp](#2.3 服务器端——TcpServer.hpp)
- [2.4 实现客户端------TcpClient.cc](#2.4 实现客户端——TcpClient.cc)
- [2.5 测试](#2.5 测试)
- [3. 守护进程化](#3. 守护进程化)
0. 头文件准备
1. 错误信息头文件------Comm.hpp
cpp
#pragma once
enum{
Usage_Err = 1, // 使用方式错误
Socket_Err, // Socket错误
Bind_Err, // Bind错误
Listen_Err // 监听失败
};
2. 封装地址------InetAddr.hpp
cpp
#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);
char buffer[INET_ADDRSTRLEN]; // 这个宏表示IPV4的地址大小
inet_ntop(AF_INET, &addr.sin_addr, buffer, sizeof(buffer));
_ip = buffer;
}
std::string Ip() const { return _ip; }
uint16_t Port() const { return _port;}
struct sockaddr_in& GetAddr()
{
return _addr;
}
bool operator==(const InetAddr& addr) const
{
return this->_ip == addr.Ip() && this->_port == addr.Port();
}
std::string PrintDebug()
{
std::string info = _ip;
info += ":";
info += std::to_string(_port);
return info;
}
~InetAddr()
{}
private:
std::string _ip; // ip
uint16_t _port; // 端口号
struct sockaddr_in _addr;
};
3. LockGuard.hpp
cpp
#pragma once
#include <pthread.h>
// 不定义锁,默认认为外部会给我们传入锁对象
class Mutex
{
public:
Mutex(pthread_mutex_t *lock):_lock(lock)
{}
void Lock()
{
pthread_mutex_lock(_lock);
}
void Unlock()
{
pthread_mutex_unlock(_lock);
}
~Mutex()
{}
private:
pthread_mutex_t *_lock;
};
class LockGuard
{
public:
LockGuard(pthread_mutex_t *lock):_mutex(lock)
{
_mutex.Lock();
}
~LockGuard()
{
_mutex.Unlock();
}
private:
Mutex _mutex;
};
4. 日志------Log.hpp
cpp
#pragma once
#include<iostream>
#include<string>
#include<cstdarg>
#include<ctime>
#include<fstream>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include"LockGuard.hpp"
enum
{
Debug = 0,
Info, // 正常信息
Warning, // 告警
Error, // 错误
Fatal // 致命错误
};
enum
{
Screen = 0, // 向显示器打印
OneFile, // 向一个文件打印
ClassFile // 向多个文件打印
};
// 将日志等级转换为string
std::string LevelToString(int level)
{
switch(level)
{
case Debug:
return "Debug";
case Info:
return "Info";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
default:
return "Unknown";
}
}
const int defaultstyle = Screen; // 默认风格是向显示器打印
const std::string default_filename = "log."; // 默认文件名
const std::string logdir = "log"; // 默认日志文件夹
class Log
{
public:
Log()
:_style(defaultstyle)
,_filename(default_filename)
{
mkdir(logdir.c_str(), 0775);
pthread_mutex_init(&_mutex, nullptr);
}
void Enable(int style)
{
_style = style;
}
std::string TimeStampExLocalTime()
{
time_t currtime = time(nullptr);
struct tm *curr = localtime(&currtime);
char time_buffer[128];
snprintf(time_buffer, sizeof(time_buffer), "%d-%d-%d %d:%d:%d", \
curr->tm_year+1900, curr->tm_mon+1, curr->tm_mday,\
curr->tm_hour, curr->tm_min, curr->tm_sec);
return time_buffer;
}
void WriteLogToOneFile(const std::string &logname, const std::string &message)
{
umask(0);
int fd = open(logname.c_str(), O_CREAT | O_WRONLY | O_APPEND, 0666);
if(fd < 0) return;
write(fd, message.c_str(), message.size());
close(fd);
}
void WriteLogToClassFile(const std::string &levelstr, const std::string &message)
{
std::string logname = logdir;
logname += "/";
logname += _filename;
logname += levelstr;
WriteLogToOneFile(logname, message);
}
void WriteLog(const std::string &levelstr, const std::string &message)
{
switch(_style)
{
case Screen:
std::cout << message;
break;
case OneFile:
WriteLogToClassFile("all", message);
break;
case ClassFile:
WriteLogToClassFile(levelstr, message);
break;
default:
break;
}
}
void LogMessage(int level, const char* format, ...) // 类C的一个日志接口
{
char rightbuffer[1024];
va_list args; // 这是一个char *类型(或者void *)的指针
va_start(args, format); // 让arg指向可变部分
vsnprintf(rightbuffer, sizeof(rightbuffer), format, args); // 将可变部分按照指定格式写入到content中
va_end(args); // 释放args, args = nullptr
char leftbuffer[1024];
std::string levelstr = LevelToString(level);
std::string currtime = TimeStampExLocalTime(); // 获取当前时间
std::string idstr = std::to_string(getpid());
snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%s][%s] ", \
levelstr.c_str(), currtime.c_str(), idstr.c_str());
std::string loginfo = leftbuffer;
loginfo += rightbuffer;
{
LockGuard lockguard(&_mutex);
WriteLog(levelstr, loginfo);
}
}
~Log()
{}
private:
int _style;
std::string _filename;
pthread_mutex_t _mutex;
};
// 配置log
Log lg;
class Config
{
public:
Config()
{
// 在此配置
lg.Enable(Screen);
}
~Config()
{}
};
Config config;
5. nocopy.hpp
cpp
#pragma once
class nocopy
{
public:
nocopy()
{}
~nocopy()
{}
nocopy(const nocopy&) = delete;
const nocopy& operator=(const nocopy&) = delete;
};
6. 封装线程类------Thread.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <functional>
template<class T>
using func_t = std::function<void(T&)>;
template<class T>
class Thread
{
public:
Thread(std::string threadname, func_t<T> func, T &data)
:_tid(0), _threadname(threadname), _isrunning(false), _func(func), _data(data)
{}
static void *ThreadRoutine(void* args)
{
Thread *ts = static_cast<Thread *>(args);
ts->_func(ts->_data);
return nullptr;
}
bool Start()
{
int n = pthread_create(&_tid, nullptr, ThreadRoutine, this);
if(n == 0)
{
_isrunning = true;
return true;
}
else return false;
}
bool Join()
{
if(!_isrunning) return false;
int n = pthread_join(_tid, nullptr);
if(n == 0)
{
_isrunning = false;
return true;
}
return false;
}
bool IsRunning()
{
return _isrunning;
}
std::string ThreadName()
{
return _threadname;
}
~Thread()
{}
private:
pthread_t _tid; // 库级别线程id
std::string _threadname; // 线程名
bool _isrunning; // 运行状态
func_t<T> _func; // 线程执行的回调方法
T _data;
};
7. 线程池------ThreadPool.hpp
cpp
#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>
#include <vector>
#include <functional>
#include "Log.hpp"
#include "Thread.hpp"
#include "LockGuard.hpp"
namespace ThreadNs
{
static const int default_num = 5; // 默认线程池中的线程个数
class ThreadData
{
public:
ThreadData(const std::string &threadname)
: _threadname(threadname)
{
}
~ThreadData()
{
}
public:
std::string _threadname;
};
template <class T>
class ThreadPool
{
private:
ThreadPool(int thread_num = default_num)
: _thread_num(thread_num)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_cond, nullptr);
// 构建指定个数的线程
for (int i = 0; i < _thread_num; i++)
{
// 待优化
std::string threadname = "thread-";
threadname += std::to_string(i + 1);
ThreadData td(threadname);
Thread<ThreadData> t(threadname,
std::bind(&ThreadPool<T>::ThreadRun,
this, std::placeholders::_1),
td);
_threads.push_back(t);
lg.LogMessage(Info, "%s is created...\n", threadname.c_str());
}
}
ThreadPool(const ThreadPool<T> &tp) = delete;
const ThreadPool<T> &operator=(const ThreadPool<T> &tp) = delete;
public:
// 有线程安全问题
static ThreadPool<T> *GetInstance()
{
if (instance == nullptr)
{
LockGuard lockguard(&sig_lock);
if (instance == nullptr)
{
lg.LogMessage(Info, "创建单例成功...\n");
instance = new ThreadPool<T>();
}
}
return instance;
}
bool Start()
{
// 启动
for (auto &thread : _threads)
{
thread.Start();
lg.LogMessage(Info, "%s is running ...\n", thread.ThreadName().c_str());
}
return true;
}
void ThreadWait(const ThreadData &td)
{
lg.LogMessage(Debug, "no task, %s is sleeping...\n", td._threadname.c_str());
pthread_cond_wait(&_cond, &_mutex);
}
void ThreadWakeup()
{
pthread_cond_signal(&_cond);
}
void ThreadRun(ThreadData &td)
{
while (true)
{
// 取任务
T t;
{
LockGuard lockguard(&_mutex);
while (_q.empty())
{
ThreadWait(td);
lg.LogMessage(Debug, "thread, %s wake up.\n", td._threadname.c_str());
}
t = _q.front();
_q.pop();
}
// 处理任务
t();
}
}
void Push(T in)
{
// lg.LogMessage(Debug, "other thread push a task, task is: %s\n", in.PrintTask().c_str());
{
LockGuard lockgurad(&_mutex);
_q.push(in);
ThreadWakeup();
}
}
// for debug
void Wait()
{
for (auto &thread : _threads)
{
thread.Join();
}
}
~ThreadPool()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);
}
private:
std::queue<T> _q; // 任务队列
std::vector<Thread<ThreadData>> _threads;
int _thread_num; // 线程个数
pthread_mutex_t _mutex;
pthread_cond_t _cond;
static ThreadPool<T> *instance;
static pthread_mutex_t sig_lock;
// 扩展1:
int _task_num;
int _thread_num_low_water;
int _thread_num_high_water;
int _task_num_low_water;
int _task_num_high_water;
// 扩展2:多进程+多线程
};
template <class T>
ThreadPool<T> *ThreadPool<T>::instance = nullptr;
template <class T>
pthread_mutex_t ThreadPool<T>::sig_lock = PTHREAD_MUTEX_INITIALIZER;
}
1. UDP聊天室
1.1 需求分析
- 需要有一个线程池,这个线程池应该在服务器初始化时就创建好,我们这里选择用单例实现;
- 实现一个
Route
功能,将信息转发给所有在线用户,服务器接收到一个消息,就应将一个转发任务,Push
进线程池,然后继续接收信息; - 既然这样,那么肯定需要一个容器来存储所有在线用户的信息,这里我们选择
vector
。访问这个vector
时,一定要上锁,因为会存在多线程访问vector
的情况; Route
功能需要遍历vector
,也要记得上锁。
1.2 实现服务端------UdpServer.hpp
1. UdpServer.hpp头文件
cpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <cerrno>
#include <functional>
#include "nocopy.hpp"
#include "Log.hpp"
#include "Comm.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"
const static int defaultfd = -1;
const static uint16_t defaultport = 8888; // 默认端口
const static int defaultsize = 1024; // 默认收发消息时的缓冲区大小
using task_t = std::function<void()>;
class UdpServer : public nocopy
{
public:
UdpServer(uint16_t port = defaultport)
:_port(port), _sockfd(defaultfd)
{
pthread_mutex_init(&_mutex, nullptr);
}
void Init()
{
// 1. 创建socket(本质就是创建了文件细节)
_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // udp协议
if (_sockfd < 0)
{
// 创建socket失败
lg.LogMessage(Fatal, "socket error, %d : %s\n", errno, strerror(errno));
exit(Socket_Err);
}
lg.LogMessage(Info, "socket success, sockfd: %d\n", _sockfd); // 创建socket成功
// 2. 绑定(指定网络信息)
struct sockaddr_in local;
bzero(&local, sizeof(local)); // 初始化local结构体
local.sin_family = AF_INET;
local.sin_port = htons(_port); // 端口号转网络序列
// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 这个函数干两件事情 a. 将字符串风格ip转为4字节ip b. 将4字节ip变为网络序列
local.sin_addr.s_addr = INADDR_ANY; // 绑定任意ip(这个宏实际上就是0)
// 将网络信息设置进内核(网络信息和文件信息关联)
int n = ::bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
if(n != 0)
{
lg.LogMessage(Fatal, "bind error, %d : %s\n", errno, strerror(errno));
exit(Bind_Err);
}
ThreadNs::ThreadPool<task_t>::GetInstance()->Start();
}
// 添加进在线列表(先检查是否已经在表中,已经在就不用插入了)
void AddOnlineUser(const InetAddr &addr)
{
LockGuard lockguard(&_mutex);
for(auto &user : _online_users)
{
if(user == addr) return;
}
_online_users.push_back(addr);
lg.LogMessage(Info, "%s:%hu join OnlineQueue\n", addr.Ip().c_str(), addr.Port());
}
// 广播消息
void Route(std::string message)
{
LockGuard Lockguard(&_mutex);
for(auto &user : _online_users)
{
sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&user.GetAddr(), sizeof(user.GetAddr()));
lg.LogMessage(Debug, "server send message to %s:%hu\n", user.Ip().c_str(), user.Port());
}
}
void Start()
{
char buffer[defaultsize];
// 服务器永远不退出
for(;;)
{
struct sockaddr_in peer; // 远端
socklen_t len = sizeof(peer); // 指明peer的长度,不能乱写
// 收消息
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
if(n > 0)
{
InetAddr addr(peer);
AddOnlineUser(addr);
buffer[n] = 0; // 最后一个字符串设置为'\0'
std::string message = "[" + addr.Ip() + ":" + std::to_string(addr.Port()) + "]# " + buffer;
task_t task = std::bind(&UdpServer::Route, this, message);
ThreadNs::ThreadPool<task_t>::GetInstance()->Push(task);
}
}
}
~UdpServer()
{
close(_sockfd);
pthread_mutex_destroy(&_mutex);
}
private:
uint16_t _port; // 端口号
int _sockfd; // 文件细节
std::vector<InetAddr> _online_users; // 在线用户列表
pthread_mutex_t _mutex; // 保护在线用户列表
};
2. 主程序Main.cc
cpp
#include "UdpServer.hpp"
#include "Comm.hpp"
#include <memory>
#include <string>
#include <vector>
// 使用手册
void Usage(std::string proc)
{
std::cout << "Usage : \n\t" << proc << " local_port\n" << std::endl;
}
// ./udp_server 8888
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
return Usage_Err;
}
uint16_t port = std::stoi(argv[1]); // 拿到2字节整数风格的端口号
std::unique_ptr<UdpServer> usvr = std::unique_ptr<UdpServer>(new UdpServer(port));
usvr->Init(); // 初始化服务器
usvr->Start(); // 启动服务器
return 0;
}
1.3 实现客户端------UdpClient.cc
这里要注意一下,我们实现的是多线程版的客户端,让收消息和发消息两个操作并发运行
cpp
#include <iostream>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "Thread.hpp"
#include "InetAddr.hpp"
// 使用手册
void Usage(std::string proc)
{
std::cout << "Usage : \n\t" << proc << " server_ip server_port\n" << std::endl;
}
struct ThreadData
{
ThreadData(int sockfd, InetAddr addr)
:_sockfd(sockfd), _addr(addr)
{}
~ThreadData()
{}
int _sockfd;
InetAddr _addr;
};
void ReceiverRoutine(ThreadData &data)
{
char buffer[4096];
while(true)
{
// 收消息
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t n = recvfrom(data._sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&temp, &len); // 最后两个参数一般建议都是要填的(传空指针会有坑)
if(n > 0)
{
buffer[n] = 0;
std::cerr << buffer << std::endl; // 错误流打印,方便测试
}
else break;
}
}
void SenderRoutine(ThreadData &data)
{
struct sockaddr_in addr = data._addr.GetAddr();
while(true)
{
// 我们要发的数据
std::string inbuffer;
std::cout << "Please Enter# ";
std::getline(std::cin, inbuffer);
// 给server端发消息
ssize_t n = sendto(data._sockfd, inbuffer.c_str(), inbuffer.size(), 0, (struct sockaddr*)&addr, sizeof(addr));
if(n <= 0)
{
std::cout << "send error" << std::endl;
}
}
}
// ./udp_client server_ip server_port
int main(int argc, char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
return 1;
}
std::string serverip = argv[1]; // 服务端ip
uint16_t serverport = std::stoi(argv[2]); // 服务端port
// 1. 创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
std::cerr << "socket error: " << strerror(errno) << std::endl;
return 1;
}
std::cout << "create socket success: " << sockfd << std::endl;
// 2. client要不要进行bind?一定要。但是不需要显示bind,client会在首次发送数据时自动进行bind
// 为什么?server端的ip和端口号,一定是众所周知的,不可改变的,client需要bind随机端口
// 为什么?因为client会非常多,不随机绑定端口容易出现端口冲突
// 2.1 填充server信息
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());
InetAddr addr(server);
ThreadData data(sockfd, addr);
Thread<ThreadData> recevier("receiver", ReceiverRoutine, data);
Thread<ThreadData> sender("sender", SenderRoutine, data);
recevier.Start();
sender.Start();
recevier.Join();
sender.Join();
close(sockfd);
return 0;
}
我们将收到的消息打印到了标准错误流,方便一会测试效果
1.4 测试
1. 先创建两个管道文件,一会要用到
- 客户端要将标准错误流重定向到这个管道文件中;
- 一个客户端对应一个管道文件,两个管道文件意味着模拟两个客户端。

2. 开始测试

- 这里就不做网络CS测试了,大家可以自行测试。
2. TCP服务站
2.1 需求分析
1. 大致分析
- 服务端要先提供一个服务列表,显示自己可以提供的服务都有什么;客户端连接服务时先提供这个服务;
- 客户端先看到服务列表后,选择服务,服务器根据用户输入匹配相应服务,这个匹配功能我们称之为路由功能,将路由功能
Push
进线程池执行; - 使用多线程提供的服务一般不是长服务(例如死循环服务),而是短服务。因为长服务会使得执行服务的线程长时间启动,不退出,对服务器的开销极大,很容易崩溃。特别是线程池,线程池中的线程一般是有上限的,达到上限服务器就当机,
- 我们想实现服务一共四个:
- 默认服务,返回服务列表;
- Ping服务,ping成功返回pong;
- Translate服务,根据用户输入的中文,翻译成英文返回;
- Transform服务,小写转大写。
2. 细节分析
- 所谓服务,就是一个一个的函数,我选择实现在主程序
Main.cc
中; - 启动服务器后,需要将所写的服务注册进服务器,这里就选择用哈希表实现注册表,
key
是服务名,value
是服务函数; - 关于
Translate
服务,这里我选择单独设计一个任务模块。我们需要先提供一个词典,用于初始化词库,将来会根据这个词库来进行翻译。
2.2 Translate任务模块------Translater.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>
#include <fstream>
#include "Log.hpp"
const std::string unknown = "unknown";
const std::string mydict = "./recource/dict.txt"; // 词典位置
const std::string sep = " "; // 分隔符
class word
{
// 音标,单词,释义, 同义词(可自己扩展,这里就不写了)
};
class Translater
{
public:
Translater(std::string dict_path = mydict)
: _dict_path(dict_path)
{
LoadDict();
Parse();
}
void LoadDict()
{
std::ifstream in(_dict_path);
std::string line;
while(std::getline(in, line))
{
_lines.push_back(line);
}
in.close();
lg.LogMessage(Debug, "Load dict txt success, path: %s\n", _dict_path.c_str());
}
// 按照分隔符切片
void Parse()
{
for(auto &line : _lines)
{
auto pos = line.find(sep);
if(pos == std::string::npos)
continue; // 没找到
else
{
std::string word = line.substr(0, pos);
std::string chinese = line.substr(pos + sep.size());
_dict.insert(std::make_pair(word, chinese));
}
}
lg.LogMessage(Debug, "Prase dict txt success\n");
}
void debug()
{
for( auto &line : _lines )
std::cout << line << std::endl;
for(auto &elem : _dict)
std::cout << elem.first << ":" << elem.second << std::endl;
}
std::string Excute(const std::string &word)
{
auto iter = _dict.find(word);
if(iter == _dict.end())
return unknown;
else
return _dict[word];
}
~Translater()
{}
private:
std::string _dict_path; // 字典txt所在文件路径
std::unordered_map<std::string, std::string> _dict; // 解析后的字典
std::vector<std::string> _lines; // 字典txt中一行一行的内容
};
2.3 服务器端------TcpServer.hpp
1. TcpServer.hpp头文件
cpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <unordered_map>
#include <functional>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <wait.h>
#include <signal.h>
#include <unistd.h>
#include "Log.hpp"
#include "nocopy.hpp"
#include "Comm.hpp"
#include "ThreadPool.hpp"
#include "InetAddr.hpp"
const static int default_backlog = 5;
using callback_t = std::function<void(int, InetAddr&)>;
using task_t = std::function<void()>;
class TcpServer : public nocopy
{
public:
TcpServer(uint16_t port)
: _port(port), _isrunning(false)
{}
void Init()
{
// 1. 创建套接字
_listensockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建TCP套接字
if (_listensockfd < 0)
{
lg.LogMessage(Fatal, "create socket errror, errno code: %d, error string: %s\n", errno, strerror(errno));
exit(Socket_Err);
}
lg.LogMessage(Debug, "create socket success, sockfd: %d\n", _listensockfd);
// 固定写法:解决一些少量绑定失败的问题 -- 后面讲到底层原理时再详细解释(这里要解决一个问题:服务端主动断开连接,再启动时bind失败的问题)
int opt = 1;
setsockopt(_listensockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
// 2. 填充本地网络信息,并绑定
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
if (bind(_listensockfd, (struct sockaddr *)&local, sizeof(local)))
{
lg.LogMessage(Fatal, "bind error, errno code: %d, error string: %s\n", errno, strerror(errno));
exit(Bind_Err);
}
lg.LogMessage(Debug, "bing socket success, sockfd: %d\n", _listensockfd);
// 3. 设置socket为监听状态,TCP特有
if (listen(_listensockfd, default_backlog)) // 第二个参数先不解释
{
lg.LogMessage(Fatal, "Listen socket error, errno code: %d, errror string: %s\n", errno, strerror(errno));
exit(Listen_Err);
}
lg.LogMessage(Debug, "listen socket success, sockfd: %d\n", _listensockfd);
ThreadNs::ThreadPool<task_t>::GetInstance()->Start();
// 先将默认服务添加进注册表
_regis.insert(std::make_pair("default", std::bind(&TcpServer::DefaultService, this, std::placeholders::_1, std::placeholders::_2)));
}
void Register(std::string str, callback_t func)
{
_regis[str] = func;
}
void DefaultService(int sockfd, InetAddr addr)
{
lg.LogMessage(Debug, "%s select %s success, fd: %d\n", addr.PrintDebug().c_str(), "default", sockfd);
(void)addr;
std::string table = "service list: \n\t |";
for (auto k : _regis)
{
table += k.first;
table += "|";
}
write(sockfd, table.c_str(), table.size());
}
void Router(int sockfd, InetAddr addr)
{
_regis["default"](sockfd, addr); // 先执行一下默认服务,显示服务列表
char buffer[1024];
ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
buffer[n] = 0;
if(n > 0)
{
for(auto &k: _regis)
{
if(k.first == buffer)
{
std::string outbuffer = "service on";
write(sockfd, outbuffer.c_str(), outbuffer.size());
k.second(sockfd, addr);
lg.LogMessage(Info, "%s client quit... sockfd: %d closed\n", addr.PrintDebug().c_str(), sockfd);
close(sockfd);
return;
}
}
std::string outbuffer = "no find";
write(sockfd, outbuffer.c_str(), outbuffer.size());
lg.LogMessage(Info, "%s client quit... sockfd: %d closed\n", addr.PrintDebug().c_str(), sockfd);
close(sockfd);
}
}
void Start()
{
_isrunning = true;
while (_isrunning)
{
// 4. 获取链接
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sockfd = accept(_listensockfd, (struct sockaddr *)&peer, &len);
if (sockfd < 0)
{
lg.LogMessage(Warning, "listen socket error, errno code: %d, error string: %s\n", errno, strerror(errno));
continue;
}
lg.LogMessage(Debug, "accept success, get a new sockfd: %d\n", sockfd);
// v5 线程池
task_t t = std::bind(&TcpServer::Router, this, sockfd, InetAddr(peer));
ThreadNs::ThreadPool<task_t>::GetInstance()->Push(t);
}
}
~TcpServer()
{
close(_listensockfd);
}
private:
uint16_t _port;
int _listensockfd;
bool _isrunning; // 是否启动
std::unordered_map<std::string, callback_t> _regis; // 注册表
};
2. 主程序Main.cc
cpp
#include "TcpServer.hpp"
#include "Comm.hpp"
#include <memory>
#include <unordered_map>
#include <functional>
#include <string>
#include <algorithm>
#include "Translater.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"
Translater translater; // 全局
// 使用手册
void Usage(std::string proc)
{
std::cout << "Usage : \n\t" << proc << " local_port\n" << std::endl;
}
// 未来的服务是部署在云服务器上的,我们怎么知道我们的服务未来在任何一个时刻,都是健康的呢?
// 我们可以定期(30s)向服务器发送最小服务请求,如果得到了回复,说明我们的服务器是正常的。
// 这种机制我们称之为心跳机制
void Ping(int sockfd, InetAddr &addr) // 不管用户输入什么都返回pong
{
lg.LogMessage(Debug, "%s select %s success, fd: %d\n", addr.PrintDebug().c_str(), "ping", sockfd);
char buffer[1024];
ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
if(n > 0)
{
std::string out = "pong";
write(sockfd, out.c_str(), out.size());
}
else if(n == 0)
{
lg.LogMessage(Info, "client quit...\n");
}
else
{
lg.LogMessage(Error, "read socket error, errno code: %d, error string: %s\n", errno, strerror(errno));
}
}
void Translate(int sockfd, InetAddr &addr)
{
lg.LogMessage(Debug, "%s select %s success, fd: %d\n", addr.PrintDebug().c_str(), "translate", sockfd);
// translater.debug(); // for debug
char wordbuf[128];
ssize_t n = read(sockfd, wordbuf, sizeof(wordbuf) - 1);
if(n > 0)
{
wordbuf[n] = 0;
std::string chinese = translater.Excute(wordbuf);
write(sockfd, chinese.c_str(), chinese.size());
lg.LogMessage(Debug, "%s translate service, %s->%s\n", addr.PrintDebug().c_str(), wordbuf, chinese.c_str());
}
}
// 字符串小写转大写
void Transform(int sockfd, InetAddr &addr)
{
lg.LogMessage(Debug, "%s select %s success, fd: %d\n", addr.PrintDebug().c_str(), "Transform", sockfd);
char buffer[128];
int n = read(sockfd, buffer, sizeof(buffer) - 1);
if(n > 0)
{
buffer[n] = 0;
std::string message = buffer;
std::transform(message.begin(), message.end(), message.begin(), [](unsigned char c){
return std::toupper(c);
});
write(sockfd, message.c_str(), message.size());
}
}
int main(int argc, char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
return Usage_Err;
}
uint16_t port = std::stoi(argv[1]);
std::unique_ptr<TcpServer> tsvr = std::unique_ptr<TcpServer>(new TcpServer(port));
tsvr->Init();
// 注册服务
tsvr->Register("ping", Ping);
tsvr->Register("translate", Translate);
tsvr->Register("transform", Transform);
tsvr->Start();
return 0;
}
2.4 实现客户端------TcpClient.cc
cpp
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <string>
#include <memory>
#include <cstdlib>
// 默认尝试重连次数
#define Retry_Count 5
using namespace std;
void Usage(const std::string& proc)
{
std::cout << "Usage : \n\t" << proc << " server_ip server_port\n" << std::endl;
}
bool VisitServer(std::string &serverip, uint16_t serverport, int *pcnt)
{
bool ret = true; // 返回值
ssize_t wn; // 接收write返回值
ssize_t rn; // 接收read返回值
std::string inbuffer; // 输入缓冲区
char buffer[1024]; // 网络缓冲区
// 1. 创建socket
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd < 0)
{
std::cerr << "socket error" << std::endl;
return false;
}
// 2. connect
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
inet_pton(AF_INET, serverip.c_str(), &server.sin_addr); // 更安全
int n = connect(sockfd, (struct sockaddr*)&server, sizeof(server)); // 自动进行bind
if(n < 0)
{
std::cerr << "connect error" << std::endl;
ret = false;
goto END;
}
*pcnt = 1; //重置cnt,注意重置的位置要在connect之后
rn = read(sockfd, buffer, sizeof(buffer) - 1);
if(rn > 0)
{
buffer[rn] = 0;
std::cout << buffer << std::endl;
}
else
{
ret = false;
goto END;
}
std::cout << "Please Enter# ";
getline(std::cin, inbuffer);
wn = write(sockfd, inbuffer.c_str(), inbuffer.size());
if(wn > 0)
{
rn = read(sockfd, buffer, sizeof(buffer) - 1);
buffer[rn] = 0;
std::cout << buffer << std::endl;
if(!strcmp(buffer, "no find")) // 没有找到对应服务就直接返回
goto END;
std::cout << "[service on] Please Enter# ";
getline(std::cin, inbuffer);
write(sockfd, inbuffer.c_str(), inbuffer.size());
rn = read(sockfd, buffer, sizeof(buffer) - 1);
if(rn > 0)
{
buffer[rn] = 0;
std::cout << buffer << std::endl;
}
else
{
ret = false;
goto END;
}
}
else if(wn == 0)
{
// donothing
}
else
{
ret = false;
goto END;
}
END:
close(sockfd);
return ret;
}
int main(int argc, char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
return 1;
}
std::string serverip = argv[1];
uint16_t serverport = stoi(argv[2]);
// 连接服务器失败时,进行Retry_Count次重连
int cnt = 1;
while(cnt <= Retry_Count)
{
bool result = VisitServer(serverip, serverport, &cnt); // 将cnt传入,方便重置
if(result)
{
break;
}
else
{
sleep(1);
std::cout << "server offline, retrying..., count : " << cnt++ << std::endl;
}
}
if(cnt >= Retry_Count)
{
std::cout << "server offline, client quit..." << std::endl;
}
return 0;
}
2.5 测试
1. 先看一下目录结构
- 字典在
recource
文件夹下:

2. 测试翻译服务

3. 测试ping服务

4. 测试transform服务

在多用户的场景下,我们写的代码也是没有问题的,大家可以自行测试
今天我们所写的TCP代码仍然是有bug的,因为没有处理数据边界。
3. 守护进程化
我们的服务器程序,一般都是运行在后台的,长时间启动,不能因为某一个终端的关闭就关闭了,所以要进行守护进程化
1. Daemon.hpp头文件
cpp
#pragma once
#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
const char *root = "/"; // 根目录
const char *dev_null = "/dev/null"; // linux下的一个文件,默认会丢弃所有向它写入的内容
void Daemon(bool ischdir, bool isclose)
{
// 1. 忽略可能引起程序异常退出的信号
signal(SIGCHLD, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
// 2. 让自己不要成为组长
if (fork() > 0)
exit(0);
// 3. 设置让自己成为一个新的会话,后面的代码其实是子进程在走
setsid();
// 4. 每一个进程都有自己的CWD,是否将当前进程的CWD更改为 / 根目录
if (ischdir)
chdir(root);
// 5. 已经变成守护进程了,不需要和用户的输入输出关联了
if (isclose)
{
close(0);
close(1);
close(2);
}
else
{
int fd = open(dev_null, O_RDWR);
if (fd > 0)
{
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
close(fd);
}
}
}
2. 修改Main.cc文件(以TCP服务站为例)
cpp
...
#include "Daemon.hpp"
Translater translater; // 全局
// 使用手册
void Usage(std::string proc)
{
std::cout << "Usage : \n\t" << proc << " local_port\n" << std::endl;
}
void Ping(int sockfd, InetAddr &addr) // 不管用户输入什么都返回pong
{
...
}
void Translate(int sockfd, InetAddr &addr)
{
...
}
// 字符串小写转大写
void Transform(int sockfd, InetAddr &addr)
{
...
}
int main(int argc, char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
return Usage_Err;
}
uint16_t port = std::stoi(argv[1]);
// 守护进程化
Daemon(true, false);
lg.Enable(ClassFile);
std::unique_ptr<TcpServer> tsvr = std::unique_ptr<TcpServer>(new TcpServer(port));
tsvr->Init();
// 注册服务
tsvr->Register("ping", Ping);
tsvr->Register("translate", Translate);
tsvr->Register("transform", Transform);
tsvr->Start();
return 0;
}
3. 测试

- 可以看到,服务端以守护进程的方式运行了,客户端可以正常访问。