V1版本-EchoServer
我们先实现一个从client输出到server然后server将client输出的数据又返回的非常简单的网络功能。
然后我们先封装一下我们的服务端。
nocopy
首先我们要确保实现的类不能被拷贝。那么我们可以继承一个不能被拷贝的父类nocopy:
cpp
#pragma once
class nocopy
{
public:
nocopy() {}
~nocopy() {}
nocopy(const nocopy &) = delete;
const nocopy &operator=(const nocopy &) = delete;
};
IniteServer
创建套接字
首先我们可以确定我们要在main函数里用什么功能:

那么我们现在就要开始实现初始化功能。
首先我们肯定要创建我们的套接字:

第一个参数是套接字的域,可以指定套接字使用的网络协议族,我们传入:

表示IPv4 协议族,即告诉OS IPv4通信。
第二个参数是套接字类型,即数据传输方式,我们传入:

代表无连接、不可靠、数据报,这不就是我们的UDP协议。
第三个参数protocol为具体协议类型,通常传入0即可,该函数会根据第一二个参数自动推导协议。如我们AF_INET+SOCK_DGRAM就是UDP协议。
此外我们还要注意他的返回值:

注意到返回的是文件描述符。他实际上和我们的管道类似,会创建一个内核资源进行管理。
其实我们也可以简单理解称将网卡这个硬件打开了,毕竟Linux一切皆文件。
总之我们的要有一个成员变量存放返回的文件描述符:

这时候我们就要创建套接字了,如果创建失败的话自然要输出错误信息。这里就能用上之前写的日志了。

这里SOCKET_ERROR就是我们自定义的错误码。
绑定
创建了套接字之后我们就要绑定对应的IP+端口了.
我们先回顾一下struct sockaddr。因为socket适用于各种协议的网络通信,但是各协议的地址格式不同。我们将所有地址都封装到struct sockaddr中。虽然地址格式不同,但他们首16个字节都是相同的类型,即地址类型:

我们统一用sockaddr接收,然后在前十六个字节中表明自己是什么地址,最后强转即可,很明显就是C语言的多态。
我们IPV4通信自然要用sockaddr_in结构体,然后我们要对其初始化一些数据。先来看看这个结构体有什么成员:

第一个参数有点神秘,这是我们的宏函数的高亮,转到定义后看到:

是一个将sa_prefix拼接family的宏函数。
因此我们sockaddr_in的第一个成员就是sin_family。sin_family就是我们的协议家族,实际上就是我们要填的地址类型,传入AF_INET即可。
第二个参数就是我们的端口好了,传入一个自定义的2字节整数即可。
第三个参数就是我们的家族地址,这里自然就是IPv4地址。

可以看到我们存储ipv4地址的是一个无符号整型。
但是我们用户通常喜欢传入点分十进制,我们要将其转换,不过库函数已经帮我们做了这件事了:

综上我们的参数初始化就是:

果真如此吗?
我们可是要进行网络通信的,这些能确保都是网络字节序吗?
很显然端口号还不能确保是网络字节序,所以我们要对这个无符号16位整型用一下htons(short类型从主机字节序转网络字节序):

然后就可以绑定了:

由于我们地址类型不同,大小自然也不同,所以还是要换入addr的大小的。
然后做一下出错判断即可:

Start
我们要管理一个运行状态,bool _running。
当运行时就将其置为真。
那么接下来就要开始接受信息了:

这几个参数都太显然了,我就不赘述了。

我们现在也可以简单跑一下,看一下会不会形成对应的套接字:

然后用netstat -upa指令:

然后我们就可以将收到的消息发送回client了:

Client
接下里我们实现客户端的功能:

那么接下来我们要明确一些概念。
首先呢我们的客服端要想和服务器通信,必须要预先知道服务器的ip地址和端口号!
客户端的端口号通常不能让用户自行设置,而是要交由OS随机分配。
为什么呢?
设想一下如果客户端由用户自己分配,会不会发展出客户端端口号和服务器端口号绑定的事情,然后端口号肯定就不够用了,发生冲突的概率就大了。
此外,客户端也要绑定自己的ip地址和端口号,但不需要显式绑定。在我们第一次向服务器发送信息时,OS就会帮我们绑定。
INADDR_ANY
刚才提到了我们客户端要预先知道服务器的ip地址,但是我们服务器可能有不同的ip地址。如果服务器只绑定一个ip地址显然是不好的,因此我们可以绑定INADDR_ANY(宏定义的0),意味着绑定服务器的任意IP地址:

让我们继续回归客户端的编写。
现在我们可以通过main函数选项传入服务器的ip地址和端口号:

那么完整代码:
cpp
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cerr << "Usage:" << argv[0] << "server-ip server-port" << std::endl;
exit(0);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
sockaddr_in server;
memset(&server, 0, sizeof(int));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str());
int sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
std::cerr << "create socket error" << std::endl;
exit(1);
}
while (1)
{
std::string line;
std::cout << "Please Enter# ";
std::getline(std::cin, line);
int n = sendto(sockfd, line.c_str(), sizeof(line), 0, (struct sockaddr *)&server, sizeof(server));
if (n > 0)
{
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
char buffer[1024];
int m = recvfrom(sockfd, buffer, sizeof(buffer), 0, (sockaddr *)&temp, &len);
if (m > 0)
{
buffer[m] = 0;
std::cout << buffer << std::endl;
}
else
{
break;
}
}
else
{
break;
}
}
::close(sockfd);
return 0;
}
接下来就可以测试下了:

127.0.0.1 是 IPv4 协议中定义的本地回环地址(Loopback Address),属于回环地址段 127.0.0.0/8 中的代表地址,其核心作用是让本机进程与自身通信,不经过任何物理网卡。
我们也可以用内网ip:

注意我们不能也不要绑定云服务器的公网ip。
InetAddr
上面我们获取peer的ip和接口方式过于丑陋了,简单封装一下吧:
cpp
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string>
class InetAddr
{
private:
void ToHost(const struct sockaddr_in &addr)
{
_port = addr.sin_port;
_ip = inet_ntoa(addr.sin_addr);
}
public:
InetAddr(const struct sockaddr_in &addr)
: _addr(addr)
{
ToHost(addr);
}
std::string Ip()
{
return _ip;
}
uint16_t Port()
{
return _port;
}
~InetAddr()
{
}
private:
std::string _ip;
uint16_t _port;
struct sockaddr_in _addr;
};
这样我们就能直接通过ip和port接口获取对应ip地址和端口号了。
V2版本-DictServer
我们获取数据自然不是返回数据这么简单,我们要处理一些业务。我们这里就实现一个简单的翻译功能。
我们一般需要将服务器的IO逻辑和业务逻辑解耦。传入包装器即可。
dict.txt
要翻译自然需要一个词典,我们这里就简单构造一个词典吧:
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天
Dict
接下来我们就要封装词典类。
自然我们要建立一个中英文映射关系,我们可以用一个哈希表.
然后我们还要维护词典文件的路径:

那么初始化的时候,我们要将文件里的映射关系载入到哈希表中:

最后实现翻译功能即可:

运行

将刚实现的翻译功能传入io端,就完整实现了我们的在线翻译。
来试行一下吧:

非常完美!
V3版本-简单聊天室
我们最后实现一个群聊功能。
我希望的是客户端发信息给服务器,服务器维护一个在线用户列表。然后将客户端发来的信息和在线用户列表交给路由和转发模块,这个模块通过线程池的方式将信息转发给所有在线列表的用户。
Route
接下来我们就要实现我们的转发逻辑。
首先做一些基本处理:

事实上我们是要准备实现多线程的话,自然要保证线程安全,我们在用代码的时候可以先上锁。
然后我们实现各个部分逻辑:
CheckOnlineUser:
cpp
void CheckOnlineUser(InetAddr &who)
{
LockGuard loakguard(&_mutex);
for (auto &user : _online_user)
{
if (user == who)
{
LOG(DEBUG, "%s is exits\n", who.AddStr().c_str());
return;
}
}
LOG(DEBUG, "%s is not exits,add it\n", who.AddStr().c_str());
_online_user.emplace_back(who);
}
Offline:
cpp
void Offline(InetAddr &who)
{
LockGuard loakguard(&_mutex);
auto iter = _online_user.begin();
for (; iter != _online_user.end(); iter++)
{
if (*iter == who)
{
LOG(DEBUG, "%s is offline\n", who.AddStr().c_str());
_online_user.erase(iter);
break;
}
}
}
ForwardHelper:
cpp
void ForwardHelper(int sockfd, const std::string message, InetAddr who)
{
LockGuard loakguard(&_mutex);
std::string send_message = "[" + who.AddStr() + "]# " + message;
for (auto &user : _online_user)
{
struct sockaddr_in peer = user.Addr();
LOG(DEBUG, "Forward message to %s, message is %s\n", user.AddStr().c_str(), send_message.c_str());
sendto(sockfd, send_message.c_str(), send_message.size(), 0, (sockaddr *)&peer, sizeof(peer));
}
}
Forward:
cpp
void Forward(int sockfd, const std::string &message, InetAddr &who)
{
// 1.判断该用户是否在用户列表中?如果在,不做处理,如果不在,加入在线列表
CheckOnlineUser(who);
// 如果发送QUIT或Q则退出在线列表
if (message == "QUIT" || message == "Q")
{
Offline(who);
}
// 2.转发信息
// ForwardHelper(sockfd,message);
task_t t = std::bind(&Route::ForwardHelper, this, sockfd, message, who);
ThreadPool<task_t>::GetInstance()->Equeue(t);
}
Server
现在就可以调整服务器的Start逻辑了:

Client
我们要实现客户端首先要想到一件事。
我们发送消息的时候线程是在阻塞等待的,这意味着我们不发送消息就无法接受消息。因此我们要开两个线程,一个线程发送消息,一个线程接受消息。
我们还可以将收到的消息重定向到另一个终端,这样发送消息的终端就不会被接受的消息污染。
完整逻辑
cpp
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Thread.hpp"
using namespace ThreadMoudle;
int InitClient()
{
int sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
std::cerr << "create socket error" << std::endl;
exit(1);
}
return sockfd;
}
void RecvMessage(int sockfd, const std::string &name)
{
while (true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
char buffer[1024];
int n = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, &len);
if (n > 0)
{
buffer[n] = 0;
std::cerr << buffer << std::endl;
}
else
{
std::cerr << "recvfrom error" << std::endl;
break;
}
}
}
void SendMessage(int sockfd, std::string serverip, uint16_t serverport, const std::string &name)
{
struct sockaddr_in server;
memset(&server, sizeof(server), 0);
server.sin_family = AF_INET;
server.sin_port = serverport;
server.sin_addr.s_addr = inet_addr(serverip.c_str());
std::string cli_profix = name + "# ";
while (true)
{
std::string line;
std::cout << cli_profix;
std::getline(std::cin, line);
int n = sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr *)&server, sizeof(server));
if (n < 0)
break;
}
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cerr << "Usage:" << argv[0] << "server-ip server-port" << std::endl;
exit(0);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
int sockfd = InitClient();
Thread recver("recver-thread", std::bind(&RecvMessage, sockfd, std::placeholders::_1));
Thread send("send-thread", std::bind(&SendMessage, sockfd, serverip, serverport, std::placeholders::_1));
recver.Start();
send.Start();
recver.Join();
send.Join();
::close(sockfd);
return 0;
}
尝试运行一下:

