目录
Tcp网络程序
1.listen
注意:本文的所有代码已上传gitee,如果想要更好的体验请配合整体代码观看本篇文章。
其中已经用到过的知识就不细讲了,只细谈新接触到的。
上面我们提到过Tcp面向的是字节流,而udp面向的是数据报,所以在创建socket时就有区别。
将普通的套接字文件变为监听套接字。
为什么要监听呢?因为tcp是面向连接的。何为面向?可以理解为,面向对象,就是在写代码之前将对象先写出来。那面向连接呢?此时还没有建立连接,所以要将套接字文件变为监听套接字,一直处于监听状态等待他人连接。
代码 :
static void Usage(const string proc)
{
cout << "Usage:\n\t" << proc << " port [ip]" << endl;
}
class Tcpserver
{
public:
Tcpserver(uint16_t port, const string &ip = "")
: _sock(-1), _port(port), _ip(ip)
{
}
~Tcpserver()
{
}
public:
void init()
{
// 1 create cosk
_sock = socket(AF_INET, SOCK_STREAM, 0);
if (_sock < 0)
{
logMessage(FATAL, "socket: %s%d", strerror(errno), _sock);
exit(1);
}
logMessage(DEBUG, "socket create success : %d", _sock);
// 2 bind
// 2.1 填充网络信息
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
// 这里的inet_aton 和 inet_addr的用途一样
_ip.empty() ? INADDR_ANY : (inet_aton(_ip.c_str(), &local.sin_addr));
// 2.2 bind网络信息
if (bind(_sock, (const sockaddr *)&local, sizeof(local)) < 0)
{
logMessage(FATAL, "bind: %s%d", strerror(errno), _sock);
exit(2);
}
logMessage(DEBUG, "bind success: %d", _sock);
// 3 listen
if (listen(_sock, 5) < 0) // 为什么填5我们后面讲Tcp协议时讲
{
logMessage(FATAL, "listen: %s%d", strerror(errno), _sock);
exit(3);
}
logMessage(DEBUG, "listen success: %d", _sock);
}
void start()
{
while(1)
{
logMessage(DEBUG,"Server run ...");
sleep(1);
}
}
private:
int _sock;
uint16_t _port;
string _ip;
};
int main(int argc, char *argv[])
{
if (argc != 2 && argc != 3)
{
Usage(argv[0]);
exit(3);
}
uint16_t port = atoi(argv[1]);
string ip;
if (argc == 3)
{
ip = argv[2];
}
Tcpserver tcp(port);
tcp.init();
tcp.start();
return 0;
}
结果:
2.accept
返回值: 成功返回套接字也是一个文件描述符,失败-1。第一个参数为调用socket返回的文件描述符,返回值与第一参数都是文件描述符。返回值主要是为用户提供网络服务的socket,主要是IO。第一个参数主要是为了获得新的连接,监听socket。后面两个参数的意义与recevfrom后两个参数一样。
代码:
void start()
{
while (1)
{
// 4 获取连接
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int servicesock = accept(_sock, (struct sockaddr *)&peer, &len);
if (servicesock < 0)
{
logMessage(WARNING, "accept: %s%d", strerror(errno), servicesock);
continue;
}
logMessage(DEBUG, "accept success: %d", _sock);
// 4.1 获取客户端信息
int clientport = ntohs(peer.sin_port);
string clientip = inet_ntoa(peer.sin_addr);
// logMessage(DEBUG,"Server run ...");
// sleep(1);
}
}
版本①转换大小写
我们先尝试下简单的服务,将小写转换为大写。
这里接收信息为何不用recevfrom,因为recevfrom和sendto是配套提供给udp使用的,在tcp中我们使用read和write通过文件描述符来对网卡操作,毕竟linux下一切皆文件。
代码:
start:
// 5 提供服务
// 5.1 转换大小写
tranfrom(servicesock, clientip, clientport);
void tranfrom(int sock, const string &ip, int port)
{
assert(sock >= 0);
assert(!ip.empty());
char inbuffer[1024];
while (1)
{
// 首先要接受信息 我们认为读到的是字符串,因为字符串后面自动会加'/0',我们选择不读'/0'
ssize_t s = read(sock, inbuffer, sizeof(inbuffer) - 1);
if (s > 0)
{
inbuffer[s] = '\0';
if (strcasecmp(inbuffer, "quit") == 0)
{
logMessage(DEBUG, "clinet quit %s %d", ip, port);
break;
}
logMessage(DEBUG, "Infor : %s %d >>> %s", ip, port, inbuffer);
for (int i = 0; i < s; i++)
{
if (isalpha(inbuffer[i]) && islower(inbuffer[i]))
{
inbuffer[i] = toupper(inbuffer[i]);
}
}
write(sock, inbuffer, strlen(inbuffer));
}
else if (s == 0)
{
// 代表对端关闭,client退出
logMessage(DEBUG, "clinet quit %s %d", ip, port);
break;
}
else
{
logMessage(DEBUG, "%s %d read error: %s", ip, port, strerror(errno));
break;
}
}
写到这里,我们将client来写下,要不不能验证server中写的成果。
3.connect
tcpclient需要connect来连接server,因为write没有这个功能,而udpclient中直接使用sendto兼含连接与发送。
static void Usage(const string proc)
{
cout << "Usage:\n\t"
<< "server IP ,server port" << endl;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
// 获取服务端
string serverip = argv[1];
uint16_t serverport = atoi(argv[2]);
// 1 创建socket
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
cerr << "socket error : " << strerror(errno) << endl;
exit(1);
}
// 2 connect
// 2.1 填充
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);
// 3 连接
if (connect(sock, (const struct sockaddr *)&server, sizeof(server)) != 0)
{
cerr << "connect error : " << strerror(errno) << endl;
;
exit(2);
}
cout << "connect sueccess : " << sock << endl;
string message;
while (!quit)
{
message.clear();
cout << "Plz entry : ";
getline(cin, message);
if (strcasecmp(message.c_str(), "quit") == 0)
{
quit = true;
}
ssize_t s = write(sock, message.c_str(), message.size());
if (s > 0)
{
message.resize(1024);
ssize_t s = read(sock, (char *)message.c_str(), 1024);
if (s > 0)
{
message[s] = '\0';
}
cout << "server transfer"
<< ">>> " << message << endl;
}
else if (s <= 0)
{
break;
}
}
close(sock);
return 0;
}
我们来调试一下,看看结果如何
版本②多进程
为什么会出现这种情况,两个client同时去访问server,其中一个client正常发送与接受,另一个缺阻塞住了,必须等待另一个client结束才能正常输入与接受呢?
因为我们当前的server是单进程的,其中transfer的while循环一直在提供服务,进程不结束,单进程并不会给另一个client提供服务。所以我们要写一个多进程版本。
代码:
// 5.2 多进程版本
pid_t pid = fork();
assert(pid != -1);
if(pid == 0)
{
// child 注意子进程会继承父进程的文件描述符
tranfrom(servicesock, clientip, clientport);
exit(0);
}
// parent
// 一定要关闭不然文件描述符会越来越少
close(servicesock);
// 方案一 waitpid -1 WNOHANG 具体的进程细节可以看我前面的博客
// 方案二 signal
signal(SIGCHLD,SIG_IGN);
结果:
我们查看会发现有三个进程在跑,证明多进程版本成功。
进阶版本
代码:
// 5.3 多进程进阶版
// 爷爷进程
pid_t pid = fork();
if (pid == 0)
{
// 爸爸进程
if (fork() > 0)
{
// 爸爸进程退出
exit(0);
}
// 走到这里的是由爸爸进程衍生的儿子进程
// 儿子进程此时属于孤儿进程,不用管他,可以交给操作系统去回收
tranfrom(servicesock, clientip, clientport);
}
close(servicesock);
// 回收爸爸进程,阻塞式回收并不会阻塞等待父进程退出,因为父进程直接会退出
pid_t pd = waitpid(pid, nullptr, 0);
assert(pd > 0);
结果:
在多开几个也没问题
版本③多线程
代码:
class Tcpserver; // 声明一下tcp
class ThreadData
{
public:
ThreadData(uint16_t clientport, string clientip, int sock, Tcpserver *ts)
: _clientport(clientport), _clientip(clientip), _sock(sock), _this(ts)
{
}
public:
uint16_t _clientport;
string _clientip;
int _sock;
Tcpserver *_this;
};
start:
// 5.4 多线程版本
ThreadData *td = new ThreadData(clientport, clientip, servicesock,this);
pthread_t t;
pthread_create(&t, nullptr, fuc, (void *)td);
static void *fuc(void *argc)
{
pthread_detach(pthread_self());
ThreadData *td = static_cast<ThreadData *>(argc);
td->_this->tranfrom(td->_sock, td->_clientip, td->_clientport);
delete td;
return nullptr;
}
结果:
我们多开几个client去连接server
版本④线程池
我们把提供服务的任务交给线程池来做。我们将前几次写的lock.hpp(因为线程池的关系需要锁)和线程池(这个线程池是单例模式)拿过来,并写一个Task.hpp(其中包含了回调函数的细节)。
threadpool.hpp
#include "util.hpp"
#include "lock.hpp"
using namespace std;
int gThreadNum = 15;
template <class T>
class ThreadPool
{
private:
ThreadPool(int threadNum = gThreadNum) : threadNum_(threadNum), isStart_(false)
{
assert(threadNum_ > 0);
pthread_mutex_init(&mutex_, nullptr);
pthread_cond_init(&cond_, nullptr);
}
ThreadPool(const ThreadPool<T> &) = delete;
void operator=(const ThreadPool<T>&) = delete;
public:
static ThreadPool<T> *getInstance()
{
static Mutex mutex;
if (nullptr == instance) //仅仅是过滤重复的判断
{
LockGuard lockguard(&mutex); //进入代码块,加锁。退出代码块,自动解锁
if (nullptr == instance)
{
instance = new ThreadPool<T>();
}
}
return instance;
}
//类内成员, 成员函数,都有默认参数this
static void *threadRoutine(void *args)
{
pthread_detach(pthread_self());
ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
// prctl(PR_SET_NAME, "follower"); // 更改线程名称
while (1)
{
tp->lockQueue();
while (!tp->haveTask())
{
tp->waitForTask();
}
//这个任务就被拿到了线程的上下文中
T t = tp->pop();
tp->unlockQueue();
t(); // 让指定的先处理这个任务
}
}
void start()
{
assert(!isStart_);
for (int i = 0; i < threadNum_; i++)
{
pthread_t temp;
pthread_create(&temp, nullptr, threadRoutine, this);
}
isStart_ = true;
}
void push(const T &in)
{
lockQueue();
taskQueue_.push(in);
choiceThreadForHandler();
unlockQueue();
}
~ThreadPool()
{
pthread_mutex_destroy(&mutex_);
pthread_cond_destroy(&cond_);
}
int threadNum()
{
return threadNum_;
}
private:
void lockQueue() { pthread_mutex_lock(&mutex_); }
void unlockQueue() { pthread_mutex_unlock(&mutex_); }
bool haveTask() { return !taskQueue_.empty(); }
void waitForTask() { pthread_cond_wait(&cond_, &mutex_); }
void choiceThreadForHandler() { pthread_cond_signal(&cond_); }
T pop()
{
T temp = taskQueue_.front();
taskQueue_.pop();
return temp;
}
private:
bool isStart_;
int threadNum_;
queue<T> taskQueue_;
pthread_mutex_t mutex_;
pthread_cond_t cond_;
static ThreadPool<T> *instance;
// const static int a = 100;
};
template <class T>
ThreadPool<T> *ThreadPool<T>::instance = nullptr;
Task.hpp
class Task
{
public:
//等价于
// typedef std::function<void (int, std::string, uint16_t)> callback_t;
using callback_t = std::function<void (int, std::string, uint16_t)>;
private:
int sock_; // 给用户提供IO服务的sock
uint16_t port_; // client port
std::string ip_; // client ip
callback_t func_; // 回调方法
public:
Task():sock_(-1), port_(-1)
{}
Task(int sock, std::string ip, uint16_t port, callback_t func)
: sock_(sock), ip_(ip), port_(port), func_(func)
{}
void operator () ()
{
logMessage(DEBUG, "线程ID[%p] 处理%s:%d的请求正在进行中......",\
pthread_self(), ip_.c_str(), port_);
func_(sock_, ip_, port_);
logMessage(DEBUG, "线程ID[%p] 处理%s:%d的请求已经结束了......",\
pthread_self(), ip_.c_str(), port_);
}
~Task()
{}
};
注意现在tranfrom函数放在了类的外面。
// 在init函数中初始化线程池
init:
// 4 加载线程池
// 当前的线程池是单例模式
_tp = ThreadPool<Task>::getInstance();
// 在start函数中运行线程池
start:
// 启动线程池
_tp->start();
// 5.5 线程池版本
Task t(servicesock, clientip, clientport, tranfrom);
_tp->push(t);
这几步的目的是初始化线程池,运行线程池,将Task任务传入线程池中,调用Task任务中的回调函数。
结果:
使用线程池的结果就是每一个处理任务的线程都是独立不同的线程,但是现在还是具有缺点,因为线程池中的线程个数有限,并且这次的任务tranfrom是个死循环,如果一直申请任务,任务不退出,最后就不能申请任务了。
版本⑤远程获取服务端shell信息版本
这次我们要用到一个新的函数。
作用是输入命令,将命令执行完的结果保存在文件中。
代码:
start:
// 5.6 远程获取服务端shell信息版本
Task t(servicesock, clientip, clientport, execCommand);
_tp->push(t);
void execCommand(int sock, string ip, uint16_t port)
{
assert(sock >= 0);
assert(!ip.empty());
char command[1024];
while (1)
{
// 首先要接受信息
ssize_t s = read(sock, command, sizeof(command) - 1);
if (s > 0)
{
command[s] = '\0';
string safe = command;
// 防止恶意操作
if ((string::npos != safe.find("rm")) || (string::npos != safe.find("unlink")))
{
break;
}
FILE *fp = popen(command, "r");
if (fp == nullptr)
{
logMessage(FATAL, "popen fail , command is : %s ,because : %s ", command, strerror(errno));
break;
}
logMessage(DEBUG, "%s %d 执行的任务是:[%s] ", ip.c_str(), port, command);
char line[1024];
while (fgets(line, sizeof(line) - 1, fp) != nullptr)
{
// 将文件的内容写到line中,再从line中写到servicesock中
write(sock, line, strlen(line));
}
pclose(fp);
logMessage(DEBUG, "%s %d 的[%s]任务执行完毕 ", ip.c_str(), port, command);
}
else if (s == 0)
{
// 代表对端关闭,client退出
logMessage(DEBUG, "clinet quit %s %d", ip.c_str(), port);
break;
}
else
{
logMessage(DEBUG, "%s %d read error: %s", ip.c_str(), port, strerror(errno));
break;
}
}
// client退出会走到这里
// 将提供服务的文件描述符关掉
close(sock);
}
结果:
版本⑥守护进程
一般而言,对外提供的服务器,都是以守护进程(精灵进程)的方式在工作,除非用户手动关闭,否则一直会在运行。
这里的PPID是父进程id,PID是这个进程的id,PGID是进程组的id,此次重要的是SID,SID为当前进程的会话id。
PGID是运行的一组进程的第一个进程的PID。
当前这几个进程都属于PGID为28753的这个进程组。
那什么又是会话呢?
在我们登陆linux或者windows时,linux服务器就会形成一个叫会话的东西,它是由一个前台进程组(必须要有)和0个或多个后台进程组构成。
有时当window特别卡的时候,我们可以选择注销再登陆,这样电脑就不卡了。背后的原因有注销相当于将当前会话中的进程都删掉,这样就不会卡了。
一般而言,一个会话中的初始进程都是bash,这个会话的id就是bash的pid。
最后的进程为bash进程,sleep这几个进程属于bash这个会话中的进程,所以sid与bash的pid相同。
我们今天要做的是将我们的服务器单独形成一个新的会话,不然在原先的会话中会受到用户登陆 注销对会话的影响。
像这种自成进程组,自成新会话,周而复始的去运行的进程,就叫做守护进程或者精灵进程。
如何让进程变为守护进程呢?我们要使用setsid并做其他手段。
哪一个进程调用这个函数就会变成守护进程,返回值为该进程的pid,前提是该进程不能是进程组的组长,就是该进程的进程组id不能与该进程的id相同。
在使用setsid之前我们要做到:
必做:
①用子进程调用setsid
②close(0,1,2),将标准输出标准输入标准错误对应的文件描述符都关闭,一旦成为守护进程就跟键盘显示屏输入无关了,因为输入和输出都是从网络中写入或获取的。(但这种方法很少有人做)。
③打开/dev/null,并且进行对0,1,2的重定向。(/dev/null是一个linux下的垃圾桶,凡是从/dev/null里面读写一概被丢弃)。
选做:
①忽略SIGPIPI信号,因为server在作为写端写的时候如果client关闭管道,会导致client发送SIGPIPE信号向server,导致server关闭,我们要想server不被影响,可以用signal来使server忽略SIGPIPE。
②更改进程的工作目录,chdir。
代码: 在main函数中调用init和start之前调用daemonize
void daemonize()
{
// 1. 忽略SIGPIPE信号
signal(SIGPIPE, SIG_IGN);
// 2. 改变当前工作目录
// chdir() 这次就不做了
// 3. 让自己成为进程组组长
if (fork() > 0)
exit(1);
// 4. 调用setsid
setsid();
// 5. 重定向0,1,2
int fd = 0;
if ((fd = open("/dev/null", O_RDWR)) != -1)
{
dup2(fd,STDIN_FILENO);
dup2(fd,STDOUT_FILENO);
dup2(fd,STDERR_FILENO);
// 6. 关闭fd
if(fd > STDERR_FILENO) close(fd);
}
}
结果:
我们的tcpserver犹如sshd一样成了一个守护进程。再次运行clinet也是可以的。
现在会有问题,我们的日志呢?全都到垃圾桶去了,我们需要将日志输入到日志文件中。我们需要将打印日志的函数修改一下。
代码:
#define LOGFIFE "tcpserver.log"
const char *log_level[] = {"DEBUG", "NOTICE", "WARNING", "FATAL"};
void logMessage(int level, const char *format, ...)
{
assert(level >= DEBUG);
assert(level <= FATAL);
char logInfor[1024];
char *name = getenv("USER");
va_list ap;
va_start(ap, format);
vsnprintf(logInfor, sizeof(logInfor) - 1, format, ap);
va_end(ap);
umask(0);
int fd = open(LOGFIFE, O_WRONLY | O_CREAT | O_APPEND,0666);
FILE * out = (level == FATAL) ? stderr : stdout;
dup2(fd,1);
dup2(fd,2);
fprintf(out,"%s | %u | %s | %s\n",\
log_level[level],\
(unsigned int)time(nullptr),\
name == nullptr ? "Unkown" : name,\
logInfor
);
// 将C缓冲区中的内容刷新到os中
fflush(out);
// 将os中的数据尽快刷到磁盘中
fsync(fd);
}
结果:
如何证明它是守护进程?
像其他与会话紧密相连的进程不是守护进程的进程。 在断开连接之后重新登陆,在未断开连接之前运行的进程就无了。
而守护进程只要开始运行,就与世界无关了,独立的会话独立的进程组,除非用kill -9将该进程删除,不让会一直运行。
如何不使用系统提供的daemon和自己手动去让进程变为守护进程的方法来让一个进程变为守护进程,可以使用
可以发现当前的nohup.out所占的空间在不断变大,打开就是a.out输出到stdout的信息。
查看它的sid发现还是和bash是同一个会话组的。
再次重启之后,发现a.out还在运行并且自成一个会话。
至于一些关于打印与服务器安全退出的杂项这就不讲了,有兴趣的可以看下我的gitiee。
问题一:
如果将服务器退出,客户端还在链接,再次将服务器重新启动会发现客户端链接不上重新启动之后的服务器。
问题二:
listen的第二个参数在本片文章中没有讲。
问题三:
popen在输入不识别的命令时,不会返回NULL,还是会打开文件但是不会文件结束标识,会导致客户端阻塞。
这三个问题后续都会讲到,请耐心等待。
版本⑦网络计算器
先前的版本信息之间的传输都是按“字符串”的方式来接收与发送的,如果接收与发送的信息是结构化的数据怎么办呢?
可不可以这样,以计算数字为例:
这样不挺好的吗?
弊端就是每一个系统之间的结构体对齐规则不一样,当前对象所占的大小为12字节,如果客户端将数据解释为10字节,就会造成数据丢失。
当前我们可以将这个结构体转换成“字符串”并制定规则,由对方接收。
将结构化的数据转化为字符串或者字节流的方式叫做序列化。
相反则叫做反序列化。
除了将结构化的数据转化为字符串我们还要将字符串的长度加在序列化之后的字符串的首字母之前。至于为什么我如果连续向通信管道之中发送数据,一堆信息都被对面读取但是在反序列化时不知道将多少数据作为一条消息还是所有的信息都是一条,在每一条消息发送之前带上信息大小便于更好地反序列化。这些过程就是在制定协议。
至于要做网络计算器,我们首先搭出部分框架。
客户端:main函数中建立连接之后
// 4 计算
string message;
while (!quit)
{
message.clear();
cout << "请输入表达式: ";
getline(cin, message);
if (strcasecmp(message.c_str(), "quit") == 0)
{
quit = true;
continue;
}
Request req;
if (!buyRequest(message, &req))
{
continue;
}
string str;
req.serialize(&str);
cout << "message->serialize: " << str << endl;
str = encode(str, str.size());
cout << "str->encode: " << str << endl;
ssize_t s = write(sock, str.c_str(), str.size());
if (s > 0)
{
char buff[1024];
size_t s = read(sock, buff, sizeof(buff) - 1);
if (s > 0)
{
buff[s] = 0;
string package = buff;
cout << "Debug->getmessage: " << package << endl;
Responce resp;
uint32_t len = 0;
string str = decode(package, &len);
if (len > 0)
{
package = str;
cout << "Debug->decode: " << package << endl;
resp.Deserialize(package);
cout << " result : " << resp._result << " "
<< " resultcode : " << resp._resultcode << endl;
}
}
}
else if (s <= 0)
{
break;
}
}
服务端:在主函数中依据前面线程池调用回调函数的方法
void netcalculate(int sock, string ip, uint16_t port)
{
assert(sock >= 0);
assert(!ip.empty());
Request req;
string inbuff;
while (1)
{
char buff[128];
int sz = read(sock, buff, sizeof(buff));
if (sz == 0)
{
// 代表对端关闭,client退出
logMessage(DEBUG, "clinet quit %s %d", ip.c_str(), port);
break;
}
else if (sz < 0)
{
logMessage(DEBUG, "%s %d read error: %s", ip.c_str(), port, strerror(errno));
break;
}
buff[sz] = 0;
inbuff += buff;
// 例如:9\r\n888 + 666\r\n
// 1 检查是否接收到有一个完整的序列化后的字符串
uint32_t packagelen = 0;
string package = decode(inbuff, &packagelen);
// 无法读取一个完整的序列化后的字符串,重新读取
if (packagelen == 0)
continue;
// 上一步得到了len,将字符串最前端的数字去掉
// 2 反序列化
if (req.Deserialize(package))
{
req.debug();
// 3 逻辑处理 得出算数结果
Responce respon = calculate(req);
// 4 序列化得到的结果
string respPackage;
respon.serialize(&respPackage);
// 5 encode序列化后的字符串
respPackage = encode(respPackage,respPackage.size());
// 6 发送
write(sock,respPackage.c_str(),respPackage.size());
}
}
}
下面是序列化反序列化等函数的实现:
#define CRLF "\r\n"
#define CRLFLEN strlen(CRLF)
#define SPACE " "
#define SPACELEN strlen(SPACE)
#define OPS "+-*/%"
static Responce calculate(const Request &req)
{
Responce resp;
switch (req._opr)
{
case '+':
resp._result = req._x + req._y;
break;
case '-':
resp._result = req._x - req._y;
break;
case '*':
resp._result = req._x * req._y;
break;
case '/':
{
if (req._y == 0)
{
resp._resultcode = 1; // 1 除零错误
resp._result = INT32_MAX;
}
else
resp._result = req._x / req._y;
break;
}
case '%':
{
if (req._y == 0)
{
resp._resultcode = 2; // 2 模零错误
resp._result = INT32_MAX;
}
else
resp._result = req._x % req._y;
break;
}
default:
resp._resultcode = 3; // 非法输入
break;
}
return resp;
}
// encode 为整个序列化之后的字符串添加长度
string encode(const string &in, uint32_t len)
{
// "_resultcode _result" -> "len\r\n_resultcode _result\r\n"
string ret = to_string(len);
ret += CRLF;
ret += in;
ret += CRLF;
return ret;
}
// 1 可以作为检查函数,检查有没有读取到完整序列化并encode过的字符串
// 必须有完整长度 具有与长度相符的有效载荷
// 2 由上述条件可以返回有效载荷和有效长度
// decode 为整个序列化之后的字符串提取长度
// 9\r\n888 + 666\r\n \r\n88981\n
string decode(string &in, uint32_t *len)
{
assert(len);
// 初始检查
*len = 0;
size_t pos = in.find(CRLF);
if (pos == string::npos)
return "";
// 1 提取长度
string slen = in.substr(0, pos);
int ilen = atoi(slen.c_str());
// 2 确认有效载荷是否符合要求
int sz = in.size() - 2 * CRLFLEN - pos;
if (sz < ilen)
return "";
// 3 提取888 + 666
string package = in.substr(pos + CRLFLEN, ilen);
*len = ilen;
// 4 将提取完毕的字符串从读取的字符串删除便于下次decode
int remveLen = slen.size() + 2 * CRLFLEN + package.size();
in.erase(0, remveLen);
// 5 返回
return package;
}
// 定制请求
class Request
{
public:
Request()
{
}
~Request()
{
}
// 序列化
void serialize(string *out)
{
string x = to_string(_x);
string y = to_string(_y);
*out = x;
*out += SPACE;
*out += _opr;
*out += SPACE;
*out += y;
}
// 反序列化
bool Deserialize(const string &in)
{
// 888 + 666
size_t firpos = in.find(SPACE);
if (firpos == string::npos)
return false;
size_t secpos = in.rfind(SPACE);
if (secpos == string::npos)
return false;
string date1 = in.substr(0, firpos);
string date2 = in.substr(secpos + SPACELEN);
string opr = in.substr(firpos + SPACELEN, secpos - (firpos + SPACELEN));
if (opr.size() != 1)
return false;
_x = atoi(date1.c_str());
_y = atoi(date2.c_str());
_opr = opr[0];
return true;
}
void debug()
{
cout << "---------------------------------------------" << endl;
cout << " _x: " << _x << " _opr: " << _opr << " _y: " << _y<<endl;
cout << "---------------------------------------------" << endl;
}
public:
int _x;
char _opr;
int _y;
};
// 定制响应
class Responce
{
public:
Responce() : _resultcode(0), _result(0)
{
}
~Responce()
{
}
// 序列化
void serialize(string *out)
{
// "_resultcode _result"
string code = to_string(_resultcode);
string res = to_string(_result);
*out = code;
*out += SPACE;
*out += res;
}
// 反序列化
bool Deserialize(const string &in)
{
// "_resultcode _result"
size_t pos = in.find(SPACE);
if (pos == string::npos)
{
return false;
}
string rcode = in.substr(0, pos);
string result = in.substr(pos + SPACELEN);
_resultcode = atoi(rcode.c_str());
_result = atoi(result.c_str());
return true;
}
void debug()
{
cout << "---------------------------------------------" << endl;
cout << " result: " << _result << " resultcode: " << _resultcode << endl;
cout << "---------------------------------------------" << endl;
}
public:
int _resultcode;
int _result;
};
bool buyRequest(string &msg, Request *req)
{
// 将1+1输入的字符串存储到对象中
char buff[1024];
snprintf(buff, sizeof(buff), "%s", msg.c_str());
char *left = strtok(buff, OPS);
if (left == nullptr)
{
return false;
}
char *right = strtok(nullptr, OPS);
if (right == nullptr)
{
return false;
}
char mid = msg[strlen(left)];
req->_x = atoi(left);
req->_y = atoi(right);
req->_opr = mid;
return true;
}
写了这么多看下实战效果。
服务端:
客户端:
写了这么长代码,是否有简单的方式就将结构化的数据转换成便于发送的数据呢,我们接下来可以使用下json,这是别人的方案。在使用json时,它作为一个独立的第三方库,我们要使用
sudo yum install -y jsoncpp-devel
来下载使用。
注意makefie中:编译时要带上-ljsoncpp,否则会导致连接出错。
代码:
request:
// #define MYSOLUTION 1
void serialize(string *out)
{
#ifdef MYSOLUTION
string x = to_string(_x);
string y = to_string(_y);
*out = x;
*out += SPACE;
*out += _opr;
*out += SPACE;
*out += y;
#else
// 使用json
// 1 value对象是万能对象 什么类型都能接收
// 2 json是基于k-v
// 3 json有两套方法
// 4 json会将数据结合转换为字符串
Json::Value root;
root["x"] = _x;
root["y"] = _y;
root["opr"] = _opr;
Json::FastWriter fw;
*out = fw.write(root);
#endif
}
// 反序列化
bool Deserialize(const string &in)
{
#ifdef MYSOLUTION
// 888 + 666
size_t firpos = in.find(SPACE);
if (firpos == string::npos)
return false;
size_t secpos = in.rfind(SPACE);
if (secpos == string::npos)
return false;
string date1 = in.substr(0, firpos);
string date2 = in.substr(secpos + SPACELEN);
string opr = in.substr(firpos + SPACELEN, secpos - (firpos + SPACELEN));
if (opr.size() != 1)
return false;
_x = atoi(date1.c_str());
_y = atoi(date2.c_str());
_opr = opr[0];
return true;
#else
// 使用json
Json::Value root;
Json::Reader rd;
rd.parse(in, root);
_x = root["x"].asInt();
_y = root["y"].asInt();
_opr = root["opr"].asInt();
return true;
#endif
}
Responce:
void serialize(string *out)
{
#ifdef MYSOLUTION
// "_resultcode _result"
string code = to_string(_resultcode);
string res = to_string(_result);
*out = code;
*out += SPACE;
*out += res;
#else
// json
Json::Value root;
root["resultcode"] = _resultcode;
root["result"] = _result;
Json::FastWriter fw;
*out = fw.write(root);
#endif
}
// 反序列化
bool Deserialize(const string &in)
{
#ifdef MYSOLUTION
// "_resultcode _result"
size_t pos = in.find(SPACE);
if (pos == string::npos)
{
return false;
}
string rcode = in.substr(0, pos);
string result = in.substr(pos + SPACELEN);
_resultcode = atoi(rcode.c_str());
_result = atoi(result.c_str());
return true;
#else
// json
Json::Value root;
Json::Reader rd;
rd.parse(in, root);
_resultcode = root["resultcode"].asInt();
_result = root["result"].asInt();
return true;
#endif
}
结果:
将Json::FastWriter fw改为Json::StyledWriter fw。这是另外一种方案。
如何随时改变关于序列化与反序列化的方案,改变宏是一种方法,在makefie中增加内容也是一种方法。
代码:
makefile:
.PHONY:all
all:tcpclient tcpserver
Method=-DMYSOLUTION
tcpclient:tcpclient.cc
g++ -o $@ $^ $(Method) -std=c++11 -ljsoncpp -lpthread
tcpserver:tcpserver.cc
g++ -o $@ $^ $(Method) -std=c++11 -ljsoncpp -lpthread
.PHONY:clean
clean:
rm -f tcpclient tcpserver tcpserver.log
现在默认使用我们的solution。
结果:
将Mothed屏蔽。
写了这么多,总共包括
1.基本系统socket套接字的使用(listen等)
2.基本的协议定制(序列化等)
3.业务的实现(计算器等)
写到这里关于Tcp网络程序的编写也告一段落了,不知道是否有人会看到这里,感谢观看,我们下次再见。这次的Tcp网络程序其中还有很多BUG限于时间和学识的约束,以后会有机会来完善它的。