在本文的这个系列,会涉及到不同协议的基本使用到背后的原理机制,那么从一开始我们先实操,比如我们先尝试编写一款具有回显功能的Udp服务器,再尝试对它加一点业务,最后,我们甚至可以使用多线程部分进行服务器的一个升级。
用到多线程的部分比如我们要写一个群聊服务器,像微信的群聊那样,每个人都可以收发消息,当然了,这是后话了,这个的基础建立在了多线程的基础之上,所以我们着重点还是在udp协议本身的使用。
那么本文将会尝试编写两款udp服务器,一个是具有回显功能的udp服务器,一个是带一点点业务的udp服务器,比如带翻译功能的那种。
正式开始之前,我们如果不知道从哪里开始,那么我们不妨想想C++是一款具有什么特点的语言,它是一门面向对象的语言吧?
那么在udp_server_v1的这个服务器里面,涉及到的对象有谁?
那肯定是:udp服务器本身。
所以首当其冲的就是我们直接先来一个udpserver.hpp再说,即我们要封装一个udp服务器的类:
class udp
{
};
对于一个服务器来说,当我们通过一个服务器转发消息的时候,这个服务器发生了拷贝会怎么样?是不是会存在消息重复,进而会导致内存的问题等?所以对于一个服务器来说,最重要的一个点就是不能进行拷贝。
那么让这个类不能进行拷贝有很多种做法,一种是我们可以私有它的拷贝,私有它的operator==,另一种是我们可以通过让它继承不能发生拷贝的类,比如nocopy这个类,这个类的拷贝和赋值都被私有了。
那么第一个问题:如果禁止拷贝? 就完美的解决了。
#pragma once
class nocopy
{
public:
nocopy(){}
~nocopy(){}
nocopy(const nocopy&) = delete;
const nocopy& operator=(const nocopy&) = delete;
};
接下来第二个问题是,咱们的udp服务器应该要有哪些成员变量?或者说咱们udp这个对象应该具备哪些属性?
首先,对于网络通信来说,就是通过创建了socket套接字,通过socket接口返回了对应的文件fd,我们才得以通信,那么对于服务器来说,知道对应的fd是必不可少的,这是第一个属性了,通信的时候知道了对应的fd,有了fd之后,我们不妨想想什么是真正的socket?
在网络的基础概念中我们提及了网络通信实际上是两个进程在通信,那么我们确认两个进程的唯一标准就是对应的端口号加对应的主机Ip地址。
所以咱们的服务器应该要有的属性还要有port 和 std::string ip,可是事实真的如此吗?
实际上咱们的服务器并不需要特定的Ip地址,咱们的服务器填充对应的sockaddr_in的信息的时候,如果我们指定了特定的ip地址,那么也就代表咱们这个服务器它只能接收特定地址的数据,但是如果咱们的本机具有多个ip呢?
那么数据的目的地址不管是A还是B还是C,咱们是不是都可以接收了?所以一般对于服务器的信息填充,咱们一般设置为0,到后面使用的就是对应的宏了。
static const int gsockfd = -1;
static const uint16_t gport = 8888;
class udpserver
{
public:
udpserver(int sockfd = gsockfd,uint16_t port = gport)
: _isrunning(false),_sockfd(sockfd), _port(port)
{
}
~udpserver()
{
if(_sockfd < 0) ::close(_sockfd);
}
private:
uint16_t _port;
int _sockfd;
/* std::string _serverip; server端不需要ip */
bool _isrunning;
};
到了这里,咱们对于udpserver的大体框架咱们就清楚了。
那么对于一个服务器来说,一般涉及到的就是初始化服务器和启动对应的服务,比如咱们这里的服务就是回显对应的字符串。
那么我们先考虑第一个问题,我们应该如何初始化咱们的服务器?
对于初始化服务器来说,咱们可以简单的分为这么几步,首先我们要创建对应的套接字,接着是填充对应的服务器信息,最后是bind套接字和对应的服务器信息,对于udp来说,它本身是比较简单的,所以初始化服务器来说,也是比较简单的。
首先,创建对应的套接字我们使用的是socket:
它涉及到的头文件是sys/types.h sys/socket.h,其中有三个参数,分为是domain type protocol,对于第一个参数来说,它表示我们选择什么协议,我们这里选择IPv4的协议,对应的就是AF_INET。对于第二个参数来说,我们因为使用的是UDP协议,所以我们往下看我们会看到这么一句话:
翻译过来就是无连接的,不可靠传输。这实际上对应上了udp的特点。所以第二个类型参数我们选择这个。第三个参数我们直接传0就行,咱们暂时不管。
到这里咱们的套接字创建就好了,因为实际上返回的是文件描述符,而在底层实现中,文件描述符是没有负数的,所以一旦创建失败,它就会返回-1,我们可以通过返回值来判断是否创建成功了。
接着就是服务器信息的查询,这里我们唯一要注意一个点就是主机转网络以及对于服务器本身结构体信息的先清空再填充。清空的时候如果我们清空错了是有可能导致后面的sin_family出错的等。
这里我们简单认识一下scokaddr_in结构体吧,它就是我们要认识的结构体:
/* Structure describing an Internet socket address. */
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr)
- __SOCKADDR_COMMON_SIZE
- sizeof (in_port_t)
- sizeof (struct in_addr)];
};
这是它的源文件定义,我们看in_port_t sin_port,这实际上就是对应的端口号,我们再转到in_port_t的定义也没啥用,实际上就是一个16位的整数,sin_addr就比较神奇了,它实际上是一个ip地址,但是这个Ip地址再次被封装了起来,使用的是in_addr:
当我们进到in_addr的里面,会发现这个结构体只有一个内容,就是32位整数的数据,也就是4字节的数据这是。
有了对应的ip和Port,我们还有一个对应的地址簇,当我们转到它的定义里面去的时候,会发现里面这是一个简单的宏定义,其中使用到了我们在C语言阶段学习的连接符##,这里其实就是简单定义了一个sa_family_t类型的sin_family参数,我们不管那么多,用的时候我们直接填充就可以了。
以上是填充的基本认识。
填充完毕,也就是我们具备了对应的socket套接字和服务器对应的结构体,光有那可不行,我们需要把这两个bind起来,就像我们有锤子和钉子,不一起使用怎么能订住东西呢?
对于bind函数的使用是:
可以看到bind对应的参数我们都有了,直接使用之后,判断对应的返回值就可以了,那么到了这里,我们的初始化服务器就完成了:
void Initserver()
{
// create sockfd
_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 别又int
if (_sockfd < 0)
{
std::cout << "sockfd error" << std::endl;
exit(SOCKFD_ERROR);
}
std::cout << "sockfd success: " << _sockfd << std::endl;
// server imformation
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(_port); // htons
server.sin_addr.s_addr = INADDR_ANY;
// bind
socklen_t len = sizeof(server);
int n = ::bind(_sockfd, (struct sockaddr *)&server, len);
if (n < 0)
{
std::cout << "Bind error" << std::endl;
exit(BIND_ERROR);
}
std::cout << "Bind success" << std::endl;
}
这里面的宏参数都是笔者自己定义的,这样看起来更系统化一点。
接下来我们要考虑的问题就是服务器初始化结束之后,我们应该如何启动对应的服务?
到这里可能有同学就说了,服务嘛,咱们定义一个函数,客户端访问的时候我们就在服务器这里调用这个函数就可以了。
实际上这样是可以的,只不过这种办法丧失了解耦这个近乎完美的功能。
为什么这么说呢?因为在初识协议的部分,协议的一个非常大的作用是可以实现分层的解耦问题,比如人与人通信,协议出错了我们可以到对应的层发现并解决问题,如果我们把所有的都糅合在了一起,那不就太乱了吗?
所以我们开发的时候要遵从的一个非常重要的点是高耦合低内聚。
那么在服务器这里,我们要实现的就是,服务器能执行方法,但是这个方法是什么它不知道,它只知道调用就可以了。
不过在回显服务器这里,我们暂时不用考虑这么多,这里叭叭叭这么多是实际上是为了给udp_server_v2打基础。
在这里我们启动的服务就是返回收到的字符串,那么势必要用到两个函数:
一个是recvfrom一个是sendto函数,光从名字咱们也能知道这两个函数是干什么用的。
对于recvfrom函数来说,对应的参数分别是套接字,接收缓冲区,接收字节大小,标志位,接收的机器的结构体和对应的大小。
你别看这么多,简单想清楚了写的十分顺溜,我们从客户端那里接收到了消息,消息存放到哪里?从哪个套接字接收?缓冲区是哪个?缓冲区的大小是多大?
这些问题咱们想清楚了,参数那就太简单了。
对于sendto同理。咱这里就不解释了。
void start()
{
_isrunning = true;
while (_isrunning)
{
// recvfrom
char inbuffer[1024];
struct sockaddr_in clinet;
socklen_t len = sizeof(clinet);
ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&clinet, &len);
if (n < 0)
{
perror("recvfrom");
exit(RECVFORM_ERROR);
}
// send
else
{
std::string info = "[lazy_client say]#";
info += inbuffer;
std::cout << info << std::endl;
std::string ans;
ans += inbuffer;
ssize_t n = sendto(_sockfd, ans.c_str(), ans.size(), 0, (struct sockaddr*)&clinet, len);
if(n < 0)
{
std::cout << "sendto error" << std::endl;
exit(SENDTO_ERROR);
}
}
}
_isrunning = false;
}
那么在消息回显的这个功能这里,咱们只需要做两件事,先收消息,然后发消息就可以了。
对于udpserver.hpp咱们也就基本完成了。
接下来就是客户端的编写和服务端的编写
对于服务端来说:
#include "udpserver.hpp"
#include <memory>
int main(int args, char* argv[])
{
if(args != 2)
{
std::cout << "parameter num error!!!" << std::endl;
exit(SERVER_PARAMETER_ERROR);
}
int _serverport = std::stoi(argv[1]);
std::unique_ptr<udpserver> usvr = std::make_unique<udpserver>(_serverport); // C++14
usvr->Initserver();
usvr->start();
return 0;
}
老实说,没有什么特别要注意的,不过是我们可以通过智能指针new一个udpserver的实例化出来,然后初始化,启动服务就行。
不过,有意思的是服务器虽然说不需要IP地址,但是它需要自己的端口号,咱们可以自己指定,但是指定的端口号不能特殊了,像什么80端口号,就不是我们能用的了。
而端口号的传入,我们可以使用命令行参数进行参数,同理,客户端是需要的,所以我们在客户端也是都可以这么干的。
对于客户端来说:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
先短暂的打个插曲,这几个头文件,在网络中基本是刚需,所以我们可以简单的记忆一下。
好了,步入整题,对于客户端来说,也就是发个消息而已,不过和服务器一样,它们都有一个共同的点是服务一旦启动了,如果不主动退出就一直运行,所以我们一般来说都是使用while(true)来循环走这个过程。
这里有一个问题,对于服务器来说,服务器需要bind自己的socket和sockaddr_in的信息,换句话说,服务器需要bind套接字和端口号ip那些的关系,客户端是否需要bind呢?
答案是肯定的,但是,客户端并不需要显示的bind对应的端口号ip,因为这些都让操作系统给它做了,所以对于客户端要考虑的事儿就是怎么收发消息,那就简单了,用前面的函数就可以了:
int main(int args, char* argv[])
{
if(args != 3)
{
std::cout << "client parameter error!!!" << std::endl;
exit(-1);
}
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0) exit(-2);
while(true)
{
// sendto
struct sockaddr_in server;
memset(&server,0, sizeof(server));
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(argv[1]);
server.sin_port = htons(std::stoi(argv[2]));
std::string information;
std::getline(std::cin, information);
ssize_t n = ::sendto(sockfd, information.c_str(), sizeof(n), 0, (struct sockaddr*)&server, sizeof(server));
if(n < 0)
{
perror("sendto");
exit(-3);
}
// recvfrom
char inbuffer[1024];
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t m = ::recvfrom(sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&temp, &len);
inbuffer[m] = 0;
if(m < 0)
{
perror("recvfrom");
exit(-4);
}
std::cout << "[lazy_server say]#" << inbuffer;
}
return 0;
}
虽然这里比较草率,详细的主要还是在服务器介绍的比较多,这里其实类比一下就可以了。
对于v2版本,咱这里就把相关的文件传上来,因为这个版本要注意的也就只有解耦和一点std::bind的使用,所以笔者这里选择上传对应的代码了:
Common.h:
#pragma once
enum
{
SOCKFD_ERROR = 1,
BIND_ERROR,
RECVFORM_ERROR,
SENDTO_ERROR,
SERVER_PARAMETER_ERROR,
CLIENT_PARA_ERRROR,
FSTREAM_ERROR
};
dict.hpp
#pragma once
#include <unordered_map>
#include <iostream>
#include <fstream>
#include "Common.hpp"
static const std::string sep = ":";
class Dict
{
private:
void Dict_init()
{
// open profiles
std::fstream in(_path);
if(!in.is_open())
{
perror("is_open");
exit(FSTREAM_ERROR);
}
// open success
std::string info;
while(std::getline(in, info))
{
// 定位
size_t pos = info.find(sep);
if(pos == std::string::npos) continue; // empty line
// 分割
std::string key = info.substr(0, pos);
if(key.empty()) continue; // not complete
std::string value = info.substr(pos+sep.size());
if(value.empty()) continue; // not complete
// 插入
_dict.insert(std::make_pair(key,value));
}
in.close();
}
public:
Dict(std::string path = "./dict.txt")
:_path(path)
{
Dict_init();
}
std::string Translate(const std::string& word)
{
auto iter = _dict.find(word);
if(iter == _dict.end()) return "Not exist";
return iter->second;
}
private:
std::unordered_map<std::string, std::string> _dict;
std::string _path;
};
dict.txt
apple:苹果
banana:香蕉
cat:猫
dog:狗
book:书
pen:笔
happy:快乐的
sad:悲伤的
run:跑
jump:跳
teacher:老师
student:学生
car:汽车
bus:公交车
love:爱
hate:恨
hello:你好
goodbye:再见
summer:夏天
winter:冬天
InetAddr.hpp
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <iostream>
class InetAddr
{
private:
void ToHost(struct sockaddr_in addr)
{
_port = htons(addr.sin_port);
_ip = inet_ntoa(addr.sin_addr);
}
public:
InetAddr(struct sockaddr_in addr)
:_addr(addr)
{
ToHost(_addr);
}
uint16_t Port()
{
return _port;
}
std::string IP()
{
return _ip;
}
private:
struct sockaddr_in _addr;
uint16_t _port;
std::string _ip;
};
clientMain.cc
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <iostream>
#include <unistd.h>
#include <cstring>
#include "Common.hpp"
int main(int args, char* argv[])
{
if(args != 3)
{
std::cout << "client parameter error!!!" << std::endl;
exit(CLIENT_PARA_ERRROR);
}
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0) exit(-2);
while(true)
{
// sendto
struct sockaddr_in server;
memset(&server,0, sizeof(server));
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(argv[1]);
server.sin_port = htons(std::stoi(argv[2]));
std::cout << "[lazy_find]#";
std::string information;
std::getline(std::cin, information);
ssize_t n = ::sendto(sockfd, information.c_str(), sizeof(n), 0, (struct sockaddr*)&server, sizeof(server));
if(n < 0)
{
perror("sendto");
exit(SENDTO_ERROR);
}
// recvfrom
char inbuffer[1024];
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t m = ::recvfrom(sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&temp, &len);
inbuffer[m] = 0;
if(m < 0)
{
perror("recvfrom");
exit(RECVFORM_ERROR);
}
std::cout << "[lazy_result]#" << inbuffer << std::endl;
}
return 0;
}
udpserver.hpp
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <iostream>
#include <functional>
#include "InetAddr.hpp"
#include "Common.hpp"
static const int gsockfd = -1;
static const uint16_t gport = 8888;
using func_t = std::function<std::string(const std::string&)>;
class udpserver
{
public:
udpserver(func_t func,int sockfd = gsockfd,uint16_t port = gport)
: _isrunning(false),_sockfd(sockfd), _port(port),_func(func)
{
}
void Initserver()
{
// create sockfd
_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 别又int
if (_sockfd < 0)
{
std::cout << "sockfd error" << std::endl;
exit(SOCKFD_ERROR);
}
std::cout << "sockfd success: " << _sockfd << std::endl;
// server imformation
struct sockaddr_in server;
memset(&server, 9, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(_port); // htons
server.sin_addr.s_addr = INADDR_ANY;
// bind
socklen_t len = sizeof(server);
int n = ::bind(_sockfd, (struct sockaddr *)&server, len);
if (n < 0)
{
std::cout << "Bind error" << std::endl;
exit(BIND_ERROR);
}
std::cout << "Bind success" << std::endl;
}
void start()
{
_isrunning = true;
while (_isrunning)
{
// recvfrom
char inbuffer[1024];
struct sockaddr_in client;
socklen_t len = sizeof(client);
ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&client, &len);
if (n < 0)
{
perror("recvfrom");
exit(RECVFORM_ERROR);
}
// send
else
{
InetAddr addr(client);
std::string client_info = addr.IP() + std::to_string(addr.Port());
// std::string info = "[lazy_client find]#";
printf("[%s:%s]find#", addr.IP().c_str(), std::to_string(addr.Port()).c_str());
std::cout << inbuffer << std::endl;
// info += inbuffer;
// std::cout << info << std::endl;
// 处理业务
std::string ans = _func(inbuffer);
ssize_t n = sendto(_sockfd, ans.c_str(), ans.size(), 0, (struct sockaddr*)&client, len);
if(n < 0)
{
std::cout << "sendto error" << std::endl;
exit(SENDTO_ERROR);
}
}
}
_isrunning = false;
}
~udpserver()
{
if(_sockfd < 0) ::close(_sockfd);
}
private:
uint16_t _port;
int _sockfd;
func_t _func;
/* std::string _serverip; server端不需要ip */
bool _isrunning;
};
serverMain.cc
#include "udpserver.hpp"
#include <memory>
#include <functional>
#include "dict.hpp"
int main(int args, char* argv[])
{
if(args != 2)
{
std::cout << "parameter num error!!!" << std::endl;
exit(SERVER_PARAMETER_ERROR);
}
int _serverport = std::stoi(argv[1]);
Dict dict;
func_t func = std::bind(&Dict::Translate, dict, std::placeholders::_1);
std::unique_ptr<udpserver> usvr = std::make_unique<udpserver>(func,_serverport); // C++14
usvr->Initserver();
usvr->start();
return 0;
}
感谢阅读!
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有