全是好玩的小功能~
文章目录
- 前言
- 一、udp服务器实现网络在线英汉互译
- 二、udp服务器实现网络远程操作
- 总结
前言
在上一篇文章中我们详细的讲解了udp服务器的实现步骤,把用到的每一个接口都进行了详细的讲解,而我们在上一篇只是简单的网络通信功能,很多地方还不完善,所以在这一篇文章我们会将上一篇文章用到的代码做修改,写出一个网络版的英汉互译以及一个大型的网络聊天室。
一、udp服务器实现网络在线英汉互译
我们在一篇文章中缺少了对数据做处理这一部分,所以我们先设计如何进行文件处理:
我们先用包装器定义一个函数类型:
这句代码的意思是将返回值为void,参数为string,uint16_t,string的包装器做重命名,重命名为func_t,这样做的目的是等会处理数据的时候要用到端口号,ip等参数。然后我们在类内定义一个函数类型的对象:
然后我们让用户创建服务器的时候需要传过来一个函数方法,目的是未来让服务器去做这个方法,所以我们在构造函数初始化一下:
udpServer(const func_t &cb,const uint16_t& port,const string ip = defaultIp)
:_port(port)
,_ip(ip)
,_sockfd(-1)
,_callback(cb)
{
}
然后我们在启动服务器的最后让服务器去调用我们的回调方法,目前就先简单的让服务器将客户端的ip和端口号和发送的数据传给回调方法:
下面我们就在server.cc文件中添加一个回调方法,并且创建服务器的时候将这个方法传给服务器:
void handerMessage(string clientip,uint16_t clientport,string message)
{
//就可以对message进行特定的业务处理,而不关心message怎么来的---------server通信和业务逻辑解耦
}
// ./udpServer port
int main(int argc,char* argv[])
{
if (argc!=2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);
unique_ptr<udpServer> usvr(new udpServer(handerMessage,port));
usvr->InitServer();
usvr->start();
return 0;
}
下面我们用map来建立一个英文和中文的映射关系:
我们先写一个字典,其实就是在文件中保存英汉对应关系即可:
因为只是做测试所以我们就只写了五个单词,下一步就是将文件里的数据读入map了,由于我们读文件需要用到c++文件的接口,所以我们先定义一个静态变量保存我们刚刚创建的字典的路径:
然后我们编写初始化字典接口的代码:
static void initDict()
{
ifstream in(dictText,ios::binary);
if (!in.is_open())
{
cerr<<" open file "<<dictText<<" error "<<endl;
exit(OPEN_ERR);
}
string line;
while (getline(in,line))
{
cout<<line<<endl;
}
in.close();
}
首先打开文件的方式我们设为二进制方式,然后判断文件是否打开成功,如果不成功则打印出错并且返回错误码,这里同样增加了一个文件的错误码:
enum
{
USAGE_ERR = 1
,SOCKET_ERROR
,BIND_ERR
,OPEN_ERR
};
然后我们用一个string的对象等会将文件中读到的数据放到string对象当中,我们用getline获取文件中的每一行数据,注意getline是获取一行的函数。现在我们先实验一下看能否正确读取文件,所以我们先打印,最后记得关闭文件:
main函数中我们先不启动服务器,就只测试文件能否正常打开,可以看到是没问题的,下面我们将getline获取到的字符串插入到map中:
static bool cutString(string& line, string* s1, string* s2,const string& flag)
{
size_t sz = flag.size();
size_t pos = line.find(flag);
if (pos!=string::npos)
{
*s1 = line.substr(0,pos);
*s2 = line.substr(pos+sz);
return true;
}
return false;
}
static void initDict()
{
ifstream in(dictText,ios::binary);
if (!in.is_open())
{
cerr<<" open file "<<dictText<<" error "<<endl;
exit(OPEN_ERR);
}
string line;
string key,value;
while (getline(in,line))
{
//cout<<line<<endl;
if (cutString(line,&key,&value,":"))
{
_dict.insert(make_pair(key,value));
}
}
in.close();
}
要想插入我们就必须将读到的字符串分割为英文和中文,我们在填字典的时候用了":"做分割,所以我们可以设计一个切割字符串的函数,将参数传入,key和value是输出型参数,我们传入空的key和value到时候接口会给我们返回处理好的key和value,同时我们可以将分割符也传入,这样以后修改的时候直接修改参数就好了。下面我们写一个打印接口来测试map是否正常:
static void debugDict()
{
for (auto& dt:_dict)
{
cout<<dt.first<<" # "<<dt.second<<endl;
}
}
经过测试我们可以看到是没有问题的,下面我们开始编写服务器执行回调函数的代码:
void handerMessage(int sockfd,string clientip,uint16_t clientport,string message)
{
//就可以对message进行特定的业务处理,而不关心message怎么来的---------server通信和业务逻辑解耦
auto it = _dict.find(message);
string response_message;
if (it == _dict.end())
{
cout << "Server notfind message" << endl;
response_message = "字典内无此单词的中文";
}
else
{
response_message = it->second;
}
// 构建结构体,把处理过的数据再返回
struct sockaddr_in client;
socklen_t len = sizeof(client);
bzero(&client, sizeof(client));
client.sin_family = AF_INET;
client.sin_port = htons(clientport);
client.sin_addr.s_addr = inet_addr(clientip.c_str());
// 构建好结构体后,我们要把处理的数据发给谁呢?当然是客户端了,客户端给我们发数据我们再将处理后的数据发回给客户端
sendto(sockfd, response_message.c_str(), response_message.size(), 0, (struct sockaddr *)&client, len);
}
首先服务器进入回调函数后,我们需要先对用户发来的消息做处理,如果找不到消息我们就打印找不到消息(这里同时给服务端和客户端打印,给服务端打印的目的是让程序员添加没有录入的单词),如果找到了我们就拿到map中该消息的value值,然后我们还需要将这个处理过的消息返回给客户端(注意:不管能否找到单词对应的中文,都要将数据返回到客户端),既然要返回肯定需要客户端的ip和端口号以及我们用sendto函数需要用到的文件描述符,所以我们的回调函数应该多加一个参数用来接收文件描述符:
然后我们就可以像之前那样先构建结构体,然后在结构体中填充客户端的IP和端口号,然后将这个结构体通过sendto接口返回给客户端,因为我们处理过的消息是用string接收的,所以可以直接用string的c_str()接口和size来当sendto的第二个和第三个参数。
既然写了服务端给客户端发回消息的代码,那么我们当然需要写客户端接收服务端消息的代码,所以我们将客户端代码修改一下:
void run()
{
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);
string message;
char buffer[1024];
while (!_quit)
{
cout<<"Please Enter# ";
cin>>message;
sendto(_sockfd,message.c_str(),message.size(),0,(struct sockaddr*)&server,sizeof(server));
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
int n = recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&temp,&len);
if (n > 0)
{
buffer[n] = 0;
}
cout<<"服务器的翻译结果# "<<buffer<<endl;
}
}
上面代码不变,主要是接收数据的部分,接收数据需要用到recvfrom函数,这个函数需要文件描述符,接收的数据要存在哪个缓冲区,接收数据的类型,还有我们准备好的空结构体,这个函数的作用是这样的:服务器给我们发送了消息,那么这个接口就会将服务器的套接字信息填充到我们创建的新结构体中,所以我们不需要填充结构体,下面我们就运行起来:
通过运行结果我们可以看到程序是没有问题的,当然我们也可以支持对字典文件的热加载,什么是热加载呢?就是程序没有结束就可以加载到新添加的字典,这就类似于我们游戏里的不停机更新一样。设计原理也很简单,我们直接对终止信号做捕捉,一旦终止就不再执行终止的逻辑而是去执行初始化字典的逻辑,下面我们就试一下:
首先在main函数中对2号信号做捕捉,然后去执行reload方法,然后我们再写一下reload:
void reload(int signo)
{
(void)signal;
initDict();
}
下面我们先重新运行起来之前的代码,然后在字典中添加单词看看是否能够成功翻译:
下面我们直接添加goodman:
然后我们对服务器发送2号信号,2号信号就是我们常用的ctrl +c:
通过运行结果可以看到我们成功实现了简单的热加载。
二、udp服务器实现网络远程操作
下面我们在实现一个小功能,就是我们给服务器发一下linux下的指令,然后服务器帮我们运行指令后给我们把运行结果返回,而且我们实现起来并不困难,只需要修改我们的回调函数就可以了,下面我们就来试试:
我们首先认识一个接口popen:
popen这个函数实际上就是我们之前用的三个接口的组合版pipe + fork + exec*,还记得我们之前自己实现的命令行解析吗,就是用这三个接口完成的,下面我们讲讲popen:
第一个参数是我们未来要执行的命令字符串,比如:pwd, ls命令一样,如何执行命令呢实际上就是popen底层给我们fork创建一个子进程,然后用exec程序替换最后通过管道以文件的方式把结果给我们返回。第二个参数是以什么类型打开,比如我们文件的r,w参数一样。
void execCommand(int sockfd,string clientip,uint16_t clientport,string cmd)
{
string response;
FILE* fp = popen(cmd.c_str(),"r");
if (fp==nullptr) response = cmd + " exec failed";
char line[1024];
while (fgets(line,sizeof(line)-1,fp))
{
response+=line;
}
pclose(fp);
}
先创建一个用于返回的string类,然后我们调用popen接口,以只读的方式,如果调用失败就给返回字符串加上调用失败的信息,如果成功我们就定义一个缓冲区,然后循环式的读取popen函数给我们返回的指令运行的结果,拿到结果后我们将文件关闭即可。下面就和之前一样了,我们需要将结果返回给客户端,所以需要创建结构体:
void execCommand(int sockfd,string clientip,uint16_t clientport,string cmd)
{
string response;
FILE* fp = popen(cmd.c_str(),"r");
if (fp==nullptr) response = cmd + " exec failed";
char line[1024];
while (fgets(line,sizeof(line)-1,fp))
{
response+=line;
}
pclose(fp);
struct sockaddr_in client;
bzero(&client,sizeof(client));
socklen_t len = sizeof(client);
client.sin_family = AF_INET;
client.sin_port = htons(clientport);
client.sin_addr.s_addr = inet_addr(clientip.c_str());
sendto(sockfd,response.c_str(),response.size(),0,(struct sockaddr*)&client,len);
}
当然为了安全起见,我们应该在前面判断避免使用rm等危险的指令,因为我们写的服务器可以有多个客户端来操作我们自己linux,所以必须避免有坏人给我们做rm等指令:
当然经过测试我们还有一个小问题,那就是我们做客户端让输入的时候用的cin,而cin遇到空格就停止了没有办法输入ls -a -l这样的命令,所以我们修改一下原先的输入操作:
void run()
{
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);
string message;
char buffer[1024];
while (!_quit)
{
/* cout<<"Please Enter# ";
cin>>message; */
char commandbuffer[1024];
fgets(commandbuffer,sizeof(commandbuffer)-1,stdin);
message = commandbuffer;
sendto(_sockfd,message.c_str(),message.size(),0,(struct sockaddr*)&server,sizeof(server));
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
int n = recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&temp,&len);
if (n > 0)
{
buffer[n] = 0;
}
//cout<<"服务器的翻译结果# "<<buffer<<endl;
cout<<"服务器的执行结果# "<<buffer<<endl;
}
}
我们用fgets去输入就不用考虑空格的问题了,下面我们运行起来:
运行后我们发现缓冲区有问题,我们touch创建文件的时候是不应该给我们返回信息的,结果却把上次缓冲区的信息给我们返回了,所以我们可以改进一下,当服务器没有给我们发送指令的结果的时候,说明这个指令是无结果的,就像touch一样创建后需要自己查看,所以我们只需要在结束服务端发送的消息的时候判断一下是否返回的字节数为0,如果为0那就清空缓冲区:
上图中我们对temp结构体填充是不正确的,因为recvfrom接口会帮我们填充的,所以不需要填充。
然后我们重新编译:
可以看到我们的执行结果是没有问题的,这样就实现了两个简单的小功能,我们在下一篇文章中还会用udp服务器写一个大型的网络聊天室,通过这三个例子我相信大家可以深刻的掌握udp服务器了。