下面我们先对服务器端进行封装。
一、简单回显服务器
我们要实现一个简单回显服务器和客服端的数据回显,客户端进行发送消息,服务器收到消息后,再将这个消息回显给我们的用户端。
就类似我们在Linux上使用的echo指令。
整个程序的核心就是套接字的相关接口还有使用UDP协议进行实现。
首先我们需要两个部分,一个是服务器端,用户端。

如上所示。
下面我们先对服务器端进行封装。
EchoServer.hpp
1、初始化Init
首先,对于一个服务器,我们初始时要对其进行初始化操作,然后再去启动,那么我们需要一个Init接口,然后初始化完成,我们再进行启动,所以还需要一个启动接口:Start
前面我们学习系统部分的时候讲到,Linux下一切皆文件,不论我们是使用UDP还是使用TCP,那么我们进行网络通信前,都要将网络文件先打开,那么我们要打开一个网络文件要使用下面这个接口:

要使用其要包含头文件<sys/socket.h>
下面我们来看其返回值:
其返回值是一个整数,那么其返回值:打开成功那么就返回的是一个文件描述符fd。
然后失败返回-1并且会设置其错误信息。
下面是对其参数的认识:
首先是domain,其是一个域,其实就是我们未来创建这个网络文件,这个文件的使用范围,就比如我们是要进行本地通信还是跨网络通信

如上所示,如果我们想要的是本地通信,那么就传入AF_UNIX
如果是网络通信那么就传入AF_INET。其他的我们不需要太过关心。
然后是type:
这个参数就是确定我们通信的类型,就比如说是我们的字节流还是用户数据报的方式。

第三个参数,就是我们要使用的那种协议,就比如是使用UDP协议还是TCP协议,但是前面,第二个参数设置好了,那么通信的协议基本就确定了,所以一般这个参数我们设置为0.
所以对于socket的参数我们设置的也基本是固定的:
第一个参数我们设置为AF_INET,第二个参数就在S0CK_STREAM和SCOK_DGRAM上选择,第三个参数就设置为0即可。
所以就只有第二个参数是变化的,那么其第一个就是我们的TCP第二个就是UDP。
所以未来,我们的服务器端,需要一个文件描述符,表示我们打开的网络文件的fd。

如上,我们就可以将一个网络文件套接字创建了,那么对于错误信息的输出,我们可以引入前面我们写的日志系统进行打印。

那么我们上面将创建好的网络文件的文件描述符进行打印,那么其文件描述符肯定为3:

那么我们初始化,首先要创建一个套接字socke他,那么其本质上就是创建一个网络文件。
那么我们的网络有其IP信息还有其端口号,那么我们也要将其进行设置,那么我们的成员变量还要设置一个ip和端口号,但是有个问题就是,我们前面所看到的端口号是:192.1.1.0这种样式的,这个叫做字符串类型的IP,但是我们发现其是由4个整数和三个点组成的,这种IP地址为字符串风格的点分十进制IP地址,是让人看的。那么其大小就要达到32字节了,所以在网络端其是会转换成4字节IP的。

如上就是我们的IP和端口号,对于服务器来说其_port必须是固定的。
这个是由用户进行设置的。
未来在使用上,我们希望其和我们在Linux下使用一样,./server_udp ip port这个样式进行使用,那么,在我们的主程序中,我们可以如下进行设置:

这个是描述网络socket的结构体,可以看到其中包括了sin_port这个就是我们的端口号,然后还有的就是sin_addr这个就是我们的id。后面是sin_zero是填充字段,我们不需要关注。
然后其还包含了AF_INET,但是有个疑问了,就是我们创建socket的时候,就已经传入了AF_INET了,那么为啥我们这边也要有呢?
这是给我们后续使用别的函数接口的时候进行判断使用的。
但是我们发现上面的结构体并没有出现AF_INET,其实是在第一个那个宏定义中:

其就被替换成下面的东西,然后上面的##,在宏中,其会将两边的进行拼接,那么我们传入的sin_,那么就会被替换成sin_family,那么总的就是sa-family_t sin_famliy。
这个数据类型实际上就是一个无符号长整型。
所以我们对于网络信息的填充如下:

然后有个问题,就是这个数据,是否已经存在我们打开的网络文件呢?
实际上并没有,所以下面我们还需要将这个信息和我们打开的socket进行绑定。
首先,发送方的IP和port都要带入到网络中 ,所以我们在服务器端,要将IP和port进行处理。
如下:

下面进行socket和网络信息进行绑定。
要使用下面这个接口:

首先,第一个参数,就是我们要进行绑定的socket,返回值一样成功返回0,失败返回-1,并且设置错误信息。
第二个参数,我们发现是一个struct sockaddr类型的结构体,那么我们需要将我们的信息进行强制类型转换,第三个参数就是我们的长度。
如下:

所以总的初始化代码如下:
void Init()
{
// 第一步创建套接字socket,本质就是打开一个网络文件
_socketfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_socketfd < 0)
{
LOG(LogLevel::FATAL) << "create socket error";
exit(SOCKET_ERR);
}
LOG(LogLevel::INFO) << "create scoket success,socket:" << _socketfd;
// 第二步,设置网络信息IP,
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_addr.s_addr = inet_addr(_ip.c_str());
local.sin_port = htons(_port);
// 第三步,将信息设置到socket中
int n = bind(_socketfd, (struct sockaddr *)&local, sizeof(local));
if(n<0)
{
LOG(LogLevel::FATAL)<<"create socket error";
exit(BIND_ERR);
}
LOG(LogLevel::INFO)<<"bind socket success,ip:"<<_ip<<"port:"<<_port;
}
2、运行Start
在服务器运行中,其实际上是一个死循环,但是对于数据的发送和接收,其不再是前面的字节流文件流等,所以我们使用下面的函数:

首先是其返回值,就是我们所读到多少字节的数据。失败就返回-1。
下面我们来看其参数:
第一个参数:scokfd,就是我们要绑定的那个网络文件。
第二个参数:其是一个缓冲区,未来就是用来存放我们收到的数据的,实际上是一个void*类型 的,那么其未来可以接收不同类型的数据。
第三个参数:size_t len就设置为我们缓冲区的长度即可。
第四个参数:就是我们要其阻塞读取还是非阻塞读取,我们就设置为0,阻塞读取即可。
第五个参数:和上面使用bind的类似。用来获取对方ip和port的数据
第六个参数:和bind的第三个参数一样
补充:
未来我们获取对方的发送的信息的时候,那么我们不仅需要信息本身,还需要获取到发送方是谁,方便后续回复,所以需要第五个参数,获取发送方的IP和prot。
代码如下:

对于收信息错误,我们并不需要退出。上面我们对于数据,我们先认为传递的是字符串。
那么我们服务器端,接收到消息,那么就要将这个消息发送给接收方用户。
那么就要使用到下面这个接口:

首先其返回值,其失败的话,也是返回-1,成功那么就会返回发送的字节数。
对于发送数据的问题,有下面三个疑问:
首先是我们要发送什么数据,然后我们要发送给谁,如何发送给对方。
那么在这个发送函数中,也要我们传入一个缓冲区,就是第二个参数。
那么发送给谁,那么就是发送给对方的IP+prot。那么就是最后两个参数。
发送的方式,是通过对应的sockfd决定。
我们接收到消息的时候,对方也传入了其对应的IP和prot,所以我们可以如下使用:
代码如下:
void Start()
{
char inbuffer[1024];
while (true)
{
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
ssize_t n=recvfrom(_socketfd,inbuffer,sizeof(inbuffer)-1,0,(struct sockaddr*)&peer,&len);
if(n>0)
{
inbuffer[n]=0;
LOG(LogLevel::DEBUG)<<"cilent say#"<<inbuffer;
std::string echo_string="server echo#";
echo_string+=inbuffer;
sendto(_socketfd,echo_string.c_str(),echo_string.size(),0,(struct sockaddr*)&peer,len);
}
else
{
LOG(LogLevel::ERROR)<<"recvform error";
}
}
}
如上,对于服务器端的设计就大致完成了。
EchoClient.cc
首先,我们的客户端需要进行网络通信,那么其也需要创建一个网络文件,那么其也得进行网络文件的创建:

那么我们的用户端的socket就创建好了,下面我们的用户端也要有其对应的IP和port,这是在网络中标识自己的唯一标志。
但是要注意的是,我们对于客户端的bind,不要去显示bind,这是为啥呢?
因为对于服务端,其未来是1:n的情况,那么我们服务端的端口号一般是固定的。
但是对于用户端,我们对其是多少并不关心,其只需要存在就可以。
并且未来用户端也会打开多个服务端,那么如果显示绑定,那么可能会出现端口号冲突的问题。
所以对于用户端,我们使用随机端口的方式,由OS进行选择。
而且我们发现,日常使用软件的时候,都是先给服务端进行数据发送,所以OS底层会隐式自动进行获取随机端口,然后进行bind IP和prot。
下面我们完成对用户端的消息发送:
首先我们要获取到用户的输入:

但是有个问题,我们得知道要进行通信的服务器,所以这个要我们的用户进行输入,所以在使用这个程序前,我们要告诉用户输入对于服务器的IP和prot。
但是有个问题,就是我们用户又是如何知道对方的IP和prot的呢?
这个信息是内置到客户端中的,是直接包含在内的。所以我们是直接进行硬编码的。
上面我们就把发送方式,还有发送的数据是啥搞定,下面就是发送给谁了,所以我们还需要一个struct socket_in 结构体变量。

未来,我们的发送方要将服务端的IP和prot也要发送给服务器,所以其IP和prot也要转换为网络序列的。
给服务器端发送完数据后,那么就要接收服务器端发送来的数据了。

下面我们来测试两个端口是否能够进行通信。
首先,我们先来进行本地通信,那么在对于本地通信,那么在输入IP的时候,要设置为127.0.0.1
然后对于端口号,我们可以先随便设置,下面我们设置为8080。

上面我们使用的127.0.0.1IP地址,其是本地环回地址,其只是将数据在协议栈中走一遍,不会经过网络。
可以用于本地通信或者网络代码的测试。
那么我们要通过网络进行数据交流又如何操作呢?
首先,我们先补充下面几个问题:
1、云服务器的公网IP,是禁止被显示bind的
2、在服务器开发的时候,不建议用户显示bind的IP地址
3、云服务器的公网IP,是禁止被bind的,但是并不是不能被使用
在我们的云服务器中,是存在多个IP地址的,如果我们的服务器bind了IP,那么其未来只能和这个IP的机器进行通信了,但是未来我们的服务器是要和多个IP的用户进行通信的,所以这也是为啥不能显示bind其IP。
所以我们是需要服务器和任意IP绑定,主要其端口号一样即可。

所以我们将服务器端的IP绑定的IP进行设置为INADDR_ANY。
那么下面我们再来运行看看:

可以看到服务器端和用户端此时可以进行通信
在我们的服务器端,我们也要将用户端的IP和prot获取下来,所以我们在收到用户数据的时候要在服务器端进行获取。

未来,我们的服务器是会被多个用户访问的,所以我们可以将这些用户进行管理。
下面我们来认识一个指令:
netstat
那么其可以把我们当前机器中和网络和通信相关的信息打印出来。
其有如下选项:

二、简单的英译汉的⽹络字典
上面我们实现的是实现网络中进行通信,但是没有任何的业务,所以下面我们将上面的,对于数据进行翻译处理。
上面,我们对于收到数据,对于数据的处理也放在了服务器端,那么下面,我们想让服务器只负责数据的收发,而数据的处理交给别的层次去进行。
代码如下:
#pragma once
#include <iostream>
#include <string>
#include <unordered_map>
using namespace NS_LOG_MODULE;
static const std::string default_dict = "./dict.txt";
static const std::string sep = ": "; // 分隔符
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";
auto pos = line.find(sep);
if (pos == std::string::npos)
{
LOG(LogLevel::WARNING) << "Format:" << line << "success";
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();
}
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;
};
三、多用户的服务器
上面我们的服务器和用户端,我们发现用户端都是单用户的,所以下面我们可以通过多线程的方式实现。
那么我们服务器端,将用户进行管理起来,多各个用户进行任何的收发。
我们可以将前面写的线程池引入,我们的服务器端将数据发送到线程池,通过线程池进行任务的转发。
后续我们服务端,对于用户端的管理,我们对于IP和prot进行管道,来确定用户的唯一性。
代码如下:
#pragma onec
#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)
{
if (!SearchUser(addr))
{
return;
}
for (auto iter = _users.begin(); iter != _users.end(); iter++)
{
if (*iter == addr)
{
_users.erase(iter);
break;
}
}
}
bool SearchUser(const InetAddr &addr)
{
for (auto &users : _users)
{
if (users == addr)
{
return true;
}
}
return false;
}
bool ModUser(const InetAddr &addr)
{
DelUser(addr);
AddUser(addr);
}
~UserManager()
{
}
private:
std::vector<InetAddr> _users;
};
这是对多用户进行管理的部分。
那么我们服务器的对于数据转发到我们的用户,所以下面我们再设置一个路由转发的功能:
代码如下:
#pragma once
#include<iostream>
#include<string>
#include<memory>
#include<sys/socket.h>
#include"Mutex.hpp"
#include"UserManager.hpp"
class Route
{
public:
Route()
:_uma(std::unique_ptr<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;
};